[feat] add otel adapters
This commit is contained in:
parent
1588efb9ca
commit
96af9c08f5
10 changed files with 321 additions and 8 deletions
6
Makefile
6
Makefile
|
|
@ -3,7 +3,9 @@
|
|||
COMPOSE ?= docker-compose
|
||||
APP_API_TOKEN ?= local-api-token
|
||||
APP_SIGNING_KEY ?= local-signing-key
|
||||
APP_OTEL_EXPORTER_ENDPOINT ?= http://localhost:4318
|
||||
APP_OTEL_LOGS_ENDPOINT ?= http://localhost:4318/v1/logs
|
||||
APP_OTEL_METRICS_ENDPOINT ?= http://localhost:4318/v1/metrics
|
||||
APP_OTEL_TRACES_ENDPOINT ?= http://localhost:4318/v1/traces
|
||||
OTEL_METRIC_EXPORT_INTERVAL ?= 1000
|
||||
|
||||
help:
|
||||
|
|
@ -38,7 +40,7 @@ run:
|
|||
APP_API_TOKEN="$(APP_API_TOKEN)" APP_SIGNING_KEY="$(APP_SIGNING_KEY)" uv run main.py
|
||||
|
||||
run-otel:
|
||||
APP_API_TOKEN="$(APP_API_TOKEN)" APP_SIGNING_KEY="$(APP_SIGNING_KEY)" APP_LOGGING_LEVEL=WARNING APP_OTEL_EXPORTER_ENDPOINT="$(APP_OTEL_EXPORTER_ENDPOINT)" OTEL_METRIC_EXPORT_INTERVAL="$(OTEL_METRIC_EXPORT_INTERVAL)" uv run main.py
|
||||
APP_API_TOKEN="$(APP_API_TOKEN)" APP_SIGNING_KEY="$(APP_SIGNING_KEY)" APP_LOGGING_LEVEL=WARNING APP_OTEL_LOGS_ENDPOINT="$(APP_OTEL_LOGS_ENDPOINT)" APP_OTEL_METRICS_ENDPOINT="$(APP_OTEL_METRICS_ENDPOINT)" APP_OTEL_TRACES_ENDPOINT="$(APP_OTEL_TRACES_ENDPOINT)" OTEL_METRIC_EXPORT_INTERVAL="$(OTEL_METRIC_EXPORT_INTERVAL)" uv run main.py
|
||||
|
||||
compose-build:
|
||||
$(COMPOSE) build
|
||||
|
|
|
|||
|
|
@ -84,12 +84,26 @@ def load_config(
|
|||
env_values,
|
||||
'APP_OTEL_SERVICE_NAME',
|
||||
),
|
||||
exporter_endpoint=_yaml_or_env_str(
|
||||
logs_endpoint=_yaml_or_env_str(
|
||||
otel_section,
|
||||
'exporter_endpoint',
|
||||
'otel.exporter_endpoint',
|
||||
'logs_endpoint',
|
||||
'otel.logs_endpoint',
|
||||
env_values,
|
||||
'APP_OTEL_EXPORTER_ENDPOINT',
|
||||
'APP_OTEL_LOGS_ENDPOINT',
|
||||
),
|
||||
metrics_endpoint=_yaml_or_env_str(
|
||||
otel_section,
|
||||
'metrics_endpoint',
|
||||
'otel.metrics_endpoint',
|
||||
env_values,
|
||||
'APP_OTEL_METRICS_ENDPOINT',
|
||||
),
|
||||
traces_endpoint=_yaml_or_env_str(
|
||||
otel_section,
|
||||
'traces_endpoint',
|
||||
'otel.traces_endpoint',
|
||||
env_values,
|
||||
'APP_OTEL_TRACES_ENDPOINT',
|
||||
),
|
||||
metric_export_interval=_yaml_or_env_int(
|
||||
otel_section,
|
||||
|
|
|
|||
|
|
@ -22,7 +22,9 @@ class LoggingConfig:
|
|||
@dataclass(frozen=True, slots=True)
|
||||
class OtelConfig:
|
||||
service_name: str
|
||||
exporter_endpoint: str
|
||||
logs_endpoint: str
|
||||
metrics_endpoint: str
|
||||
traces_endpoint: str
|
||||
metric_export_interval: int
|
||||
|
||||
|
||||
|
|
|
|||
0
adapter/otel/__init__.py
Normal file
0
adapter/otel/__init__.py
Normal file
85
adapter/otel/bootstrap.py
Normal file
85
adapter/otel/bootstrap.py
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
from dataclasses import dataclass
|
||||
|
||||
from opentelemetry import metrics, trace
|
||||
from opentelemetry._logs import set_logger_provider
|
||||
from opentelemetry.exporter.otlp.proto.http._log_exporter import OTLPLogExporter
|
||||
from opentelemetry.exporter.otlp.proto.http.metric_exporter import OTLPMetricExporter
|
||||
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
|
||||
from opentelemetry.sdk._logs import LoggerProvider
|
||||
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor
|
||||
from opentelemetry.sdk.metrics import MeterProvider
|
||||
from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
|
||||
from opentelemetry.sdk.resources import Resource
|
||||
from opentelemetry.sdk.trace import TracerProvider
|
||||
from opentelemetry.sdk.trace.export import BatchSpanProcessor
|
||||
|
||||
from adapter.config.model import AppConfig
|
||||
|
||||
from .logging import OtelLogger
|
||||
from .metrics import OtelMetrics
|
||||
from .tracing import OtelTracer
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class OtelRuntime:
|
||||
logger_provider: LoggerProvider
|
||||
meter_provider: MeterProvider
|
||||
tracer_provider: TracerProvider
|
||||
logger: OtelLogger
|
||||
metrics: OtelMetrics
|
||||
tracer: OtelTracer
|
||||
|
||||
def shutdown(self) -> None:
|
||||
self.meter_provider.shutdown()
|
||||
self.tracer_provider.shutdown()
|
||||
self.logger_provider.shutdown()
|
||||
|
||||
|
||||
def setup_otel(config: AppConfig) -> OtelRuntime:
|
||||
if config.otel.metric_export_interval <= 0:
|
||||
raise ValueError('invalid metric export interval')
|
||||
|
||||
resource = Resource.create(
|
||||
{
|
||||
'service.name': config.otel.service_name,
|
||||
'deployment.environment': config.app.env,
|
||||
}
|
||||
)
|
||||
|
||||
logger_provider = LoggerProvider(resource=resource, shutdown_on_exit=False)
|
||||
logger_provider.add_log_record_processor(
|
||||
BatchLogRecordProcessor(OTLPLogExporter(endpoint=config.otel.logs_endpoint))
|
||||
)
|
||||
|
||||
metric_reader = PeriodicExportingMetricReader(
|
||||
OTLPMetricExporter(endpoint=config.otel.metrics_endpoint),
|
||||
export_interval_millis=config.otel.metric_export_interval,
|
||||
)
|
||||
meter_provider = MeterProvider(
|
||||
metric_readers=[metric_reader],
|
||||
resource=resource,
|
||||
shutdown_on_exit=False,
|
||||
)
|
||||
|
||||
tracer_provider = TracerProvider(resource=resource, shutdown_on_exit=False)
|
||||
tracer_provider.add_span_processor(
|
||||
BatchSpanProcessor(OTLPSpanExporter(endpoint=config.otel.traces_endpoint))
|
||||
)
|
||||
|
||||
set_logger_provider(logger_provider)
|
||||
metrics.set_meter_provider(meter_provider)
|
||||
trace.set_tracer_provider(tracer_provider)
|
||||
|
||||
scope_name = config.app.name
|
||||
logger = OtelLogger(logger_provider.get_logger(scope_name), config.logging.level)
|
||||
metrics_adapter = OtelMetrics(meter_provider.get_meter(scope_name))
|
||||
tracer = OtelTracer(tracer_provider.get_tracer(scope_name))
|
||||
|
||||
return OtelRuntime(
|
||||
logger_provider=logger_provider,
|
||||
meter_provider=meter_provider,
|
||||
tracer_provider=tracer_provider,
|
||||
logger=logger,
|
||||
metrics=metrics_adapter,
|
||||
tracer=tracer,
|
||||
)
|
||||
57
adapter/otel/logging.py
Normal file
57
adapter/otel/logging.py
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
from opentelemetry._logs import Logger as OtelApiLogger
|
||||
from opentelemetry._logs import SeverityNumber
|
||||
|
||||
from usecase.interface import Attrs
|
||||
|
||||
_LEVELS = {
|
||||
'DEBUG': 10,
|
||||
'INFO': 20,
|
||||
'WARN': 30,
|
||||
'WARNING': 30,
|
||||
'ERROR': 40,
|
||||
'FATAL': 50,
|
||||
'CRITICAL': 50,
|
||||
}
|
||||
|
||||
|
||||
class OtelLogger:
|
||||
def __init__(self, logger: OtelApiLogger, level: str) -> None:
|
||||
self._logger = logger
|
||||
self._threshold = _log_level_value(level)
|
||||
|
||||
def debug(self, message: str, attrs: Attrs | None = None) -> None:
|
||||
self._emit('DEBUG', SeverityNumber.DEBUG, _LEVELS['DEBUG'], message, attrs)
|
||||
|
||||
def info(self, message: str, attrs: Attrs | None = None) -> None:
|
||||
self._emit('INFO', SeverityNumber.INFO, _LEVELS['INFO'], message, attrs)
|
||||
|
||||
def warning(self, message: str, attrs: Attrs | None = None) -> None:
|
||||
self._emit('WARN', SeverityNumber.WARN, _LEVELS['WARNING'], message, attrs)
|
||||
|
||||
def error(self, message: str, attrs: Attrs | None = None) -> None:
|
||||
self._emit('ERROR', SeverityNumber.ERROR, _LEVELS['ERROR'], message, attrs)
|
||||
|
||||
def _emit(
|
||||
self,
|
||||
severity_text: str,
|
||||
severity_number: SeverityNumber,
|
||||
level: int,
|
||||
message: str,
|
||||
attrs: Attrs | None,
|
||||
) -> None:
|
||||
if level < self._threshold:
|
||||
return
|
||||
|
||||
self._logger.emit(
|
||||
severity_text=severity_text,
|
||||
severity_number=severity_number,
|
||||
body=message,
|
||||
attributes=None if attrs is None else dict(attrs),
|
||||
)
|
||||
|
||||
|
||||
def _log_level_value(level: str) -> int:
|
||||
normalized = level.strip().upper()
|
||||
if normalized not in _LEVELS:
|
||||
raise ValueError('invalid log level')
|
||||
return _LEVELS[normalized]
|
||||
59
adapter/otel/metrics.py
Normal file
59
adapter/otel/metrics.py
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
from threading import Lock
|
||||
|
||||
from opentelemetry.metrics import Counter, Histogram, Meter
|
||||
|
||||
from usecase.interface import Attrs
|
||||
|
||||
|
||||
class OtelMetrics:
|
||||
def __init__(self, meter: Meter) -> None:
|
||||
self._meter = meter
|
||||
self._lock = Lock()
|
||||
self._counters: dict[str, Counter] = {}
|
||||
self._histograms: dict[str, Histogram] = {}
|
||||
|
||||
def increment(
|
||||
self,
|
||||
name: str,
|
||||
value: int = 1,
|
||||
attrs: Attrs | None = None,
|
||||
) -> None:
|
||||
self._counter(name).add(
|
||||
value,
|
||||
attributes=None if attrs is None else dict(attrs),
|
||||
)
|
||||
|
||||
def record(
|
||||
self,
|
||||
name: str,
|
||||
value: float,
|
||||
attrs: Attrs | None = None,
|
||||
) -> None:
|
||||
self._histogram(name).record(
|
||||
value,
|
||||
attributes=None if attrs is None else dict(attrs),
|
||||
)
|
||||
|
||||
def _counter(self, name: str) -> Counter:
|
||||
counter = self._counters.get(name)
|
||||
if counter is not None:
|
||||
return counter
|
||||
|
||||
with self._lock:
|
||||
counter = self._counters.get(name)
|
||||
if counter is None:
|
||||
counter = self._meter.create_counter(name)
|
||||
self._counters[name] = counter
|
||||
return counter
|
||||
|
||||
def _histogram(self, name: str) -> Histogram:
|
||||
histogram = self._histograms.get(name)
|
||||
if histogram is not None:
|
||||
return histogram
|
||||
|
||||
with self._lock:
|
||||
histogram = self._histograms.get(name)
|
||||
if histogram is None:
|
||||
histogram = self._meter.create_histogram(name)
|
||||
self._histograms[name] = histogram
|
||||
return histogram
|
||||
65
adapter/otel/tracing.py
Normal file
65
adapter/otel/tracing.py
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
from contextlib import AbstractContextManager
|
||||
from types import TracebackType
|
||||
|
||||
from opentelemetry.trace import Span as OtelApiSpan
|
||||
from opentelemetry.trace import Status, StatusCode
|
||||
from opentelemetry.trace import Tracer as OtelApiTracer
|
||||
|
||||
from usecase.interface import Attrs, AttrValue
|
||||
|
||||
|
||||
class OtelSpan:
|
||||
def __init__(self, span: OtelApiSpan) -> None:
|
||||
self._span = span
|
||||
self._error_recorded = False
|
||||
|
||||
@property
|
||||
def error_recorded(self) -> bool:
|
||||
return self._error_recorded
|
||||
|
||||
def set_attribute(self, name: str, value: AttrValue) -> None:
|
||||
self._span.set_attribute(name, value)
|
||||
|
||||
def record_error(self, error: Exception) -> None:
|
||||
self._span.record_exception(error)
|
||||
self._span.set_status(Status(StatusCode.ERROR, str(error)))
|
||||
self._error_recorded = True
|
||||
|
||||
|
||||
class OtelSpanContext:
|
||||
def __init__(self, context_manager: AbstractContextManager[OtelApiSpan]) -> None:
|
||||
self._context_manager = context_manager
|
||||
self._span: OtelSpan | None = None
|
||||
|
||||
def __enter__(self) -> OtelSpan:
|
||||
self._span = OtelSpan(self._context_manager.__enter__())
|
||||
return self._span
|
||||
|
||||
def __exit__(
|
||||
self,
|
||||
exc_type: type[BaseException] | None,
|
||||
exc: BaseException | None,
|
||||
traceback: TracebackType | None,
|
||||
) -> bool | None:
|
||||
if isinstance(exc, Exception) and self._span is not None:
|
||||
if not self._span.error_recorded:
|
||||
self._span.record_error(exc)
|
||||
return self._context_manager.__exit__(exc_type, exc, traceback)
|
||||
|
||||
|
||||
class OtelTracer:
|
||||
def __init__(self, tracer: OtelApiTracer) -> None:
|
||||
self._tracer = tracer
|
||||
|
||||
def start_span(
|
||||
self,
|
||||
name: str,
|
||||
attrs: Attrs | None = None,
|
||||
) -> OtelSpanContext:
|
||||
context_manager = self._tracer.start_as_current_span(
|
||||
name,
|
||||
attributes=None if attrs is None else dict(attrs),
|
||||
record_exception=False,
|
||||
set_status_on_exception=True,
|
||||
)
|
||||
return OtelSpanContext(context_manager)
|
||||
|
|
@ -12,7 +12,9 @@ logging:
|
|||
|
||||
otel:
|
||||
service_name: web-python-skelet
|
||||
exporter_endpoint: http://localhost:4318
|
||||
logs_endpoint: http://localhost:4318/v1/logs
|
||||
metrics_endpoint: http://localhost:4318/v1/metrics
|
||||
traces_endpoint: http://localhost:4318/v1/traces
|
||||
metric_export_interval: 1000
|
||||
|
||||
security:
|
||||
|
|
|
|||
27
config/otel-collector.yaml
Normal file
27
config/otel-collector.yaml
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
receivers:
|
||||
otlp:
|
||||
protocols:
|
||||
http:
|
||||
endpoint: 0.0.0.0:4318
|
||||
|
||||
processors:
|
||||
batch: {}
|
||||
|
||||
exporters:
|
||||
debug:
|
||||
verbosity: detailed
|
||||
|
||||
service:
|
||||
pipelines:
|
||||
logs:
|
||||
receivers: [otlp]
|
||||
processors: [batch]
|
||||
exporters: [debug]
|
||||
metrics:
|
||||
receivers: [otlp]
|
||||
processors: [batch]
|
||||
exporters: [debug]
|
||||
traces:
|
||||
receivers: [otlp]
|
||||
processors: [batch]
|
||||
exporters: [debug]
|
||||
Loading…
Add table
Add a link
Reference in a new issue