mirror of
https://github.com/openobserve/openobserve.git
synced 2026-11-04 08:09:27 +00:00
197 lines
5.3 KiB
Python
197 lines
5.3 KiB
Python
"""Hand-rolled OTLP protobuf encoder for logs.
|
|
|
|
A generated `*_pb2` module cannot express these tests: the Python protobuf
|
|
runtime rejects non-UTF-8 on a `string` field at set time, and the whole point
|
|
here is to send exactly the payload the Go collector marshals happily — a
|
|
`string` field holding raw bytes. So the wire format is built by hand and every
|
|
string field takes `bytes`.
|
|
|
|
Field numbers below follow opentelemetry-proto v1 (logs/v1/logs.proto,
|
|
common/v1/common.proto, resource/v1/resource.proto).
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import struct
|
|
|
|
WIRE_VARINT = 0
|
|
WIRE_FIXED64 = 1
|
|
WIRE_LEN = 2
|
|
WIRE_FIXED32 = 5
|
|
|
|
|
|
# ----- primitives -----
|
|
|
|
|
|
def varint(value: int) -> bytes:
|
|
"""Base-128 varint, the encoding every protobuf tag and length uses."""
|
|
out = bytearray()
|
|
while True:
|
|
byte = value & 0x7F
|
|
value >>= 7
|
|
if value:
|
|
out.append(byte | 0x80)
|
|
else:
|
|
out.append(byte)
|
|
return bytes(out)
|
|
|
|
|
|
def tag(number: int, wire: int) -> bytes:
|
|
return varint((number << 3) | wire)
|
|
|
|
|
|
def delimited(number: int, payload: bytes) -> bytes:
|
|
"""Wire type 2 — carries `string`, `bytes` and nested messages alike."""
|
|
return tag(number, WIRE_LEN) + varint(len(payload)) + payload
|
|
|
|
|
|
def varint_field(number: int, value: int) -> bytes:
|
|
return tag(number, WIRE_VARINT) + varint(value)
|
|
|
|
|
|
def fixed64_field(number: int, value: int) -> bytes:
|
|
return tag(number, WIRE_FIXED64) + struct.pack("<Q", value)
|
|
|
|
|
|
def fixed32_field(number: int, value: int) -> bytes:
|
|
return tag(number, WIRE_FIXED32) + struct.pack("<I", value)
|
|
|
|
|
|
# ----- common.v1.AnyValue -----
|
|
|
|
|
|
def any_string(raw: bytes) -> bytes:
|
|
return delimited(1, raw)
|
|
|
|
|
|
def any_bool(value: bool) -> bytes:
|
|
return varint_field(2, 1 if value else 0)
|
|
|
|
|
|
def any_int(value: int) -> bytes:
|
|
return varint_field(3, value)
|
|
|
|
|
|
def any_double(value: float) -> bytes:
|
|
return tag(4, WIRE_FIXED64) + struct.pack("<d", value)
|
|
|
|
|
|
def any_array(values: list[bytes]) -> bytes:
|
|
"""ArrayValue.values is field 1, repeated AnyValue."""
|
|
return delimited(5, b"".join(delimited(1, v) for v in values))
|
|
|
|
|
|
def any_kvlist(pairs: list[tuple[bytes, bytes]]) -> bytes:
|
|
"""KeyValueList.values is field 1, repeated KeyValue."""
|
|
return delimited(6, b"".join(delimited(1, key_value(k, v)) for k, v in pairs))
|
|
|
|
|
|
def any_bytes(raw: bytes) -> bytes:
|
|
"""AnyValue.bytes_value — never sanitized, which is what the tests pin."""
|
|
return delimited(7, raw)
|
|
|
|
|
|
def key_value(key: bytes, value: bytes) -> bytes:
|
|
"""common.v1.KeyValue: key=1 (string), value=2 (AnyValue)."""
|
|
return delimited(1, key) + delimited(2, value)
|
|
|
|
|
|
# ----- logs.v1 -----
|
|
|
|
|
|
def log_record(
|
|
*,
|
|
time_unix_nano: int,
|
|
body: bytes | None = None,
|
|
attributes: list[bytes] | None = None,
|
|
severity_number: int | None = None,
|
|
severity_text: bytes | None = None,
|
|
trace_id: bytes | None = None,
|
|
span_id: bytes | None = None,
|
|
flags: int | None = None,
|
|
event_name: bytes | None = None,
|
|
) -> bytes:
|
|
"""`attributes` are pre-encoded KeyValue messages from `key_value`."""
|
|
out = fixed64_field(1, time_unix_nano)
|
|
if severity_number is not None:
|
|
out += varint_field(2, severity_number)
|
|
if severity_text is not None:
|
|
out += delimited(3, severity_text)
|
|
if body is not None:
|
|
out += delimited(5, body)
|
|
for attr in attributes or []:
|
|
out += delimited(6, attr)
|
|
if flags is not None:
|
|
out += fixed32_field(8, flags)
|
|
if trace_id is not None:
|
|
out += delimited(9, trace_id)
|
|
if span_id is not None:
|
|
out += delimited(10, span_id)
|
|
out += fixed64_field(11, time_unix_nano)
|
|
if event_name is not None:
|
|
out += delimited(12, event_name)
|
|
return out
|
|
|
|
|
|
def instrumentation_scope(
|
|
*,
|
|
name: bytes | None = None,
|
|
version: bytes | None = None,
|
|
attributes: list[bytes] | None = None,
|
|
) -> bytes:
|
|
out = b""
|
|
if name is not None:
|
|
out += delimited(1, name)
|
|
if version is not None:
|
|
out += delimited(2, version)
|
|
for attr in attributes or []:
|
|
out += delimited(3, attr)
|
|
return out
|
|
|
|
|
|
def scope_logs(
|
|
records: list[bytes],
|
|
*,
|
|
scope: bytes | None = None,
|
|
schema_url: bytes | None = None,
|
|
) -> bytes:
|
|
out = b""
|
|
if scope is not None:
|
|
out += delimited(1, scope)
|
|
for record in records:
|
|
out += delimited(2, record)
|
|
if schema_url is not None:
|
|
out += delimited(3, schema_url)
|
|
return out
|
|
|
|
|
|
def resource(attributes: list[bytes]) -> bytes:
|
|
return b"".join(delimited(1, attr) for attr in attributes)
|
|
|
|
|
|
def resource_logs(
|
|
scopes: list[bytes],
|
|
*,
|
|
resource_attributes: list[bytes] | None = None,
|
|
schema_url: bytes | None = None,
|
|
) -> bytes:
|
|
out = b""
|
|
if resource_attributes is not None:
|
|
out += delimited(1, resource(resource_attributes))
|
|
for scope in scopes:
|
|
out += delimited(2, scope)
|
|
if schema_url is not None:
|
|
out += delimited(3, schema_url)
|
|
return out
|
|
|
|
|
|
def export_logs_request(resource_logs_list: list[bytes]) -> bytes:
|
|
return b"".join(delimited(1, rl) for rl in resource_logs_list)
|
|
|
|
|
|
def simple_logs_request(records: list[bytes], **kw) -> bytes:
|
|
"""One ResourceLogs holding one ScopeLogs holding `records`."""
|
|
scope = kw.pop("scope", None)
|
|
return export_logs_request(
|
|
[resource_logs([scope_logs(records, scope=scope)], **kw)]
|
|
)
|