Skip to content

Instantly share code, notes, and snippets.

@erik4github
Created January 10, 2025 19:53
Show Gist options
  • Save erik4github/33d0b755bad0427016075db2ba8dd406 to your computer and use it in GitHub Desktop.
Save erik4github/33d0b755bad0427016075db2ba8dd406 to your computer and use it in GitHub Desktop.
logger = Logger(
service="my-lambda-service", # or any service name
utc=True,
# Setting formatter_cls doesn't exist directly—pass an instance as below
logger_formatter=OtelJsonFormatter(),
# log_level="INFO", # optionally set a log level
)
import json
import os
import logging
import time
from typing import Any, Dict
from aws_lambda_powertools import Logger
from aws_lambda_powertools.logging.formatter import JsonFormatter
def _map_python_level_to_severity_number(levelno: int) -> int:
"""Map Python's logging level numbers to an approximate OTel severity_number."""
# Common mapping, can be adjusted
if levelno <= logging.DEBUG:
return 5 # TRACE or DEBUG range
elif levelno <= logging.INFO:
return 9 # INFO
elif levelno <= logging.WARNING:
return 13 # WARN
elif levelno <= logging.ERROR:
return 17 # ERROR
else:
return 21 # FATAL
class OtelJsonFormatter(JsonFormatter):
"""
Custom JSON formatter to produce logs that align with OpenTelemetry log data model.
"""
def format(self, record: logging.LogRecord) -> str:
# Default attributes from the record
log_message = record.getMessage() # main log string
level_text = record.levelname # e.g. INFO, DEBUG, ERROR
level_number = _map_python_level_to_severity_number(record.levelno)
# The timestamp when the record was created, in RFC3339 or epoch
# record.created is float seconds since epoch. Convert to milliseconds or ISO 8601.
# For OTel logs, you can use an RFC3339/ISO 8601. We'll do an ISO8601 here.
timestamp = time.strftime("%Y-%m-%dT%H:%M:%S", time.gmtime(record.created)) \
+ f".{int((record.created - int(record.created)) * 1_000_000):06d}Z"
# Build the resource block. In a Lambda, you can capture function name, region, etc.
resource = {
"cloud.provider": "aws",
"cloud.platform": "aws_lambda",
"cloud.region": os.environ.get("AWS_REGION"),
"faas.name": os.environ.get("AWS_LAMBDA_FUNCTION_NAME"),
"faas.version": os.environ.get("AWS_LAMBDA_FUNCTION_VERSION"),
}
# If you want trace_id/span_id correlation from X-Ray, parse them from environment
# or from a custom context. This is a simplified example.
xray_trace_id = os.environ.get("_X_AMZN_TRACE_ID", "")
# A typical X-Amzn-Trace-Id looks like:
# Root=1-6483fcc6-2efd98a76b34c26a6609337e;Parent=1234567890abcdef;Sampled=1
trace_id = None
span_id = None
if "Root=" in xray_trace_id and "Parent=" in xray_trace_id:
# Extract Root=1-xxxxxx-xxxxxxxxxxx
root_part = xray_trace_id.split("Root=")[1].split(";")[0]
# OTel’s 128-bit trace IDs do not match 1-xxxx-xxxx directly,
# but you can transform them for correlation. For brevity:
trace_id = root_part.replace("-", "")
parent_part = xray_trace_id.split("Parent=")[1].split(";")[0]
span_id = parent_part
# Additional fields you might store as attributes
# This merges any custom fields from the record's "extra" dictionary
# (i.e., `logger.info("...", extra={"key": "value"})`)
attributes: Dict[str, Any] = {}
if hasattr(record, "extra"):
attributes.update(record.extra)
# Construct the OpenTelemetry-like log record
otel_log = {
"timestamp": timestamp,
"severity_text": level_text,
"severity_number": level_number,
"body": log_message,
"attributes": attributes,
"resource": resource,
}
# Add trace info if present
if trace_id:
otel_log["trace_id"] = trace_id
if span_id:
otel_log["span_id"] = span_id
# Return as a JSON string
return json.dumps(otel_log, ensure_ascii=False)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment