Files

109 lines
4.2 KiB
Python

"""OTLP/gRPC export helpers: send logs, traces and metrics over a real gRPC channel."""
from __future__ import annotations
import base64
import os
import time
import uuid
from dataclasses import dataclass
from urllib.parse import urlparse
import grpc
from opentelemetry.proto.collector.logs.v1 import logs_service_pb2, logs_service_pb2_grpc
from opentelemetry.proto.collector.metrics.v1 import metrics_service_pb2, metrics_service_pb2_grpc
from opentelemetry.proto.collector.trace.v1 import trace_service_pb2, trace_service_pb2_grpc
from opentelemetry.proto.common.v1 import common_pb2
from opentelemetry.proto.logs.v1 import logs_pb2
from opentelemetry.proto.metrics.v1 import metrics_pb2
from opentelemetry.proto.resource.v1 import resource_pb2
from opentelemetry.proto.trace.v1 import trace_pb2
STALE_OFFSET_NANOS = 60 * 24 * 60 * 60 * 1_000_000_000
@dataclass
class ExportResult:
code: grpc.StatusCode
details: str = ""
rejected: int = 0
error_message: str = ""
has_partial_success: bool = False
def grpc_target() -> str:
"""host:port of the OTLP gRPC listener, from ZO_GRPC_ADDR or ZO_BASE_URL's host plus ZO_GRPC_PORT."""
if os.environ.get("ZO_GRPC_ADDR"):
return os.environ["ZO_GRPC_ADDR"]
host = urlparse(os.environ.get("ZO_BASE_URL", "http://localhost:5080")).hostname or "localhost"
return f"{host}:{os.environ.get('ZO_GRPC_PORT', '5081')}"
def metadata(stream: str, org: str = "default") -> list[tuple[str, str]]:
creds = f"{os.environ['ZO_ROOT_USER_EMAIL']}:{os.environ['ZO_ROOT_USER_PASSWORD']}".encode()
return [
("authorization", "Basic " + base64.b64encode(creds).decode()),
("organization", org),
("stream-name", stream),
]
def now_nanos() -> int:
return time.time_ns()
def _resource() -> resource_pb2.Resource:
return resource_pb2.Resource(attributes=[
common_pb2.KeyValue(key="service.name", value=common_pb2.AnyValue(string_value="otlp-grpc-test")),
])
def _call(fn, request, stream: str, rejected_field: str) -> ExportResult:
try:
resp = fn(request, metadata=metadata(stream), timeout=30)
except grpc.RpcError as e:
return ExportResult(code=e.code(), details=e.details() or "")
if not resp.HasField("partial_success"):
return ExportResult(code=grpc.StatusCode.OK)
ps = resp.partial_success
return ExportResult(
code=grpc.StatusCode.OK,
rejected=getattr(ps, rejected_field),
error_message=ps.error_message,
has_partial_success=True,
)
def export_logs(channel: grpc.Channel, stream: str, times: list[int]) -> ExportResult:
records = [
logs_pb2.LogRecord(time_unix_nano=t, body=common_pb2.AnyValue(string_value=f"otlp-grpc-test {i}"))
for i, t in enumerate(times)
]
req = logs_service_pb2.ExportLogsServiceRequest(resource_logs=[
logs_pb2.ResourceLogs(resource=_resource(), scope_logs=[logs_pb2.ScopeLogs(log_records=records)]),
])
return _call(logs_service_pb2_grpc.LogsServiceStub(channel).Export, req, stream, "rejected_log_records")
def export_spans(channel: grpc.Channel, stream: str, times: list[int]) -> ExportResult:
spans = [
trace_pb2.Span(
trace_id=uuid.uuid4().bytes, span_id=uuid.uuid4().bytes[:8], name=f"op-{i}",
start_time_unix_nano=t, end_time_unix_nano=t + 1_000_000,
)
for i, t in enumerate(times)
]
req = trace_service_pb2.ExportTraceServiceRequest(resource_spans=[
trace_pb2.ResourceSpans(resource=_resource(), scope_spans=[trace_pb2.ScopeSpans(spans=spans)]),
])
return _call(trace_service_pb2_grpc.TraceServiceStub(channel).Export, req, stream, "rejected_spans")
def export_gauge(channel: grpc.Channel, stream: str, metric: str, times: list[int]) -> ExportResult:
points = [metrics_pb2.NumberDataPoint(time_unix_nano=t, as_double=1.0) for t in times]
req = metrics_service_pb2.ExportMetricsServiceRequest(resource_metrics=[
metrics_pb2.ResourceMetrics(resource=_resource(), scope_metrics=[metrics_pb2.ScopeMetrics(metrics=[
metrics_pb2.Metric(name=metric, gauge=metrics_pb2.Gauge(data_points=points)),
])]),
])
return _call(metrics_service_pb2_grpc.MetricsServiceStub(channel).Export, req, stream, "rejected_data_points")