Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
3ec54a9
Add class stubs for otel bridge
basepi Nov 19, 2021
2713b13
Switch execution_context to use stacks and store span extra data
basepi Dec 1, 2021
2e6b3d4
Add stub for span.py
basepi Dec 1, 2021
781dc51
Switch to tuples for execution_context
basepi Jan 13, 2022
e44e195
Switch span ending to unset_span instead of setting parent
basepi Jan 13, 2022
f7810b6
Update elasticapm/context/contextvars.py
basepi Jan 14, 2022
2bbc87c
Use `with_extra` when fetching spans
basepi Jan 18, 2022
9bbd996
Implement otel Span wrapper
basepi Jan 24, 2022
4da03fd
More Tracer work
basepi Jan 26, 2022
1db14bc
Transaction creation + utils
basepi Jan 27, 2022
00b2696
Handle transaction activation/deactivation
basepi Feb 2, 2022
2c43687
Add context api
basepi Feb 3, 2022
7776f67
Remove `extra` from execution_context
basepi Feb 3, 2022
11bb7f4
Simplify to make Hound happy
basepi Feb 3, 2022
649aac5
Handle SpanKind + otel attributes properly
basepi Feb 3, 2022
a0c08fe
Add transaction type inference
basepi Feb 7, 2022
477ae51
Add span type/subtype
basepi Feb 8, 2022
3b10883
Handle otel attributes for exceptions
basepi Feb 8, 2022
d62ce9a
Handle end_time for otel spans
basepi Feb 8, 2022
f99f6ac
Handle tracestate and traceflags
basepi Feb 8, 2022
b6a33fd
Handle set_status_on_exception
basepi Feb 8, 2022
55b65ae
Handle remote tracestate
basepi Feb 8, 2022
360e89b
Instrument on Tracer instantiation
basepi Feb 8, 2022
108361d
Merge remote-tracking branch 'upstream/main' into otel
basepi Feb 8, 2022
cbcd719
Add first test (and many fixes for it)
basepi Feb 15, 2022
b8b1f32
Remove compat
basepi Feb 15, 2022
4bb37d3
Fix other failing tests
basepi Feb 15, 2022
513278e
Add another test (plus more fixes)
basepi Feb 15, 2022
27d3fcf
Merge remote-tracking branch 'upstream/main' into otel
basepi Feb 16, 2022
2678177
Remove contextvars/threadlocal test for py3.6
basepi Feb 16, 2022
cd15f03
Don't make Tracer a singleton (should fix tests?)
basepi Feb 16, 2022
6031d8b
Add test for spans without auto-attach
basepi Feb 16, 2022
f2d2754
Rename otel_tests.py to tests.py
basepi Mar 1, 2022
9e9ea9c
Add more tests
basepi Mar 1, 2022
2e0c2ea
Docs + a couple of changes to more closely match opentracing bridge
basepi Mar 1, 2022
5fc8c0d
Merge remote-tracking branch 'upstream/main' into otel
basepi Mar 1, 2022
a6bbc37
Deprecate opentracing bridge
basepi Mar 2, 2022
bff1d31
More docs
basepi Mar 2, 2022
b29b2ce
Move opentelemetry to its own matrix
basepi Mar 2, 2022
bd74132
Merge remote-tracking branch 'upstream/main' into otel
basepi Mar 2, 2022
8ff7fcd
Merge branch 'main' into otel
basepi Mar 3, 2022
0d934a2
Review comments for context.py
basepi Mar 7, 2022
dfb6534
Reintroduce test_execution_context_backing
basepi Mar 7, 2022
e3e2fc3
Fix a typo
basepi Mar 7, 2022
b9ff8d6
Add note about extra get_client() check
basepi Mar 7, 2022
8343fc9
Use `end_time` correctly
basepi Mar 8, 2022
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 37 additions & 2 deletions elasticapm/context/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,48 @@

class BaseContext(object):
def set_transaction(self, transaction):
"""
Set the transaction for the current execution context
"""
raise NotImplementedError

def get_transaction(self, clear=False):
"""
Get the transaction for the current execution context
If clear=True, also set the transaction to None for the current
execution context.
"""
raise NotImplementedError

def set_span(self, span, extra=None):
"""
Set the active span for the current execution context.
The previously-activated span will be saved to be re-activated later.
Optionally, `extra` data can be provided and will be saved alongside
the span.
"""
raise NotImplementedError

def set_span(self, span):
def get_span(self, extra=False):
"""
Get the active span for the current execution context.
If extra=True, a tuple will be returned with the span and its extra
data: (span, extra)
"""
raise NotImplementedError

def get_span(self):
def unset_span(self, extra=False, clear_all=False):
"""
De-activate the current span. If a span was previously active, it will
become active again.
Returns the de-activated span. If extra=True, a tuple will be returned
with the span and its extra data: (span, extra)
If clear_all=True, all spans will be cleared and no span will be active.
"""
raise NotImplementedError
66 changes: 58 additions & 8 deletions elasticapm/context/contextvars.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,15 @@

class ContextVarsContext(BaseContext):
elasticapm_transaction_var = contextvars.ContextVar("elasticapm_transaction_var")
elasticapm_span_var = contextvars.ContextVar("elasticapm_span_var")
elasticapm_spans_var = contextvars.ContextVar("elasticapm_spans_var", default=())

def get_transaction(self, clear=False):
"""
Get the transaction for the current execution context

If clear=True, also set the transaction to None for the current
execution context.
"""
try:
transaction = self.elasticapm_transaction_var.get()
if clear:
Expand All @@ -50,16 +56,60 @@ def get_transaction(self, clear=False):
return None

def set_transaction(self, transaction):
"""
Set the transaction for the current execution context
"""
self.elasticapm_transaction_var.set(transaction)

def get_span(self):
try:
return self.elasticapm_span_var.get()
except LookupError:
return None
def get_span(self, extra=False):
"""
Get the active span for the current execution context.

If extra=True, a tuple will be returned with the span and its extra
data: (span, extra)
"""
spans = self.elasticapm_span_var.get()
span = (None, None)
if spans:
span = spans[-1]
if extra:
return span
else:
return span[0]

def set_span(self, span, extra=None):
"""
Set the active span for the current execution context.

The previously-activated span will be saved to be re-activated later.

Optionally, `extra` data can be provided and will be saved alongside
the span.
"""
self.elasticapm_spans_var.set(self.elasticapm_span_var.get() + (span, extra))

def unset_span(self, extra=False, clear_all=False):
"""
De-activate the current span. If a span was previously active, it will
become active again.

Returns the de-activated span. If extra=True, a tuple will be returned
with the span and its extra data: (span, extra)

def set_span(self, span):
self.elasticapm_span_var.set(span)
If clear_all=True, all spans will be cleared and no span will be active.
"""
spans = self.elasticapm_span_var.get()
span = (None, None)
if spans:
span = spans[-1]
if clear_all:
self.elasticapm_spans_var.set(())
else:
self.elasticapm_spans_var.set(spans[0:-1])
if extra:
return span
else:
return span[0]


execution_context = ContextVarsContext()
63 changes: 55 additions & 8 deletions elasticapm/context/threadlocal.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,28 +37,75 @@
class ThreadLocalContext(BaseContext):
thread_local = threading.local()
thread_local.transaction = None
thread_local.span = None
thread_local.spans = ()

def get_transaction(self, clear=False):
"""
Get the transaction registered for the current thread.
Get the transaction for the current execution context

:return:
:rtype: Transaction
If clear=True, also set the transaction to None for the current
execution context.
"""
transaction = getattr(self.thread_local, "transaction", None)
if clear:
self.thread_local.transaction = None
return transaction

def set_transaction(self, transaction):
"""
Set the transaction for the current execution context
"""
self.thread_local.transaction = transaction

def get_span(self):
return getattr(self.thread_local, "span", None)
def get_span(self, extra=False):
"""
Get the active span for the current execution context.

If extra=True, a tuple will be returned with the span and its extra
data: (span, extra)
"""
spans = getattr(self.thread_local, "spans", ())
span = (None, None)
if spans:
span = spans[-1]
if extra:
return span
else:
return span[0]

def set_span(self, span, extra=None):
"""
Set the active span for the current execution context.

The previously-activated span will be saved to be re-activated later.

Optionally, `extra` data can be provided and will be saved alongside
the span.
"""
self.thread_local.spans = self.thread_local.spans + (span, extra)

def set_span(self, span):
self.thread_local.span = span
def unset_span(self, extra=False, clear_all=False):
"""
De-activate the current span. If a span was previously active, it will
become active again.

Returns the de-activated span. If extra=True, a tuple will be returned
with the span and its extra data: (span, extra)

If clear_all=True, all spans will be cleared and no span will be active.
"""
spans = getattr(self.thread_local, "spans", [])
span = (None, None)
if spans:
span = spans[-1]
if clear_all:
self.thread_local.spans = ()
else:
self.thread_local.spans = spans[0:-1]
if extra:
return span
else:
return span[0]


execution_context = ThreadLocalContext()
33 changes: 33 additions & 0 deletions elasticapm/contrib/opentelemetry/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# BSD 3-Clause License
#
# Copyright (c) 2019, Elasticsearch BV
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# * Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.


def get_tracer(name):
pass
123 changes: 123 additions & 0 deletions elasticapm/contrib/opentelemetry/span.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
# BSD 3-Clause License
#
# Copyright (c) 2019, Elasticsearch BV
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# * Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

import types as python_types
import typing

from opentelemetry import trace as oteltrace
from opentelemetry.trace.span import SpanContext
from opentelemetry.trace.status import Status
from opentelemetry.util import types


class Span(oteltrace.Span):
"""A span represents a single operation within a trace."""

def end(self, end_time: typing.Optional[int] = None) -> None:
"""Sets the current time as the span's end time.
The span's end time is the wall time at which the operation finished.
Only the first call to `end` should modify the span, and
implementations are free to ignore or raise on further calls.
"""

def get_span_context(self) -> "SpanContext":
"""Gets the span's SpanContext.
Get an immutable, serializable identifier for this span that can be
used to create new child spans.
Returns:
A :class:`opentelemetry.trace.SpanContext` with a copy of this span's immutable state.
"""

def set_attributes(self, attributes: typing.Dict[str, types.AttributeValue]) -> None:
"""Sets Attributes.
Sets Attributes with the key and value passed as arguments dict.
Note: The behavior of `None` value attributes is undefined, and hence strongly discouraged.
"""

def set_attribute(self, key: str, value: types.AttributeValue) -> None:
"""Sets an Attribute.
Sets a single Attribute with the key and value passed as arguments.
Note: The behavior of `None` value attributes is undefined, and hence strongly discouraged.
"""

def add_event(
self,
name: str,
attributes: types.Attributes = None,
timestamp: typing.Optional[int] = None,
) -> None:
"""Adds an `Event`.
Adds a single `Event` with the name and, optionally, a timestamp and
attributes passed as arguments. Implementations should generate a
timestamp if the `timestamp` argument is omitted.
"""

def update_name(self, name: str) -> None:
"""Updates the `Span` name.
This will override the name provided via :func:`opentelemetry.trace.Tracer.start_span`.
Upon this update, any sampling behavior based on Span name will depend
on the implementation.
"""

def is_recording(self) -> bool:
"""Returns whether this span will be recorded.
Returns true if this Span is active and recording information like
events with the add_event operation and attributes using set_attribute.
"""

def set_status(self, status: Status) -> None:
"""Sets the Status of the Span. If used, this will override the default
Span status.
"""

def record_exception(
self,
exception: Exception,
attributes: types.Attributes = None,
timestamp: typing.Optional[int] = None,
escaped: bool = False,
) -> None:
"""Records an exception as a span event."""

def __enter__(self) -> "Span":
"""Invoked when `Span` is used as a context manager.
Returns the `Span` itself.
"""
return self

def __exit__(
self,
exc_type: typing.Optional[typing.Type[BaseException]],
exc_val: typing.Optional[BaseException],
exc_tb: typing.Optional[python_types.TracebackType],
) -> None:
"""Ends context manager and calls `end` on the `Span`."""

self.end()
Loading