From 2032ca9ab3985032119bb66c0193cf78aef2fd4e Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 12 Mar 2026 21:56:42 +0000 Subject: [PATCH] Standardize logging config across the codebase - Enhance healthchain/utils/logger.py with centralized get_logger() that configures a single handler on the root 'healthchain' logger - Support HEALTHCHAIN_LOG_LEVEL env var (DEBUG/INFO/WARNING/ERROR/CRITICAL) - Migrate all 50 modules from raw `import logging` + `logging.getLogger` to `from healthchain.utils.logger import get_logger` - Consistent log format and color formatting inherited via propagation - Backward-compatible: existing add_handlers() still works Closes #124 --- healthchain/__init__.py | 8 +-- healthchain/config/base.py | 4 +- healthchain/config/validators.py | 4 +- healthchain/fhir/dataframe.py | 4 +- healthchain/fhir/elementhelpers.py | 4 +- healthchain/fhir/readers.py | 4 +- healthchain/fhir/resourcehelpers.py | 4 +- healthchain/fhir/version.py | 4 +- healthchain/gateway/api/app.py | 4 +- healthchain/gateway/base.py | 4 +- healthchain/gateway/cds/__init__.py | 4 +- healthchain/gateway/clients/auth.py | 4 +- .../gateway/clients/fhir/aio/client.py | 4 +- healthchain/gateway/clients/fhir/base.py | 4 +- .../gateway/clients/fhir/sync/client.py | 4 +- .../gateway/clients/fhir/sync/connection.py | 4 +- healthchain/gateway/events/dispatcher.py | 4 +- healthchain/gateway/fhir/aio.py | 4 +- healthchain/gateway/fhir/base.py | 4 +- healthchain/gateway/fhir/errors.py | 4 +- healthchain/gateway/fhir/sync.py | 4 +- healthchain/gateway/soap/fastapiserver.py | 4 +- healthchain/gateway/soap/notereader.py | 4 +- healthchain/interop/__init__.py | 4 +- healthchain/interop/config_manager.py | 4 +- healthchain/interop/engine.py | 4 +- healthchain/interop/generators/base.py | 4 +- healthchain/interop/generators/cda.py | 4 +- healthchain/interop/generators/fhir.py | 4 +- healthchain/interop/parsers/cda.py | 4 +- healthchain/interop/template_registry.py | 4 +- healthchain/io/adapters/cdaadapter.py | 4 +- healthchain/io/adapters/cdsfhiradapter.py | 4 +- healthchain/io/containers/document.py | 4 +- healthchain/models/requests/cdarequest.py | 4 +- healthchain/models/responses/cdaresponse.py | 4 +- healthchain/pipeline/base.py | 4 +- .../pipeline/components/cdscardcreator.py | 4 +- .../components/fhirproblemextractor.py | 4 +- .../pipeline/components/integrations.py | 4 +- healthchain/pipeline/mixins.py | 4 +- healthchain/sandbox/datasets.py | 4 +- .../sandbox/generators/cdsdatagenerator.py | 4 +- healthchain/sandbox/loaders/mimic.py | 4 +- healthchain/sandbox/loaders/synthea.py | 4 +- healthchain/sandbox/requestconstructors.py | 4 +- healthchain/sandbox/sandboxclient.py | 4 +- healthchain/sandbox/utils.py | 4 +- healthchain/utils/__init__.py | 3 +- healthchain/utils/logger.py | 64 +++++++++++++++++-- 50 files changed, 156 insertions(+), 107 deletions(-) diff --git a/healthchain/__init__.py b/healthchain/__init__.py index 92fb633c..66819234 100644 --- a/healthchain/__init__.py +++ b/healthchain/__init__.py @@ -1,17 +1,13 @@ -import logging import warnings -from .utils.logger import add_handlers +from .utils.logger import get_logger from .config.base import ConfigManager, ValidationLevel # Enable deprecation warnings warnings.filterwarnings("always", category=DeprecationWarning, module="healthchain") -logger = logging.getLogger(__name__) - -add_handlers(logger) -logger.setLevel(logging.INFO) +logger = get_logger(__name__) # Export them at the top level __all__ = ["ConfigManager", "ValidationLevel", "api", "ehr", "sandbox"] diff --git a/healthchain/config/base.py b/healthchain/config/base.py index f14d33fe..d7d9e38e 100644 --- a/healthchain/config/base.py +++ b/healthchain/config/base.py @@ -1,10 +1,10 @@ import yaml -import logging +from healthchain.utils.logger import get_logger import os from pathlib import Path from typing import Dict, Any, Optional, List -log = logging.getLogger(__name__) +log = get_logger(__name__) def _deep_merge(target: Dict, source: Dict) -> None: diff --git a/healthchain/config/validators.py b/healthchain/config/validators.py index 4541d180..ff175e83 100644 --- a/healthchain/config/validators.py +++ b/healthchain/config/validators.py @@ -4,11 +4,11 @@ This module provides validation models and utilities for configuration files. """ -import logging +from healthchain.utils.logger import get_logger from pydantic import BaseModel, ValidationError, field_validator, ConfigDict from typing import Dict, List, Any, Optional, Type, Union -logger = logging.getLogger(__name__) +logger = get_logger(__name__) # # Base Models diff --git a/healthchain/fhir/dataframe.py b/healthchain/fhir/dataframe.py index 0c9bfdb2..07d9c4a8 100644 --- a/healthchain/fhir/dataframe.py +++ b/healthchain/fhir/dataframe.py @@ -7,7 +7,7 @@ """ import pandas as pd -import logging +from healthchain.utils.logger import get_logger from typing import Any, Dict, List, Union, Optional, Literal from collections import defaultdict @@ -20,7 +20,7 @@ encode_gender, ) -logger = logging.getLogger(__name__) +logger = get_logger(__name__) # Resource handler registry diff --git a/healthchain/fhir/elementhelpers.py b/healthchain/fhir/elementhelpers.py index 9f86811f..8733af96 100644 --- a/healthchain/fhir/elementhelpers.py +++ b/healthchain/fhir/elementhelpers.py @@ -4,7 +4,7 @@ as building blocks within FHIR resources (e.g., CodeableConcept, Attachment, Coding). """ -import logging +from healthchain.utils.logger import get_logger import base64 import datetime @@ -13,7 +13,7 @@ if TYPE_CHECKING: from healthchain.fhir.version import FHIRVersion -logger = logging.getLogger(__name__) +logger = get_logger(__name__) def create_single_codeable_concept( diff --git a/healthchain/fhir/readers.py b/healthchain/fhir/readers.py index 00c2ad4f..a9a4667b 100644 --- a/healthchain/fhir/readers.py +++ b/healthchain/fhir/readers.py @@ -4,7 +4,7 @@ and reading data from FHIR resources. """ -import logging +from healthchain.utils.logger import get_logger import importlib import re @@ -12,7 +12,7 @@ from fhir.resources.resource import Resource from fhir.resources.documentreference import DocumentReference -logger = logging.getLogger(__name__) +logger = get_logger(__name__) def _fix_timezone_naive_datetimes(data: Any) -> Any: diff --git a/healthchain/fhir/resourcehelpers.py b/healthchain/fhir/resourcehelpers.py index dcac457e..37f9a873 100644 --- a/healthchain/fhir/resourcehelpers.py +++ b/healthchain/fhir/resourcehelpers.py @@ -10,7 +10,7 @@ Parameters marked REQUIRED are required by FHIR specification. """ -import logging +from healthchain.utils.logger import get_logger import datetime from typing import List, Optional, Dict, Any, Union, TYPE_CHECKING @@ -29,7 +29,7 @@ if TYPE_CHECKING: from healthchain.fhir.version import FHIRVersion -logger = logging.getLogger(__name__) +logger = get_logger(__name__) def create_condition( diff --git a/healthchain/fhir/version.py b/healthchain/fhir/version.py index 19dd314e..725c06eb 100644 --- a/healthchain/fhir/version.py +++ b/healthchain/fhir/version.py @@ -23,12 +23,12 @@ """ import importlib -import logging +from healthchain.utils.logger import get_logger from contextlib import contextmanager from enum import Enum from typing import Any, Generator, Optional, Type, Union -logger = logging.getLogger(__name__) +logger = get_logger(__name__) class FHIRVersion(str, Enum): diff --git a/healthchain/gateway/api/app.py b/healthchain/gateway/api/app.py index 1ed8ab86..33a5c52b 100644 --- a/healthchain/gateway/api/app.py +++ b/healthchain/gateway/api/app.py @@ -5,7 +5,7 @@ healthcare-specific gateways, routes, middleware, and capabilities. """ -import logging +from healthchain.utils.logger import get_logger from contextlib import asynccontextmanager from datetime import datetime @@ -21,7 +21,7 @@ from healthchain.gateway.events.dispatcher import EventDispatcher from healthchain.gateway.api.dependencies import get_app -logger = logging.getLogger(__name__) +logger = get_logger(__name__) class HealthChainAPI(FastAPI): diff --git a/healthchain/gateway/base.py b/healthchain/gateway/base.py index 2bf262d2..7e0e9edc 100644 --- a/healthchain/gateway/base.py +++ b/healthchain/gateway/base.py @@ -5,7 +5,7 @@ architecture of the gateway system. """ -import logging +from healthchain.utils.logger import get_logger import asyncio from abc import ABC @@ -16,7 +16,7 @@ from healthchain.gateway.api.protocols import EventDispatcherProtocol -logger = logging.getLogger(__name__) +logger = get_logger(__name__) # Type variables for self-referencing return types and generic gateways diff --git a/healthchain/gateway/cds/__init__.py b/healthchain/gateway/cds/__init__.py index 6bbd6357..a6e9e209 100644 --- a/healthchain/gateway/cds/__init__.py +++ b/healthchain/gateway/cds/__init__.py @@ -1,4 +1,4 @@ -import logging +from healthchain.utils.logger import get_logger from typing import Any, Callable, Dict, List, Optional, TypeVar, Union @@ -14,7 +14,7 @@ from healthchain.sandbox.workflows import UseCaseMapping -logger = logging.getLogger(__name__) +logger = get_logger(__name__) # Type variable for self-referencing return types T = TypeVar("T", bound="CDSHooksService") diff --git a/healthchain/gateway/clients/auth.py b/healthchain/gateway/clients/auth.py index e20cabf3..762225eb 100644 --- a/healthchain/gateway/clients/auth.py +++ b/healthchain/gateway/clients/auth.py @@ -5,7 +5,7 @@ management and refresh. """ -import logging +from healthchain.utils.logger import get_logger import os import uuid import asyncio @@ -17,7 +17,7 @@ from pydantic import BaseModel -logger = logging.getLogger(__name__) +logger = get_logger(__name__) class OAuth2Config(BaseModel): diff --git a/healthchain/gateway/clients/fhir/aio/client.py b/healthchain/gateway/clients/fhir/aio/client.py index f190c370..c118a7ed 100644 --- a/healthchain/gateway/clients/fhir/aio/client.py +++ b/healthchain/gateway/clients/fhir/aio/client.py @@ -1,4 +1,4 @@ -import logging +from healthchain.utils.logger import get_logger import httpx from typing import Any, Dict, Type, Union @@ -11,7 +11,7 @@ from healthchain.gateway.clients.fhir.base import FHIRAuthConfig, FHIRServerInterface -logger = logging.getLogger(__name__) +logger = get_logger(__name__) class AsyncFHIRClient(FHIRServerInterface): diff --git a/healthchain/gateway/clients/fhir/base.py b/healthchain/gateway/clients/fhir/base.py index b733476f..b7120502 100644 --- a/healthchain/gateway/clients/fhir/base.py +++ b/healthchain/gateway/clients/fhir/base.py @@ -1,5 +1,5 @@ import json -import logging +from healthchain.utils.logger import get_logger import httpx from abc import ABC, abstractmethod @@ -15,7 +15,7 @@ from pydantic import BaseModel -logger = logging.getLogger(__name__) +logger = get_logger(__name__) class FHIRClientError(Exception): diff --git a/healthchain/gateway/clients/fhir/sync/client.py b/healthchain/gateway/clients/fhir/sync/client.py index 5d92d41c..bdb11882 100644 --- a/healthchain/gateway/clients/fhir/sync/client.py +++ b/healthchain/gateway/clients/fhir/sync/client.py @@ -1,4 +1,4 @@ -import logging +from healthchain.utils.logger import get_logger import httpx from typing import Any, Dict, Type, Union @@ -11,7 +11,7 @@ from healthchain.gateway.clients.fhir.base import FHIRAuthConfig, FHIRServerInterface -logger = logging.getLogger(__name__) +logger = get_logger(__name__) class FHIRClient(FHIRServerInterface): diff --git a/healthchain/gateway/clients/fhir/sync/connection.py b/healthchain/gateway/clients/fhir/sync/connection.py index eb70a4f6..87941c3c 100644 --- a/healthchain/gateway/clients/fhir/sync/connection.py +++ b/healthchain/gateway/clients/fhir/sync/connection.py @@ -1,4 +1,4 @@ -import logging +from healthchain.utils.logger import get_logger import urllib.parse from typing import Dict @@ -7,7 +7,7 @@ from healthchain.gateway.fhir.errors import FHIRConnectionError -logger = logging.getLogger(__name__) +logger = get_logger(__name__) class FHIRConnectionManager: diff --git a/healthchain/gateway/events/dispatcher.py b/healthchain/gateway/events/dispatcher.py index 0a881383..98c3bb95 100644 --- a/healthchain/gateway/events/dispatcher.py +++ b/healthchain/gateway/events/dispatcher.py @@ -1,4 +1,4 @@ -import logging +from healthchain.utils.logger import get_logger import asyncio from enum import Enum from pydantic import BaseModel @@ -10,7 +10,7 @@ from fastapi_events.middleware import EventHandlerASGIMiddleware -logger = logging.getLogger(__name__) +logger = get_logger(__name__) class EHREventType(Enum): diff --git a/healthchain/gateway/fhir/aio.py b/healthchain/gateway/fhir/aio.py index 44c849bb..4acb6b61 100644 --- a/healthchain/gateway/fhir/aio.py +++ b/healthchain/gateway/fhir/aio.py @@ -1,4 +1,4 @@ -import logging +from healthchain.utils.logger import get_logger from contextlib import asynccontextmanager from typing import Any, Dict, Optional, Type @@ -15,7 +15,7 @@ from healthchain.fhir import add_provenance_metadata -logger = logging.getLogger(__name__) +logger = get_logger(__name__) class AsyncFHIRGateway(BaseFHIRGateway): diff --git a/healthchain/gateway/fhir/base.py b/healthchain/gateway/fhir/base.py index 07705c7a..5f3b4dd8 100644 --- a/healthchain/gateway/fhir/base.py +++ b/healthchain/gateway/fhir/base.py @@ -1,4 +1,4 @@ -import logging +from healthchain.utils.logger import get_logger import inspect import warnings @@ -14,7 +14,7 @@ from healthchain.gateway.base import BaseGateway -logger = logging.getLogger(__name__) +logger = get_logger(__name__) # Type variable for FHIR Resource diff --git a/healthchain/gateway/fhir/errors.py b/healthchain/gateway/fhir/errors.py index f1368bb3..ccd418ec 100644 --- a/healthchain/gateway/fhir/errors.py +++ b/healthchain/gateway/fhir/errors.py @@ -5,10 +5,10 @@ including status code mapping, error formatting, and exception types. """ -import logging +from healthchain.utils.logger import get_logger from typing import Optional -logger = logging.getLogger(__name__) +logger = get_logger(__name__) class FHIRConnectionError(Exception): diff --git a/healthchain/gateway/fhir/sync.py b/healthchain/gateway/fhir/sync.py index 6a2bf464..59517861 100644 --- a/healthchain/gateway/fhir/sync.py +++ b/healthchain/gateway/fhir/sync.py @@ -1,4 +1,4 @@ -import logging +from healthchain.utils.logger import get_logger from typing import Any, Dict, Type, Optional @@ -13,7 +13,7 @@ from healthchain.fhir import add_provenance_metadata -logger = logging.getLogger(__name__) +logger = get_logger(__name__) class FHIRGateway(BaseFHIRGateway): diff --git a/healthchain/gateway/soap/fastapiserver.py b/healthchain/gateway/soap/fastapiserver.py index c5ce5ebf..8b5cbc5d 100644 --- a/healthchain/gateway/soap/fastapiserver.py +++ b/healthchain/gateway/soap/fastapiserver.py @@ -5,10 +5,10 @@ from healthchain.models.requests.cdarequest import CdaRequest from healthchain.models.responses.cdaresponse import CdaResponse import base64 -import logging +from healthchain.utils.logger import get_logger from pathlib import Path -logger = logging.getLogger(__name__) +logger = get_logger(__name__) # SOAP namespace for envelope SOAP_NS = "http://schemas.xmlsoap.org/soap/envelope/" diff --git a/healthchain/gateway/soap/notereader.py b/healthchain/gateway/soap/notereader.py index ff10e4f4..36992be0 100644 --- a/healthchain/gateway/soap/notereader.py +++ b/healthchain/gateway/soap/notereader.py @@ -5,7 +5,7 @@ Epic's CDA document processing services. """ -import logging +from healthchain.utils.logger import get_logger from typing import Any, Callable, Dict, Optional, TypeVar, Union @@ -19,7 +19,7 @@ from healthchain.models.responses.cdaresponse import CdaResponse from healthchain.gateway.soap.fastapiserver import create_fastapi_soap_router -logger = logging.getLogger(__name__) +logger = get_logger(__name__) # Type variable for self-referencing return types diff --git a/healthchain/interop/__init__.py b/healthchain/interop/__init__.py index 7a7db681..f672591c 100644 --- a/healthchain/interop/__init__.py +++ b/healthchain/interop/__init__.py @@ -13,7 +13,7 @@ from .generators.cda import CDAGenerator from .generators.fhir import FHIRGenerator -import logging +from healthchain.utils.logger import get_logger from pathlib import Path from typing import Optional, Union @@ -106,7 +106,7 @@ def create_interop( Raises: ValueError: If config_dir doesn't exist or if validation_level/environment has invalid values """ - logger = logging.getLogger(__name__) + logger = get_logger(__name__) if config_dir is None: # Use bundled configs as default diff --git a/healthchain/interop/config_manager.py b/healthchain/interop/config_manager.py index 7080330f..51d7aa26 100644 --- a/healthchain/interop/config_manager.py +++ b/healthchain/interop/config_manager.py @@ -4,7 +4,7 @@ This module provides specialized configuration management for interoperability. """ -import logging +from healthchain.utils.logger import get_logger from pathlib import Path from typing import Dict, Optional, List, Type @@ -18,7 +18,7 @@ validate_cda_section_config_model, ) -log = logging.getLogger(__name__) +log = get_logger(__name__) class InteropConfigManager(ConfigManager): diff --git a/healthchain/interop/engine.py b/healthchain/interop/engine.py index db7258d5..be2e06a1 100644 --- a/healthchain/interop/engine.py +++ b/healthchain/interop/engine.py @@ -1,4 +1,4 @@ -import logging +from healthchain.utils.logger import get_logger from functools import cached_property from typing import List, Union, Optional, Any @@ -20,7 +20,7 @@ from healthchain.interop.generators.fhir import FHIRGenerator from healthchain.interop.filters import create_default_filters -log = logging.getLogger(__name__) +log = get_logger(__name__) def normalize_resource_list( diff --git a/healthchain/interop/generators/base.py b/healthchain/interop/generators/base.py index 0a8ae393..8f4ef2b4 100644 --- a/healthchain/interop/generators/base.py +++ b/healthchain/interop/generators/base.py @@ -4,7 +4,7 @@ This module provides the abstract base class for all generators. """ -import logging +from healthchain.utils.logger import get_logger import json from abc import ABC, abstractmethod from typing import Dict, Any, Optional @@ -15,7 +15,7 @@ from healthchain.interop.template_registry import TemplateRegistry from healthchain.interop.filters import clean_empty -log = logging.getLogger(__name__) +log = get_logger(__name__) class BaseGenerator(ABC): diff --git a/healthchain/interop/generators/cda.py b/healthchain/interop/generators/cda.py index 7551ed1a..3b260fde 100644 --- a/healthchain/interop/generators/cda.py +++ b/healthchain/interop/generators/cda.py @@ -4,7 +4,7 @@ This module provides functionality for generating CDA documents. """ -import logging +from healthchain.utils.logger import get_logger import re import xmltodict import uuid @@ -15,7 +15,7 @@ from healthchain.interop.models.cda import ClinicalDocument from healthchain.interop.generators.base import BaseGenerator -log = logging.getLogger(__name__) +log = get_logger(__name__) def _find_section_key_for_resource_type( diff --git a/healthchain/interop/generators/fhir.py b/healthchain/interop/generators/fhir.py index 40e9926f..b17f9616 100644 --- a/healthchain/interop/generators/fhir.py +++ b/healthchain/interop/generators/fhir.py @@ -5,7 +5,7 @@ """ import uuid -import logging +from healthchain.utils.logger import get_logger from typing import Dict, List, Optional, Type, Any from fhir.resources.resource import Resource @@ -16,7 +16,7 @@ from healthchain.interop.types import FormatType -log = logging.getLogger(__name__) +log = get_logger(__name__) class FHIRGenerator(BaseGenerator): diff --git a/healthchain/interop/parsers/cda.py b/healthchain/interop/parsers/cda.py index 57240dad..59a4ccf1 100644 --- a/healthchain/interop/parsers/cda.py +++ b/healthchain/interop/parsers/cda.py @@ -5,7 +5,7 @@ """ import xmltodict -import logging +from healthchain.utils.logger import get_logger from typing import Dict, List from healthchain.interop.models.cda import ClinicalDocument @@ -13,7 +13,7 @@ from healthchain.interop.config_manager import InteropConfigManager from healthchain.interop.parsers.base import BaseParser -log = logging.getLogger(__name__) +log = get_logger(__name__) class CDAParser(BaseParser): diff --git a/healthchain/interop/template_registry.py b/healthchain/interop/template_registry.py index cda58802..9d211355 100644 --- a/healthchain/interop/template_registry.py +++ b/healthchain/interop/template_registry.py @@ -1,10 +1,10 @@ -import logging +from healthchain.utils.logger import get_logger from pathlib import Path from typing import Dict, Callable from liquid import Environment, FileSystemLoader, Template -log = logging.getLogger(__name__) +log = get_logger(__name__) class TemplateRegistry: diff --git a/healthchain/io/adapters/cdaadapter.py b/healthchain/io/adapters/cdaadapter.py index 91d5d456..ef4643c6 100644 --- a/healthchain/io/adapters/cdaadapter.py +++ b/healthchain/io/adapters/cdaadapter.py @@ -1,4 +1,4 @@ -import logging +from healthchain.utils.logger import get_logger from typing import Optional from healthchain.io.containers import Document @@ -17,7 +17,7 @@ from fhir.resources.allergyintolerance import AllergyIntolerance from fhir.resources.documentreference import DocumentReference -log = logging.getLogger(__name__) +log = get_logger(__name__) class CdaAdapter(BaseAdapter[CdaRequest, CdaResponse]): diff --git a/healthchain/io/adapters/cdsfhiradapter.py b/healthchain/io/adapters/cdsfhiradapter.py index 42d36572..31cedd1c 100644 --- a/healthchain/io/adapters/cdsfhiradapter.py +++ b/healthchain/io/adapters/cdsfhiradapter.py @@ -1,4 +1,4 @@ -import logging +from healthchain.utils.logger import get_logger from typing import Optional, Any from fhir.resources.documentreference import DocumentReference @@ -9,7 +9,7 @@ from healthchain.models.responses.cdsresponse import CDSResponse from healthchain.fhir import read_content_attachment, convert_prefetch_to_fhir_objects -log = logging.getLogger(__name__) +log = get_logger(__name__) class CdsFhirAdapter(BaseAdapter[CDSRequest, CDSResponse]): diff --git a/healthchain/io/containers/document.py b/healthchain/io/containers/document.py index 81f09d2b..f5a402b2 100644 --- a/healthchain/io/containers/document.py +++ b/healthchain/io/containers/document.py @@ -1,4 +1,4 @@ -import logging +from healthchain.utils.logger import get_logger from dataclasses import dataclass, field from typing import Any, Dict, Iterator, List, Optional, Union @@ -32,7 +32,7 @@ set_condition_category, ) -logger = logging.getLogger(__name__) +logger = get_logger(__name__) @dataclass diff --git a/healthchain/models/requests/cdarequest.py b/healthchain/models/requests/cdarequest.py index 131d1fbd..d3b248ad 100644 --- a/healthchain/models/requests/cdarequest.py +++ b/healthchain/models/requests/cdarequest.py @@ -1,13 +1,13 @@ import base64 import xmltodict -import logging +from healthchain.utils.logger import get_logger from pydantic import BaseModel from typing import Dict, Optional from healthchain.utils.utils import search_key -log = logging.getLogger(__name__) +log = get_logger(__name__) class CdaRequest(BaseModel): diff --git a/healthchain/models/responses/cdaresponse.py b/healthchain/models/responses/cdaresponse.py index 3f6b677a..60bc352f 100644 --- a/healthchain/models/responses/cdaresponse.py +++ b/healthchain/models/responses/cdaresponse.py @@ -1,6 +1,6 @@ import base64 import xmltodict -import logging +from healthchain.utils.logger import get_logger from pydantic import BaseModel from typing import Optional, Dict @@ -8,7 +8,7 @@ from healthchain.utils.utils import search_key -log = logging.getLogger(__name__) +log = get_logger(__name__) class CdaResponse(BaseModel): diff --git a/healthchain/pipeline/base.py b/healthchain/pipeline/base.py index 24ed9b06..1837c7ac 100644 --- a/healthchain/pipeline/base.py +++ b/healthchain/pipeline/base.py @@ -1,4 +1,4 @@ -import logging +from healthchain.utils.logger import get_logger from abc import ABC, abstractmethod from inspect import signature from pathlib import Path @@ -22,7 +22,7 @@ from healthchain.io.containers import DataContainer from healthchain.pipeline.components.base import BaseComponent -logger = logging.getLogger(__name__) +logger = get_logger(__name__) T = TypeVar("T") diff --git a/healthchain/pipeline/components/cdscardcreator.py b/healthchain/pipeline/components/cdscardcreator.py index 64f33e05..0baebcfb 100644 --- a/healthchain/pipeline/components/cdscardcreator.py +++ b/healthchain/pipeline/components/cdscardcreator.py @@ -1,4 +1,4 @@ -import logging +from healthchain.utils.logger import get_logger import json from typing import Optional, Dict, Any, Union from jinja2 import Template @@ -9,7 +9,7 @@ from healthchain.models.responses.cdsresponse import Card, Source, IndicatorEnum -logger = logging.getLogger(__name__) +logger = get_logger(__name__) class CdsCardCreator(BaseComponent[str]): diff --git a/healthchain/pipeline/components/fhirproblemextractor.py b/healthchain/pipeline/components/fhirproblemextractor.py index f9892a8a..5b4ade3c 100644 --- a/healthchain/pipeline/components/fhirproblemextractor.py +++ b/healthchain/pipeline/components/fhirproblemextractor.py @@ -1,11 +1,11 @@ """Component for extracting FHIR problem lists from NLP annotations.""" -import logging +from healthchain.utils.logger import get_logger from healthchain.pipeline.components.base import BaseComponent from healthchain.io.containers import Document -logger = logging.getLogger(__name__) +logger = get_logger(__name__) class FHIRProblemListExtractor(BaseComponent[Document]): diff --git a/healthchain/pipeline/components/integrations.py b/healthchain/pipeline/components/integrations.py index 9e0c8c3d..8793caac 100644 --- a/healthchain/pipeline/components/integrations.py +++ b/healthchain/pipeline/components/integrations.py @@ -1,4 +1,4 @@ -import logging +from healthchain.utils.logger import get_logger from typing import Any, Callable, TypeVar from spacy.language import Language from functools import wraps @@ -10,7 +10,7 @@ T = TypeVar("T") -log = logging.getLogger(__name__) +log = get_logger(__name__) def requires_package(package_name: str, import_path: str) -> Callable: diff --git a/healthchain/pipeline/mixins.py b/healthchain/pipeline/mixins.py index a2d8e636..73dfb202 100644 --- a/healthchain/pipeline/mixins.py +++ b/healthchain/pipeline/mixins.py @@ -1,10 +1,10 @@ -import logging +from healthchain.utils.logger import get_logger from healthchain.pipeline.base import ModelConfig from healthchain.pipeline.modelrouter import ModelRouter -logger = logging.getLogger(__name__) +logger = get_logger(__name__) class ModelRoutingMixin: diff --git a/healthchain/sandbox/datasets.py b/healthchain/sandbox/datasets.py index d86842f2..d9d63e39 100644 --- a/healthchain/sandbox/datasets.py +++ b/healthchain/sandbox/datasets.py @@ -4,14 +4,14 @@ Provides a centralized registry for loading test datasets like MIMIC-on-FHIR and Synthea. """ -import logging +from healthchain.utils.logger import get_logger from typing import Any, Dict, List from healthchain.sandbox.base import DatasetLoader -log = logging.getLogger(__name__) +log = get_logger(__name__) class DatasetRegistry: diff --git a/healthchain/sandbox/generators/cdsdatagenerator.py b/healthchain/sandbox/generators/cdsdatagenerator.py index 2837c315..a9446ea0 100644 --- a/healthchain/sandbox/generators/cdsdatagenerator.py +++ b/healthchain/sandbox/generators/cdsdatagenerator.py @@ -1,6 +1,6 @@ import random import csv -import logging +from healthchain.utils.logger import get_logger from typing import Callable, Dict, Optional, List from pathlib import Path @@ -12,7 +12,7 @@ from healthchain.sandbox.workflows import Workflow -logger = logging.getLogger(__name__) +logger = get_logger(__name__) # TODO: generate test context - move from hook models diff --git a/healthchain/sandbox/loaders/mimic.py b/healthchain/sandbox/loaders/mimic.py index 57e03219..ff75fbef 100644 --- a/healthchain/sandbox/loaders/mimic.py +++ b/healthchain/sandbox/loaders/mimic.py @@ -4,7 +4,7 @@ Loads patient data from the MIMIC-IV-on-FHIR dataset for testing and demos. """ -import logging +from healthchain.utils.logger import get_logger import random from pathlib import Path from typing import Any, Dict, List, Optional, Union @@ -13,7 +13,7 @@ from healthchain.sandbox.datasets import DatasetLoader -log = logging.getLogger(__name__) +log = get_logger(__name__) class MimicOnFHIRLoader(DatasetLoader): diff --git a/healthchain/sandbox/loaders/synthea.py b/healthchain/sandbox/loaders/synthea.py index aded1aa9..f5a76ac1 100644 --- a/healthchain/sandbox/loaders/synthea.py +++ b/healthchain/sandbox/loaders/synthea.py @@ -5,7 +5,7 @@ """ import json -import logging +from healthchain.utils.logger import get_logger import random from pathlib import Path from typing import Dict, List, Optional @@ -14,7 +14,7 @@ from healthchain.sandbox.datasets import DatasetLoader -log = logging.getLogger(__name__) +log = get_logger(__name__) class SyntheaFHIRPatientLoader(DatasetLoader): diff --git a/healthchain/sandbox/requestconstructors.py b/healthchain/sandbox/requestconstructors.py index 5b0222f6..1fcfd31b 100644 --- a/healthchain/sandbox/requestconstructors.py +++ b/healthchain/sandbox/requestconstructors.py @@ -5,7 +5,7 @@ - ClinDocRequestConstructor: Builds NoteReader requests (SOAP/XML) """ -import logging +from healthchain.utils.logger import get_logger import base64 import pkgutil import xmltodict @@ -29,7 +29,7 @@ ) -log = logging.getLogger(__name__) +log = get_logger(__name__) class CdsRequestConstructor(BaseRequestConstructor): diff --git a/healthchain/sandbox/sandboxclient.py b/healthchain/sandbox/sandboxclient.py index 6919e8b3..91aee5c1 100644 --- a/healthchain/sandbox/sandboxclient.py +++ b/healthchain/sandbox/sandboxclient.py @@ -5,7 +5,7 @@ """ import json -import logging +from healthchain.utils.logger import get_logger import uuid import httpx @@ -23,7 +23,7 @@ ) -log = logging.getLogger(__name__) +log = get_logger(__name__) class SandboxClient: diff --git a/healthchain/sandbox/utils.py b/healthchain/sandbox/utils.py index 87fee88b..84fbecbf 100644 --- a/healthchain/sandbox/utils.py +++ b/healthchain/sandbox/utils.py @@ -1,11 +1,11 @@ import json -import logging +from healthchain.utils.logger import get_logger from pathlib import Path from datetime import datetime -log = logging.getLogger(__name__) +log = get_logger(__name__) def find_attributes_of_type(instance, target_type): diff --git a/healthchain/utils/__init__.py b/healthchain/utils/__init__.py index a4bcd2e2..f86a96d0 100644 --- a/healthchain/utils/__init__.py +++ b/healthchain/utils/__init__.py @@ -1,3 +1,4 @@ from .idgenerator import IdGenerator +from .logger import get_logger -__all__ = ["IdGenerator"] +__all__ = ["IdGenerator", "get_logger"] diff --git a/healthchain/utils/logger.py b/healthchain/utils/logger.py index 999ac9d5..f80b5857 100644 --- a/healthchain/utils/logger.py +++ b/healthchain/utils/logger.py @@ -1,11 +1,18 @@ import logging +import os + from colorama import init, Fore, Back init(autoreset=True) +_LOG_FORMAT = "%(levelname)s: %(asctime)-10s [%(name)s]: %(message)s" +_DEFAULT_LEVEL = "INFO" +_ENV_VAR_LOG_LEVEL = "HEALTHCHAIN_LOG_LEVEL" + +_configured = False + class ColorFormatter(logging.Formatter): - # Change this dictionary to suit your coloring needs! COLORS = { "WARNING": Fore.YELLOW, "ERROR": Fore.RED, @@ -24,13 +31,58 @@ def format(self, record): return logging.Formatter.format(self, record) -def add_handlers(log): +def _get_log_level() -> int: + """Get the log level from environment variable or default.""" + level_name = os.environ.get(_ENV_VAR_LOG_LEVEL, _DEFAULT_LEVEL).upper() + return getattr(logging, level_name, logging.INFO) + + +def _configure_root_healthchain_logger() -> None: + """Configure the root 'healthchain' logger once. + + Sets up a single console handler with color formatting on the + top-level 'healthchain' logger. Child loggers obtained via + ``get_logger`` inherit this handler through standard library + propagation, so no per-module handler setup is needed. + """ + global _configured + if _configured: + return + + root_logger = logging.getLogger("healthchain") + if not root_logger.handlers: + formatter = ColorFormatter(_LOG_FORMAT) + handler = logging.StreamHandler() + handler.setFormatter(formatter) + root_logger.addHandler(handler) + + root_logger.setLevel(_get_log_level()) + _configured = True + + +def get_logger(name: str) -> logging.Logger: + """Return a logger under the ``healthchain`` namespace. + + All loggers share the handler and level configured on the root + ``healthchain`` logger. The level can be controlled at runtime + via the ``HEALTHCHAIN_LOG_LEVEL`` environment variable + (DEBUG, INFO, WARNING, ERROR, CRITICAL). + + Args: + name: Typically ``__name__`` of the calling module. + + Returns: + A :class:`logging.Logger` instance. + """ + _configure_root_healthchain_logger() + return logging.getLogger(name) + + +# Keep backward compatibility with existing code that imports add_handlers +def add_handlers(log: logging.Logger) -> logging.Logger: if len(log.handlers) == 0: - formatter = ColorFormatter( - "%(levelname)s: %(asctime)-10s [%(name)s]: %(message)s" - ) + formatter = ColorFormatter(_LOG_FORMAT) ch = logging.StreamHandler() ch.setFormatter(formatter) log.addHandler(ch) - return log