Skip to content

protobuf: load schemas via --registry, including BSR modules - #39

Merged
achille-roussel merged 7 commits into
mainfrom
protobuf-registry
May 20, 2026
Merged

protobuf: load schemas via --registry, including BSR modules#39
achille-roussel merged 7 commits into
mainfrom
protobuf-registry

Conversation

@achille-roussel

Copy link
Copy Markdown
Contributor

Summary

Adds a way to feed protobuf descriptors into the stripes CLI at run time, so the protobuf renderer can decode .binpb (and protojson) payloads it wasn't compiled with.

  • --registry repeats and dispatches by value shape:
    • *.binpbset / *.protoset / *.pb → FileDescriptorSet bytes
    • *.proto → compiled in-process via bufbuild/protocompile
    • buf.build/<owner>/<module>[:ref] → shells out to buf build -o - (the BSR path)
  • --include adds "protoc -I"-style import roots so a remote tree of .proto files needs its root supplied only once.
  • --schema names the message; unresolvable schemas are a hard error.
  • .binpb extension auto-detects to the protobuf renderer; -f protobuf on a .json input goes through protojson.

End-to-end OTLP example (real BSR + real buf):

stripes \
  --registry buf.build/opentelemetry/opentelemetry \
  --schema   opentelemetry.proto.trace.v1.TracesData \
  s3://bucket/trace.binpb

Second commit fixes a longstanding wrap bug: long string values inside deeply-nested messages overflowed the terminal because the wrap budget didn't account for the chain indent added by PrefixWriters, wordwrap padded each line with trailing spaces, and lipgloss padded multi-line blocks to the widest line.

Test plan

  • go test ./... green (18 packages)
  • Unit tests in protobuf/schema for descriptor sets, .proto, protoc-I cross-directory resolution, BSR via a stub-buf, conflict/missing/unsupported inputs
  • CLI testscripts for --registry/--schema happy path, error paths, and the BSR path via the stub binary
  • Smoke-tested end-to-end against the real OTLP module on buf.build with a non-trivial TracesData payload
  • Regression test for the wrap fix at 4 levels of nesting, asserting no line exceeds the requested width

🤖 Generated with Claude Code

achille-roussel and others added 7 commits May 20, 2026 11:39
Adds --registry and --include flags to the stripes CLI so the protobuf
renderer can resolve message descriptors at run time instead of relying
on whatever is compiled into the binary. --schema names the message;
--registry repeats and dispatches by value shape:

  *.binpbset / *.protoset / *.pb     FileDescriptorSet bytes
  *.proto                            compiled in-process via protocompile
  buf.build/<owner>/<module>[:ref]   shells out to "buf build -o -"

--include adds "protoc -I"-style import roots for .proto resolution, so a
remote tree of .proto files needs its root supplied only once. All paths
accept tigerblock URIs (s3://, gs://, https://, …), and a missing or
unresolvable --schema is a hard error before any decoding.

Also routes "-f protobuf" on a .json input through protojson, auto-
detects .binpb, and preserves the +suffix on application/<type>+json so
the protobuf renderer can pick its encoding without a separate Format
registration. Loaded files are merged into protoregistry.GlobalFiles for
the process so the renderer's existing GlobalFiles-first lookup just
works.

Tests cover descriptor sets, single .proto files, protoc -I cross-
directory resolution, BSR-via-buf (using a stub-buf binary), conflict
and missing-file paths, and the new CLI surface end-to-end.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
wrapProtobufString budgeted against the raw terminal width, ignoring
the chain indent that PrefixWriters prepend on output. At deep nesting
(e.g. resource_spans → scope_spans → spans → attributes → value on an
OTLP trace) the wrapped lines overflowed by 2*depth columns and the
terminal soft-wrapped mid-word.

Three fixes together:

  - Thread an indentedStyles copy through every PrefixWriter creation
    so the recursive renderer sees Width minus the indent it inherits.
  - Strip wordwrap's per-line trailing-space padding before joining,
    and bump the first-line margin from 4 to 16 chars to cover the
    "<field_name>: " prefix.
  - Style each wrapped line individually so lipgloss does not treat a
    multi-line block as a layout box and pad short lines to the widest
    one (which silently re-inserted the same trailing spaces).

Regression test TestProtobufWrapRespectsNestingDepth builds a 4-level-
deep OTLP-shaped message holding a long string and asserts every
rendered line fits within the requested width.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When a source's content type carries a "messageType" MIME parameter
(e.g. application/protobuf; messageType="foo.Bar" — the convention
gRPC and several HTTP-protobuf tools use), the protobuf renderer now
falls back to it when --schema is not explicit. For HTTP(S) sources
that already advertise both the encoding and the message name in a
single header, that means "just point stripes at the URL" works
without --schema or --registry as long as the descriptor is in scope.

Three pieces, intentionally narrow:

  - stripes.Func aliases application/x-FOO to application/FOO when the
    canonical form is registered. RFC 6648 deprecated the x- prefix
    but plenty of tools still emit it (application/x-protobuf is the
    historical spelling of application/protobuf). The fallback only
    fires when the x- form itself isn't registered, so deliberate
    text/x-dockerfile-style registrations still win directly.
  - protobuf.rendererFor falls back to params["messagetype"] when
    schemaURL is empty (mime.ParseMediaType lowercases parameter
    names). Explicit --schema still wins over the MIME parameter.
  - cmd/stripes threads tigerblock storage's info.ContentType through
    renderOne as a hint, consulted between user --format and the
    filename/sniff cascade. --content-type and --format from the user
    still win.

Integration test serves a wrapperspb.StringValue payload from the
existing httptest server with content-type application/x-protobuf;
messageType="google.protobuf.StringValue" and asserts the CLI
auto-decodes it. wrapperspb is linked into the stripes binary via the
runtime so the descriptor resolves from GlobalTypes with no --registry.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
schema.LoadRegistry's path loop and uriResolver.FindFileByPath both
called storage.GetObject one path at a time. For .binpbset reads from
s3/https, multi-registry invocations, or .proto trees with several
include roots, that turned what could be a single round-trip wall-time
into N. Hand both loops to firetiger-oss/concurrent.Run, which fans
out across a bounded goroutine pool and yields results in input order
so user-precedence on conflicts is preserved and protoregistry.Files
writes stay sequential.

Restructure LoadRegistry into fetch-then-register: fetchSchemaInput
does the per-path I/O (readURI for descriptor sets, bufBuild for
buf.build refs, no-op for .proto sources) and runs under
concurrent.Run; the outer loop ranges over the in-order results and
hands descriptor-set bytes to loadDescriptorSetBytes serially.
uriResolver.FindFileByPath similarly fans the readURI calls across
its include roots and returns the first successful in-order hit.

Test scaffolding gains a STUB_BUF_DELAY_MS env var on stub-buf so the
new TestLoadRegistryConcurrentFetch can measure 3 module fetches
under a 500ms artificial delay and assert wall-clock stays under 2×
delay — concurrent runs land near 1× delay (~650ms), a serial loop
would land at ≥3× delay (~1500ms), and the threshold sits cleanly in
the gap.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
protoregistry.Files.RangeFiles has the exact shape of an iter.Seq
callback (func(T) bool), so Go 1.23+ range-over-func works on it
directly. Inline the callback as a for-range loop, drop the outer
closed-over err variable (we can return directly from the loop body
now), and replace the bool returns with continue / explicit return.
Same control flow, less indentation, no closure.
New subpackage protobuf/otlp side-effect-imports the OpenTelemetry
protobuf Go bindings (trace/logs/metrics/v1 plus their common/resource
imports) so the descriptors land in protoregistry.GlobalTypes at
process start. The stripes CLI binary imports the package directly.

With this in place, an OTLP payload served via HTTP with
"Content-Type: application/x-protobuf; messageType=
opentelemetry.proto.trace.v1.TracesData" decodes end-to-end with
zero flags — no --registry, no --schema, no buf subprocess. The
combination of (a) the previously-added Content-Type messageType
auto-detect and (b) pre-registered descriptors collapses to "just
point stripes at the URL."

Binary-size cost: ~290 KB on the ~77 MB CLI. Acceptable for canonical
schemas. Collector wrappers (ExportTraceServiceRequest etc.) are
intentionally excluded to avoid pulling grpc-gateway into the runtime
closure; users decoding raw OTLP/gRPC export requests can still pass
--registry buf.build/opentelemetry/opentelemetry explicitly.

Test scaffolding gains a second HTTP endpoint /proto/otlp serving a
real TracesData payload and a testscript that hits it with no flags.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pure formatting fix flagged by CI's gofmt check:

  - cli_test.go: import group ordering — third-party imports sort
    alphabetically by path, so github.com/rogpeppe/... must come
    before go.opentelemetry.io/...
  - func_test.go: realign trailing // comments after introducing a
    shorter row in TestFuncXPrefixAlias.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@achille-roussel
achille-roussel merged commit 19f756a into main May 20, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant