Skip to content

Commit c98f016

Browse files
committed
add create_logger_provider/configure_logger_provider for declarative config
Implements LoggerProvider instantiation from declarative config files, following the same env-var-suppression pattern as create_meter_provider. BatchLogRecordProcessor defaults use spec values (1000ms schedule_delay), overriding the Python SDK's incorrect 5000ms env-var default. Assisted-by: Claude Sonnet 4.6
1 parent 7f51034 commit c98f016

File tree

3 files changed

+673
-0
lines changed

3 files changed

+673
-0
lines changed
Lines changed: 299 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,299 @@
1+
# Copyright The OpenTelemetry Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from __future__ import annotations
16+
17+
import logging
18+
from typing import Dict, Optional
19+
20+
from opentelemetry._logs import set_logger_provider
21+
from opentelemetry.sdk._configuration._exceptions import ConfigurationError
22+
from opentelemetry.sdk._configuration.models import (
23+
BatchLogRecordProcessor as BatchLogRecordProcessorConfig,
24+
)
25+
from opentelemetry.sdk._configuration.models import (
26+
LoggerProvider as LoggerProviderConfig,
27+
)
28+
from opentelemetry.sdk._configuration.models import (
29+
LogRecordExporter as LogRecordExporterConfig,
30+
)
31+
from opentelemetry.sdk._configuration.models import (
32+
LogRecordProcessor as LogRecordProcessorConfig,
33+
)
34+
from opentelemetry.sdk._configuration.models import (
35+
OtlpGrpcExporter as OtlpGrpcExporterConfig,
36+
)
37+
from opentelemetry.sdk._configuration.models import (
38+
OtlpHttpExporter as OtlpHttpExporterConfig,
39+
)
40+
from opentelemetry.sdk._configuration.models import (
41+
SimpleLogRecordProcessor as SimpleLogRecordProcessorConfig,
42+
)
43+
from opentelemetry.sdk._logs import LoggerProvider
44+
from opentelemetry.sdk._logs._internal.export import (
45+
BatchLogRecordProcessor,
46+
ConsoleLogRecordExporter,
47+
LogRecordExporter,
48+
SimpleLogRecordProcessor,
49+
)
50+
from opentelemetry.sdk.resources import Resource
51+
52+
_logger = logging.getLogger(__name__)
53+
54+
# BatchLogRecordProcessor defaults per OTel spec (milliseconds).
55+
# Note: The Python SDK reads OTEL_BLRP_SCHEDULE_DELAY with an incorrect default
56+
# of 5000ms. The spec mandates 1000ms. We pass 1000 explicitly here to match
57+
# the spec and suppress env var reading.
58+
_DEFAULT_SCHEDULE_DELAY_MILLIS = 1000
59+
_DEFAULT_EXPORT_TIMEOUT_MILLIS = 30000
60+
_DEFAULT_MAX_QUEUE_SIZE = 2048
61+
_DEFAULT_MAX_EXPORT_BATCH_SIZE = 512
62+
63+
64+
def _parse_headers(
65+
headers: Optional[list],
66+
headers_list: Optional[str],
67+
) -> Optional[Dict[str, str]]:
68+
"""Merge headers struct and headers_list into a dict.
69+
70+
Returns None if neither is set, letting the exporter read env vars.
71+
headers struct takes priority over headers_list for the same key.
72+
"""
73+
if headers is None and headers_list is None:
74+
return None
75+
result: Dict[str, str] = {}
76+
if headers_list:
77+
for item in headers_list.split(","):
78+
item = item.strip()
79+
if "=" in item:
80+
key, value = item.split("=", 1)
81+
result[key.strip()] = value.strip()
82+
elif item:
83+
_logger.warning(
84+
"Invalid header pair in headers_list (missing '='): %s",
85+
item,
86+
)
87+
if headers:
88+
for pair in headers:
89+
result[pair.name] = pair.value or ""
90+
return result
91+
92+
93+
def _map_compression(
94+
value: Optional[str], compression_enum: type
95+
) -> Optional[object]:
96+
"""Map a compression string to the given Compression enum value."""
97+
if value is None or value.lower() == "none":
98+
return None
99+
if value.lower() == "gzip":
100+
return compression_enum.Gzip # type: ignore[attr-defined]
101+
raise ConfigurationError(
102+
f"Unsupported compression value '{value}'. Supported values: 'gzip', 'none'."
103+
)
104+
105+
106+
def _create_console_log_exporter() -> ConsoleLogRecordExporter:
107+
"""Create a ConsoleLogRecordExporter."""
108+
return ConsoleLogRecordExporter()
109+
110+
111+
def _create_otlp_http_log_exporter(
112+
config: OtlpHttpExporterConfig,
113+
) -> LogRecordExporter:
114+
"""Create an OTLP HTTP log exporter from config."""
115+
try:
116+
# pylint: disable=import-outside-toplevel,no-name-in-module
117+
from opentelemetry.exporter.otlp.proto.http import ( # type: ignore[import-untyped] # noqa: PLC0415
118+
Compression,
119+
)
120+
from opentelemetry.exporter.otlp.proto.http._log_exporter import ( # type: ignore[import-untyped] # noqa: PLC0415
121+
OTLPLogExporter,
122+
)
123+
except ImportError as exc:
124+
raise ConfigurationError(
125+
"otlp_http log exporter requires 'opentelemetry-exporter-otlp-proto-http'. "
126+
"Install it with: pip install opentelemetry-exporter-otlp-proto-http"
127+
) from exc
128+
129+
compression = _map_compression(config.compression, Compression)
130+
headers = _parse_headers(config.headers, config.headers_list)
131+
timeout = (config.timeout / 1000.0) if config.timeout is not None else None
132+
133+
return OTLPLogExporter( # type: ignore[return-value]
134+
endpoint=config.endpoint,
135+
headers=headers,
136+
timeout=timeout,
137+
compression=compression, # type: ignore[arg-type]
138+
)
139+
140+
141+
def _create_otlp_grpc_log_exporter(
142+
config: OtlpGrpcExporterConfig,
143+
) -> LogRecordExporter:
144+
"""Create an OTLP gRPC log exporter from config."""
145+
try:
146+
# pylint: disable=import-outside-toplevel,no-name-in-module
147+
import grpc # type: ignore[import-untyped] # noqa: PLC0415
148+
149+
from opentelemetry.exporter.otlp.proto.grpc._log_exporter import ( # type: ignore[import-untyped] # noqa: PLC0415
150+
OTLPLogExporter,
151+
)
152+
except ImportError as exc:
153+
raise ConfigurationError(
154+
"otlp_grpc log exporter requires 'opentelemetry-exporter-otlp-proto-grpc'. "
155+
"Install it with: pip install opentelemetry-exporter-otlp-proto-grpc"
156+
) from exc
157+
158+
compression = _map_compression(config.compression, grpc.Compression)
159+
headers = _parse_headers(config.headers, config.headers_list)
160+
timeout = (config.timeout / 1000.0) if config.timeout is not None else None
161+
162+
return OTLPLogExporter( # type: ignore[return-value]
163+
endpoint=config.endpoint,
164+
headers=headers,
165+
timeout=timeout,
166+
compression=compression, # type: ignore[arg-type]
167+
)
168+
169+
170+
def _create_log_record_exporter(
171+
config: LogRecordExporterConfig,
172+
) -> LogRecordExporter:
173+
"""Create a log record exporter from config."""
174+
if config.console is not None:
175+
return _create_console_log_exporter()
176+
if config.otlp_http is not None:
177+
return _create_otlp_http_log_exporter(config.otlp_http)
178+
if config.otlp_grpc is not None:
179+
return _create_otlp_grpc_log_exporter(config.otlp_grpc)
180+
if config.otlp_file_development is not None:
181+
raise ConfigurationError(
182+
"otlp_file_development log exporter is experimental and not yet supported."
183+
)
184+
raise ConfigurationError(
185+
"No exporter type specified in log record exporter config. "
186+
"Supported types: console, otlp_http, otlp_grpc."
187+
)
188+
189+
190+
def _create_batch_log_record_processor(
191+
config: BatchLogRecordProcessorConfig,
192+
) -> BatchLogRecordProcessor:
193+
"""Create a BatchLogRecordProcessor from config.
194+
195+
Passes explicit defaults to suppress OTEL_BLRP_* env var reading.
196+
"""
197+
exporter = _create_log_record_exporter(config.exporter)
198+
schedule_delay = (
199+
config.schedule_delay
200+
if config.schedule_delay is not None
201+
else _DEFAULT_SCHEDULE_DELAY_MILLIS
202+
)
203+
export_timeout = (
204+
config.export_timeout
205+
if config.export_timeout is not None
206+
else _DEFAULT_EXPORT_TIMEOUT_MILLIS
207+
)
208+
max_queue_size = (
209+
config.max_queue_size
210+
if config.max_queue_size is not None
211+
else _DEFAULT_MAX_QUEUE_SIZE
212+
)
213+
max_export_batch_size = (
214+
config.max_export_batch_size
215+
if config.max_export_batch_size is not None
216+
else _DEFAULT_MAX_EXPORT_BATCH_SIZE
217+
)
218+
return BatchLogRecordProcessor(
219+
exporter=exporter,
220+
schedule_delay_millis=float(schedule_delay),
221+
export_timeout_millis=float(export_timeout),
222+
max_queue_size=max_queue_size,
223+
max_export_batch_size=max_export_batch_size,
224+
)
225+
226+
227+
def _create_simple_log_record_processor(
228+
config: SimpleLogRecordProcessorConfig,
229+
) -> SimpleLogRecordProcessor:
230+
"""Create a SimpleLogRecordProcessor from config."""
231+
exporter = _create_log_record_exporter(config.exporter)
232+
return SimpleLogRecordProcessor(exporter)
233+
234+
235+
def _create_log_record_processor(
236+
config: LogRecordProcessorConfig,
237+
) -> BatchLogRecordProcessor | SimpleLogRecordProcessor:
238+
"""Create a log record processor from config."""
239+
if config.batch is not None:
240+
return _create_batch_log_record_processor(config.batch)
241+
if config.simple is not None:
242+
return _create_simple_log_record_processor(config.simple)
243+
raise ConfigurationError(
244+
"No processor type specified in log record processor config. "
245+
"Supported types: batch, simple."
246+
)
247+
248+
249+
def create_logger_provider(
250+
config: Optional[LoggerProviderConfig],
251+
resource: Optional[Resource] = None,
252+
) -> LoggerProvider:
253+
"""Create an SDK LoggerProvider from declarative config.
254+
255+
Does NOT read OTEL_BLRP_* or other env vars for values explicitly
256+
controlled by the config. Absent config values use OTel spec defaults.
257+
258+
Args:
259+
config: LoggerProvider config from the parsed config file, or None.
260+
resource: Resource to attach to the provider.
261+
262+
Returns:
263+
A configured LoggerProvider.
264+
"""
265+
provider = LoggerProvider(resource=resource)
266+
267+
if config is None:
268+
return provider
269+
270+
if config.limits is not None:
271+
_logger.warning(
272+
"log_record_limits are specified in config but are not supported "
273+
"by the Python SDK LoggerProvider constructor; limits will be ignored."
274+
)
275+
276+
for processor_config in config.processors:
277+
provider.add_log_record_processor(
278+
_create_log_record_processor(processor_config)
279+
)
280+
281+
return provider
282+
283+
284+
def configure_logger_provider(
285+
config: Optional[LoggerProviderConfig],
286+
resource: Optional[Resource] = None,
287+
) -> None:
288+
"""Configure the global LoggerProvider from declarative config.
289+
290+
When config is None (logger_provider section absent from config file),
291+
the global is not set — matching Java/JS SDK behavior.
292+
293+
Args:
294+
config: LoggerProvider config from the parsed config file, or None.
295+
resource: Resource to attach to the provider.
296+
"""
297+
if config is None:
298+
return
299+
set_logger_provider(create_logger_provider(config, resource))

opentelemetry-sdk/src/opentelemetry/sdk/_configuration/file/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@
2424
'1.0'
2525
"""
2626

27+
from opentelemetry.sdk._configuration._logger_provider import (
28+
configure_logger_provider,
29+
create_logger_provider,
30+
)
2731
from opentelemetry.sdk._configuration._propagator import (
2832
configure_propagator,
2933
create_propagator,
@@ -46,4 +50,6 @@
4650
"create_resource",
4751
"create_propagator",
4852
"configure_propagator",
53+
"create_logger_provider",
54+
"configure_logger_provider",
4955
]

0 commit comments

Comments
 (0)