Skip to content

Commit fbb455d

Browse files
anuraagalzchenxrmx
authored
feat(sdk): implement exporter metrics (#4976)
* feat(sdk): implement exporter metrics * changelog * Lint * All optional * Fix * Cleanup * Update HTTP exporters * Format * context manager * defaults * Fixes * Fix merge --------- Co-authored-by: Leighton Chen <lechen@microsoft.com> Co-authored-by: Riccardo Magliocchetti <riccardo.magliocchetti@gmail.com>
1 parent 90c0319 commit fbb455d

16 files changed

Lines changed: 1159 additions & 207 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7070
([#5012](https://github.com/open-telemetry/opentelemetry-python/pull/5012))
7171
- `opentelemetry-sdk`: upgrade vendored OTel configuration schema from v1.0.0-rc.3 to v1.0.0
7272
([#4965](https://github.com/open-telemetry/opentelemetry-python/pull/4965))
73+
- `opentelemetry-sdk`: implement exporter metrics
74+
([#4976](https://github.com/open-telemetry/opentelemetry-python/pull/4976))
7375
- improve check-links ci job
7476
([#4978](https://github.com/open-telemetry/opentelemetry-python/pull/4978))
7577
- Resolve some Pyright type errors in Span/ReadableSpan and utility stubs
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
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 __future__ import annotations
16+
17+
from collections import Counter
18+
from contextlib import contextmanager
19+
from dataclasses import dataclass
20+
from time import perf_counter
21+
from typing import TYPE_CHECKING, Iterator
22+
23+
from opentelemetry.metrics import MeterProvider, get_meter_provider
24+
from opentelemetry.semconv._incubating.attributes.otel_attributes import (
25+
OTEL_COMPONENT_NAME,
26+
OTEL_COMPONENT_TYPE,
27+
OtelComponentTypeValues,
28+
)
29+
from opentelemetry.semconv._incubating.metrics.otel_metrics import (
30+
create_otel_sdk_exporter_log_exported,
31+
create_otel_sdk_exporter_log_inflight,
32+
create_otel_sdk_exporter_metric_data_point_exported,
33+
create_otel_sdk_exporter_metric_data_point_inflight,
34+
create_otel_sdk_exporter_operation_duration,
35+
create_otel_sdk_exporter_span_exported,
36+
create_otel_sdk_exporter_span_inflight,
37+
)
38+
from opentelemetry.semconv.attributes.error_attributes import ERROR_TYPE
39+
from opentelemetry.semconv.attributes.server_attributes import (
40+
SERVER_ADDRESS,
41+
SERVER_PORT,
42+
)
43+
44+
if TYPE_CHECKING:
45+
from typing import Literal
46+
from urllib.parse import ParseResult as UrlParseResult
47+
48+
from opentelemetry.util.types import Attributes, AttributeValue
49+
50+
_component_counter = Counter()
51+
52+
53+
@dataclass
54+
class ExportResult:
55+
error: Exception | None = None
56+
error_attrs: Attributes = None
57+
58+
59+
class ExporterMetrics:
60+
def __init__(
61+
self,
62+
component_type: OtelComponentTypeValues | None,
63+
signal: Literal["traces", "metrics", "logs"],
64+
endpoint: UrlParseResult,
65+
meter_provider: MeterProvider | None,
66+
) -> None:
67+
if signal == "traces":
68+
create_exported = create_otel_sdk_exporter_span_exported
69+
create_inflight = create_otel_sdk_exporter_span_inflight
70+
elif signal == "logs":
71+
create_exported = create_otel_sdk_exporter_log_exported
72+
create_inflight = create_otel_sdk_exporter_log_inflight
73+
else:
74+
create_exported = (
75+
create_otel_sdk_exporter_metric_data_point_exported
76+
)
77+
create_inflight = (
78+
create_otel_sdk_exporter_metric_data_point_inflight
79+
)
80+
81+
port = endpoint.port
82+
if port is None:
83+
if endpoint.scheme == "https":
84+
port = 443
85+
elif endpoint.scheme == "http":
86+
port = 80
87+
88+
component_type = (
89+
component_type or OtelComponentTypeValues("unknown_otlp_exporter")
90+
).value
91+
count = _component_counter[component_type]
92+
_component_counter[component_type] = count + 1
93+
self._standard_attrs: dict[str, AttributeValue] = {
94+
OTEL_COMPONENT_TYPE: component_type,
95+
OTEL_COMPONENT_NAME: f"{component_type}/{count}",
96+
}
97+
if endpoint.hostname:
98+
self._standard_attrs[SERVER_ADDRESS] = endpoint.hostname
99+
if port is not None:
100+
self._standard_attrs[SERVER_PORT] = port
101+
102+
meter_provider = meter_provider or get_meter_provider()
103+
meter = meter_provider.get_meter("opentelemetry-sdk")
104+
self._inflight = create_inflight(meter)
105+
self._exported = create_exported(meter)
106+
self._duration = create_otel_sdk_exporter_operation_duration(meter)
107+
108+
@contextmanager
109+
def export_operation(self, num_items: int) -> Iterator[ExportResult]:
110+
start_time = perf_counter()
111+
self._inflight.add(num_items, self._standard_attrs)
112+
113+
result = ExportResult()
114+
try:
115+
yield result
116+
finally:
117+
error = result.error
118+
error_attrs = result.error_attrs
119+
120+
end_time = perf_counter()
121+
self._inflight.add(-num_items, self._standard_attrs)
122+
exported_attrs = (
123+
{**self._standard_attrs, ERROR_TYPE: type(error).__qualname__}
124+
if error
125+
else self._standard_attrs
126+
)
127+
self._exported.add(num_items, exported_attrs)
128+
duration_attrs = (
129+
{**exported_attrs, **error_attrs}
130+
if error_attrs
131+
else exported_attrs
132+
)
133+
self._duration.record(end_time - start_time, duration_attrs)

exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_log_exporter/__init__.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
_get_credentials,
2323
environ_to_compression,
2424
)
25+
from opentelemetry.metrics import MeterProvider
2526
from opentelemetry.proto.collector.logs.v1.logs_service_pb2 import (
2627
ExportLogsServiceRequest,
2728
)
@@ -44,6 +45,9 @@
4445
OTEL_EXPORTER_OTLP_LOGS_INSECURE,
4546
OTEL_EXPORTER_OTLP_LOGS_TIMEOUT,
4647
)
48+
from opentelemetry.semconv._incubating.attributes.otel_attributes import (
49+
OtelComponentTypeValues,
50+
)
4751

4852

4953
class OTLPLogExporter(
@@ -66,6 +70,8 @@ def __init__(
6670
timeout: Optional[float] = None,
6771
compression: Optional[Compression] = None,
6872
channel_options: Optional[Tuple[Tuple[str, str]]] = None,
73+
*,
74+
meter_provider: Optional[MeterProvider] = None,
6975
):
7076
insecure_logs = environ.get(OTEL_EXPORTER_OTLP_LOGS_INSECURE)
7177
if insecure is None and insecure_logs is not None:
@@ -105,13 +111,19 @@ def __init__(
105111
stub=LogsServiceStub,
106112
result=LogRecordExportResult,
107113
channel_options=channel_options,
114+
component_type=OtelComponentTypeValues.OTLP_GRPC_LOG_EXPORTER,
115+
signal="logs",
116+
meter_provider=meter_provider,
108117
)
109118

110119
def _translate_data(
111120
self, data: Sequence[ReadableLogRecord]
112121
) -> ExportLogsServiceRequest:
113122
return encode_logs(data)
114123

124+
def _count_data(self, data: Sequence[ReadableLogRecord]):
125+
return len(data)
126+
115127
def export( # type: ignore [reportIncompatibleMethodOverride]
116128
self,
117129
batch: Sequence[ReadableLogRecord],

0 commit comments

Comments
 (0)