- Updated all OpenTelemetry packages to latest versions: - opentelemetry-api: 1.27.0 → 1.39.1 - opentelemetry-sdk: 1.27.0 → 1.39.1 - opentelemetry-exporter-otlp-proto-grpc: 1.27.0 → 1.39.1 - opentelemetry-exporter-otlp-proto-http: 1.27.0 → 1.39.1 - opentelemetry-instrumentation-fastapi: 0.48b0 → 0.60b1 - opentelemetry-instrumentation-httpx: 0.48b0 → 0.60b1 - opentelemetry-instrumentation-redis: 0.48b0 → 0.60b1 - opentelemetry-instrumentation-sqlalchemy: 0.48b0 → 0.60b1 - Removed prometheus-client==0.23.1 from all services - Unified all services to use the same monitoring package versions Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
221 lines
7.0 KiB
Python
221 lines
7.0 KiB
Python
"""
|
|
OpenTelemetry Logs Integration for SigNoz
|
|
Exports structured logs to SigNoz via OpenTelemetry Collector
|
|
"""
|
|
|
|
import os
|
|
import logging
|
|
import structlog
|
|
from typing import Optional
|
|
from opentelemetry._logs import set_logger_provider
|
|
from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler
|
|
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor
|
|
try:
|
|
from opentelemetry.exporter.otlp.proto.http._log_exporter import OTLPLogExporter
|
|
except ImportError:
|
|
try:
|
|
from opentelemetry.exporter.otlp.proto.http.log_exporter import OTLPLogExporter
|
|
except ImportError:
|
|
OTLPLogExporter = None
|
|
from opentelemetry.sdk.resources import Resource, SERVICE_NAME, SERVICE_VERSION
|
|
|
|
logger = structlog.get_logger()
|
|
|
|
|
|
def setup_otel_logging(
|
|
service_name: str,
|
|
service_version: str = "1.0.0",
|
|
otel_endpoint: Optional[str] = None,
|
|
enable_console: bool = True
|
|
) -> Optional[LoggingHandler]:
|
|
"""
|
|
Setup OpenTelemetry logging to export logs to SigNoz.
|
|
|
|
This integrates with Python's standard logging to automatically
|
|
export all log records to SigNoz via the OTLP protocol.
|
|
|
|
Args:
|
|
service_name: Name of the service (e.g., "auth-service")
|
|
service_version: Version of the service
|
|
otel_endpoint: OpenTelemetry collector endpoint (default from env)
|
|
enable_console: Whether to also log to console (default: True)
|
|
|
|
Returns:
|
|
LoggingHandler instance if successful, None otherwise
|
|
|
|
Example:
|
|
from shared.monitoring.logs_exporter import setup_otel_logging
|
|
|
|
# Setup during service initialization
|
|
setup_otel_logging("auth-service", "1.0.0")
|
|
|
|
# Now all standard logging calls will be exported to SigNoz
|
|
import logging
|
|
logger = logging.getLogger(__name__)
|
|
logger.info("This will appear in SigNoz!")
|
|
"""
|
|
|
|
# Check if logging export is enabled
|
|
if os.getenv("OTEL_LOGS_EXPORTER", "").lower() != "otlp":
|
|
logger.info(
|
|
"OpenTelemetry logs export disabled",
|
|
service=service_name,
|
|
reason="OTEL_LOGS_EXPORTER not set to 'otlp'"
|
|
)
|
|
return None
|
|
|
|
# Get OTLP endpoint from environment or parameter
|
|
if otel_endpoint is None:
|
|
otel_endpoint = os.getenv(
|
|
"OTEL_EXPORTER_OTLP_ENDPOINT",
|
|
os.getenv("OTEL_COLLECTOR_ENDPOINT", "http://signoz-otel-collector.signoz:4318")
|
|
)
|
|
|
|
# Ensure endpoint has /v1/logs path for HTTP
|
|
if not otel_endpoint.endswith("/v1/logs"):
|
|
otel_endpoint = f"{otel_endpoint}/v1/logs"
|
|
|
|
try:
|
|
# Check if OTLPLogExporter is available
|
|
if OTLPLogExporter is None:
|
|
logger.warning(
|
|
"OpenTelemetry HTTP OTLP exporter not available",
|
|
service=service_name,
|
|
reason="opentelemetry-exporter-otlp-proto-http package not installed"
|
|
)
|
|
return None
|
|
|
|
# Create resource with service information
|
|
resource = Resource(attributes={
|
|
SERVICE_NAME: service_name,
|
|
SERVICE_VERSION: service_version,
|
|
"deployment.environment": os.getenv("ENVIRONMENT", "development"),
|
|
"k8s.namespace.name": os.getenv("K8S_NAMESPACE", "bakery-ia"),
|
|
"k8s.pod.name": os.getenv("HOSTNAME", "unknown"),
|
|
})
|
|
|
|
# Configure logger provider
|
|
logger_provider = LoggerProvider(resource=resource)
|
|
set_logger_provider(logger_provider)
|
|
|
|
# Configure OTLP exporter for logs
|
|
otlp_exporter = OTLPLogExporter(
|
|
endpoint=otel_endpoint,
|
|
timeout=10
|
|
)
|
|
|
|
# Add log record processor with batching
|
|
log_processor = BatchLogRecordProcessor(otlp_exporter)
|
|
logger_provider.add_log_record_processor(log_processor)
|
|
|
|
# Create logging handler that bridges standard logging to OpenTelemetry
|
|
otel_handler = LoggingHandler(
|
|
level=logging.NOTSET, # Capture all levels
|
|
logger_provider=logger_provider
|
|
)
|
|
|
|
# Add handler to root logger
|
|
root_logger = logging.getLogger()
|
|
root_logger.addHandler(otel_handler)
|
|
|
|
logger.info(
|
|
"OpenTelemetry logs export configured",
|
|
service=service_name,
|
|
otel_endpoint=otel_endpoint,
|
|
console_logging=enable_console
|
|
)
|
|
|
|
return otel_handler
|
|
|
|
except Exception as e:
|
|
logger.error(
|
|
"Failed to setup OpenTelemetry logs export",
|
|
service=service_name,
|
|
error=str(e),
|
|
reason="Will continue with standard logging only"
|
|
)
|
|
return None
|
|
|
|
|
|
def add_log_context(**context):
|
|
"""
|
|
Add contextual information to logs that will be sent to SigNoz.
|
|
|
|
This is useful for adding request IDs, user IDs, tenant IDs, etc.
|
|
that help with filtering and correlation in SigNoz.
|
|
|
|
Args:
|
|
**context: Key-value pairs to add to log context
|
|
|
|
Example:
|
|
from shared.monitoring.logs_exporter import add_log_context
|
|
|
|
# Add context for current request
|
|
add_log_context(
|
|
request_id="req_123",
|
|
user_id="user_456",
|
|
tenant_id="tenant_789"
|
|
)
|
|
|
|
# Now all logs will include this context
|
|
logger.info("Processing order") # Will include request_id, user_id, tenant_id
|
|
"""
|
|
# This works with structlog's context binding
|
|
bound_logger = structlog.get_logger()
|
|
return bound_logger.bind(**context)
|
|
|
|
|
|
def get_current_trace_context() -> dict:
|
|
"""
|
|
Get current trace context for log correlation.
|
|
|
|
Returns a dict with trace_id and span_id if available,
|
|
which can be added to log records for correlation with traces.
|
|
|
|
Returns:
|
|
Dict with trace_id and span_id, or empty dict if no active trace
|
|
|
|
Example:
|
|
from shared.monitoring.logs_exporter import get_current_trace_context
|
|
|
|
# Get trace context and add to logs
|
|
trace_ctx = get_current_trace_context()
|
|
logger.info("Processing request", **trace_ctx)
|
|
"""
|
|
from opentelemetry import trace
|
|
|
|
span = trace.get_current_span()
|
|
if span and span.get_span_context().is_valid:
|
|
return {
|
|
"trace_id": format(span.get_span_context().trace_id, '032x'),
|
|
"span_id": format(span.get_span_context().span_id, '016x'),
|
|
}
|
|
return {}
|
|
|
|
|
|
class StructlogOTELProcessor:
|
|
"""
|
|
Structlog processor that adds OpenTelemetry trace context to logs.
|
|
|
|
This automatically adds trace_id and span_id to all log records,
|
|
enabling correlation between logs and traces in SigNoz.
|
|
|
|
Usage:
|
|
import structlog
|
|
from shared.monitoring.logs_exporter import StructlogOTELProcessor
|
|
|
|
structlog.configure(
|
|
processors=[
|
|
StructlogOTELProcessor(),
|
|
# ... other processors
|
|
]
|
|
)
|
|
"""
|
|
|
|
def __call__(self, logger, method_name, event_dict):
|
|
"""Add trace context to log event"""
|
|
trace_ctx = get_current_trace_context()
|
|
if trace_ctx:
|
|
event_dict.update(trace_ctx)
|
|
return event_dict
|