Nikhil Anandani
03/13/2025, 9:25 AMimport os
import logging
import json
import sys
import structlog
from typing import Any, Dict, Optional
from opentelemetry import trace, _logs
from opentelemetry.exporter.otlp.proto.grpc._log_exporter import OTLPLogExporter
from opentelemetry.sdk._logs import LoggerProvider
from opentelemetry.sdk._logs._internal.export import BatchLogRecordProcessor
from opentelemetry.trace import Status, StatusCode
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.instrumentation.logging import LoggingInstrumentor
# --- ✅ Enable Fork Safety for macOS / multiprocessing ---
os.environ['OBJC_DISABLE_INITIALIZE_FORK_SAFETY'] = 'YES'
# --- ✅ Load Configuration ---
try:
from app.config import settings
except ImportError:
class FallbackSettings:
JSON_LOGS = True
LOG_LEVEL = "DEBUG"
OTEL_SERVICE_NAME = "fastapi-app"
OTEL_EXPORTER_OTLP_ENDPOINT = "<http://localhost:4317>" # OpenTelemetry Collector
settings = FallbackSettings()
LOG_LEVEL = os.getenv('LOG_LEVEL', settings.LOG_LEVEL).upper()
OTEL_SERVICE_NAME = os.getenv('OTEL_SERVICE_NAME', settings.OTEL_SERVICE_NAME)
OTEL_EXPORTER_OTLP_ENDPOINT = os.getenv('OTEL_EXPORTER_OTLP_ENDPOINT', "<http://localhost:4317>")
ENVIRONMENT = os.getenv("ENVIRONMENT", "development")
# --- ✅ Configure OpenTelemetry Logger Provider (Direct to Collector) ---
logger_provider = LoggerProvider()
_logs.set_logger_provider(logger_provider)
# Set up OTLP Log Exporter (Directly to SigNoz/OpenTelemetry)
otlp_log_exporter = OTLPLogExporter(endpoint=OTEL_EXPORTER_OTLP_ENDPOINT, insecure=True)
logger_provider.add_log_record_processor(BatchLogRecordProcessor(otlp_log_exporter))
# --- ✅ Configure OpenTelemetry Tracing (Direct to Collector) ---
tracer_provider = TracerProvider()
trace.set_tracer_provider(tracer_provider)
otlp_trace_exporter = OTLPSpanExporter(endpoint=OTEL_EXPORTER_OTLP_ENDPOINT, insecure=True)
tracer_provider.add_span_processor(BatchSpanProcessor(otlp_trace_exporter))
# --- ✅ Instrument Python Logging with OpenTelemetry ---
LoggingInstrumentor().instrument(set_logging_format=True)
# --- ✅ OpenTelemetry Context Injection for Logs ---
def add_trace_context(_, __, event_dict: Dict[str, Any]) -> Dict[str, Any]:
"""Attach OpenTelemetry trace context to structured logs."""
try:
span = trace.get_current_span()
if span:
span_context = span.get_span_context()
if span_context.is_valid:
event_dict.update({
"trace_id": format(span_context.trace_id, "032x"),
"span_id": format(span_context.span_id, "016x"),
"service.name": OTEL_SERVICE_NAME,
"environment": ENVIRONMENT,
})
except Exception as e:
event_dict["error.logging"] = str(e)
return event_dict
# --- ✅ Configure Python Logging (Ensure Logs Flow to OpenTelemetry) ---
log_level_num = getattr(logging, LOG_LEVEL, logging.INFO)
console_handler = logging.StreamHandler(sys.stdout)
console_handler.setFormatter(logging.Formatter('%(asctime)s [%(levelname)s] %(message)s', '%Y-%m-%d %H:%M:%S'))
console_handler.setLevel(log_level_num)
json_handler = logging.StreamHandler(sys.stderr)
json_handler.setLevel(log_level_num)
root_logger = logging.getLogger()
root_logger.setLevel(log_level_num)
root_logger.handlers = []
root_logger.addHandler(console_handler)
root_logger.addHandler(json_handler)
# Silence noisy loggers
for logger_name in ["urllib3.connectionpool", "urllib3", "asyncio", "charset_normalizer"]:
logging.getLogger(logger_name).setLevel(logging.WARNING)
# --- ✅ Configure `structlog` ---
structlog.configure(
processors=[
structlog.contextvars.merge_contextvars,
structlog.stdlib.filter_by_level,
structlog.stdlib.add_logger_name,
structlog.stdlib.add_log_level,
structlog.processors.TimeStamper(fmt="iso", utc=True),
add_trace_context, # Attach OpenTelemetry trace info
structlog.processors.format_exc_info,
structlog.processors.JSONRenderer(),
],
context_class=dict,
logger_factory=structlog.stdlib.LoggerFactory(),
wrapper_class=structlog.stdlib.BoundLogger,
cache_logger_on_first_use=True,
)
# --- ✅ Initialize Structlog Logger ---
logger = structlog.get_logger(OTEL_SERVICE_NAME).bind(
service=OTEL_SERVICE_NAME,
environment=ENVIRONMENT,
)
# --- ✅ Context Manager for OpenTelemetry Tracing ---
class LogSpan:
"""Context manager to create spans and attach trace context to logs."""
def __init__(self, name: str, attributes: Optional[Dict[str, Any]] = None):
self.name = name
self.attributes = attributes or {}
self.tracer = trace.get_tracer(OTEL_SERVICE_NAME)
self.span = None
def __enter__(self):
self.span = self.tracer.start_as_current_span(self.name)
for key, value in self.attributes.items():
self.span.set_attribute(key, value)
return self
def __exit__(self, exc_type, exc_val, exc_tb):
if exc_type is not None:
self.span.set_status(Status(StatusCode.ERROR))
self.span.record_exception(exc_val)
self.span.end()
return False
# --- ✅ Ensure Logs are Sent to OpenTelemetry ---
def ensure_otlp_logging():
"""Ensure logs and traces are properly sent to OpenTelemetry."""
try:
# Ensure tracing and logging are configured
logger.info("✅ Successfully configured OpenTelemetry logging & tracing", endpoint=OTEL_EXPORTER_OTLP_ENDPOINT)
return True
except Exception as e:
logger.error("❌ Failed to configure OpenTelemetry logging", error=str(e), exc_info=True)
return False
# --- ✅ Test Log Messages (Ensure They Appear in OpenTelemetry Collector) ---
logger.debug("🐍 Debug message from structlog - Direct to OpenTelemetry")
logger.info("🚀 Structlog is now sending logs directly to OpenTelemetry Collector")
# --- ✅ Expose Logger for Use in Other Modules ---
__all__ = ['logger', 'LogSpan', 'ensure_otlp_logging']
Hi Team, I'm stuck in configuring up my own logger to send logs to signoz. I see API traces and sql traces which I guess is auto-instrumented. I want to see the logs logged by the above logger in signoz. Any thing that I'm doing wrong here. Or any simpler version of structlog that I can use to send logs.
p.s- I'm using a self hosted version of signozNagesh Bansal
03/15/2025, 1:02 PMNikhil Anandani
03/17/2025, 4:49 AM