Skip to content

Commit cce5ac2

Browse files
authored
Add TraceLogger (census-instrumentation#536)
Add custom logging class for trace correlation.
1 parent a51fe7d commit cce5ac2

File tree

3 files changed

+480
-0
lines changed

3 files changed

+480
-0
lines changed

opencensus/log/__init__.py

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
# Copyright 2019, OpenCensus Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from collections import namedtuple
16+
from copy import copy
17+
import logging
18+
19+
from opencensus.trace import execution_context
20+
21+
22+
_meta_logger = logging.getLogger(__name__)
23+
24+
TRACE_ID_KEY = 'traceId'
25+
SPAN_ID_KEY = 'spanId'
26+
SAMPLING_DECISION_KEY = 'traceSampled'
27+
28+
LogAttrs = namedtuple('LogAttrs', ['trace_id', 'span_id', 'sampling_decision'])
29+
ATTR_DEFAULTS = LogAttrs("00000000000000000000000000000000",
30+
"0000000000000000", False)
31+
32+
33+
def get_log_attrs():
34+
"""Get logging attributes from the opencensus context.
35+
36+
:rtype: :class:`LogAttrs`
37+
:return: The current span's trace ID, span ID, and sampling decision.
38+
"""
39+
try:
40+
tracer = execution_context.get_opencensus_tracer()
41+
if tracer is None:
42+
raise RuntimeError
43+
except Exception: # noqa
44+
_meta_logger.error("Failed to get opencensus tracer")
45+
return ATTR_DEFAULTS
46+
47+
try:
48+
trace_id = tracer.span_context.trace_id
49+
if trace_id is None:
50+
trace_id = ATTR_DEFAULTS.trace_id
51+
except Exception: # noqa
52+
_meta_logger.error("Failed to get opencensus trace ID")
53+
trace_id = ATTR_DEFAULTS.trace_id
54+
55+
try:
56+
span_id = tracer.span_context.span_id
57+
if span_id is None:
58+
span_id = ATTR_DEFAULTS.span_id
59+
except Exception: # noqa
60+
_meta_logger.error("Failed to get opencensus span ID")
61+
span_id = ATTR_DEFAULTS.span_id
62+
63+
try:
64+
sampling_decision = tracer.span_context.trace_options.get_enabled
65+
if sampling_decision is None:
66+
sampling_decision = ATTR_DEFAULTS.sampling_decision
67+
except AttributeError:
68+
sampling_decision = ATTR_DEFAULTS.sampling_decision
69+
except Exception: # noqa
70+
_meta_logger.error("Failed to get opencensus sampling decision")
71+
sampling_decision = ATTR_DEFAULTS.sampling_decision
72+
73+
return LogAttrs(trace_id, span_id, sampling_decision)
74+
75+
76+
def _set_extra_attrs(extra):
77+
trace_id, span_id, sampling_decision = get_log_attrs()
78+
extra.setdefault(TRACE_ID_KEY, trace_id)
79+
extra.setdefault(SPAN_ID_KEY, span_id)
80+
extra.setdefault(SAMPLING_DECISION_KEY, sampling_decision)
81+
82+
83+
# See
84+
# https://docs.python.org/3.7/library/logging.html#loggeradapter-objects,
85+
# https://docs.python.org/3.7/howto/logging-cookbook.html#context-info
86+
class TraceLoggingAdapter(logging.LoggerAdapter):
87+
"""Adapter to add opencensus context attrs to records."""
88+
def process(self, msg, kwargs):
89+
kwargs = copy(kwargs)
90+
if self.extra:
91+
extra = copy(self.extra)
92+
else:
93+
extra = {}
94+
extra.update(kwargs.get('extra', {}))
95+
_set_extra_attrs(extra)
96+
kwargs['extra'] = extra
97+
98+
return (msg, kwargs)
99+
100+
101+
# This is the idiomatic way to stack logger customizations, see
102+
# https://docs.python.org/3.7/library/logging.html#logging.getLoggerClass
103+
class TraceLogger(logging.getLoggerClass()):
104+
"""Logger class that adds opencensus context attrs to records."""
105+
def makeRecord(self, *args, **kwargs):
106+
try:
107+
extra = args[8]
108+
if extra is None:
109+
extra = {}
110+
args = tuple(list(args[:8]) + [extra] + list(args[9:]))
111+
except IndexError: # pragma: NO COVER
112+
extra = kwargs.setdefault('extra', {})
113+
if extra is None:
114+
kwargs['extra'] = extra
115+
_set_extra_attrs(extra)
116+
return super(TraceLogger, self).makeRecord(*args, **kwargs)
117+
118+
119+
def use_oc_logging():
120+
"""Replace the global default logging class with `TraceLogger`.
121+
122+
Loggers created after calling `use_oc_logging` will produce `LogRecord`s
123+
with extra traceId, spanId, and traceSampled attributes from the opencensus
124+
context.
125+
"""
126+
logging.setLoggerClass(TraceLogger)

requirements-test.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ retrying==1.3.3
77
WebOb==1.7.3
88
thrift==0.10.0
99
prometheus_client==0.5.0
10+
unittest2==1.1.0

0 commit comments

Comments
 (0)