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

Commit 3876a66

Browse files
feat: Add Environment & Services support (#213)
Tracing scenario for environment-aware teams: * Dataset no longer propagates across services for new environment key. * Add field `service.name` in addition to already added `service_name` * `ServiceName` "required"†. If unset, will warn and set `service_name` AND `service.name` to `unknown_service:<process>` or `unknown_service:language`. * Service name is used to populate dataset name; if `unknown_service.*` then truncate to`unknown_service` for dataset name. * Trim whitespace from dataset name * `WriteKey` required. If unset, will warn. * `Dataset` **ignored**. If unset, no warning (because no longer used). If set, will warn to clarify that it will be ignored in favor of service name. Tracing scenario for classic (non-environment-aware) teams: * Add field `service.name` in addition to already added `service_name` * `ServiceName` "required"†. If unset, will warn. * `WriteKey` required. If unset, will warn. * `Dataset` required. If unset, will warn and default to `beeline-<language>`. † While not technically required, service name is highly encouraged to avoid `unknown_service` and to instead properly describe the data being sent to Honeycomb (and the data being viewed in Honeycomb UI).
1 parent 0e9aad4 commit 3876a66

File tree

8 files changed

+182
-12
lines changed

8 files changed

+182
-12
lines changed

beeline/__init__.py

Lines changed: 64 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from beeline.version import VERSION
1111
from beeline import internal
1212
import beeline.propagation.default
13+
import beeline.propagation
1314
import sys
1415
# pyflakes
1516
assert internal
@@ -74,15 +75,68 @@ def __init__(self,
7475
if debug:
7576
self._init_logger()
7677

78+
def IsClassicKey(writekey):
79+
return len(writekey) == 32
80+
7781
# allow setting some values from the environment
7882
if not writekey:
7983
writekey = os.environ.get('HONEYCOMB_WRITEKEY', '')
84+
# also check API_KEY just in case
85+
if not writekey:
86+
writekey = os.environ.get('HONEYCOMB_API_KEY', '')
87+
if not writekey:
88+
logging.error(
89+
'writekey not set! set the writekey if you want to send data to honeycomb'
90+
)
91+
92+
if IsClassicKey(writekey):
93+
if not dataset:
94+
dataset = os.environ.get('HONEYCOMB_DATASET', '')
95+
if not dataset:
96+
logging.error(
97+
'dataset not set! set a value for dataset if you want to send data to honeycomb'
98+
)
99+
else:
100+
if dataset:
101+
logging.error(
102+
'dataset will be ignored in favor of service name'
103+
)
80104

81-
if not dataset:
82-
dataset = os.environ.get('HONEYCOMB_DATASET', '')
83-
105+
default_service_name = "unknown_service"
106+
process_name = os.environ.get('PROCESS_EXECUTABLE_NAME', '')
84107
if not service_name:
85-
service_name = os.environ.get('HONEYCOMB_SERVICE', dataset)
108+
service_name = os.environ.get('HONEYCOMB_SERVICE')
109+
# also check SERVICE_NAME just in case
110+
if not service_name:
111+
service_name = os.environ.get('SERVICE_NAME', '')
112+
# no service name, set default
113+
if not service_name:
114+
service_name = default_service_name
115+
if process_name:
116+
service_name += ":" + process_name
117+
else:
118+
service_name += ":python"
119+
logging.error(
120+
'service name not set! service name will be ' + service_name
121+
)
122+
if not IsClassicKey(writekey):
123+
logging.error(
124+
'data will be sent to unknown_service'
125+
)
126+
127+
if not IsClassicKey(writekey):
128+
# set dataset based on service name
129+
dataset = service_name
130+
if dataset.strip() != dataset:
131+
# whitespace detected. trim whitespace, warn on diff
132+
logging.error(
133+
'service name has unexpected spaces'
134+
)
135+
dataset = service_name.strip()
136+
137+
# set default, truncate to unknown_service if needed
138+
if dataset == "" or dataset.startswith("unknown_service"):
139+
dataset = "unknown_service"
86140

87141
self.client = Client(
88142
writekey=writekey, dataset=dataset, sample_rate=sample_rate,
@@ -94,16 +148,16 @@ def __init__(self,
94148
debug=debug,
95149
)
96150

151+
if IsClassicKey(writekey):
152+
beeline.propagation.propagate_dataset = True
153+
else:
154+
beeline.propagation.propagate_dataset = False
155+
97156
self.log('initialized honeycomb client: writekey=%s dataset=%s service_name=%s',
98157
writekey, dataset, service_name)
99-
if not writekey:
100-
self.log(
101-
'writekey not set! set the writekey if you want to send data to honeycomb')
102-
if not dataset:
103-
self.log(
104-
'dataset not set! set a value for dataset if you want to send data to honeycomb')
105158

106159
self.client.add_field('service_name', service_name)
160+
self.client.add_field('service.name', service_name)
107161
self.client.add_field('meta.beeline_version', VERSION)
108162
self.client.add_field('meta.local_hostname', socket.gethostname())
109163

beeline/propagation/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
import beeline
44

55

6+
def propagate_dataset():
7+
return bool
8+
9+
610
class PropagationContext(object):
711
'''
812
PropagationContext represents information that can either be read from, or

beeline/propagation/honeycomb.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ def marshal_propagation_context(propagation_context):
5151
"parent_id={}".format(propagation_context.parent_id),
5252
"context={}".format(trace_fields)]
5353

54-
if propagation_context.dataset:
54+
if beeline.propagation.propagate_dataset and propagation_context.dataset:
5555
components.insert(0, "dataset={}".format(quote(propagation_context.dataset)))
5656

5757
trace_header = "{};{}".format(version, ",".join(components))
@@ -94,7 +94,7 @@ def unmarshal_propagation_context_with_dataset(trace_header):
9494
parent_id = v
9595
elif k == 'context':
9696
context = json.loads(base64.b64decode(v.encode()).decode())
97-
elif k == 'dataset':
97+
elif k == 'dataset' and beeline.propagation.propagate_dataset:
9898
dataset = unquote(v)
9999

100100
# context should be a dict

beeline/propagation/test_honeycomb.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import unittest
2+
import beeline.propagation
23
from beeline.propagation import DictRequest, PropagationContext
34
import beeline.propagation.honeycomb as hc
45

@@ -21,6 +22,7 @@ def test_roundtrip(self):
2122

2223
def test_roundtrip_with_dataset(self):
2324
'''Verify that we can successfully roundtrip (marshal and unmarshal)'''
25+
beeline.propagation.propagate_dataset = True
2426
dataset = "blorp blorp"
2527
trace_id = "bloop"
2628
parent_id = "scoop"
@@ -34,6 +36,22 @@ def test_roundtrip_with_dataset(self):
3436
self.assertEqual(parent_id, new_parent_id)
3537
self.assertEqual(trace_fields, new_trace_fields)
3638

39+
def test_roundtrip_with_dataset_propagation_disabled(self):
40+
'''Verify that we can successfully roundtrip (marshal and unmarshal) without dataset propagation'''
41+
beeline.propagation.propagate_dataset = False
42+
dataset = "blorp blorp"
43+
trace_id = "bloop"
44+
parent_id = "scoop"
45+
trace_fields = {"key": "value"}
46+
pc = PropagationContext(trace_id, parent_id, trace_fields, dataset)
47+
header = hc.marshal_propagation_context(pc)
48+
new_trace_id, new_parent_id, new_trace_fields, new_dataset = hc.unmarshal_propagation_context_with_dataset(
49+
header)
50+
self.assertIsNone(new_dataset)
51+
self.assertEqual(trace_id, new_trace_id)
52+
self.assertEqual(parent_id, new_parent_id)
53+
self.assertEqual(trace_fields, new_trace_fields)
54+
3755

3856
class TestHoneycombHTTPTraceParserHook(unittest.TestCase):
3957
def test_has_header(self):
@@ -55,6 +73,7 @@ def test_no_header(self):
5573

5674
class TestHoneycombHTTPTracePropagationHook(unittest.TestCase):
5775
def test_generates_correct_header(self):
76+
beeline.propagation.propagate_dataset = True
5877
dataset = "blorp blorp"
5978
trace_id = "bloop"
6079
parent_id = "scoop"
@@ -65,3 +84,16 @@ def test_generates_correct_header(self):
6584
self.assertIn('X-Honeycomb-Trace', headers)
6685
self.assertEqual(headers['X-Honeycomb-Trace'],
6786
"1;dataset=blorp%20blorp,trace_id=bloop,parent_id=scoop,context=eyJrZXkiOiAidmFsdWUifQ==")
87+
88+
def test_generates_correct_header_with_dataset_propagation_disabled(self):
89+
beeline.propagation.propagate_dataset = False
90+
dataset = "blorp blorp"
91+
trace_id = "bloop"
92+
parent_id = "scoop"
93+
trace_fields = {"key": "value"}
94+
pc = PropagationContext(
95+
trace_id, parent_id, trace_fields, dataset)
96+
headers = hc.http_trace_propagation_hook(pc)
97+
self.assertIn('X-Honeycomb-Trace', headers)
98+
self.assertEqual(headers['X-Honeycomb-Trace'],
99+
"1;trace_id=bloop,parent_id=scoop,context=eyJrZXkiOiAidmFsdWUifQ==")

beeline/test_trace.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import re
88
import os
99
import binascii
10+
import beeline.propagation
1011

1112
from libhoney import Event
1213

@@ -624,6 +625,7 @@ def test_span_context(self):
624625

625626
class TestPropagationHooks(unittest.TestCase):
626627
def test_propagate_and_start_trace_uses_honeycomb_header(self):
628+
beeline.propagation.propagate_dataset = True
627629
# FIXME: Test basics, including error handling and custom hooks
628630

629631
# implicitly tests finish_span
@@ -653,6 +655,40 @@ def test_propagate_and_start_trace_uses_honeycomb_header(self):
653655
# ensure that there is no current trace
654656
self.assertIsNone(tracer._trace)
655657

658+
def test_propagate_and_start_trace_uses_honeycomb_header_with_dataset_propagation_disabled(self):
659+
beeline.propagation.propagate_dataset = False
660+
# FIXME: Test basics, including error handling and custom hooks
661+
662+
# implicitly tests finish_span
663+
m_client = Mock()
664+
# these values are used before sending
665+
m_client.new_event.return_value.start_time = datetime.datetime.now()
666+
m_client.new_event.return_value.sample_rate = 1
667+
tracer = SynchronousTracer(m_client)
668+
669+
header_value = '1;dataset=flibble,trace_id=bloop,parent_id=scoop,context=e30K'
670+
req = DictRequest({
671+
# case shouldn't matter
672+
'X-HoNEyComb-TrACE': header_value,
673+
})
674+
675+
span = tracer.propagate_and_start_trace(
676+
context={'big': 'important_stuff'}, request=req)
677+
self.assertEqual(tracer._trace.stack[0], span)
678+
self.assertEqual(span.trace_id, "bloop")
679+
self.assertEqual(span.parent_id, "scoop")
680+
681+
tracer.finish_trace(span)
682+
683+
# testing the absence of dataset
684+
self.assertNotEqual(getattr(span.event, 'dataset'), "flibble")
685+
self.assertNotIsInstance(span.event.dataset, str)
686+
687+
# ensure the event is sent
688+
span.event.send_presampled.assert_called_once_with()
689+
# ensure that there is no current trace
690+
self.assertIsNone(tracer._trace)
691+
656692
def test_propagate_and_start_trace_uses_w3c_header_as_fallback(self):
657693
# implicitly tests finish_span
658694
m_client = Mock()

examples/hello-world/README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# hello world example
2+
3+
## Setup and Run
4+
5+
1. Set environment variable for `HONEYCOMB_API_KEY`
6+
1. In top-level directory of repo, run `poetry build` to create a wheel package in the `dist` directory
7+
1. In hello-world directory, ensure version of beeline in `pyproject.toml` matches the wheel package
8+
1. In hello-world directory, run `poetry install`
9+
1. In hello-world directory, run `poetry run python3 app.py`

examples/hello-world/app.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import beeline
2+
import os
3+
4+
beeline.init(
5+
# Get this via https://ui.honeycomb.io/account after signing up for Honeycomb
6+
writekey=os.environ.get('HONEYCOMB_API_KEY'),
7+
api_host=os.environ.get('HONEYCOMB_API_ENDPOINT', 'http://api.honeycomb.io:443'),
8+
# The name of your app is a good choice to start with
9+
# dataset='my-python-beeline-app', # only needed for classic
10+
service_name=os.environ.get('SERVICE_NAME', 'my-python-app'),
11+
debug=True, # enable to see telemetry in console
12+
)
13+
14+
@beeline.traced(name='hello_world')
15+
def hello_world():
16+
print('hello world')
17+
beeline.add_context_field('message', 'hello world')
18+
19+
hello_world()
20+
21+
beeline.close()

examples/hello-world/pyproject.toml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
[tool.poetry]
2+
name = "hello-world"
3+
version = "0.1.0"
4+
description = "print hello world"
5+
authors = ["honeycombio"]
6+
7+
[tool.poetry.dependencies]
8+
python = "^3.9"
9+
honeycomb-beeline = {path = "../../dist/honeycomb_beeline-3.42.99-py3-none-any.whl", develop = false}
10+
# honeycomb-beeline = "^3.2.0"
11+
12+
[build-system]
13+
requires = ["poetry-core>=1.0.0"]
14+
build-backend = "poetry.core.masonry.api"

0 commit comments

Comments
 (0)