Skip to content

Commit

Permalink
Add OpenCensus trace bridge/shim (#3210)
Browse files Browse the repository at this point in the history
  • Loading branch information
aabmass committed Mar 17, 2023
1 parent 6379c1c commit f40be51
Show file tree
Hide file tree
Showing 8 changed files with 766 additions and 2 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
([#3138](https://github.com/open-telemetry/opentelemetry-python/pull/3138))
- Add exponential histogram
([#2964](https://github.com/open-telemetry/opentelemetry-python/pull/2964))
- Add OpenCensus trace bridge/shim
([#3210](https://github.com/open-telemetry/opentelemetry-python/pull/3210))

## Version 1.16.0/0.37b0 (2023-02-17)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,16 @@
already instrumented using OpenCensus to start using OpenTelemetry with minimal effort, without
having to rewrite large portions of the codebase.
"""

from opentelemetry.shim.opencensus._patch import install_shim, uninstall_shim

__all__ = [
"install_shim",
"uninstall_shim",
]

# TODO: Decide when this should be called.
# 1. defensive import in opentelemetry-api
# 2. defensive import directly in OpenCensus, although that would require a release
# 3. ask the user to do it
# install_shim()
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from logging import getLogger
from typing import Optional

from opencensus.trace.tracer import Tracer
from opencensus.trace.tracers.noop_tracer import NoopTracer

from opentelemetry import trace
from opentelemetry.shim.opencensus._shim_tracer import ShimTracer
from opentelemetry.shim.opencensus.version import __version__

_logger = getLogger(__name__)


def install_shim(
tracer_provider: Optional[trace.TracerProvider] = None,
) -> None:
otel_tracer = trace.get_tracer(
"opentelemetry-opencensus-shim",
__version__,
tracer_provider=tracer_provider,
)
shim_tracer = ShimTracer(NoopTracer(), otel_tracer=otel_tracer)

def fget_tracer(self) -> ShimTracer:
return shim_tracer

def fset_tracer(self, value) -> None:
# ignore attempts to set the value
pass

# Tracer's constructor sets self.tracer to either a NoopTracer or ContextTracer depending
# on sampler:
# https://github.com/census-instrumentation/opencensus-python/blob/2e08df591b507612b3968be8c2538dedbf8fab37/opencensus/trace/tracer.py#L63.
# We monkeypatch Tracer.tracer with a property to return the shim instance instead. This
# makes all instances of Tracer (even those already created) use the ShimTracer singleton.
Tracer.tracer = property(fget_tracer, fset_tracer)
_logger.info("Installed OpenCensus shim")


def uninstall_shim() -> None:
if hasattr(Tracer, "tracer"):
del Tracer.tracer
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import logging
from datetime import datetime
from typing import TYPE_CHECKING

import wrapt
from opencensus.trace.base_span import BaseSpan
from opencensus.trace.span import SpanKind
from opencensus.trace.status import Status
from opencensus.trace.time_event import MessageEvent

from opentelemetry import context, trace

if TYPE_CHECKING:
from opentelemetry.shim.opencensus._shim_tracer import ShimTracer

_logger = logging.getLogger(__name__)

# Copied from Java
# https://github.com/open-telemetry/opentelemetry-java/blob/0d3a04669e51b33ea47b29399a7af00012d25ccb/opencensus-shim/src/main/java/io/opentelemetry/opencensusshim/SpanConverter.java#L24-L27
_MESSAGE_EVENT_ATTRIBUTE_KEY_TYPE = "message.event.type"
_MESSAGE_EVENT_ATTRIBUTE_KEY_SIZE_UNCOMPRESSED = (
"message.event.size.uncompressed"
)
_MESSAGE_EVENT_ATTRIBUTE_KEY_SIZE_COMPRESSED = "message.event.size.compressed"

_MESSAGE_EVENT_TYPE_STR_MAPPING = {
0: "TYPE_UNSPECIFIED",
1: "SENT",
2: "RECEIVED",
}


def _opencensus_time_to_nanos(timestamp: str) -> int:
"""Converts an OpenCensus formatted time string (ISO 8601 with Z) to time.time_ns style
unix timestamp
"""
# format taken from
# https://github.com/census-instrumentation/opencensus-python/blob/c38c71b9285e71de94d0185ff3c5bf65ee163345/opencensus/common/utils/__init__.py#L76
#
# datetime.fromisoformat() does not work with the added "Z" until python 3.11
seconds_float = datetime.strptime(
timestamp, "%Y-%m-%dT%H:%M:%S.%fZ"
).timestamp()
return round(seconds_float * 1e9)


# pylint: disable=abstract-method
class ShimSpan(wrapt.ObjectProxy):
def __init__(
self,
wrapped: BaseSpan,
*,
otel_span: trace.Span,
shim_tracer: "ShimTracer",
) -> None:
super().__init__(wrapped)
self._self_otel_span = otel_span
self._self_shim_tracer = shim_tracer
self._self_token: object = None

# Set a few values for BlankSpan members (they appear to be part of the "public" API
# even though they are not documented in BaseSpan). Some instrumentations may use these
# and not expect an AttributeError to be raised. Set values from OTel where possible
# and let ObjectProxy defer to the wrapped BlankSpan otherwise.
sc = self._self_otel_span.get_span_context()
self.same_process_as_parent_span = not sc.is_remote
self.span_id = sc.span_id

def span(self, name="child_span"):
return self._self_shim_tracer.start_span(name=name)

def add_attribute(self, attribute_key, attribute_value):
self._self_otel_span.set_attribute(attribute_key, attribute_value)

def add_annotation(self, description, **attrs):
self._self_otel_span.add_event(description, attrs)

def add_message_event(self, message_event: MessageEvent):
attrs = {
_MESSAGE_EVENT_ATTRIBUTE_KEY_TYPE: _MESSAGE_EVENT_TYPE_STR_MAPPING[
message_event.type
],
}
if message_event.uncompressed_size_bytes is not None:
attrs[
_MESSAGE_EVENT_ATTRIBUTE_KEY_SIZE_UNCOMPRESSED
] = message_event.uncompressed_size_bytes
if message_event.compressed_size_bytes is not None:
attrs[
_MESSAGE_EVENT_ATTRIBUTE_KEY_SIZE_COMPRESSED
] = message_event.compressed_size_bytes

timestamp = _opencensus_time_to_nanos(message_event.timestamp)
self._self_otel_span.add_event(
str(message_event.id),
attrs,
timestamp=timestamp,
)

# pylint: disable=no-self-use
def add_link(self, link):
"""span links do not work with the shim because the OpenCensus Tracer does not accept
links in start_span(). Same issue applies to SpanKind. Also see:
https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/compatibility/opencensus.md#known-incompatibilities
"""
_logger.warning(
"OpenTelemetry does not support links added after a span is created."
)

@property
def span_kind(self):
"""Setting span_kind does not work with the shim because the OpenCensus Tracer does not
accept the param in start_span() and there's no way to set OTel span kind after
start_span().
"""
return SpanKind.UNSPECIFIED

@span_kind.setter
def span_kind(self, value):
_logger.warning(
"OpenTelemetry does not support setting span kind after a span is created."
)

def set_status(self, status: Status):
self._self_otel_span.set_status(
trace.StatusCode.OK if status.is_ok else trace.StatusCode.ERROR,
status.description,
)

def finish(self):
"""Note this method does not pop the span from current context. Use Tracer.end_span()
or a `with span: ...` statement (contextmanager) to do that.
"""
self._self_otel_span.end()

def __enter__(self):
self._self_otel_span.__enter__()
return self

# pylint: disable=arguments-differ
def __exit__(self, exception_type, exception_value, traceback):
self._self_otel_span.__exit__(
exception_type, exception_value, traceback
)
# OpenCensus Span.__exit__() calls Tracer.end_span()
# https://github.com/census-instrumentation/opencensus-python/blob/2e08df591b507612b3968be8c2538dedbf8fab37/opencensus/trace/span.py#L390
# but that would cause the OTel span to be ended twice. Instead just detach it from
# context directly.
context.detach(self._self_token)
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import logging

import wrapt
from opencensus.trace.blank_span import BlankSpan
from opencensus.trace.tracers.base import Tracer as BaseTracer

from opentelemetry import context, trace
from opentelemetry.shim.opencensus._shim_span import ShimSpan

_logger = logging.getLogger(__name__)

_SHIM_SPAN_KEY = context.create_key("opencensus-shim-span-key")


def set_shim_span_in_context(
span: ShimSpan, ctx: context.Context
) -> context.Context:
return context.set_value(_SHIM_SPAN_KEY, span, ctx)


def get_shim_span_in_context() -> ShimSpan:
return context.get_value(_SHIM_SPAN_KEY)


# pylint: disable=abstract-method
class ShimTracer(wrapt.ObjectProxy):
def __init__(
self, wrapped: BaseTracer, *, otel_tracer: trace.Tracer
) -> None:
super().__init__(wrapped)
self._self_otel_tracer = otel_tracer

# For now, finish() is not implemented by the shim. It would require keeping a list of all
# spans created so they can all be finished.
# def finish(self):
# """End spans and send to reporter."""

def span(self, name="span"):
return self.start_span(name=name)

def start_span(self, name="span"):
span = self._self_otel_tracer.start_span(name)
shim_span = ShimSpan(
BlankSpan(name=name, context_tracer=self),
otel_span=span,
shim_tracer=self,
)

ctx = trace.set_span_in_context(span)
ctx = set_shim_span_in_context(shim_span, ctx)

# OpenCensus's ContextTracer calls execution_context.set_current_span(span) which is
# equivalent to the below. This can cause context to leak but is equivalent.
# pylint: disable=protected-access
shim_span._self_token = context.attach(ctx)
return shim_span

def end_span(self):
"""Finishes the current span in the context and pops restores the context from before
the span was started.
"""
span = self.current_span()
if not span:
_logger.warning("No active span, cannot do end_span.")
return

span.finish()
# pylint: disable=protected-access
context.detach(span._self_token)

# pylint: disable=no-self-use
def current_span(self):
return get_shim_span_in_context()

def add_attribute_to_current_span(self, attribute_key, attribute_value):
self.current_span().add_attribute(attribute_key, attribute_value)

0 comments on commit f40be51

Please sign in to comment.