Skip to content
This repository was archived by the owner on Aug 13, 2025. It is now read-only.

Commit e2c760e

Browse files
authored
Merge pull request #43 from honeycombio/tredman.span-wrapper
[tracing] Add function decorator 'wrap' to API
2 parents 47cd48c + 2ed7523 commit e2c760e

File tree

4 files changed

+92
-13
lines changed

4 files changed

+92
-13
lines changed

beeline/__init__.py

Lines changed: 55 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ def add_field(self, name, value):
8585
8686
`beeline.add_field("my field", "my value")`
8787
88-
If a field is being attributed to the wrong span/event,
88+
If a field is being attributed to the wrong span/event,
8989
make sure that `new_event` and `close_event` calls are matched.
9090
'''
9191
# fetch the current event from our tracer
@@ -113,7 +113,7 @@ def tracer(self, name, trace_id=None, parent_id=None):
113113
return self.tracer_impl(name=name, trace_id=trace_id, parent_id=parent_id)
114114

115115
def new_event(self, data=None, trace_name=''):
116-
''' DEPRECATED: Helper method that wraps `start_trace` and
116+
''' DEPRECATED: Helper method that wraps `start_trace` and
117117
`start_span`. It is better to use these methods as it provides
118118
better control and context around how traces are implemented in your
119119
app.
@@ -164,6 +164,15 @@ def send_all(self):
164164
self.tracer_impl.finish_span(span)
165165
span = self.tracer_impl.get_active_span()
166166

167+
def traced(self, name, trace_id=None, parent_id=None):
168+
def wrapped(fn, *args, **kwargs):
169+
def inner(*args, **kwargs):
170+
with self.tracer(name=name, trace_id=trace_id, parent_id=parent_id):
171+
return fn(*args, **kwargs)
172+
return inner
173+
174+
return wrapped
175+
167176
def _run_hooks_and_send(self, ev):
168177
''' internal - run any defined hooks on the event and send '''
169178
presampled = False
@@ -308,7 +317,7 @@ def add_context_field(name, value):
308317

309318
def remove_context_field(name):
310319
''' Remove a single field from the current span.
311-
320+
312321
```
313322
beeline.add_context({ "first_field": "a", "second_field": "b"})
314323
beeline.remove_context_field("second_field")
@@ -323,7 +332,7 @@ def remove_context_field(name):
323332
def add_trace_field(name, value):
324333
''' Similar to `add_context_field` - adds a field to the current span, but
325334
also to all other future spans in this trace. Trace context fields will be
326-
propagated to downstream services if using instrumented libraries
335+
propagated to downstream services if using instrumented libraries
327336
like `requests`.
328337
329338
Args:
@@ -334,8 +343,8 @@ def add_trace_field(name, value):
334343
_GBL.tracer_impl.add_trace_field(name=name, value=value)
335344

336345
def remove_trace_field(name):
337-
''' Removes a trace context field from the current span. This will not
338-
affect other existing spans, but will prevent the field from being
346+
''' Removes a trace context field from the current span. This will not
347+
affect other existing spans, but will prevent the field from being
339348
propagated to new spans.
340349
341350
Args:
@@ -446,7 +455,7 @@ def marshal_trace_context():
446455

447456

448457
def new_event(data=None, trace_name=''):
449-
''' DEPRECATED: Helper method that wraps `start_trace` and
458+
''' DEPRECATED: Helper method that wraps `start_trace` and
450459
`start_span`. It is better to use these methods as it provides
451460
better control and context around how traces are implemented in your
452461
app.
@@ -464,7 +473,7 @@ def new_event(data=None, trace_name=''):
464473
'''
465474
if _GBL:
466475
_GBL.new_event(data=data, trace_name=trace_name)
467-
476+
468477
def send_event():
469478
''' DEPRECATED: Sends the currently active event (current span),
470479
if it exists.
@@ -508,3 +517,41 @@ def close():
508517
_GBL.close()
509518

510519
_GBL = None
520+
521+
def traced(name, trace_id=None, parent_id=None):
522+
'''
523+
Function decorator to wrap an entire function in a trace span. If no trace
524+
is active in the current thread, starts a new trace, and the wrapping span
525+
will be a root span. If a trace is active, creates a child span of the
526+
existing trace.
527+
528+
Example use:
529+
530+
```
531+
@traced(name="my_expensive_function")
532+
def my_func(n):
533+
recursive_fib(n)
534+
535+
my_func(100)
536+
```
537+
538+
Args:
539+
- `name`: a descriptive name for the this trace span, i.e. "function_name". This is required.
540+
- `trace_id`: the trace_id to use. If None, will be automatically generated.
541+
Use this if you want to explicitly resume a trace in this application that was
542+
initiated in another application, and you have the upstream trace_id.
543+
- `parent_id`: If trace_id is set, will populate the root span's parent
544+
with this id.
545+
'''
546+
547+
_beeline = get_beeline()
548+
if not _beeline:
549+
# just pass through if not initialized
550+
def wrapped(fn, *args, **kwargs):
551+
def inner(*args, **kwargs):
552+
return fn(*args, **kwargs)
553+
return inner
554+
555+
return wrapped
556+
557+
return _beeline.traced(name, trace_id, parent_id)

beeline/test_beeline.py

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,9 @@
77

88
class TestBeeline(unittest.TestCase):
99
def setUp(self):
10+
self.addCleanup(patch.stopall)
1011
self.m_gbl = patch('beeline._GBL').start()
1112

12-
def tearDown(self):
13-
self.m_gbl.stop()
14-
1513
def test_send_event(self):
1614
''' test correct behavior for send_event '''
1715
_beeline = beeline.Beeline()
@@ -158,3 +156,37 @@ def test_marshal_trace_context_returns_value(self):
158156
self.m_gbl.tracer_impl.marshal_trace_context.return_value = 'asdf'
159157
val = beeline.marshal_trace_context()
160158
self.assertEqual(val, 'asdf')
159+
160+
def test_trace_wrapper(self):
161+
''' ensure that the trace wrapper decorarates a function and starts a trace '''
162+
_beeline = beeline.Beeline()
163+
with patch('beeline.get_beeline') as m_gbl:
164+
m_gbl.return_value = _beeline
165+
_beeline.tracer_impl._run_hooks_and_send = Mock()
166+
167+
@beeline.traced(name="my_sum")
168+
def my_sum(a, b):
169+
return a + b
170+
171+
# this should accept the function's arguments normally and return the function's value
172+
# if there is one
173+
self.assertEqual(my_sum(1, 2), 3)
174+
# check that an event was sent, from which we can infer that the function was wrapped
175+
self.assertTrue(_beeline.tracer_impl._run_hooks_and_send.called)
176+
177+
class TestBeelineNotInitialized(unittest.TestCase):
178+
def setUp(self):
179+
self.addCleanup(patch.stopall)
180+
self.m_gbl = patch('beeline.get_beeline').start()
181+
self.m_gbl.return_value = None
182+
183+
def test_trace_wrapper(self):
184+
''' ensure the trace wrapper doesn't break if the beeline is not initialized '''
185+
self.assertIsNone(beeline.get_beeline())
186+
@beeline.traced(name="my_sum")
187+
def my_sum(a, b):
188+
return a + b
189+
190+
# this should not crash if the beeline isn't initialized
191+
# it should also accept arguments normally and return the function's value
192+
self.assertEqual(my_sum(1, 2), 3)

beeline/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
VERSION = '2.3.1'
1+
VERSION = '2.4.0'

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
setup(
55
python_requires='>=2.7',
66
name='honeycomb-beeline',
7-
version='2.3.1',
7+
version='2.4.0',
88
description='Honeycomb library for easy instrumentation',
99
url='https://github.com/honeycombio/beeline-python',
1010
author='Honeycomb.io',

0 commit comments

Comments
 (0)