Skip to content

Commit 7652590

Browse files
Merge pull request #11 from appoptics/add-custom-propagator
NH-2313 Add basic TraceState handling and W3C trace context propagation
2 parents e1e56af + 7e758df commit 7652590

12 files changed

+1016
-110
lines changed
Original file line numberDiff line numberDiff line change
@@ -1 +1,10 @@
11
__version__ = "0.0.1"
2+
3+
COMMA = ","
4+
COMMA_W3C_SANITIZED = "...."
5+
EQUALS = "="
6+
EQUALS_W3C_SANITIZED = "####"
7+
SW_TRACESTATE_KEY = "sw"
8+
OTEL_CONTEXT_SW_OPTIONS_KEY = "sw_xtraceoptions"
9+
OTEL_CONTEXT_SW_SIGNATURE_KEY = "sw_signature"
10+
DEFAULT_SW_TRACES_EXPORTER = "solarwinds_exporter"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
"""Module to initialize OpenTelemetry SDK components to work with SolarWinds backend"""
2+
3+
import logging
4+
from os import environ
5+
from pkg_resources import (
6+
iter_entry_points,
7+
load_entry_point
8+
)
9+
10+
from opentelemetry import trace
11+
from opentelemetry.environment_variables import (
12+
OTEL_PROPAGATORS,
13+
OTEL_TRACES_EXPORTER
14+
)
15+
from opentelemetry.instrumentation.propagators import set_global_response_propagator
16+
from opentelemetry.propagate import set_global_textmap
17+
from opentelemetry.propagators.composite import CompositePropagator
18+
from opentelemetry.sdk._configuration import _OTelSDKConfigurator
19+
from opentelemetry.sdk.environment_variables import OTEL_TRACES_SAMPLER
20+
from opentelemetry.sdk.trace import (
21+
sampling,
22+
TracerProvider
23+
)
24+
from opentelemetry.sdk.trace.export import BatchSpanProcessor
25+
26+
from opentelemetry_distro_solarwinds import DEFAULT_SW_TRACES_EXPORTER
27+
from opentelemetry_distro_solarwinds.extension.oboe import Reporter
28+
from opentelemetry_distro_solarwinds.response_propagator import SolarWindsTraceResponsePropagator
29+
30+
logger = logging.getLogger(__name__)
31+
32+
class SolarWindsConfigurator(_OTelSDKConfigurator):
33+
"""OpenTelemetry Configurator for initializing SolarWinds-reporting SDK components"""
34+
35+
# Cannot set as env default because not part of OTel Python _KNOWN_SAMPLERS
36+
# https://github.com/open-telemetry/opentelemetry-python/blob/main/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py#L364-L380
37+
_DEFAULT_SW_TRACES_SAMPLER = "solarwinds_sampler"
38+
39+
def _configure(self, **kwargs):
40+
"""Configure OTel sampler, exporter, propagator, response propagator"""
41+
reporter = self._initialize_solarwinds_reporter()
42+
self._configure_sampler()
43+
self._configure_exporter(reporter)
44+
self._configure_propagator()
45+
# Set global HTTP response propagator
46+
set_global_response_propagator(SolarWindsTraceResponsePropagator())
47+
48+
def _configure_sampler(self):
49+
"""Always configure SolarWinds OTel sampler"""
50+
try:
51+
sampler = load_entry_point(
52+
"opentelemetry_distro_solarwinds",
53+
"opentelemetry_traces_sampler",
54+
self._DEFAULT_SW_TRACES_SAMPLER
55+
)()
56+
except:
57+
logger.exception(
58+
"Failed to load configured sampler {}".format(
59+
self._DEFAULT_SW_TRACES_SAMPLER
60+
)
61+
)
62+
raise
63+
trace.set_tracer_provider(
64+
TracerProvider(sampler=sampler)
65+
)
66+
67+
def _configure_exporter(self, reporter):
68+
"""Configure SolarWinds or env-specified OTel span exporter.
69+
Initialization of SolarWinds exporter requires a liboboe reporter."""
70+
exporter = None
71+
environ_exporter_name = environ.get(OTEL_TRACES_EXPORTER)
72+
73+
if environ_exporter_name == DEFAULT_SW_TRACES_EXPORTER:
74+
try:
75+
exporter = load_entry_point(
76+
"opentelemetry_distro_solarwinds",
77+
"opentelemetry_traces_exporter",
78+
environ_exporter_name
79+
)(reporter)
80+
except:
81+
logger.exception(
82+
"Failed to load configured exporter {} with reporter".format(
83+
environ_exporter_name
84+
)
85+
)
86+
raise
87+
else:
88+
try:
89+
exporter = next(
90+
iter_entry_points(
91+
"opentelemetry_traces_exporter",
92+
environ_exporter_name
93+
)
94+
).load()()
95+
except:
96+
logger.exception(
97+
"Failed to load configured exporter {}".format(
98+
environ_exporter_name
99+
)
100+
)
101+
raise
102+
span_processor = BatchSpanProcessor(exporter)
103+
trace.get_tracer_provider().add_span_processor(span_processor)
104+
105+
def _configure_propagator(self):
106+
"""Configure CompositePropagator with SolarWinds and other propagators"""
107+
propagators = []
108+
environ_propagators_names = environ.get(OTEL_PROPAGATORS).split(",")
109+
for propagator_name in environ_propagators_names:
110+
try:
111+
propagators.append(
112+
next(
113+
iter_entry_points("opentelemetry_propagator", propagator_name)
114+
).load()()
115+
)
116+
except Exception:
117+
logger.exception(
118+
"Failed to load configured propagator {}".format(
119+
propagator_name
120+
)
121+
)
122+
raise
123+
set_global_textmap(CompositePropagator(propagators))
124+
125+
def _initialize_solarwinds_reporter(self) -> Reporter:
126+
"""Initialize SolarWinds reporter used by sampler and exporter. This establishes collector and sampling settings in a background thread."""
127+
log_level = environ.get('SOLARWINDS_DEBUG_LEVEL', 3)
128+
try:
129+
log_level = int(log_level)
130+
except ValueError:
131+
log_level = 3
132+
# TODO make some of these customizable
133+
return Reporter(
134+
hostname_alias='',
135+
log_level=log_level,
136+
log_file_path='',
137+
max_transactions=-1,
138+
max_flush_wait_time=-1,
139+
events_flush_interval=-1,
140+
max_request_size_bytes=-1,
141+
reporter='ssl',
142+
host=environ.get('SOLARWINDS_COLLECTOR', ''),
143+
service_key=environ.get('SOLARWINDS_SERVICE_KEY', ''),
144+
trusted_path='',
145+
buffer_size=-1,
146+
trace_metrics=-1,
147+
histogram_precision=-1,
148+
token_bucket_capacity=-1,
149+
token_bucket_rate=-1,
150+
file_single=0,
151+
ec2_metadata_timeout=1000,
152+
grpc_proxy='',
153+
stdout_clear_nonblocking=0,
154+
is_grpc_clean_hack_enabled=False,
155+
w3c_trace_format=1,
156+
)
+44-16
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,52 @@
1-
"""Module to configure OpenTelemetry agent to work with SolarWinds backend"""
1+
"""Module to configure OpenTelemetry to work with SolarWinds backend"""
22

3-
from opentelemetry import trace
3+
import logging
4+
from os import environ
5+
6+
from opentelemetry.environment_variables import (
7+
OTEL_PROPAGATORS,
8+
OTEL_TRACES_EXPORTER
9+
)
410
from opentelemetry.instrumentation.distro import BaseDistro
5-
from opentelemetry.sdk.trace import TracerProvider
6-
from opentelemetry.sdk.trace.export import BatchSpanProcessor
711

8-
from opentelemetry_distro_solarwinds.exporter import SolarWindsSpanExporter
9-
from opentelemetry_distro_solarwinds.sampler import ParentBasedAoSampler
12+
from opentelemetry_distro_solarwinds import DEFAULT_SW_TRACES_EXPORTER
1013

14+
logger = logging.getLogger(__name__)
1115

1216
class SolarWindsDistro(BaseDistro):
13-
"""SolarWinds custom distro for OpenTelemetry agents.
17+
"""OpenTelemetry Distro for SolarWinds reporting environment"""
18+
19+
_TRACECONTEXT_PROPAGATOR = "tracecontext"
20+
_SW_PROPAGATOR = "solarwinds_propagator"
21+
_DEFAULT_SW_PROPAGATORS = [
22+
_TRACECONTEXT_PROPAGATOR,
23+
"baggage",
24+
_SW_PROPAGATOR,
25+
]
1426

15-
With this custom distro, the following functionality is introduced:
16-
- no functionality added at this time
17-
"""
1827
def _configure(self, **kwargs):
19-
# automatically make use of custom SolarWinds sampler
20-
trace.set_tracer_provider(
21-
TracerProvider(sampler=ParentBasedAoSampler()))
22-
# Automatically configure the SolarWinds Span exporter
23-
span_exporter = BatchSpanProcessor(SolarWindsSpanExporter())
24-
trace.get_tracer_provider().add_span_processor(span_exporter)
28+
"""Configure OTel exporter and propagators"""
29+
environ.setdefault(OTEL_TRACES_EXPORTER, DEFAULT_SW_TRACES_EXPORTER)
30+
31+
environ_propagators = environ.get(
32+
OTEL_PROPAGATORS,
33+
",".join(self._DEFAULT_SW_PROPAGATORS)
34+
).split(",")
35+
# If not using the default propagators,
36+
# can any arbitrary list BUT
37+
# (1) must include tracecontext and solarwinds_propagator
38+
# (2) tracecontext must be before solarwinds_propagator
39+
if environ_propagators != self._DEFAULT_SW_PROPAGATORS:
40+
if not self._TRACECONTEXT_PROPAGATOR in environ_propagators or \
41+
not self._SW_PROPAGATOR in environ_propagators:
42+
raise ValueError("Must include tracecontext and solarwinds_propagator in OTEL_PROPAGATORS to use SolarWinds Observability.")
43+
44+
if environ_propagators.index(self._SW_PROPAGATOR) \
45+
< environ_propagators.index(self._TRACECONTEXT_PROPAGATOR):
46+
raise ValueError("tracecontext must be before solarwinds_propagator in OTEL_PROPAGATORS to use SolarWinds Observability.")
47+
environ[OTEL_PROPAGATORS] = ",".join(environ_propagators)
48+
49+
logger.debug("Configured SolarWindsDistro: {}, {}".format(
50+
environ.get(OTEL_TRACES_EXPORTER),
51+
environ.get(OTEL_PROPAGATORS)
52+
))

opentelemetry_distro_solarwinds/exporter.py

+21-52
Original file line numberDiff line numberDiff line change
@@ -5,26 +5,25 @@
55
"""
66

77
import logging
8-
import os
98

109
from opentelemetry.sdk.trace.export import SpanExporter
1110

12-
from opentelemetry_distro_solarwinds.extension.oboe import (Context, Metadata,
13-
Reporter)
14-
from opentelemetry_distro_solarwinds.ot_ao_transformer import transform_id
11+
from opentelemetry_distro_solarwinds.extension.oboe import (
12+
Context,
13+
Metadata
14+
)
15+
from opentelemetry_distro_solarwinds.w3c_transformer import W3CTransformer
1516

1617
logger = logging.getLogger(__file__)
1718

1819

1920
class SolarWindsSpanExporter(SpanExporter):
20-
"""SolarWinds span exporter.
21-
22-
Reports instrumentation data to the SolarWinds backend.
21+
"""SolarWinds custom span exporter for the SolarWinds backend.
22+
Initialization requires a liboboe reporter.
2323
"""
24-
def __init__(self, *args, **kw_args):
24+
def __init__(self, reporter, *args, **kw_args):
2525
super().__init__(*args, **kw_args)
26-
self.reporter = None
27-
self._initialize_solarwinds_reporter()
26+
self.reporter = reporter
2827

2928
def export(self, spans):
3029
"""Export to AO events and report via liboboe.
@@ -36,31 +35,31 @@ def export(self, spans):
3635
md = self._build_metadata(span.get_span_context())
3736
if span.parent and span.parent.is_valid:
3837
# If there is a parent, we need to add an edge to this parent to this entry event
39-
logger.debug("Continue trace from %s", md.toString())
38+
logger.debug("Continue trace from {}".format(md.toString()))
4039
parent_md = self._build_metadata(span.parent)
41-
evt = Context.startTrace(md, int(span.start_time / 1000),
40+
evt = Context.createEntry(md, int(span.start_time / 1000),
4241
parent_md)
4342
else:
4443
# In OpenTelemrtry, there are no events with individual IDs, but only a span ID
4544
# and trace ID. Thus, the entry event needs to be generated such that it has the
4645
# same op ID as the span ID of the OTel span.
47-
logger.debug("Start a new trace %s", md.toString())
48-
evt = Context.startTrace(md, int(span.start_time / 1000))
46+
logger.debug("Start a new trace {}".format(md.toString()))
47+
evt = Context.createEntry(md, int(span.start_time / 1000))
4948
evt.addInfo('Layer', span.name)
5049
evt.addInfo('Language', 'Python')
5150
for k, v in span.attributes.items():
5251
evt.addInfo(k, v)
53-
self.reporter.sendReport(evt)
52+
self.reporter.sendReport(evt, False)
5453

5554
for event in span.events:
5655
if event.name == 'exception':
5756
self._report_exception_event(event)
5857
else:
5958
self._report_info_event(event)
6059

61-
evt = Context.stopTrace(int(span.end_time / 1000))
60+
evt = Context.createExit(int(span.end_time / 1000))
6261
evt.addInfo('Layer', span.name)
63-
self.reporter.sendReport(evt)
62+
self.reporter.sendReport(evt, False)
6463

6564
def _report_exception_event(self, event):
6665
evt = Context.createEvent(int(event.timestamp / 1000))
@@ -76,7 +75,7 @@ def _report_exception_event(self, event):
7675
if k not in ('exception.type', 'exception.message',
7776
'exception.stacktrace'):
7877
evt.addInfo(k, v)
79-
self.reporter.sendReport(evt)
78+
self.reporter.sendReport(evt, False)
8079

8180
def _report_info_event(self, event):
8281
print("Found info event")
@@ -86,40 +85,10 @@ def _report_info_event(self, event):
8685
evt.addInfo('Label', 'info')
8786
for k, v in event.attributes.items():
8887
evt.addInfo(k, v)
89-
self.reporter.sendReport(evt)
90-
91-
def _initialize_solarwinds_reporter(self):
92-
"""Initialize liboboe."""
93-
log_level = os.environ.get('SOLARWINDS_DEBUG_LEVEL', 3)
94-
try:
95-
log_level = int(log_level)
96-
except ValueError:
97-
log_level = 3
98-
self.reporter = Reporter(
99-
hostname_alias='',
100-
log_level=log_level,
101-
log_file_path='',
102-
max_transactions=-1,
103-
max_flush_wait_time=-1,
104-
events_flush_interval=-1,
105-
max_request_size_bytes=-1,
106-
reporter='ssl',
107-
host=os.environ.get('SOLARWINDS_COLLECTOR', ''),
108-
service_key=os.environ.get('SOLARWINDS_SERVICE_KEY', ''),
109-
trusted_path='',
110-
buffer_size=-1,
111-
trace_metrics=-1,
112-
histogram_precision=-1,
113-
token_bucket_capacity=-1,
114-
token_bucket_rate=-1,
115-
file_single=0,
116-
ec2_metadata_timeout=1000,
117-
grpc_proxy='',
118-
stdout_clear_nonblocking=0,
119-
is_grpc_clean_hack_enabled=False,
120-
w3c_trace_format=1,
121-
)
88+
self.reporter.sendReport(evt, False)
12289

12390
@staticmethod
12491
def _build_metadata(span_context):
125-
return Metadata.fromString(transform_id(span_context))
92+
return Metadata.fromString(
93+
W3CTransformer.traceparent_from_context(span_context)
94+
)

opentelemetry_distro_solarwinds/ot_ao_transformer.py

-17
This file was deleted.

0 commit comments

Comments
 (0)