Skip to content

Commit 6a502a6

Browse files
stevewwpglombardo
authored andcommitted
New logging instrumentation (#128)
* logging instrumentation * fix k * error handling * fix up registered kind * logging tests
1 parent e8c5b36 commit 6a502a6

File tree

6 files changed

+123
-4
lines changed

6 files changed

+123
-4
lines changed

instana/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ def boot_agent():
6363
# Import & initialize instrumentation
6464
if (sys.version_info >= (3, 4)) and (sys.version_info < (3, 7)):
6565
from .instrumentation import asynqp # noqa
66+
from .instrumentation import logging # noqa
6667
from .instrumentation import mysqlpython # noqa
6768
from .instrumentation import redis # noqa
6869
from .instrumentation import sqlalchemy # noqa

instana/instrumentation/logging.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
from __future__ import absolute_import
2+
3+
import wrapt
4+
import logging
5+
import sys
6+
7+
from ..log import logger
8+
from ..singletons import tracer
9+
10+
@wrapt.patch_function_wrapper('logging', 'Logger._log')
11+
def log_with_instana(wrapped, instance, argv, kwargs):
12+
# argv[0] = level
13+
# argv[1] = message
14+
# argv[2] = args for message
15+
try:
16+
parent_span = tracer.active_span
17+
18+
# Only needed if we're tracing and serious log
19+
if parent_span and argv[0] >= logging.WARN:
20+
# get the formatted log message
21+
msg = argv[1] % argv[2]
22+
23+
# get additional information if an exception is being handled
24+
parameters = None
25+
(t, v, tb) = sys.exc_info()
26+
if t is not None and v is not None:
27+
parameters = '{} {}'.format(t , v)
28+
29+
# create logging span
30+
with tracer.start_active_span('log', child_of=parent_span) as scope:
31+
scope.span.log_kv({ 'message': msg })
32+
if parameters is not None:
33+
scope.span.log_kv({ 'parameters': parameters })
34+
# extra tags for an error
35+
if argv[0] >= logging.ERROR:
36+
scope.span.set_tag('error', True)
37+
ec = scope.span.tags.get('ec', 0)
38+
scope.span.set_tag('ec', ec + 1)
39+
except Exception as e:
40+
logger.debug('Exception: %s', e, exc_info=True)
41+
finally:
42+
return wrapped(*argv, **kwargs)
43+
44+
logger.debug('Instrumenting logging')
45+

instana/json_span.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
class JsonSpan(object):
2+
k = None
23
t = 0
34
p = None
45
s = 0
@@ -36,6 +37,7 @@ class Data(object):
3637
service = None
3738
sqlalchemy = None
3839
soap = None
40+
log = None
3941

4042
def __init__(self, **kwds):
4143
self.__dict__.update(kwds)

instana/recorder.py

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,11 @@
2424

2525
class InstanaRecorder(SpanRecorder):
2626
registered_spans = ("django", "memcache", "mysql", "rabbitmq", "redis",
27-
"rpc-client", "rpc-server", "sqlalchemy", "soap", "urllib3", "wsgi")
27+
"rpc-client", "rpc-server", "sqlalchemy", "soap", "urllib3", "wsgi", "log")
2828
http_spans = ("django", "wsgi", "urllib3", "soap")
2929

3030
exit_spans = ("memcache", "mysql", "rabbitmq", "redis", "rpc-client",
31-
"sqlalchemy", "soap", "urllib3")
31+
"sqlalchemy", "soap", "urllib3", "log")
3232
entry_spans = ("django", "wsgi", "rabbitmq", "rpc-server")
3333

3434
entry_kind = ["entry", "server", "consumer"]
@@ -95,6 +95,13 @@ def build_registered_span(self, span):
9595
""" Takes a BasicSpan and converts it into a registered JsonSpan """
9696
data = Data(baggage=span.context.baggage)
9797

98+
kind = 1 # entry
99+
if span.operation_name in self.exit_spans:
100+
kind = 2 # exit
101+
# log is a special case as it is not entry nor exit
102+
if span.operation_name == "log":
103+
kind = 3 # intermediate span
104+
98105
logs = self.collect_logs(span)
99106
if len(logs) > 0:
100107
if data.custom is None:
@@ -116,6 +123,8 @@ def build_registered_span(self, span):
116123
sort=span.tags.pop('sort', None),
117124
address=span.tags.pop('address', None),
118125
key=span.tags.pop('key', None))
126+
if data.rabbitmq.sort == 'consume':
127+
kind = 1 # entry
119128

120129
if span.operation_name == "redis":
121130
data.redis = RedisData(connection=span.tags.pop('connection', None),
@@ -152,10 +161,21 @@ def build_registered_span(self, span):
152161
tskey = list(data.custom.logs.keys())[0]
153162
data.mysql.error = data.custom.logs[tskey]['message']
154163

164+
if span.operation_name == "log":
165+
data.log = {}
166+
# use last special key values
167+
# TODO - logic might need a tweak here
168+
for l in span.logs:
169+
if "message" in l.key_values:
170+
data.log["message"] = l.key_values.pop("message", None)
171+
if "parameters" in l.key_values:
172+
data.log["parameters"] = l.key_values.pop("parameters", None)
173+
155174
entity_from = {'e': instana.singletons.agent.from_.pid,
156175
'h': instana.singletons.agent.from_.agentUuid}
157176

158177
json_span = JsonSpan(n=span.operation_name,
178+
k=kind,
159179
t=span.context.trace_id,
160180
p=span.parent_id,
161181
s=span.context.span_id,
@@ -234,7 +254,8 @@ def get_span_kind(self, span):
234254
elif span.tags["span.kind"] in self.exit_kind:
235255
kind = "exit"
236256
else:
237-
kind = "local"
257+
kind = "intermediate"
258+
238259
return kind
239260

240261
def collect_logs(self, span):

tests/test_logging.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
from __future__ import absolute_import
2+
3+
import logging
4+
import unittest
5+
from instana.singletons import tracer
6+
7+
8+
class TestLogging(unittest.TestCase):
9+
def setUp(self):
10+
""" Clear all spans before a test run """
11+
self.recorder = tracer.recorder
12+
self.recorder.clear_spans()
13+
self.logger = logging.getLogger('unit test')
14+
15+
def tearDown(self):
16+
""" Do nothing for now """
17+
return None
18+
19+
def test_no_span(self):
20+
with tracer.start_active_span('test'):
21+
self.logger.info('info message')
22+
23+
24+
spans = self.recorder.queued_spans()
25+
self.assertEqual(1, len(spans))
26+
27+
def test_extra_span(self):
28+
with tracer.start_active_span('test'):
29+
self.logger.warn('foo %s', 'bar')
30+
31+
spans = self.recorder.queued_spans()
32+
self.assertEqual(2, len(spans))
33+
self.assertEqual(3, spans[0].k) # intermediate kind
34+
35+
self.assertEqual('foo bar', spans[0].data.log.get('message'))
36+
37+
def test_parameters(self):
38+
with tracer.start_active_span('test'):
39+
try:
40+
a = 42
41+
b = 0
42+
c = a / b
43+
except Exception as e:
44+
self.logger.exception('Exception: %s', str(e))
45+
46+
spans = self.recorder.queued_spans()
47+
self.assertEqual(2, len(spans))
48+
49+
self.assertIsNotNone(spans[0].data.log.get('parameters'))
50+

tests/test_ot_span.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,4 +127,4 @@ def test_span_kind(self):
127127
assert_equals('exit', span.data.sdk.Type)
128128

129129
span = spans[4]
130-
assert_equals('local', span.data.sdk.Type)
130+
assert_equals('intermediate', span.data.sdk.Type)

0 commit comments

Comments
 (0)