#!/usr/bin/env python3 """ Minimal OTLP HTTP/protobuf trace receiver for Dagger CI timing. Usage: python3 ci/otel-receiver.py --port-file=/tmp/otel.port Caller sets: OTEL_EXPORTER_OTLP_ENDPOINT=http://127.0.0.1: OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf """ import argparse import signal import struct import sys import threading from http.server import BaseHTTPRequestHandler, HTTPServer # ── Minimal protobuf binary decoder ───────────────────────────────────────── # Only decodes the fields we need; skips everything else safely. def _varint(buf, pos): n, shift = 0, 0 while pos < len(buf): b = buf[pos]; pos += 1 n |= (b & 0x7F) << shift shift += 7 if not (b & 0x80): return n, pos raise ValueError("truncated varint") def _fields(buf): """Yield (field_num, wire_type, raw_value) for each field in a message.""" pos = 0 while pos < len(buf): tag, pos = _varint(buf, pos) wt, fn = tag & 7, tag >> 3 if wt == 0: # varint v, pos = _varint(buf, pos) elif wt == 1: # fixed64 v = struct.unpack_from("8} SPAN') print("─" * (6 + 2 + 8 + 2 + NAME_W + 20)) for r in rows: status = "CACHED" if r["cached"] else "LIVE" name = r["name"] if len(name) > NAME_W: name = name[: NAME_W - 1] + "…" print(f'{status:<6} {r["dur"]:7.2f}s {name}') print(f"\n{len(rows)} spans total") def main(): ap = argparse.ArgumentParser() ap.add_argument("--port-file", default="") args = ap.parse_args() server = HTTPServer(("127.0.0.1", 0), _Handler) if args.port_file: with open(args.port_file, "w") as f: f.write(str(server.server_address[1])) def _shutdown(sig, frame): threading.Thread(target=server.shutdown, daemon=True).start() signal.signal(signal.SIGTERM, _shutdown) signal.signal(signal.SIGINT, _shutdown) server.serve_forever() _report() if __name__ == "__main__": main()