|
1 | | -"""Internal module for setting up OpenTelemetry meter provider.""" |
| 1 | +"""Internal module for setting up OpenTelemetry meter and logger providers.""" |
2 | 2 |
|
3 | 3 | import logging |
4 | 4 | import os |
5 | | -from typing import Optional |
| 5 | +from typing import Optional, cast |
6 | 6 |
|
7 | 7 | from opentelemetry import metrics |
| 8 | +from opentelemetry._logs import get_logger_provider, set_logger_provider |
| 9 | +from opentelemetry.exporter.otlp.proto.grpc._log_exporter import ( |
| 10 | + OTLPLogExporter as GRPCLogExporter, |
| 11 | +) |
8 | 12 | from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import ( |
9 | 13 | OTLPMetricExporter as GRPCMetricExporter, |
10 | 14 | ) |
| 15 | +from opentelemetry.exporter.otlp.proto.http._log_exporter import ( |
| 16 | + OTLPLogExporter as HTTPLogExporter, |
| 17 | +) |
11 | 18 | from opentelemetry.exporter.otlp.proto.http.metric_exporter import ( |
12 | 19 | OTLPMetricExporter as HTTPMetricExporter, |
13 | 20 | ) |
| 21 | +from opentelemetry.instrumentation.logging.handler import LoggingHandler |
| 22 | +from opentelemetry.sdk._logs import LoggerProvider |
| 23 | +from opentelemetry.sdk._logs.export import BatchLogRecordProcessor |
14 | 24 | from opentelemetry.sdk.metrics import ( |
15 | 25 | MeterProvider, |
16 | 26 | Counter, |
|
36 | 46 |
|
37 | 47 | logger = logging.getLogger(__name__) |
38 | 48 |
|
| 49 | + |
| 50 | +def _merge_sdk_resource_into_log_provider( |
| 51 | + provider: LoggerProvider, sdk_resource: Resource |
| 52 | +) -> None: |
| 53 | + """OTel SDK has no public API to swap a LoggerProvider's Resource after construction.""" |
| 54 | + provider._resource = provider.resource.merge(sdk_resource) |
| 55 | + with provider._active_loggers_lock: |
| 56 | + for logger_instance in provider._active_loggers: |
| 57 | + logger_instance._resource = provider._resource |
| 58 | + logger.info( |
| 59 | + "Merged sap-cloud-sdk resource attrs onto wrapper-installed LoggerProvider" |
| 60 | + ) |
| 61 | + |
| 62 | + |
| 63 | +def _root_logger_has_otel_handler() -> bool: |
| 64 | + # sitecustomize.py installs sdk._logs.LoggingHandler, not the instrumentation-layer one — check both. |
| 65 | + try: |
| 66 | + from opentelemetry.sdk._logs import LoggingHandler as _SDKLoggingHandler |
| 67 | + |
| 68 | + handler_types: tuple[type, ...] = (LoggingHandler, _SDKLoggingHandler) |
| 69 | + except ImportError: |
| 70 | + handler_types = (LoggingHandler,) |
| 71 | + return any(isinstance(h, handler_types) for h in logging.getLogger().handlers) |
| 72 | + |
| 73 | + |
39 | 74 | # Global meter provider |
40 | 75 | _meter_provider: Optional[MeterProvider] = None |
41 | 76 | _meter: Optional[metrics.Meter] = None |
42 | 77 |
|
| 78 | +# Global logger provider |
| 79 | +_log_provider: Optional[LoggerProvider] = None |
| 80 | + |
43 | 81 |
|
44 | 82 | def get_meter() -> metrics.Meter: |
45 | 83 | """Get or create the global meter instance. |
@@ -76,6 +114,70 @@ def shutdown() -> None: |
76 | 114 | _meter_provider = None |
77 | 115 |
|
78 | 116 |
|
| 117 | +def setup_log_provider() -> Optional[LoggerProvider]: |
| 118 | + """Set up the global OTel LoggerProvider using the shared resource attributes. |
| 119 | +
|
| 120 | + Installs a LoggingHandler on the root stdlib logger so all existing |
| 121 | + logging.getLogger(...) calls in the app flow through OTel automatically. |
| 122 | + No-op when telemetry is disabled. |
| 123 | + """ |
| 124 | + global _log_provider |
| 125 | + |
| 126 | + if _log_provider is not None: |
| 127 | + return _log_provider |
| 128 | + |
| 129 | + config = get_config() |
| 130 | + if not config.enabled: |
| 131 | + return None |
| 132 | + |
| 133 | + try: |
| 134 | + resource = Resource.create(create_resource_attributes_from_env()) |
| 135 | + existing = cast(LoggerProvider, get_logger_provider()) |
| 136 | + |
| 137 | + if isinstance(existing, LoggerProvider): |
| 138 | + # Never add a second BatchLogRecordProcessor — the platform's provider |
| 139 | + # already has one, and a second processor doubles every exported log record. |
| 140 | + logger.warning( |
| 141 | + "Global LoggerProvider was already set by another library. " |
| 142 | + "Merging sap.cloud_sdk.* resource attributes into the existing provider." |
| 143 | + ) |
| 144 | + _merge_sdk_resource_into_log_provider(existing, resource) |
| 145 | + if not _root_logger_has_otel_handler(): |
| 146 | + logging.getLogger().addHandler(LoggingHandler(logger_provider=existing)) |
| 147 | + _log_provider = existing |
| 148 | + else: |
| 149 | + provider = LoggerProvider(resource=resource) |
| 150 | + provider.add_log_record_processor( |
| 151 | + BatchLogRecordProcessor(_create_log_exporter()) |
| 152 | + ) |
| 153 | + set_logger_provider(provider) |
| 154 | + logging.getLogger().addHandler(LoggingHandler(logger_provider=provider)) |
| 155 | + _log_provider = provider |
| 156 | + |
| 157 | + logger.info( |
| 158 | + f"OpenTelemetry log provider initialized. " |
| 159 | + f"Service: {config.service_name}, " |
| 160 | + f"Endpoint: {config.otlp_endpoint}" |
| 161 | + ) |
| 162 | + return _log_provider |
| 163 | + |
| 164 | + except Exception as e: |
| 165 | + logger.error(f"Failed to initialize OpenTelemetry log provider: {e}") |
| 166 | + return None |
| 167 | + |
| 168 | + |
| 169 | +def _create_log_exporter(): |
| 170 | + protocol = os.getenv(ENV_OTLP_PROTOCOL, "grpc").lower() |
| 171 | + exporter_classes = {"grpc": GRPCLogExporter, "http/protobuf": HTTPLogExporter} |
| 172 | + |
| 173 | + if protocol not in exporter_classes: |
| 174 | + raise ValueError( |
| 175 | + f"Unsupported OTEL_EXPORTER_OTLP_PROTOCOL: '{protocol}'. " |
| 176 | + "Supported values are 'grpc' and 'http/protobuf'." |
| 177 | + ) |
| 178 | + return exporter_classes[protocol]() |
| 179 | + |
| 180 | + |
79 | 181 | def _create_metric_exporter(): |
80 | 182 | protocol = os.getenv(ENV_OTLP_PROTOCOL, "grpc").lower() |
81 | 183 | exporter_classes = {"grpc": GRPCMetricExporter, "http/protobuf": HTTPMetricExporter} |
|
0 commit comments