app/observability.py (106 lines of code) (raw):
import logging
from logging.config import dictConfig
import sys
from typing import Tuple
# Instrumentation
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
from opentelemetry.sdk.resources import SERVICE_NAME, Resource
# Trace things
from opentelemetry import trace
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import (
BatchSpanProcessor,
ConsoleSpanExporter,
SpanExporter,
)
# Metrics things
from opentelemetry import metrics
from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import OTLPMetricExporter
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.metrics.export import (
ConsoleMetricExporter,
MetricExporter,
PeriodicExportingMetricReader,
)
# Logging things
from opentelemetry.instrumentation.logging import LoggingInstrumentor
from pythonjsonlogger.json import JsonFormatter
from app.settings import settings
logger = logging.getLogger(__name__)
def get_exporter(
endpoint: str, console: bool = False
) -> Tuple[SpanExporter, MetricExporter]:
if console:
return (ConsoleSpanExporter(), ConsoleMetricExporter())
return (OTLPSpanExporter(endpoint, True), OTLPMetricExporter(endpoint, True))
def setup_otel_exporter(app_name: str, endpoint: str):
if settings.running_unittests == 1:
# If we're running unittests, skip setting up exporter and provider so
# it's using the default no-op things
return
logger.info(
"Starting opentelemetry exporter %s", endpoint, extra={"endpoint": endpoint}
)
# Service name is required for most backends
resource = Resource.create(attributes={SERVICE_NAME: app_name})
span_exporter, metric_exporter = get_exporter(endpoint)
tracerProvider = TracerProvider(resource=resource)
processor = BatchSpanProcessor(span_exporter)
tracerProvider.add_span_processor(processor)
trace.set_tracer_provider(tracerProvider)
reader = PeriodicExportingMetricReader(metric_exporter)
meterProvider = MeterProvider(resource=resource, metric_readers=[reader])
metrics.set_meter_provider(meterProvider)
def setup_structured_logging() -> None:
LoggingInstrumentor().instrument()
handler = [
"console-pretty" if settings.environment == "development" else "console-mozlog"
]
dictConfig(
{
"version": 1,
"formatters": {
"json": {
"()": JsonFormatter,
"format": "%(asctime)s %(name)s %(levelname)s %(message)s %(otelTraceID)s %(otelSpanID)s %(otelTraceSampled)s",
"rename_fields": {
"levelname": "severity",
"asctime": "timestamp",
"otelTraceID": "logging.googleapis.com/trace",
"otelSpanID": "logging.googleapis.com/spanId",
"otelTraceSampled": "logging.googleapis.com/trace_sampled",
},
"datefmt": "%Y-%m-%dT%H:%M:%SZ",
},
"text": {
"format": "%(message)s",
},
},
"handlers": {
"console-mozlog": {
"level": logging.INFO,
"class": "logging.StreamHandler",
"formatter": "json",
"stream": sys.stdout,
},
"console-pretty": {
"level": logging.INFO,
"class": "rich.logging.RichHandler",
"formatter": "text",
},
},
"loggers": {
"app": {
"handlers": handler,
"propagate": True,
"level": logging.INFO,
},
"uvicorn": {
"handlers": handler,
"propagate": True,
"level": logging.INFO,
},
"fastapi": {
"handlers": handler,
"propagate": True,
"level": logging.INFO,
},
},
}
)
def instrument_app(app):
FastAPIInstrumentor.instrument_app(app)