Skip to content

Commit f40be51

Browse files
authored
Add OpenCensus trace bridge/shim (#3210)
1 parent 6379c1c commit f40be51

File tree

8 files changed

+766
-2
lines changed

8 files changed

+766
-2
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1717
([#3138](https://github.com/open-telemetry/opentelemetry-python/pull/3138))
1818
- Add exponential histogram
1919
([#2964](https://github.com/open-telemetry/opentelemetry-python/pull/2964))
20+
- Add OpenCensus trace bridge/shim
21+
([#3210](https://github.com/open-telemetry/opentelemetry-python/pull/3210))
2022

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

shim/opentelemetry-opencensus-shim/src/opentelemetry/shim/opencensus/__init__.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,16 @@
2222
already instrumented using OpenCensus to start using OpenTelemetry with minimal effort, without
2323
having to rewrite large portions of the codebase.
2424
"""
25+
26+
from opentelemetry.shim.opencensus._patch import install_shim, uninstall_shim
27+
28+
__all__ = [
29+
"install_shim",
30+
"uninstall_shim",
31+
]
32+
33+
# TODO: Decide when this should be called.
34+
# 1. defensive import in opentelemetry-api
35+
# 2. defensive import directly in OpenCensus, although that would require a release
36+
# 3. ask the user to do it
37+
# install_shim()
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# Copyright The OpenTelemetry Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from logging import getLogger
16+
from typing import Optional
17+
18+
from opencensus.trace.tracer import Tracer
19+
from opencensus.trace.tracers.noop_tracer import NoopTracer
20+
21+
from opentelemetry import trace
22+
from opentelemetry.shim.opencensus._shim_tracer import ShimTracer
23+
from opentelemetry.shim.opencensus.version import __version__
24+
25+
_logger = getLogger(__name__)
26+
27+
28+
def install_shim(
29+
tracer_provider: Optional[trace.TracerProvider] = None,
30+
) -> None:
31+
otel_tracer = trace.get_tracer(
32+
"opentelemetry-opencensus-shim",
33+
__version__,
34+
tracer_provider=tracer_provider,
35+
)
36+
shim_tracer = ShimTracer(NoopTracer(), otel_tracer=otel_tracer)
37+
38+
def fget_tracer(self) -> ShimTracer:
39+
return shim_tracer
40+
41+
def fset_tracer(self, value) -> None:
42+
# ignore attempts to set the value
43+
pass
44+
45+
# Tracer's constructor sets self.tracer to either a NoopTracer or ContextTracer depending
46+
# on sampler:
47+
# https://github.com/census-instrumentation/opencensus-python/blob/2e08df591b507612b3968be8c2538dedbf8fab37/opencensus/trace/tracer.py#L63.
48+
# We monkeypatch Tracer.tracer with a property to return the shim instance instead. This
49+
# makes all instances of Tracer (even those already created) use the ShimTracer singleton.
50+
Tracer.tracer = property(fget_tracer, fset_tracer)
51+
_logger.info("Installed OpenCensus shim")
52+
53+
54+
def uninstall_shim() -> None:
55+
if hasattr(Tracer, "tracer"):
56+
del Tracer.tracer
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
# Copyright The OpenTelemetry Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import logging
16+
from datetime import datetime
17+
from typing import TYPE_CHECKING
18+
19+
import wrapt
20+
from opencensus.trace.base_span import BaseSpan
21+
from opencensus.trace.span import SpanKind
22+
from opencensus.trace.status import Status
23+
from opencensus.trace.time_event import MessageEvent
24+
25+
from opentelemetry import context, trace
26+
27+
if TYPE_CHECKING:
28+
from opentelemetry.shim.opencensus._shim_tracer import ShimTracer
29+
30+
_logger = logging.getLogger(__name__)
31+
32+
# Copied from Java
33+
# https://github.com/open-telemetry/opentelemetry-java/blob/0d3a04669e51b33ea47b29399a7af00012d25ccb/opencensus-shim/src/main/java/io/opentelemetry/opencensusshim/SpanConverter.java#L24-L27
34+
_MESSAGE_EVENT_ATTRIBUTE_KEY_TYPE = "message.event.type"
35+
_MESSAGE_EVENT_ATTRIBUTE_KEY_SIZE_UNCOMPRESSED = (
36+
"message.event.size.uncompressed"
37+
)
38+
_MESSAGE_EVENT_ATTRIBUTE_KEY_SIZE_COMPRESSED = "message.event.size.compressed"
39+
40+
_MESSAGE_EVENT_TYPE_STR_MAPPING = {
41+
0: "TYPE_UNSPECIFIED",
42+
1: "SENT",
43+
2: "RECEIVED",
44+
}
45+
46+
47+
def _opencensus_time_to_nanos(timestamp: str) -> int:
48+
"""Converts an OpenCensus formatted time string (ISO 8601 with Z) to time.time_ns style
49+
unix timestamp
50+
"""
51+
# format taken from
52+
# https://github.com/census-instrumentation/opencensus-python/blob/c38c71b9285e71de94d0185ff3c5bf65ee163345/opencensus/common/utils/__init__.py#L76
53+
#
54+
# datetime.fromisoformat() does not work with the added "Z" until python 3.11
55+
seconds_float = datetime.strptime(
56+
timestamp, "%Y-%m-%dT%H:%M:%S.%fZ"
57+
).timestamp()
58+
return round(seconds_float * 1e9)
59+
60+
61+
# pylint: disable=abstract-method
62+
class ShimSpan(wrapt.ObjectProxy):
63+
def __init__(
64+
self,
65+
wrapped: BaseSpan,
66+
*,
67+
otel_span: trace.Span,
68+
shim_tracer: "ShimTracer",
69+
) -> None:
70+
super().__init__(wrapped)
71+
self._self_otel_span = otel_span
72+
self._self_shim_tracer = shim_tracer
73+
self._self_token: object = None
74+
75+
# Set a few values for BlankSpan members (they appear to be part of the "public" API
76+
# even though they are not documented in BaseSpan). Some instrumentations may use these
77+
# and not expect an AttributeError to be raised. Set values from OTel where possible
78+
# and let ObjectProxy defer to the wrapped BlankSpan otherwise.
79+
sc = self._self_otel_span.get_span_context()
80+
self.same_process_as_parent_span = not sc.is_remote
81+
self.span_id = sc.span_id
82+
83+
def span(self, name="child_span"):
84+
return self._self_shim_tracer.start_span(name=name)
85+
86+
def add_attribute(self, attribute_key, attribute_value):
87+
self._self_otel_span.set_attribute(attribute_key, attribute_value)
88+
89+
def add_annotation(self, description, **attrs):
90+
self._self_otel_span.add_event(description, attrs)
91+
92+
def add_message_event(self, message_event: MessageEvent):
93+
attrs = {
94+
_MESSAGE_EVENT_ATTRIBUTE_KEY_TYPE: _MESSAGE_EVENT_TYPE_STR_MAPPING[
95+
message_event.type
96+
],
97+
}
98+
if message_event.uncompressed_size_bytes is not None:
99+
attrs[
100+
_MESSAGE_EVENT_ATTRIBUTE_KEY_SIZE_UNCOMPRESSED
101+
] = message_event.uncompressed_size_bytes
102+
if message_event.compressed_size_bytes is not None:
103+
attrs[
104+
_MESSAGE_EVENT_ATTRIBUTE_KEY_SIZE_COMPRESSED
105+
] = message_event.compressed_size_bytes
106+
107+
timestamp = _opencensus_time_to_nanos(message_event.timestamp)
108+
self._self_otel_span.add_event(
109+
str(message_event.id),
110+
attrs,
111+
timestamp=timestamp,
112+
)
113+
114+
# pylint: disable=no-self-use
115+
def add_link(self, link):
116+
"""span links do not work with the shim because the OpenCensus Tracer does not accept
117+
links in start_span(). Same issue applies to SpanKind. Also see:
118+
https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/compatibility/opencensus.md#known-incompatibilities
119+
"""
120+
_logger.warning(
121+
"OpenTelemetry does not support links added after a span is created."
122+
)
123+
124+
@property
125+
def span_kind(self):
126+
"""Setting span_kind does not work with the shim because the OpenCensus Tracer does not
127+
accept the param in start_span() and there's no way to set OTel span kind after
128+
start_span().
129+
"""
130+
return SpanKind.UNSPECIFIED
131+
132+
@span_kind.setter
133+
def span_kind(self, value):
134+
_logger.warning(
135+
"OpenTelemetry does not support setting span kind after a span is created."
136+
)
137+
138+
def set_status(self, status: Status):
139+
self._self_otel_span.set_status(
140+
trace.StatusCode.OK if status.is_ok else trace.StatusCode.ERROR,
141+
status.description,
142+
)
143+
144+
def finish(self):
145+
"""Note this method does not pop the span from current context. Use Tracer.end_span()
146+
or a `with span: ...` statement (contextmanager) to do that.
147+
"""
148+
self._self_otel_span.end()
149+
150+
def __enter__(self):
151+
self._self_otel_span.__enter__()
152+
return self
153+
154+
# pylint: disable=arguments-differ
155+
def __exit__(self, exception_type, exception_value, traceback):
156+
self._self_otel_span.__exit__(
157+
exception_type, exception_value, traceback
158+
)
159+
# OpenCensus Span.__exit__() calls Tracer.end_span()
160+
# https://github.com/census-instrumentation/opencensus-python/blob/2e08df591b507612b3968be8c2538dedbf8fab37/opencensus/trace/span.py#L390
161+
# but that would cause the OTel span to be ended twice. Instead just detach it from
162+
# context directly.
163+
context.detach(self._self_token)
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# Copyright The OpenTelemetry Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import logging
16+
17+
import wrapt
18+
from opencensus.trace.blank_span import BlankSpan
19+
from opencensus.trace.tracers.base import Tracer as BaseTracer
20+
21+
from opentelemetry import context, trace
22+
from opentelemetry.shim.opencensus._shim_span import ShimSpan
23+
24+
_logger = logging.getLogger(__name__)
25+
26+
_SHIM_SPAN_KEY = context.create_key("opencensus-shim-span-key")
27+
28+
29+
def set_shim_span_in_context(
30+
span: ShimSpan, ctx: context.Context
31+
) -> context.Context:
32+
return context.set_value(_SHIM_SPAN_KEY, span, ctx)
33+
34+
35+
def get_shim_span_in_context() -> ShimSpan:
36+
return context.get_value(_SHIM_SPAN_KEY)
37+
38+
39+
# pylint: disable=abstract-method
40+
class ShimTracer(wrapt.ObjectProxy):
41+
def __init__(
42+
self, wrapped: BaseTracer, *, otel_tracer: trace.Tracer
43+
) -> None:
44+
super().__init__(wrapped)
45+
self._self_otel_tracer = otel_tracer
46+
47+
# For now, finish() is not implemented by the shim. It would require keeping a list of all
48+
# spans created so they can all be finished.
49+
# def finish(self):
50+
# """End spans and send to reporter."""
51+
52+
def span(self, name="span"):
53+
return self.start_span(name=name)
54+
55+
def start_span(self, name="span"):
56+
span = self._self_otel_tracer.start_span(name)
57+
shim_span = ShimSpan(
58+
BlankSpan(name=name, context_tracer=self),
59+
otel_span=span,
60+
shim_tracer=self,
61+
)
62+
63+
ctx = trace.set_span_in_context(span)
64+
ctx = set_shim_span_in_context(shim_span, ctx)
65+
66+
# OpenCensus's ContextTracer calls execution_context.set_current_span(span) which is
67+
# equivalent to the below. This can cause context to leak but is equivalent.
68+
# pylint: disable=protected-access
69+
shim_span._self_token = context.attach(ctx)
70+
return shim_span
71+
72+
def end_span(self):
73+
"""Finishes the current span in the context and pops restores the context from before
74+
the span was started.
75+
"""
76+
span = self.current_span()
77+
if not span:
78+
_logger.warning("No active span, cannot do end_span.")
79+
return
80+
81+
span.finish()
82+
# pylint: disable=protected-access
83+
context.detach(span._self_token)
84+
85+
# pylint: disable=no-self-use
86+
def current_span(self):
87+
return get_shim_span_in_context()
88+
89+
def add_attribute_to_current_span(self, attribute_key, attribute_value):
90+
self.current_span().add_attribute(attribute_key, attribute_value)

0 commit comments

Comments
 (0)