Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .changelog/5704.fixed
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
`opentelemetry-exporter-zipkin-json`: warn and fall back to the default instead of raising `ValueError` when `OTEL_EXPORTER_ZIPKIN_TIMEOUT` holds a non-integer value; the same fix is applied to `opentelemetry-exporter-zipkin-proto-http`.
4 changes: 0 additions & 4 deletions .pylintrc
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,6 @@ persistent=yes
# Specify a configuration file.
#rcfile=

# When enabled, pylint would attempt to guess common misconfiguration and emit
# user-friendly hints instead of false-positive error messages.
suggestion-mode=yes

# Allow loading of arbitrary C extensions. Extensions are imported into the
# active Python interpreter and may run arbitrary code.
unsafe-load-any-extension=no
Expand Down
2 changes: 1 addition & 1 deletion dev-requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
pylint==3.3.4
pylint==4.0.9
griffe==1.15.0
httpretty==1.1.4
inline-snapshot==0.34.2
Expand Down
Original file line number Diff line number Diff line change
@@ -1,2 +1,19 @@
# Copyright The OpenTelemetry Authors
# SPDX-License-Identifier: Apache-2.0

"""Encoders shared by the OTLP JSON exporters.

This package converts the telemetry that the OpenTelemetry SDK holds in memory
into the dataclasses of ``opentelemetry.proto_json``, whose ``to_dict`` methods
produce the OTLP JSON wire representation. It performs no input and output of
its own. ``opentelemetry-exporter-otlp-json-http`` and
``opentelemetry-exporter-otlp-json-file`` depend on this package, which is what
makes the two of them encode the three signals identically.

The three entry points are ``encode_spans`` in
``opentelemetry.exporter.otlp.json.common.trace_encoder``, ``encode_metrics``
in ``opentelemetry.exporter.otlp.json.common.metrics_encoder`` and
``encode_logs`` in ``opentelemetry.exporter.otlp.json.common._log_encoder``.
Everything under ``opentelemetry.exporter.otlp.json.common._internal`` is
private and can change in any release.
"""
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,22 @@

# pyright: reportCallIssue=false

"""Encoding helpers shared by the trace, metrics and log encoders.

What the three signals have in common is what this module covers: the resource
and the instrumentation scope that every exported batch is grouped by, the
attribute lists that hang off nearly every OTLP message, and the fixed-width
byte form of trace and span identifiers that ``to_dict`` on the enclosing
message renders as hexadecimal.

Nothing here is public, and nothing outside this distribution imports these
names. The trace, metrics and log encoder packages under
``opentelemetry.exporter.otlp.json.common._internal`` each import the ones they
need, which is not the same set for all three. ``_encode_key_value`` is
imported by none of them: it is reached only from ``_encode_value`` and
``_encode_attributes`` in this module.
"""

from __future__ import annotations

import logging
Expand Down Expand Up @@ -33,6 +49,20 @@
def _encode_instrumentation_scope(
instrumentation_scope: InstrumentationScope | None,
) -> JSONInstrumentationScope:
"""Encode an SDK instrumentation scope as its OTLP JSON message.

Args:
instrumentation_scope: the scope recorded on the telemetry being
exported, or ``None`` when the telemetry carries no scope.

Returns:
A ``JSONInstrumentationScope`` carrying the name, the version and the
attributes of the scope. The schema URL of the scope is not encoded
here: it belongs to the enclosing ``ScopeSpans``, ``ScopeMetrics`` or
``ScopeLogs`` message, and each signal encoder sets it there. When
``instrumentation_scope`` is ``None`` the result is a message left at
its field defaults, which serialises to an empty JSON object.
"""
return (
JSONInstrumentationScope(
name=instrumentation_scope.name,
Expand All @@ -45,11 +75,45 @@ def _encode_instrumentation_scope(


def _encode_resource(resource: Resource) -> JSONResource:
"""Encode an SDK resource as its OTLP JSON message.

Args:
resource: the resource the telemetry being exported was produced
under.

Returns:
A ``JSONResource`` carrying the attributes of the resource. The schema
URL of the resource is not encoded here: it belongs to the enclosing
``ResourceSpans``, ``ResourceMetrics`` or ``ResourceLogs`` message, and
each signal encoder sets it there.
"""
return JSONResource(attributes=_encode_attributes(resource.attributes))


# pylint: disable-next=too-many-return-statements
def _encode_value(value: Any) -> JSONAnyValue:
"""Encode one attribute value or log body as an OTLP JSON ``AnyValue``.

A mapping or a sequence is encoded recursively, so a nested value becomes a
tree of ``AnyValue`` messages. Mapping keys are passed through ``str``,
because OTLP has no representation for a non-string key. The order in which
the types are tested is load bearing: ``bool`` is tested before ``int``
because ``bool`` is a subclass of ``int``, and ``str`` and ``bytes`` are
tested before ``Sequence`` because both register as sequences.

Args:
value: the value to encode.

Returns:
The encoded ``JSONAnyValue``. For a ``value`` of ``None`` that is a
message with no field set, which serialises to an empty JSON object
rather than to a JSON ``null``.

Raises:
TypeError: if ``value`` is of a type OTLP has no representation for.
``_encode_attributes`` catches this for an attribute, but a log
body of such a type reaches the caller.
"""
if value is None:
return JSONAnyValue()
if isinstance(value, bool):
Expand All @@ -72,20 +136,78 @@ def _encode_value(value: Any) -> JSONAnyValue:


def _encode_key_value(key: str, value: Any) -> JSONKeyValue:
"""Encode one named value as an OTLP JSON ``KeyValue``.

Args:
key: the name, used unchanged.
value: the value, encoded by ``_encode_value``.

Returns:
The encoded ``JSONKeyValue``.

Raises:
TypeError: if ``_encode_value`` has no representation for ``value``.
"""
return JSONKeyValue(key=key, value=_encode_value(value))


def _encode_span_id(span_id: int) -> bytes:
"""Encode a span identifier as the eight bytes OTLP gives it.

Args:
span_id: the identifier as the SDK holds it, an unsigned integer of at
most 64 bits.

Returns:
The identifier as eight big-endian bytes, which ``to_dict`` on the
enclosing message then renders as a sixteen-character lowercase
hexadecimal string.

Raises:
OverflowError: if ``span_id`` is negative or needs more than eight
bytes.
"""
return span_id.to_bytes(length=8, byteorder="big", signed=False)


def _encode_trace_id(trace_id: int) -> bytes:
"""Encode a trace identifier as the sixteen bytes OTLP gives it.

Args:
trace_id: the identifier as the SDK holds it, an unsigned integer of at
most 128 bits.

Returns:
The identifier as sixteen big-endian bytes, which ``to_dict`` on the
enclosing message then renders as a thirty-two-character lowercase
hexadecimal string.

Raises:
OverflowError: if ``trace_id`` is negative or needs more than sixteen
bytes.
"""
return trace_id.to_bytes(length=16, byteorder="big", signed=False)


def _encode_attributes(
attributes: Attributes,
) -> list[JSONKeyValue]:
"""Encode an attribute mapping as a list of OTLP JSON ``KeyValue`` messages.

An attribute whose value ``_encode_value`` has no representation for does
not fail the export. The exception is logged together with the key, that
one attribute is left out, and the remaining attributes are still encoded,
so one bad value does not cost the caller the whole batch.

Args:
attributes: the attributes to encode, or ``None`` when the telemetry
carries none.

Returns:
One ``JSONKeyValue`` per attribute that encoded, in the iteration order
of ``attributes``. ``None`` and an empty mapping both give an empty
list.
"""
if not attributes:
return []
json_attributes = []
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,20 @@

# pyright: reportCallIssue=false

"""Encoding of SDK log records into OTLP JSON log messages.

``encode_logs`` is the entry point and is re-exported from
``opentelemetry.exporter.otlp.json.common._log_encoder``, which is where a log
record exporter should import it from. The rest of this module is private.

A log record exporter hands over a flat collection of records, while OTLP wants
them grouped first by resource and then by instrumentation scope. Rebuilding
that grouping is most of the work here, and it relies on both
``opentelemetry.sdk.resources.Resource`` and
``opentelemetry.sdk.util.instrumentation.InstrumentationScope`` being hashable,
so that each can be used as a dictionary key.
"""

from __future__ import annotations

from collections import defaultdict
Expand Down Expand Up @@ -34,10 +48,49 @@
def encode_logs(
batch: Collection[ReadableLogRecord],
) -> JSONExportLogsServiceRequest:
"""Encode a batch of SDK log records as an OTLP export request.

Args:
batch: the log records to export, in any order and from any number of
resources and instrumentation scopes.

Returns:
An ``ExportLogsServiceRequest`` holding every record in ``batch``,
regrouped by resource and then by instrumentation scope. An empty
``batch`` gives a request with no resource logs.

Raises:
TypeError: if the body of a record, or any value nested inside a
mapping or a sequence body, is of a type OTLP has no
representation for. Unlike an attribute of such a type, which is
dropped with a log message, a body of such a type fails the whole
batch.
"""
return JSONExportLogsServiceRequest(resource_logs=_encode_resource_logs(batch))


def _encode_log(readable_log_record: ReadableLogRecord) -> JSONLogRecord:
"""Encode one SDK log record as an OTLP JSON ``LogRecord``.

A trace or span identifier of zero is treated as absent and left out of the
result, since zero is the identifier the SDK uses for a record that was
emitted outside any span.

Args:
readable_log_record: the record to encode, together with the
instrumentation scope and the resource it was emitted under.

Returns:
The encoded ``LogRecord``. ``severity_number`` is the numeric value of
the severity, or ``None`` when the record carries no severity.
``dropped_attributes_count`` is what the SDK log record limits dropped,
and is zero when the attributes were never bounded.

Raises:
TypeError: if the body of the record, or any value nested inside a
mapping or a sequence body, is of a type OTLP has no
representation for.
"""
return JSONLogRecord(
time_unix_nano=readable_log_record.log_record.timestamp,
observed_time_unix_nano=readable_log_record.log_record.observed_timestamp,
Expand All @@ -60,6 +113,28 @@ def _encode_log(readable_log_record: ReadableLogRecord) -> JSONLogRecord:
def _encode_resource_logs(
batch: Collection[ReadableLogRecord],
) -> list[JSONResourceLogs]:
"""Group a flat collection of SDK log records into OTLP ``ResourceLogs``.

Records sharing a resource end up in one ``ResourceLogs``, and within it
records sharing an instrumentation scope end up in one ``ScopeLogs``. The
schema URL of the resource is written on the ``ResourceLogs`` and the
schema URL of the scope on the ``ScopeLogs``.

Args:
batch: the log records to group, in any order.

Returns:
One ``ResourceLogs`` per distinct resource, in the order each resource
was first seen in ``batch``. A record whose instrumentation scope is
absent or falsy is grouped under a scope of ``None``, which encodes as
an empty scope message and leaves the schema URL of the ``ScopeLogs``
unset.

Raises:
TypeError: if the body of a record, or any value nested inside a
mapping or a sequence body, is of a type OTLP has no
representation for.
"""
sdk_resource_logs = defaultdict(lambda: defaultdict(list))

for readable_log in batch:
Expand Down
Loading
Loading