Files
bakery-ia/shared/monitoring/logs_exporter.py

237 lines
7.8 KiB
Python
Raw Normal View History

"""
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
2026-01-09 14:48:44 +01:00
# For logs, we need to use the HTTP endpoint (port 4318), not the gRPC endpoint (port 4317)
if otel_endpoint is None:
2026-01-09 14:48:44 +01:00
# Try logs-specific endpoint first, then fall back to general OTLP endpoint
otel_endpoint = os.getenv(
2026-01-09 14:48:44 +01:00
"OTEL_EXPORTER_OTLP_LOGS_ENDPOINT",
2026-01-09 07:26:11 +01:00
os.getenv("OTEL_COLLECTOR_ENDPOINT", "http://signoz-otel-collector.bakery-ia:4318")
)
2026-01-09 14:48:44 +01:00
logger.info(f"Original OTLP endpoint for logs: {otel_endpoint}")
# If we got the tracing endpoint (4317), switch to logs endpoint (4318)
if otel_endpoint.endswith(":4317"):
logger.info("Converting tracing endpoint (4317) to logs endpoint (4318)")
otel_endpoint = otel_endpoint.replace(":4317", ":4318")
logger.info(f"Final OTLP endpoint for logs: {otel_endpoint}")
# Ensure endpoint has proper protocol prefix
if not otel_endpoint.startswith(("http://", "https://")):
# Default to HTTP for insecure connections
otel_endpoint = f"http://{otel_endpoint}"
# 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