diff --git a/dimos/agents_deprecated/modules/gateway/client.py b/dimos/agents_deprecated/modules/gateway/client.py index 772ca445aa..2e16243183 100644 --- a/dimos/agents_deprecated/modules/gateway/client.py +++ b/dimos/agents_deprecated/modules/gateway/client.py @@ -16,7 +16,6 @@ import asyncio from collections.abc import AsyncIterator, Iterator -import logging import os from types import TracebackType from typing import Any @@ -24,9 +23,11 @@ import httpx from tenacity import retry, stop_after_attempt, wait_exponential +from dimos.utils.logging_config import setup_logger + from .tensorzero_embedded import TensorZeroEmbeddedGateway -logger = logging.getLogger(__name__) +logger = setup_logger() class UnifiedGatewayClient: diff --git a/dimos/agents_deprecated/modules/gateway/tensorzero_embedded.py b/dimos/agents_deprecated/modules/gateway/tensorzero_embedded.py index 4708788241..aa8a9ee92d 100644 --- a/dimos/agents_deprecated/modules/gateway/tensorzero_embedded.py +++ b/dimos/agents_deprecated/modules/gateway/tensorzero_embedded.py @@ -15,11 +15,12 @@ """TensorZero embedded gateway client with correct config format.""" from collections.abc import AsyncIterator, Iterator -import logging from pathlib import Path from typing import Any -logger = logging.getLogger(__name__) +from dimos.utils.logging_config import setup_logger + +logger = setup_logger() class TensorZeroEmbeddedGateway: diff --git a/dimos/control/hardware_interface.py b/dimos/control/hardware_interface.py index b4f84a2507..3925116f97 100644 --- a/dimos/control/hardware_interface.py +++ b/dimos/control/hardware_interface.py @@ -24,17 +24,17 @@ from __future__ import annotations -import logging import time from typing import TYPE_CHECKING from dimos.hardware.manipulators.spec import ControlMode, ManipulatorAdapter +from dimos.utils.logging_config import setup_logger if TYPE_CHECKING: from dimos.control.components import HardwareComponent, HardwareId, JointName, JointState from dimos.hardware.drive_trains.spec import TwistBaseAdapter -logger = logging.getLogger(__name__) +logger = setup_logger() class ConnectedHardware: diff --git a/dimos/hardware/drive_trains/flowbase/adapter.py b/dimos/hardware/drive_trains/flowbase/adapter.py index 9fe0314f88..a4fb458308 100644 --- a/dimos/hardware/drive_trains/flowbase/adapter.py +++ b/dimos/hardware/drive_trains/flowbase/adapter.py @@ -26,16 +26,17 @@ from __future__ import annotations -import logging import threading from typing import TYPE_CHECKING import numpy as np +from dimos.utils.logging_config import setup_logger + if TYPE_CHECKING: from dimos.hardware.drive_trains.registry import TwistBaseAdapterRegistry -logger = logging.getLogger(__name__) +logger = setup_logger() class FlowBaseAdapter: diff --git a/dimos/hardware/drive_trains/registry.py b/dimos/hardware/drive_trains/registry.py index 435d61c73f..a4c49b183c 100644 --- a/dimos/hardware/drive_trains/registry.py +++ b/dimos/hardware/drive_trains/registry.py @@ -32,14 +32,15 @@ from collections.abc import Callable import importlib -import logging import os from typing import TYPE_CHECKING, Any +from dimos.utils.logging_config import setup_logger + if TYPE_CHECKING: from dimos.hardware.drive_trains.spec import TwistBaseAdapter -logger = logging.getLogger(__name__) +logger = setup_logger() class TwistBaseAdapterRegistry: diff --git a/dimos/hardware/manipulators/registry.py b/dimos/hardware/manipulators/registry.py index 9e63fa349b..a8c87c149c 100644 --- a/dimos/hardware/manipulators/registry.py +++ b/dimos/hardware/manipulators/registry.py @@ -32,13 +32,14 @@ from __future__ import annotations import importlib -import logging from typing import TYPE_CHECKING, Any +from dimos.utils.logging_config import setup_logger + if TYPE_CHECKING: from dimos.hardware.manipulators.spec import ManipulatorAdapter -logger = logging.getLogger(__name__) +logger = setup_logger() class AdapterRegistry: diff --git a/dimos/hardware/sensors/camera/gstreamer/gstreamer_camera_test_script.py b/dimos/hardware/sensors/camera/gstreamer/gstreamer_camera_test_script.py index 991fc8d076..56f0591b28 100755 --- a/dimos/hardware/sensors/camera/gstreamer/gstreamer_camera_test_script.py +++ b/dimos/hardware/sensors/camera/gstreamer/gstreamer_camera_test_script.py @@ -23,9 +23,9 @@ from dimos.hardware.sensors.camera.gstreamer.gstreamer_camera import GstreamerCameraModule from dimos.msgs.sensor_msgs.Image import Image from dimos.protocol.pubsub.impl import lcmpubsub as _lcm +from dimos.utils.logging_config import setup_logger -logging.basicConfig(level=logging.INFO) -logger = logging.getLogger(__name__) +logger = setup_logger() def main() -> None: diff --git a/dimos/models/vl/base.py b/dimos/models/vl/base.py index dfb046b58f..09ba4e0c07 100644 --- a/dimos/models/vl/base.py +++ b/dimos/models/vl/base.py @@ -2,7 +2,6 @@ from abc import ABC, abstractmethod import json -import logging from typing import Any import warnings @@ -15,8 +14,9 @@ from dimos.utils.data import get_data from dimos.utils.decorators.decorators import retry from dimos.utils.llm_utils import extract_json +from dimos.utils.logging_config import setup_logger -logger = logging.getLogger(__name__) +logger = setup_logger() class Captioner(ABC): """Interface for models that can generate image captions.""" diff --git a/dimos/project/test_get_logger.py b/dimos/project/test_get_logger.py new file mode 100644 index 0000000000..60a1f583b0 --- /dev/null +++ b/dimos/project/test_get_logger.py @@ -0,0 +1,116 @@ +# Copyright 2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os + +from dimos.constants import DIMOS_PROJECT_ROOT + +PATTERN = "= logging.getLogger" + +IGNORED_DIRS = { + ".venv", + "venv", + "__pycache__", + "node_modules", + ".git", + "dist", + "build", + ".egg-info", + ".tox", +} + +# Lines that match the pattern but are legitimate uses. +WHITELIST = [ + ("dimos/utils/logging_config.py", "logger_obj = logging.getLogger(logger_name)"), + ("dimos/utils/logging_config.py", "stdlib_logger = logging.getLogger(name)"), + ("dimos/core/coordination/python_worker.py", "lg = logging.getLogger(name)"), + ("dimos/robot/foxglove_bridge.py", "logger = logging.getLogger(logger)"), + ( + "dimos/hardware/sensors/camera/gstreamer/gstreamer_sender.py", + 'logger = logging.getLogger("gstreamer_tcp_sender")', + ), +] + + +def _is_ignored_dir(dirpath: str) -> bool: + parts = dirpath.split(os.sep) + return bool(IGNORED_DIRS.intersection(parts)) + + +def _is_whitelisted(rel_path: str, line: str) -> bool: + for allowed_path, allowed_substr in WHITELIST: + if rel_path == allowed_path and allowed_substr in line: + return True + return False + + +def find_get_logger_usages() -> list[tuple[str, int, str]]: + """Return a list of (rel_path, line_number, line_text) for every violation.""" + dimos_dir = DIMOS_PROJECT_ROOT / "dimos" + violations: list[tuple[str, int, str]] = [] + # Skip this test file. + self_path = os.path.realpath(__file__) + + for dirpath, dirnames, filenames in os.walk(dimos_dir): + dirnames[:] = [d for d in dirnames if d not in IGNORED_DIRS] + + if _is_ignored_dir(dirpath): + continue + + for fname in filenames: + if not fname.endswith(".py"): + continue + + full_path = os.path.join(dirpath, fname) + if os.path.realpath(full_path) == self_path: + continue + rel_path = os.path.relpath(full_path, DIMOS_PROJECT_ROOT) + + try: + with open(full_path, encoding="utf-8", errors="replace") as f: + for lineno, line in enumerate(f, start=1): + stripped = line.rstrip("\n") + if PATTERN not in stripped: + continue + if _is_whitelisted(rel_path, stripped): + continue + violations.append((rel_path, lineno, stripped)) + except (OSError, UnicodeDecodeError): + continue + + return violations + + +def test_no_get_logger(): + """ + Fail if any file uses `= logging.getLogger` outside the whitelist. + """ + violations = find_get_logger_usages() + if violations: + report_lines = [ + f"Found {len(violations)} forbidden use(s) of `logging.getLogger`. " + "Use `setup_logger` instead:", + "", + " from dimos.utils.logging_config import setup_logger", + "", + " logger = setup_logger()", + "", + "If the usage is legitimate (e.g. standalone script, logging " + "infrastructure, or third-party logger suppression), add it to the " + "WHITELIST in dimos/project/test_get_logger.py.", + "", + ] + for path, lineno, text in violations: + report_lines.append(f" {path}:{lineno}: {text.strip()}") + raise AssertionError("\n".join(report_lines)) diff --git a/dimos/test_no_init_files.py b/dimos/project/test_no_init_files.py similarity index 100% rename from dimos/test_no_init_files.py rename to dimos/project/test_no_init_files.py diff --git a/dimos/test_no_sections.py b/dimos/project/test_no_sections.py similarity index 100% rename from dimos/test_no_sections.py rename to dimos/project/test_no_sections.py diff --git a/dimos/protocol/service/system_configurator/base.py b/dimos/protocol/service/system_configurator/base.py index 51413ea658..57348149ac 100644 --- a/dimos/protocol/service/system_configurator/base.py +++ b/dimos/protocol/service/system_configurator/base.py @@ -15,13 +15,13 @@ from __future__ import annotations from abc import ABC, abstractmethod -import logging import os import subprocess from dimos.utils import prompt +from dimos.utils.logging_config import setup_logger -logger = logging.getLogger(__name__) +logger = setup_logger() def _read_sysctl_int(name: str) -> int | None: diff --git a/dimos/protocol/service/system_configurator/libpython.py b/dimos/protocol/service/system_configurator/libpython.py index 43f6b99ade..1aa16f4d3d 100644 --- a/dimos/protocol/service/system_configurator/libpython.py +++ b/dimos/protocol/service/system_configurator/libpython.py @@ -21,14 +21,14 @@ from __future__ import annotations -import logging from pathlib import Path import platform import sys from dimos.protocol.service.system_configurator.base import SystemConfigurator +from dimos.utils.logging_config import setup_logger -logger = logging.getLogger(__name__) +logger = setup_logger() class LibPythonConfiguratorMacOS(SystemConfigurator): diff --git a/dimos/robot/unitree/go2/connection.py b/dimos/robot/unitree/go2/connection.py index b994f0cae0..182a4af280 100644 --- a/dimos/robot/unitree/go2/connection.py +++ b/dimos/robot/unitree/go2/connection.py @@ -12,7 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -import logging import sys from threading import Thread import time @@ -32,6 +31,7 @@ from dimos.core.stream import In, Out from dimos.core.transport import LCMTransport, pSHMTransport from dimos.spec.perception import Camera, Pointcloud +from dimos.utils.logging_config import setup_logger if TYPE_CHECKING: from dimos.core.rpc_client import ModuleProxy @@ -53,7 +53,7 @@ else: from typing import TypeVar -logger = logging.getLogger(__name__) +logger = setup_logger() class ConnectionConfig(ModuleConfig): diff --git a/dimos/skills/rest/rest.py b/dimos/skills/rest/rest.py index 23369faf23..df38375776 100644 --- a/dimos/skills/rest/rest.py +++ b/dimos/skills/rest/rest.py @@ -12,14 +12,13 @@ # See the License for the specific language governing permissions and # limitations under the License. -import logging - from pydantic import Field import requests from dimos.skills.skills import AbstractSkill +from dimos.utils.logging_config import setup_logger -logger = logging.getLogger(__name__) +logger = setup_logger() class GenericRestSkill(AbstractSkill): diff --git a/dimos/skills/skills.py b/dimos/skills/skills.py index 1fbf6266ef..ec97c75d2b 100644 --- a/dimos/skills/skills.py +++ b/dimos/skills/skills.py @@ -14,21 +14,18 @@ from __future__ import annotations -import logging from typing import TYPE_CHECKING, Any from openai import pydantic_function_tool from pydantic import BaseModel from dimos.types.constants import Colors +from dimos.utils.logging_config import setup_logger if TYPE_CHECKING: from collections.abc import Iterator -# Configure logging for the module -logging.basicConfig(level=logging.INFO) -logger = logging.getLogger(__name__) -logger.setLevel(logging.INFO) +logger = setup_logger() class SkillLibrary: diff --git a/dimos/stream/data_provider.py b/dimos/stream/data_provider.py index 2a2d18d857..61fbc000d6 100644 --- a/dimos/stream/data_provider.py +++ b/dimos/stream/data_provider.py @@ -13,7 +13,6 @@ # limitations under the License. from abc import ABC -import logging import multiprocessing import reactivex as rx @@ -21,7 +20,9 @@ from reactivex.scheduler import ThreadPoolScheduler from reactivex.subject import Subject -logging.basicConfig(level=logging.INFO) +from dimos.utils.logging_config import setup_logger + +logger = setup_logger() # Create a thread pool scheduler for concurrent processing pool_scheduler = ThreadPoolScheduler(multiprocessing.cpu_count()) @@ -53,7 +54,6 @@ class ROSDataProvider(AbstractDataProvider): def __init__(self, dev_name: str = "ros_provider") -> None: super().__init__(dev_name) - self.logger = logging.getLogger(dev_name) def push_data(self, data) -> None: # type: ignore[no-untyped-def] """Push new data to the stream.""" @@ -111,7 +111,6 @@ class QueryDataProvider(AbstractDataProvider): Attributes: dev_name (str): The name of the data provider. - logger (logging.Logger): Logger instance for logging messages. """ def __init__(self, dev_name: str = "query_provider") -> None: @@ -122,7 +121,6 @@ def __init__(self, dev_name: str = "query_provider") -> None: dev_name (str): The name of the data provider. Defaults to "query_provider". """ super().__init__(dev_name) - self.logger = logging.getLogger(dev_name) def start_query_stream( self, diff --git a/dimos/stream/ros_video_provider.py b/dimos/stream/ros_video_provider.py index 1101ee5f58..e2a0fcf167 100644 --- a/dimos/stream/ros_video_provider.py +++ b/dimos/stream/ros_video_provider.py @@ -18,7 +18,6 @@ and makes them available as an Observable stream. """ -import logging import time import numpy as np @@ -26,8 +25,9 @@ from reactivex.scheduler import ThreadPoolScheduler from dimos.stream.video_provider import AbstractVideoProvider +from dimos.utils.logging_config import setup_logger -logging.basicConfig(level=logging.INFO) +logger = setup_logger() class ROSVideoProvider(AbstractVideoProvider): @@ -37,7 +37,6 @@ class ROSVideoProvider(AbstractVideoProvider): available as an Observable stream. It uses ReactiveX's Subject to broadcast frames. Attributes: - logger: Logger instance for this provider. _subject: ReactiveX Subject that broadcasts frames. _last_frame_time: Timestamp of the last received frame. """ @@ -52,10 +51,9 @@ def __init__( pool_scheduler: Optional ThreadPoolScheduler for multithreading. """ super().__init__(dev_name, pool_scheduler) - self.logger = logging.getLogger(dev_name) self._subject = Subject() # type: ignore[var-annotated] self._last_frame_time = None - self.logger.info("ROSVideoProvider initialized") + logger.info("ROSVideoProvider initialized", device=dev_name) def push_data(self, frame: np.ndarray) -> None: """Push a new frame into the provider. @@ -71,16 +69,16 @@ def push_data(self, frame: np.ndarray) -> None: current_time = time.time() if self._last_frame_time: frame_interval = current_time - self._last_frame_time - self.logger.debug( + logger.debug( f"Frame interval: {frame_interval:.3f}s ({1 / frame_interval:.1f} FPS)" ) self._last_frame_time = current_time # type: ignore[assignment] - self.logger.debug(f"Pushing frame type: {type(frame)}") + logger.debug(f"Pushing frame type: {type(frame)}") self._subject.on_next(frame) - self.logger.debug("Frame pushed") + logger.debug("Frame pushed") except Exception as e: - self.logger.error(f"Push error: {e}") + logger.error(f"Push error: {e}") raise def capture_video_as_observable(self, fps: int = 30) -> Observable: # type: ignore[type-arg] @@ -97,7 +95,7 @@ def capture_video_as_observable(self, fps: int = 30) -> Observable: # type: ign Note: The fps parameter is currently not enforced. See implementation note below. """ - self.logger.info(f"Creating observable with {fps} FPS rate limiting") + logger.info(f"Creating observable with {fps} FPS rate limiting") # TODO: Implement rate limiting using ops.throttle_with_timeout() or # ops.sample() to restrict emissions to one frame per (1/fps) seconds. # Example: ops.sample(1.0/fps) diff --git a/dimos/stream/video_provider.py b/dimos/stream/video_provider.py index 38406fd5a5..2554edd615 100644 --- a/dimos/stream/video_provider.py +++ b/dimos/stream/video_provider.py @@ -21,7 +21,6 @@ # Standard library imports from abc import ABC, abstractmethod -import logging import os from threading import Lock import time @@ -35,11 +34,10 @@ from reactivex.scheduler import ThreadPoolScheduler # Local imports +from dimos.utils.logging_config import setup_logger from dimos.utils.threadpool import get_scheduler -# Note: Logging configuration should ideally be in the application initialization, -# not in a module. Keeping it for now but with a more restricted scope. -logger = logging.getLogger(__name__) +logger = setup_logger() # Specific exception classes