176 lines
4.9 KiB
Python
176 lines
4.9 KiB
Python
![]() |
# -*- test-case-name: twisted.logger.test.test_flatten -*-
|
||
|
# Copyright (c) Twisted Matrix Laboratories.
|
||
|
# See LICENSE for details.
|
||
|
|
||
|
"""
|
||
|
Code related to "flattening" events; that is, extracting a description of all
|
||
|
relevant fields from the format string and persisting them for later
|
||
|
examination.
|
||
|
"""
|
||
|
|
||
|
from collections import defaultdict
|
||
|
from string import Formatter
|
||
|
from typing import Any, Dict, Optional
|
||
|
|
||
|
from ._interfaces import LogEvent
|
||
|
|
||
|
aFormatter = Formatter()
|
||
|
|
||
|
|
||
|
class KeyFlattener:
|
||
|
"""
|
||
|
A L{KeyFlattener} computes keys for the things within curly braces in
|
||
|
PEP-3101-style format strings as parsed by L{string.Formatter.parse}.
|
||
|
"""
|
||
|
|
||
|
def __init__(self) -> None:
|
||
|
"""
|
||
|
Initialize a L{KeyFlattener}.
|
||
|
"""
|
||
|
self.keys: Dict[str, int] = defaultdict(lambda: 0)
|
||
|
|
||
|
def flatKey(
|
||
|
self, fieldName: str, formatSpec: Optional[str], conversion: Optional[str]
|
||
|
) -> str:
|
||
|
"""
|
||
|
Compute a string key for a given field/format/conversion.
|
||
|
|
||
|
@param fieldName: A format field name.
|
||
|
@param formatSpec: A format spec.
|
||
|
@param conversion: A format field conversion type.
|
||
|
|
||
|
@return: A key specific to the given field, format and conversion, as
|
||
|
well as the occurrence of that combination within this
|
||
|
L{KeyFlattener}'s lifetime.
|
||
|
"""
|
||
|
if formatSpec is None:
|
||
|
formatSpec = ""
|
||
|
|
||
|
if conversion is None:
|
||
|
conversion = ""
|
||
|
|
||
|
result = "{fieldName}!{conversion}:{formatSpec}".format(
|
||
|
fieldName=fieldName,
|
||
|
formatSpec=formatSpec,
|
||
|
conversion=conversion,
|
||
|
)
|
||
|
self.keys[result] += 1
|
||
|
n = self.keys[result]
|
||
|
if n != 1:
|
||
|
result += "/" + str(self.keys[result])
|
||
|
return result
|
||
|
|
||
|
|
||
|
def flattenEvent(event: LogEvent) -> None:
|
||
|
"""
|
||
|
Flatten the given event by pre-associating format fields with specific
|
||
|
objects and callable results in a L{dict} put into the C{"log_flattened"}
|
||
|
key in the event.
|
||
|
|
||
|
@param event: A logging event.
|
||
|
"""
|
||
|
if event.get("log_format", None) is None:
|
||
|
return
|
||
|
|
||
|
if "log_flattened" in event:
|
||
|
fields = event["log_flattened"]
|
||
|
else:
|
||
|
fields = {}
|
||
|
|
||
|
keyFlattener = KeyFlattener()
|
||
|
|
||
|
for literalText, fieldName, formatSpec, conversion in aFormatter.parse(
|
||
|
event["log_format"]
|
||
|
):
|
||
|
if fieldName is None:
|
||
|
continue
|
||
|
|
||
|
if conversion != "r":
|
||
|
conversion = "s"
|
||
|
|
||
|
flattenedKey = keyFlattener.flatKey(fieldName, formatSpec, conversion)
|
||
|
structuredKey = keyFlattener.flatKey(fieldName, formatSpec, "")
|
||
|
|
||
|
if flattenedKey in fields:
|
||
|
# We've already seen and handled this key
|
||
|
continue
|
||
|
|
||
|
if fieldName.endswith("()"):
|
||
|
fieldName = fieldName[:-2]
|
||
|
callit = True
|
||
|
else:
|
||
|
callit = False
|
||
|
|
||
|
field = aFormatter.get_field(fieldName, (), event)
|
||
|
fieldValue = field[0]
|
||
|
|
||
|
if conversion == "r":
|
||
|
conversionFunction = repr
|
||
|
else: # Above: if conversion is not "r", it's "s"
|
||
|
conversionFunction = str
|
||
|
|
||
|
if callit:
|
||
|
fieldValue = fieldValue()
|
||
|
|
||
|
flattenedValue = conversionFunction(fieldValue)
|
||
|
fields[flattenedKey] = flattenedValue
|
||
|
fields[structuredKey] = fieldValue
|
||
|
|
||
|
if fields:
|
||
|
event["log_flattened"] = fields
|
||
|
|
||
|
|
||
|
def extractField(field: str, event: LogEvent) -> Any:
|
||
|
"""
|
||
|
Extract a given format field from the given event.
|
||
|
|
||
|
@param field: A string describing a format field or log key. This is the
|
||
|
text that would normally fall between a pair of curly braces in a
|
||
|
format string: for example, C{"key[2].attribute"}. If a conversion is
|
||
|
specified (the thing after the C{"!"} character in a format field) then
|
||
|
the result will always be str.
|
||
|
@param event: A log event.
|
||
|
|
||
|
@return: A value extracted from the field.
|
||
|
|
||
|
@raise KeyError: if the field is not found in the given event.
|
||
|
"""
|
||
|
keyFlattener = KeyFlattener()
|
||
|
|
||
|
[[literalText, fieldName, formatSpec, conversion]] = aFormatter.parse(
|
||
|
"{" + field + "}"
|
||
|
)
|
||
|
|
||
|
assert fieldName is not None
|
||
|
|
||
|
key = keyFlattener.flatKey(fieldName, formatSpec, conversion)
|
||
|
|
||
|
if "log_flattened" not in event:
|
||
|
flattenEvent(event)
|
||
|
|
||
|
return event["log_flattened"][key]
|
||
|
|
||
|
|
||
|
def flatFormat(event: LogEvent) -> str:
|
||
|
"""
|
||
|
Format an event which has been flattened with L{flattenEvent}.
|
||
|
|
||
|
@param event: A logging event.
|
||
|
|
||
|
@return: A formatted string.
|
||
|
"""
|
||
|
fieldValues = event["log_flattened"]
|
||
|
keyFlattener = KeyFlattener()
|
||
|
s = []
|
||
|
|
||
|
for literalText, fieldName, formatSpec, conversion in aFormatter.parse(
|
||
|
event["log_format"]
|
||
|
):
|
||
|
s.append(literalText)
|
||
|
|
||
|
if fieldName is not None:
|
||
|
key = keyFlattener.flatKey(fieldName, formatSpec, conversion or "s")
|
||
|
s.append(str(fieldValues[key]))
|
||
|
|
||
|
return "".join(s)
|