elasticapm/contrib/opentelemetry/trace.py (161 lines of code) (raw):
# 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 logging
import typing
from contextlib import contextmanager
from typing import Any, Iterator, Mapping, Optional, Sequence
from opentelemetry import trace as trace_api
from opentelemetry.sdk import trace as oteltrace
from opentelemetry.trace import Context, Link, SpanKind
from opentelemetry.trace.propagation import _SPAN_KEY
from opentelemetry.trace.status import Status, StatusCode
from opentelemetry.util import types
import elasticapm
from elasticapm import Client
from elasticapm.traces import execution_context
from . import context as context_api
from .span import Span
from .utils import get_span_kind, get_traceparent
logger = logging.getLogger("elasticapm.otel")
class Tracer(oteltrace.Tracer):
"""
Handles span creation and in-process context propagation.
This class provides methods for manipulating the context, creating spans,
and controlling spans' lifecycles.
"""
def __init__(
self, *args, elasticapm_client: Optional[Client] = None, config: Optional[Mapping] = None, **kwargs
) -> None: # type: ignore
self.client = elasticapm_client
if not self.client:
self.client = elasticapm.get_client()
if not self.client:
self.client = elasticapm.Client(config=config)
if self.client.config.instrument and self.client.config.enabled:
elasticapm.instrument()
def start_span(
self,
name: str,
context: Optional[Context] = None,
kind: SpanKind = SpanKind.INTERNAL,
attributes: types.Attributes = None,
links: Optional[Sequence[Link]] = None,
start_time: Optional[int] = None,
record_exception: bool = True,
set_status_on_exception: bool = True,
) -> "Span":
"""
Starts a span.
Create a new span. Start the span without setting it as the current
span in the context. To start the span and use the context in a single
method, see :meth:`start_as_current_span`.
By default the current span in the context will be used as parent, but an
explicit context can also be specified, by passing in a `Context` containing
a current `Span`. If there is no current span in the global `Context` or in
the specified context, the created span will be a root span.
The span can be used as a context manager. On exiting the context manager,
the span's end() method will be called.
Example::
# trace.get_current_span() will be used as the implicit parent.
# If none is found, the created span will be a root instance.
with tracer.start_span("one") as child:
child.add_event("child's event")
Args:
name: The name of the span to be created.
context: An optional Context containing the span's parent. Defaults to the
global context.
kind: The span's kind (relationship to parent). Note that is
meaningful even if there is no parent.
attributes: The span's attributes.
links: Links span to other spans (ignored in this bridge)
start_time: Sets the start time of a span
record_exception: Whether to record any exceptions raised within the
context as error event on the span. (ignored in this bridge)
set_status_on_exception: Only relevant if the returned span is used
in a with/context manager. Defines wether the span status will
be automatically set to ERROR when an uncaught exception is
raised in the span with block. The span status won't be set by
this mechanism if it was previously set manually.
Returns:
The newly-created span.
"""
if not record_exception:
logger.warning("record_exception was set to False, but exceptions will still be recorded for this span.")
parent_span_context = trace_api.get_current_span(context).get_span_context()
if parent_span_context is not None and not isinstance(parent_span_context, trace_api.SpanContext):
raise TypeError("parent_span_context must be a SpanContext or None.")
traceparent = get_traceparent(parent_span_context)
span = None
current_transaction = execution_context.get_transaction()
client = self.client
elastic_links = tuple(get_traceparent(link.context) for link in links) if links else None
if traceparent and current_transaction:
logger.warning(
"Remote context included when a transaction was already active. "
"Ignoring remote context and creating a Span instead."
)
elif traceparent:
elastic_span = client.begin_transaction(
"otel", trace_parent=traceparent, start=start_time, auto_activate=False, links=elastic_links
)
span = Span(
name=name,
elastic_span=elastic_span,
set_status_on_exception=set_status_on_exception,
client=self.client,
)
span.set_attributes(attributes)
elif not current_transaction:
elastic_span = client.begin_transaction("otel", start=start_time, auto_activate=False, links=elastic_links)
span = Span(
name=name,
elastic_span=elastic_span,
set_status_on_exception=set_status_on_exception,
client=self.client,
)
span.set_attributes(attributes)
else:
elastic_span = current_transaction.begin_span(
name, "otel", start=start_time, auto_activate=False, links=elastic_links
)
span = Span(
name=name,
elastic_span=elastic_span,
set_status_on_exception=set_status_on_exception,
client=self.client,
)
span.set_attributes(attributes)
spankind = get_span_kind(kind)
elastic_span.context["otel_spankind"] = spankind
return span
@contextmanager # type: ignore
def start_as_current_span(
self,
name: str,
context: Optional[Context] = None,
kind: SpanKind = SpanKind.INTERNAL,
attributes: types.Attributes = None,
links: Optional[Sequence[Link]] = None,
start_time: Optional[int] = None,
record_exception: bool = True,
set_status_on_exception: bool = True,
end_on_exit: bool = True,
) -> Iterator["Span"]:
"""
Context manager for creating a new span and set it
as the current span in this tracer's context.
Exiting the context manager will call the span's end method,
as well as return the current span to its previous value by
returning to the previous context.
Example::
with tracer.start_as_current_span("one") as parent:
parent.add_event("parent's event")
with trace.start_as_current_span("two") as child:
child.add_event("child's event")
trace.get_current_span() # returns child
trace.get_current_span() # returns parent
trace.get_current_span() # returns previously active span
This is a convenience method for creating spans attached to the
tracer's context. Applications that need more control over the span
lifetime should use :meth:`start_span` instead. For example::
with tracer.start_as_current_span(name) as span:
do_work()
is equivalent to::
span = tracer.start_span(name)
with opentelemetry.trace.use_span(span, end_on_exit=True):
do_work()
Args:
name: The name of the span to be created.
context: An optional Context containing the span's parent. Defaults to the
global context.
kind: The span's kind (relationship to parent). Note that is
meaningful even if there is no parent.
attributes: The span's attributes.
links: Links span to other spans (ignored in this bridge)
start_time: Sets the start time of a span
record_exception: Whether to record any exceptions raised within the
context as error event on the span. (ignored in this bridge)
set_status_on_exception: Only relevant if the returned span is used
in a with/context manager. Defines wether the span status will
be automatically set to ERROR when an uncaught exception is
raised in the span with block. The span status won't be set by
this mechanism if it was previously set manually.
end_on_exit: Whether to end the span automatically when leaving the
context manager.
Yields:
The newly-created span.
"""
span = self.start_span(
name=name,
context=context,
kind=kind,
attributes=attributes,
links=links,
start_time=start_time,
record_exception=record_exception,
set_status_on_exception=set_status_on_exception,
)
with use_span(
span,
end_on_exit=end_on_exit,
record_exception=record_exception,
set_status_on_exception=set_status_on_exception,
) as activated_span:
yield activated_span
def get_tracer(
instrumenting_module_name: str,
instrumenting_library_version: typing.Optional[str] = None,
tracer_provider: Optional[Any] = None,
schema_url: Optional[str] = None,
elasticapm_client: Optional[Client] = None,
config: Optional[Mapping] = None,
) -> "Tracer":
"""
Returns the Elastic-wrapped Tracer object which allows for span creation
Args:
instrumenting_module_name: The name of the instrumenting module
(usually just `__name__`)
All other args are ignored in this implementation.
"""
return Tracer(instrumenting_module_name, elasticapm_client=elasticapm_client, config=config)
def set_tracer_provider(tracer_provider: Any) -> None:
"""
No-op to match opentelemetry's `trace` module
"""
return None
def get_tracer_provider() -> None:
"""
Not implemented by otel bridge
"""
raise NotImplementedError()
@contextmanager
def use_span(
span: Span,
end_on_exit: bool = False,
record_exception: bool = True,
set_status_on_exception: bool = None,
) -> None:
"""
Takes a non-active span and activates it in the current context.
"""
if set_status_on_exception is None:
set_status_on_exception = span.set_status_on_exception
if set_status_on_exception is None:
# Default if it's not set in the span or in this context manager
set_status_on_exception = True
context_api.attach(context_api.set_value(_SPAN_KEY, span))
try:
yield span
except Exception as exc:
if record_exception:
elasticapm.get_client().capture_exception(handled=False)
if set_status_on_exception:
span.set_status(
Status(
status_code=StatusCode.ERROR,
description=f"{type(exc).__name__}: {exc}",
)
)
raise
finally:
if end_on_exit:
span.end()
else:
# Spans auto-detach when they end.
context_api.detach()