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)