As suggested, the call of json.dumps was moved to an internal helper in the StruclogFormatter, which now can be overwritten by inheriting classes.
I was unsure how and if you would like to add a recipe to the docs, but this would be a minimal configuration:
# log configuration
from collections.abc import Callable
from typing import Any
import orjson
import structlog
from ecs_logging import StructlogFormatter as EcsStruclogFormatter
from ecs_logging import _utils
def get_orjson_serializer() -> Callable[[dict[str, Any], bool], bytes]:
def serializer(data: dict[str, Any], sort: bool) -> bytes:
return orjson.dumps(
data,
option=orjson.OPT_SORT_KEYS if sort else None,
default=_utils._json_dumps_fallback,
)
return serializer
class BinaryEcsFormatter(EcsStruclogFormatter):
def _json_dumps(self, value: dict[str, Any]) -> bytes:
# Ensure that the first three fields are '@timestamp',
# 'log.level', and 'message' per ECS spec
ordered_fields = {}
try:
ordered_fields["@timestamp"] = value.pop("@timestamp")
except KeyError:
pass
# log.level can either be nested or not nested so we have to try both
try:
ordered_fields["log.level"] = value["log"].pop("level")
if not value["log"]: # Remove the 'log' dictionary if it's now empty
value.pop("log", None)
except KeyError:
try:
ordered_fields["log.level"] = value.pop("log.level")
except KeyError:
pass
try:
ordered_fields["message"] = value.pop("message")
except KeyError:
pass
serializer = get_orjson_serializer()
# Because we want to use 'sorted_keys=True' we manually build
# the first three keys and then build the rest with the serializer
if ordered_fields:
ordered_json = serializer(ordered_fields, sort=False)
if value:
return ordered_json[:-1] + b"," + serializer(value, True)[1:]
else:
return ordered_json
# If there are no fields with ordering requirements we
# pass everything into the serializer
else:
return serializer(value, sort=True)
structlog.configure(
processors=[
structlog.processors.TimeStamper(key="@timestamp", fmt="iso", utc=True),
BinaryEcsFormatter(),
],
logger_factory=structlog.BytesLoggerFactory(),
)
# your script
logger = structlog.get_logger()
if __name__ == "__main__":
logger.info("Hi there!")
As suggested, the call of
json.dumps
was moved to an internal helper in theStruclogFormatter
, which now can be overwritten by inheriting classes.I was unsure how and if you would like to add a recipe to the docs, but this would be a minimal configuration:
Should produce a correct output like:
Resolves #112