Skip to content

Commit df2d3a7

Browse files
authored
Merge pull request fluent#114 from arcivanov/issue_111
Allow for `fmt` to be a callable that formats the record.
2 parents 0cfd7e0 + 0e75d53 commit df2d3a7

File tree

2 files changed

+75
-29
lines changed

2 files changed

+75
-29
lines changed

fluent/handler.py

+48-29
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,10 @@ class FluentRecordFormatter(logging.Formatter, object):
2222
2323
Best used with server storing data in an ElasticSearch cluster for example.
2424
25-
:param fmt: a dict with format string as values to map to provided keys.
25+
:param fmt: a dict or a callable with format string as values to map to provided keys.
26+
If callable, should accept a single argument `LogRecord` and return a dict,
27+
and have a field `usesTime` that is callable and return a bool as would
28+
`FluentRecordFormatter.usesTime`
2629
:param datefmt: strftime()-compatible date/time format string.
2730
:param style: '%', '{' or '$' (used only with Python 3.2 or above)
2831
:param fill_missing_fmt_key: if True, do not raise a KeyError if the format
@@ -32,7 +35,7 @@ class FluentRecordFormatter(logging.Formatter, object):
3235
:param exclude_attrs: switches this formatter into a mode where all attributes
3336
except the ones specified by `exclude_attrs` are logged with the record as is.
3437
If `None`, operates as before, otherwise `fmt` is ignored.
35-
Can be a `list`, `tuple` or a `set`.
38+
Can be an iterable.
3639
"""
3740

3841
def __init__(self, fmt=None, datefmt=None, style='%', fill_missing_fmt_key=False, format_json=True,
@@ -63,12 +66,22 @@ def __init__(self, fmt=None, datefmt=None, style='%', fill_missing_fmt_key=False
6366
if exclude_attrs is not None:
6467
self._exc_attrs = set(exclude_attrs)
6568
self._fmt_dict = None
69+
self._formatter = self._format_by_exclusion
70+
self.usesTime = super(FluentRecordFormatter, self).usesTime
6671
else:
6772
self._exc_attrs = None
6873
if not fmt:
6974
self._fmt_dict = basic_fmt_dict
75+
self._formatter = self._format_by_dict
76+
self.usesTime = self._format_by_dict_uses_time
7077
else:
71-
self._fmt_dict = fmt
78+
if hasattr(fmt, "__call__"):
79+
self._formatter = fmt
80+
self.usesTime = fmt.usesTime
81+
else:
82+
self._fmt_dict = fmt
83+
self._formatter = self._format_by_dict
84+
self.usesTime = self._format_by_dict_uses_time
7285

7386
if format_json:
7487
self._format_msg = self._format_msg_json
@@ -90,37 +103,13 @@ def format(self, record):
90103
record.hostname = self.hostname
91104

92105
# Apply format
93-
data = {}
94-
if self._exc_attrs is not None:
95-
for key, value in record.__dict__.items():
96-
if key not in self._exc_attrs:
97-
data[key] = value
98-
else:
99-
for key, value in self._fmt_dict.items():
100-
try:
101-
if self.__style:
102-
value = self.__style(value).format(record)
103-
else:
104-
value = value % record.__dict__
105-
except KeyError as exc:
106-
value = None
107-
if not self.fill_missing_fmt_key:
108-
raise exc
109-
110-
data[key] = value
106+
data = self._formatter(record)
111107

112108
self._structuring(data, record)
113109
return data
114110

115111
def usesTime(self):
116-
if self._exc_attrs is not None:
117-
return super(FluentRecordFormatter, self).usesTime()
118-
else:
119-
if self.__style:
120-
search = self.__style.asctime_search
121-
else:
122-
search = "%(asctime)"
123-
return any([value.find(search) >= 0 for value in self._fmt_dict.values()])
112+
"""This method is substituted on construction based on settings for performance reasons"""
124113

125114
def _structuring(self, data, record):
126115
""" Melds `msg` into `data`.
@@ -153,6 +142,36 @@ def _format_msg_json(self, record, msg):
153142
def _format_msg_default(self, record, msg):
154143
return {'message': record.getMessage()}
155144

145+
def _format_by_exclusion(self, record):
146+
data = {}
147+
for key, value in record.__dict__.items():
148+
if key not in self._exc_attrs:
149+
data[key] = value
150+
return data
151+
152+
def _format_by_dict(self, record):
153+
data = {}
154+
for key, value in self._fmt_dict.items():
155+
try:
156+
if self.__style:
157+
value = self.__style(value).format(record)
158+
else:
159+
value = value % record.__dict__
160+
except KeyError as exc:
161+
value = None
162+
if not self.fill_missing_fmt_key:
163+
raise exc
164+
165+
data[key] = value
166+
return data
167+
168+
def _format_by_dict_uses_time(self):
169+
if self.__style:
170+
search = self.__style.asctime_search
171+
else:
172+
search = "%(asctime)"
173+
return any([value.find(search) >= 0 for value in self._fmt_dict.values()])
174+
156175
@staticmethod
157176
def _add_dic(data, dic):
158177
for key, value in dic.items():

tests/test_handler.py

+27
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,33 @@ def test_exclude_attrs_with_extra(self):
125125
self.assertEqual("Test with value 'test value'", data[0][2]['message'])
126126
self.assertEqual(1234, data[0][2]['x'])
127127

128+
def test_format_dynamic(self):
129+
def formatter(record):
130+
return {
131+
"message": record.message,
132+
"x": record.x,
133+
"custom_value": 1
134+
}
135+
136+
formatter.usesTime = lambda: True
137+
138+
handler = fluent.handler.FluentHandler('app.follow', port=self._port)
139+
140+
with handler:
141+
logging.basicConfig(level=logging.INFO)
142+
log = logging.getLogger('fluent.test')
143+
handler.setFormatter(
144+
fluent.handler.FluentRecordFormatter(fmt=formatter)
145+
)
146+
log.addHandler(handler)
147+
log.info("Test with value '%s'", "test value", extra={"x": 1234})
148+
log.removeHandler(handler)
149+
150+
data = self.get_data()
151+
self.assertTrue('x' in data[0][2])
152+
self.assertEqual(1234, data[0][2]['x'])
153+
self.assertEqual(1, data[0][2]['custom_value'])
154+
128155
@unittest.skipUnless(sys.version_info[0:2] >= (3, 2), 'supported with Python 3.2 or above')
129156
def test_custom_fmt_with_format_style(self):
130157
handler = fluent.handler.FluentHandler('app.follow', port=self._port)

0 commit comments

Comments
 (0)