Skip to content

Commit

Permalink
Update tracing to use OTLP/HTTP (#569)
Browse files Browse the repository at this point in the history
  • Loading branch information
mmkay authored Feb 1, 2024
1 parent 4ca83b6 commit d7a20a6
Show file tree
Hide file tree
Showing 7 changed files with 214 additions and 69 deletions.
1 change: 1 addition & 0 deletions charmcraft.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ parts:
- ops
- wheel==0.37.1
- setuptools==45.2.0
- pydantic>=2
cos-tool:
plugin: dump
source: .
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,17 @@
`@trace_charm(tracing_endpoint="my_tracing_endpoint")`
2) add to your charm a "my_tracing_endpoint" (you can name this attribute whatever you like) **property**
that returns an otlp grpc endpoint url. If you are using the `TracingEndpointProvider` as
that returns an otlp http/https endpoint url. If you are using the `TracingEndpointProvider` as
`self.tracing = TracingEndpointProvider(self)`, the implementation could be:
```
@property
def my_tracing_endpoint(self) -> Optional[str]:
'''Tempo endpoint for charm tracing'''
return self.tracing.otlp_grpc_endpoint
if self.tracing.is_ready():
return self.tracing.otlp_http_endpoint()
else:
return None
```
At this point your charm will be automatically instrumented so that:
Expand All @@ -41,14 +44,71 @@ def tracer(self) -> opentelemetry.trace.Tracer:
By default, the tracer is named after the charm type. If you wish to override that, you can pass
a different `service_name` argument to `trace_charm`.
*Upgrading from `v0`:*
If you are upgrading from `charm_tracing` v0, you need to take the following steps (assuming you already
have the newest version of the library in your charm):
1) If you need the dependency for your tests, add the following dependency to your charm project
(or, if your project had a dependency on `opentelemetry-exporter-otlp-proto-grpc` only because
of `charm_tracing` v0, you can replace it with):
`opentelemetry-exporter-otlp-proto-http>=1.21.0`.
2) Update the charm method referenced to from `@trace` and `@trace_charm`,
to return from `TracingEndpointRequirer.otlp_http_endpoint()` instead of `grpc_http`. For example:
```
from charms.tempo_k8s.v0.charm_tracing import trace_charm
@trace_charm(
tracing_endpoint="my_tracing_endpoint",
)
class MyCharm(CharmBase):
...
@property
def my_tracing_endpoint(self) -> Optional[str]:
'''Tempo endpoint for charm tracing'''
if self.tracing.is_ready():
return self.tracing.otlp_grpc_endpoint()
else:
return None
```
needs to be replaced with:
```
from charms.tempo_k8s.v1.charm_tracing import trace_charm
@trace_charm(
tracing_endpoint="my_tracing_endpoint",
)
class MyCharm(CharmBase):
...
@property
def my_tracing_endpoint(self) -> Optional[str]:
'''Tempo endpoint for charm tracing'''
if self.tracing.is_ready():
return self.tracing.otlp_http_endpoint()
else:
return None
```
3) If you were passing a certificate using `server_cert`, you need to change it to provide an *absolute* path to
the certificate file.
"""

import functools
import inspect
import logging
import os
from contextlib import contextmanager
from contextvars import ContextVar
from contextvars import Context, ContextVar, copy_context
from pathlib import Path
from typing import (
Any,
Callable,
Expand All @@ -62,8 +122,7 @@ def tracer(self) -> opentelemetry.trace.Tracer:
)

import opentelemetry
from grpc import ChannelCredentials
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import Span, TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
Expand All @@ -82,13 +141,14 @@ def tracer(self) -> opentelemetry.trace.Tracer:
LIBID = "cb1705dcd1a14ca09b2e60187d1215c7"

# Increment this major API version when introducing breaking changes
LIBAPI = 0
LIBAPI = 1

# Increment this PATCH version before using `charmcraft publish-lib` or reset
# to 0 if you are raising the major API version
LIBPATCH = 4

PYDEPS = ["opentelemetry-exporter-otlp-proto-grpc==1.17.0"]
LIBPATCH = 1

PYDEPS = ["opentelemetry-exporter-otlp-proto-http>=1.21.0"]

logger = logging.getLogger("tracing")

Expand Down Expand Up @@ -127,11 +187,29 @@ def get_current_span() -> Union[Span, None]:
return cast(Span, span)


def _get_tracer_from_context(ctx: Context) -> Optional[ContextVar]:
tracers = [v for v in ctx if v is not None and v.name == "tracer"]
if tracers:
return tracers[0]
return None


def _get_tracer() -> Optional[Tracer]:
"""Find tracer in context variable and as a fallback locate it in the full context."""
try:
return tracer.get()
except LookupError:
return None
try:
logger.debug("tracer was not found in context variable, looking up in default context")
ctx: Context = copy_context()
if context_tracer := _get_tracer_from_context(ctx):
return context_tracer.get()
else:
logger.debug("Couldn't find context var for tracer: span will be skipped")
return None
except LookupError as err:
logger.debug(f"Couldn't find tracer: span will be skipped, err: {err}")
return None


@contextmanager
Expand All @@ -141,6 +219,7 @@ def _span(name: str) -> Generator[Optional[Span], Any, Any]:
with tracer.start_as_current_span(name) as span:
yield cast(Span, span)
else:
logger.debug("tracer not found")
yield None


Expand Down Expand Up @@ -175,8 +254,8 @@ def _get_tracing_endpoint(tracing_endpoint_getter, self, charm):
f"got {tracing_endpoint} instead."
)
else:
logger.debug(f"Setting up span exporter to endpoint: {tracing_endpoint}")
return tracing_endpoint
logger.debug(f"Setting up span exporter to endpoint: {tracing_endpoint}/v1/traces")
return f"{tracing_endpoint}/v1/traces"


def _get_server_cert(server_cert_getter, self, charm):
Expand All @@ -190,9 +269,9 @@ def _get_server_cert(server_cert_getter, self, charm):
f"{charm}.{server_cert_getter} returned None; continuing with INSECURE connection."
)
return
elif not isinstance(server_cert, str):
raise TypeError(
f"{charm}.{server_cert_getter} should return a valid tls cert (string); "
elif not Path(server_cert).is_absolute():
raise ValueError(
f"{charm}.{server_cert_getter} should return a valid tls cert absolute path (string | Path)); "
f"got {server_cert} instead."
)
logger.debug("Certificate successfully retrieved.") # todo: some more validation?
Expand Down Expand Up @@ -241,14 +320,14 @@ def wrap_init(self: CharmBase, framework: Framework, *args, **kwargs):
if not tracing_endpoint:
return

server_cert: Optional[str] = (
server_cert: Optional[Union[str, Path]] = (
_get_server_cert(server_cert_getter, self, charm) if server_cert_getter else None
)
credentials = ChannelCredentials(server_cert) if server_cert else None
insecure = None if credentials else True

exporter = OTLPSpanExporter(
endpoint=tracing_endpoint, credentials=credentials, insecure=insecure, timeout=2
endpoint=tracing_endpoint,
certificate_file=str(Path(server_cert).absolute()) if server_cert else None,
timeout=2,
)

processor = BatchSpanProcessor(exporter)
Expand Down Expand Up @@ -313,28 +392,31 @@ def trace_charm(
method calls on instances of this class.
Usage:
>>> from charms.tempo_k8s.v0.charm_tracing import trace_charm
>>> from charms.tempo_k8s.v1.charm_tracing import trace_charm
>>> from charms.tempo_k8s.v1.tracing import TracingEndpointProvider
>>> from ops import CharmBase
>>>
>>> @trace_charm(
>>> tracing_endpoint="tempo_otlp_grpc_endpoint",
>>> tracing_endpoint="tempo_otlp_http_endpoint",
>>> )
>>> class MyCharm(CharmBase):
>>>
>>> def __init__(self, framework: Framework):
>>> ...
>>> self.tempo = TracingEndpointProvider(self)
>>> self.tracing = TracingEndpointProvider(self)
>>>
>>> @property
>>> def tempo_otlp_grpc_endpoint(self) -> Optional[str]:
>>> return self.tempo.otlp_grpc_endpoint
>>> def tempo_otlp_http_endpoint(self) -> Optional[str]:
>>> if self.tracing.is_ready():
>>> return self.tracing.otlp_http_endpoint()
>>> else:
>>> return None
>>>
:param server_cert: method or property on the charm type that returns an
optional tls certificate to be used when sending traces to a remote server.
optional absolute path to a tls certificate to be used when sending traces to a remote server.
If it returns None, an _insecure_ connection will be used.
:param tracing_endpoint: name of a property on the charm type that returns an
optional tempo url. If None, tracing will be effectively disabled. Else, traces will be
optional (fully resolvable) tempo url. If None, tracing will be effectively disabled. Else, traces will be
pushed to that endpoint.
:param service_name: service name tag to attach to all traces generated by this charm.
Defaults to the juju application name this charm is deployed under.
Expand Down Expand Up @@ -370,19 +452,20 @@ def _autoinstrument(
Usage:
>>> from charms.tempo_k8s.v0.charm_tracing import _autoinstrument
>>> from charms.tempo_k8s.v1.charm_tracing import _autoinstrument
>>> from ops.main import main
>>> _autoinstrument(
>>> MyCharm,
>>> tracing_endpoint_getter=MyCharm.tempo_otlp_grpc_endpoint,
>>> tracing_endpoint_getter=MyCharm.tempo_otlp_http_endpoint,
>>> service_name="MyCharm",
>>> extra_types=(Foo, Bar)
>>> )
>>> main(MyCharm)
:param charm_type: the CharmBase subclass to autoinstrument.
:param server_cert_getter: method or property on the charm type that returns an
optional tls certificate to be used when sending traces to a remote server.
optional absolute path to a tls certificate to be used when sending traces to a remote server.
This needs to be a valid path to a certificate.
:param tracing_endpoint_getter: method or property on the charm type that returns an
optional tempo url. If None, tracing will be effectively disabled. Else, traces will be
pushed to that endpoint.
Expand Down
Loading

0 comments on commit d7a20a6

Please sign in to comment.