connectrpc-otel 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +194 -0
- data/lib/connectrpc/otel/subscriber.rb +182 -0
- data/lib/connectrpc/otel/version.rb +7 -0
- data/lib/connectrpc/otel.rb +6 -0
- data/sig/connectrpc-otel.rbs +48 -0
- data/sig/manifest.yaml +2 -0
- metadata +83 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 2afdf1c6291bd39d2b2cd5c63ca15b5cd9cf60a87484715a931857daf1808342
|
|
4
|
+
data.tar.gz: 5983b85e156b331cb2ed65e235ae813dd69571865b55df69a747598d74e52723
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 816c7a986f4ca1df39040b262b57ec3337e2cf80f6404f66697d909eb0ce78cca3ca4fd657858474a272e73188814d4c3fcd0b2ff975125e55b6b96eea807ea0
|
|
7
|
+
data.tar.gz: a82429640248474cf3d9ab41b8805f7e8892109749054cb4d052d314bae4505852cb270b7eca6f975a5123fcb36f1762fbc88a2754692b996016d83aeeaab63d
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jim Myhrberg
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# connectrpc-otel
|
|
2
|
+
|
|
3
|
+
Optional, experimental OpenTelemetry traces for Connect unary/streaming, native
|
|
4
|
+
gRPC and server gRPC-Web RPCs. The only runtime dependencies are
|
|
5
|
+
`connectrpc ~> 0.1.0` and `opentelemetry-api >= 1.11.1, < 2`.
|
|
6
|
+
Core and the HTTP/framework packages do not depend on OpenTelemetry.
|
|
7
|
+
|
|
8
|
+
```ruby
|
|
9
|
+
require "connectrpc/otel"
|
|
10
|
+
|
|
11
|
+
subscriber = ConnectRPC::OTel::Subscriber.new(
|
|
12
|
+
tracer_provider: application_tracer_provider,
|
|
13
|
+
request_metadata: ["x-request-id"], response_metadata: ["x-region"]
|
|
14
|
+
)
|
|
15
|
+
instrumentation = ConnectRPC::Instrumentation.new(
|
|
16
|
+
subscribers: [subscriber], on_error: ->(error) { application_logger.warn(error.class.name) }
|
|
17
|
+
)
|
|
18
|
+
# Pass instrumentation: instrumentation to both ConnectRPC::Client.new and
|
|
19
|
+
# ConnectRPC::Server.new. Their other options and generated callers are unchanged.
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
The application owns its SDK, sampling, processors and exporters. Requiring
|
|
23
|
+
`connectrpc/otel` never loads or configures an SDK/exporter, replaces global
|
|
24
|
+
providers/propagators, or creates network clients. The default tracer provider is
|
|
25
|
+
the application's OpenTelemetry provider. An explicit W3C trace-context plus
|
|
26
|
+
baggage propagator is used by default, including when the global propagator is
|
|
27
|
+
no-op. Pass `propagator:` to select another application propagator. Span finish
|
|
28
|
+
invokes application-owned processors synchronously; a synchronous exporter can
|
|
29
|
+
block the calling thread/reactor. This package makes no nonblocking guarantee.
|
|
30
|
+
|
|
31
|
+
See the [supported 0.x contract](../../docs/support.md) for the target support
|
|
32
|
+
profile, matched package versions, compatibility policy and adoption gates.
|
|
33
|
+
|
|
34
|
+
## Spans and privacy
|
|
35
|
+
|
|
36
|
+
The adapter emits one CLIENT or SERVER span for each configured RPC side.
|
|
37
|
+
Names for declared methods are `fully.qualified.Service/Method`.
|
|
38
|
+
`rpc.system.name` is `connectrpc`, `rpc.method` is the same declared name,
|
|
39
|
+
and `rpc.response.status_code` is `OK` for known success or the lower-case
|
|
40
|
+
Connect error actually observed. Bare preflight/admission responses have no
|
|
41
|
+
invented RPC code. Error rendering may replace an oversized error with
|
|
42
|
+
`resource_exhausted`; an empty 429 has no server wire code, while clients use
|
|
43
|
+
the existing HTTP fallback `unavailable`.
|
|
44
|
+
|
|
45
|
+
The baseline is [Connect semantic conventions v1.44.0](https://github.com/open-telemetry/semantic-conventions/blob/v1.44.0/docs/rpc/connect-rpc.md),
|
|
46
|
+
which are Development. CLIENT treats every non-OK Connect code as an error;
|
|
47
|
+
SERVER treats `unknown`, `deadline_exceeded`, `unimplemented`, `internal`,
|
|
48
|
+
`unavailable`, and `data_loss` as errors. Delivered `invalid_argument`, for
|
|
49
|
+
example, leaves SERVER status UNSET. Independent operation failures and host
|
|
50
|
+
aborts are errors. `error.type` contains bounded codes or local classifications;
|
|
51
|
+
success remains UNSET. There are no automatic exception events, descriptions,
|
|
52
|
+
stacks, payloads, raw URLs/queries, certificate data or arbitrary metadata.
|
|
53
|
+
|
|
54
|
+
Unknown methods use span name `connectrpc` and `rpc.method=_OTHER`. This adapter
|
|
55
|
+
redacts `rpc.method_original` by default. That attribute is conditionally
|
|
56
|
+
required by v1.44.0 for unknown methods, so this privacy default is a specific
|
|
57
|
+
conformance limitation. This is not a claim of full semantic-convention
|
|
58
|
+
compliance. Instrumentation scope `connectrpc-otel` identifies its package
|
|
59
|
+
version and has `connectrpc.semconv.version=1.44.0`. API 1.11.1/SDK 1.13.1 support
|
|
60
|
+
scope attributes, but no tracer schema URL; none is supplied.
|
|
61
|
+
|
|
62
|
+
Metadata capture requires explicit names, at most 32 per direction, each at most
|
|
63
|
+
128 bytes. Values retain repeated arrays, limited to eight printable ASCII
|
|
64
|
+
entries of 256 bytes each, under `connectrpc.request.metadata.<name>` and
|
|
65
|
+
`connectrpc.response.metadata.<name>`. No wildcards are accepted. Selecting
|
|
66
|
+
sensitive names is the application's responsibility. Baggage propagates but
|
|
67
|
+
is not captured as span attributes. Request injection is staged and validated
|
|
68
|
+
before merging into copied metadata; invalid injection is discarded and
|
|
69
|
+
reported without changing caller metadata or protocol headers.
|
|
70
|
+
|
|
71
|
+
## Processing and delivery
|
|
72
|
+
|
|
73
|
+
Core observation spans timeout/metadata validation, codecs, compression,
|
|
74
|
+
transport and response validation. Servers retain original failures for trusted
|
|
75
|
+
neutral subscribers separately from normalized/rendered errors. The OTel
|
|
76
|
+
adapter never serializes those exception objects.
|
|
77
|
+
|
|
78
|
+
Custom attributes distinguish `connectrpc.processing.state`,
|
|
79
|
+
`connectrpc.decision` (`processed`, `admission_rejected`, `host_rejected`) and
|
|
80
|
+
`connectrpc.delivery`:
|
|
81
|
+
|
|
82
|
+
- `unobserved`: direct core or Testing transport; no host-delivery evidence.
|
|
83
|
+
- `body_closed`: portable Rack body close only.
|
|
84
|
+
- `response_finished`: Rack's response-finished callback ran. It does not prove
|
|
85
|
+
completion of other callbacks or peer receipt. This extension takes precedence
|
|
86
|
+
over body close.
|
|
87
|
+
- `completed`: native Async/Falcon full-write and cleanup boundary, including
|
|
88
|
+
Falcon HTTP/1 body callbacks. This does not promise peer/application receipt.
|
|
89
|
+
- `aborted`: cancellation, reset, write failure or forced teardown.
|
|
90
|
+
|
|
91
|
+
`connectrpc.processing.duration` is monotonic processing seconds. Measured
|
|
92
|
+
encoded sizes appear as `connectrpc.request.wire_bytes` and
|
|
93
|
+
`connectrpc.response.wire_bytes`; unknown sizes are omitted. For GET, request
|
|
94
|
+
wire bytes measure the query transport payload after percent/base64 decoding
|
|
95
|
+
and before inflation, independently of the empty HTTP body. Zero means measured
|
|
96
|
+
empty; rejected requests before measurement remain unknown. RPC spans never
|
|
97
|
+
capture request URLs, query controls or payloads. Generic HTTP instrumentation
|
|
98
|
+
and host/proxy logs have their own application-owned redaction policies. Successful
|
|
99
|
+
processing followed by delivery abort retains `OK` and ends ERROR.
|
|
100
|
+
|
|
101
|
+
Configure native hosts through their Server. Lifecycle handles are passed
|
|
102
|
+
explicitly into independent operation/setup/handler tasks. Shared connection
|
|
103
|
+
readers never retain one RPC's context. Connections/setup rejected before any
|
|
104
|
+
RPC exists are outside per-RPC observation. Rack consumers must close the body,
|
|
105
|
+
or invoke the promised response-finished callbacks; a noncompliant consumer can
|
|
106
|
+
leave a span unfinished. For native Rails controllers, override private
|
|
107
|
+
`connect_instrumentation` separately from `connect_limits`. Only failures
|
|
108
|
+
escaping Rails execution are captured; a successful `rescue_from` does not
|
|
109
|
+
mark a successful RPC as failed. Configuration failures before Server
|
|
110
|
+
construction remain outside its lifecycle.
|
|
111
|
+
|
|
112
|
+
## HTTP instrumentation coexistence
|
|
113
|
+
|
|
114
|
+
The SDK-backed suite tests Rack instrumentation 0.31.2 (stable middleware,
|
|
115
|
+
`use_rack_events: false`) and NetHTTP instrumentation 0.29.2, with the application
|
|
116
|
+
installing a W3C global propagator. Native Rails 8.1.4 is tested through its real
|
|
117
|
+
controller pipeline. Generic HTTP tracing remains enabled. The observed tree is:
|
|
118
|
+
application -> RPC CLIENT -> HTTP CLIENT -> RPC SERVER and HTTP SERVER as
|
|
119
|
+
siblings. NetHTTP injects its HTTP CLIENT; both server integrations extract that
|
|
120
|
+
serialized parent. The RPC adapter deliberately does not inspect ambient HTTP
|
|
121
|
+
spans to guess a parent. In-process Testing transport also extracts serialized
|
|
122
|
+
headers, even while the caller's RPC context is ambient. Other instrumentation
|
|
123
|
+
combinations need their own runtime verification.
|
|
124
|
+
|
|
125
|
+
## Development
|
|
126
|
+
|
|
127
|
+
From the repository root use `mise run otel:setup`, `otel:test`, `otel:check`,
|
|
128
|
+
`otel:build`, `otel:package`, `otel:example`, and `otel:verify`.
|
|
129
|
+
The independent checksummed development bundle includes the real SDK,
|
|
130
|
+
InMemorySpanExporter, HTTP instrumentations and local integration packages.
|
|
131
|
+
Existing runtime closures and locks remain independent. Caches/archives are in
|
|
132
|
+
`vendor/otel`, and coverage is in `coverage/otel/<Ruby>/`. Both pinned Ruby
|
|
133
|
+
3.4.12 and 4.0.9 CI leaves run `otel:verify`. `scripts/integration.rb` is also a
|
|
134
|
+
runnable generated-client example with a real in-memory SDK exporter.
|
|
135
|
+
|
|
136
|
+
The new lock's API/SDK were released 2026-09-16. New dependencies, including
|
|
137
|
+
native variants, were checked against primary RubyGems release timestamps and
|
|
138
|
+
API SHA256 values before adoption, under the seven-day cooldown. The versioned
|
|
139
|
+
lock preserves the eligible existing cross-package graph. Original project
|
|
140
|
+
work is [MIT licensed](LICENSE), copyright 2026 Jim Myhrberg. All package
|
|
141
|
+
archives pin `allowed_push_host=https://rubygems.org`; these checks never
|
|
142
|
+
publish or release a gem.
|
|
143
|
+
|
|
144
|
+
## Static signatures
|
|
145
|
+
|
|
146
|
+
This package owns `sig/` and its discovery manifest. Add
|
|
147
|
+
`library "connectrpc-otel"` to a strict Steep target with this gem installed.
|
|
148
|
+
See the [RBS guide](../../docs/rbs.md) for supported APIs, extension contracts,
|
|
149
|
+
partial records, runtime validation and framework-signature limits.
|
|
150
|
+
`mise run rbs:check` checks application callers for this package; strict core
|
|
151
|
+
source checking is a separate target from backend implementation checking.
|
|
152
|
+
|
|
153
|
+
## Native streaming
|
|
154
|
+
|
|
155
|
+
The core [streaming guide](../../docs/streaming.md) documents all three scoped
|
|
156
|
+
Connect streaming shapes, typed generated clients/handlers, early headers,
|
|
157
|
+
per-message limits and bounded ownership. Native Async/Falcon HTTP/2 supports
|
|
158
|
+
both codecs and identity/gzip over h2c/TLS/mTLS. Falcon additionally serves Connect server and client
|
|
159
|
+
streaming over HTTP/1.1,
|
|
160
|
+
with protobuf/ProtoJSON and identity/gzip over TCP/TLS/mTLS. The standalone
|
|
161
|
+
Async Host and original Async Transport remain H2-only. The explicit
|
|
162
|
+
`ConnectRPC::Async::HTTP1Transport` supports Connect unary, safe GET, server
|
|
163
|
+
streaming and client streaming in an application-owned reactor.
|
|
164
|
+
|
|
165
|
+
The streaming guide defines Falcon's cooperative 500 ms total final-close budget
|
|
166
|
+
and unchanged 100 ms / 256 KiB discard limits. Native Connect streaming clients
|
|
167
|
+
have a separate one-second absolute HTTP tail policy. It applies even with a nil
|
|
168
|
+
RPC deadline and can reject otherwise valid late EOF or slow intermediaries.
|
|
169
|
+
|
|
170
|
+
Streaming keeps one span and fixed lifecycle state per RPC, with no per-message
|
|
171
|
+
events or payload capture. The original processor retains SDK context and peer
|
|
172
|
+
identity through codecs and cleanup; publication, processing and full native
|
|
173
|
+
wire completion remain independent. Late application cleanup failure aborts
|
|
174
|
+
delivery. Falcon H1 records its final-flush expiry before body callbacks and
|
|
175
|
+
lifecycle observation; delayed callbacks consume that same 500 ms budget.
|
|
176
|
+
Post-delivery discard or grace failure preserves an already completed span,
|
|
177
|
+
while socket/admission ownership continues through retirement and processing join.
|
|
178
|
+
|
|
179
|
+
## Native gRPC observation
|
|
180
|
+
|
|
181
|
+
The actual protocol is fixed before subscriber start. gRPC uses the 1.44.0
|
|
182
|
+
release-candidate conventions: `rpc.system.name=grpc`, uppercase
|
|
183
|
+
`rpc.response.status_code` and code-based `error.type`, including `CANCELLED`.
|
|
184
|
+
Client and Server retain the role-specific error policy above. Allowlisted gRPC
|
|
185
|
+
metadata uses `rpc.request.metadata.<key>` and `rpc.response.metadata.<key>`.
|
|
186
|
+
Connect keeps its existing names and lower-case codes. Unknown gRPC methods use
|
|
187
|
+
span name `grpc` and `rpc.method=_OTHER`, with the same explicit privacy limit.
|
|
188
|
+
Processing and delivery remain independent; successful handler processing cannot
|
|
189
|
+
prove terminal HEADERS delivery. SDK configuration remains application-owned.
|
|
190
|
+
|
|
191
|
+
## gRPC-Web
|
|
192
|
+
|
|
193
|
+
Web calls have Call.protocol=:grpc_web before subscriber start. The project maps them to gRPC semconv 1.44.0 with rpc.system.name=grpc and bounded connectrpc.protocol=grpc_web. Status codes, metadata prefixes, error roles and original-task activation follow native gRPC.
|
|
194
|
+
See the [Web guide](../../docs/grpc-web.md) for protocol, CORS, bounds and completion rules.
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "version"
|
|
4
|
+
|
|
5
|
+
module ConnectRPC
|
|
6
|
+
module OTel
|
|
7
|
+
SEMCONV_VERSION = "1.44.0"
|
|
8
|
+
|
|
9
|
+
class Subscriber
|
|
10
|
+
SERVER_ERRORS = %w[unknown deadline_exceeded unimplemented internal unavailable data_loss].freeze
|
|
11
|
+
private_constant :SERVER_ERRORS
|
|
12
|
+
|
|
13
|
+
def initialize(tracer_provider: OpenTelemetry.tracer_provider, propagator: nil,
|
|
14
|
+
request_metadata: [], response_metadata: [])
|
|
15
|
+
@tracer = tracer_provider.tracer("connectrpc-otel", VERSION,
|
|
16
|
+
attributes: { "connectrpc.semconv.version" => SEMCONV_VERSION })
|
|
17
|
+
@propagator = propagator || OpenTelemetry::Context::Propagation::CompositeTextMapPropagator.compose_propagators(
|
|
18
|
+
[OpenTelemetry::Trace::Propagation::TraceContext.text_map_propagator,
|
|
19
|
+
OpenTelemetry::Baggage::Propagation.text_map_propagator]
|
|
20
|
+
)
|
|
21
|
+
@request_metadata = allowlist(request_metadata)
|
|
22
|
+
@response_metadata = allowlist(response_metadata)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def start(call) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
|
26
|
+
parent = if call.role == :server
|
|
27
|
+
# Always extract serialized propagation, even for an in-process call.
|
|
28
|
+
@propagator.extract(call.carrier, context: OpenTelemetry::Context.empty, getter: Getter.new)
|
|
29
|
+
else
|
|
30
|
+
OpenTelemetry::Context.current
|
|
31
|
+
end
|
|
32
|
+
name = if call.method
|
|
33
|
+
"#{call.method.service}/#{call.method.name}"
|
|
34
|
+
else
|
|
35
|
+
(%i[grpc grpc_web].include?(call.protocol) ? "grpc" : "connectrpc")
|
|
36
|
+
end
|
|
37
|
+
span = @tracer.start_span(name, kind: call.role, with_parent: parent, attributes: attributes(call, name))
|
|
38
|
+
context = OpenTelemetry::Trace.context_with_span(span, parent_context: parent)
|
|
39
|
+
begin
|
|
40
|
+
@propagator.inject(call.carrier, context: context) if call.role == :client
|
|
41
|
+
Observer.new(span, context, call.role, @request_metadata, @response_metadata, call.protocol)
|
|
42
|
+
rescue Exception # rubocop:disable Lint/RescueException -- release span and preserve fatal exceptions
|
|
43
|
+
span.finish
|
|
44
|
+
raise
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def attributes(call, name)
|
|
51
|
+
result = { "rpc.system.name" => %i[grpc grpc_web].include?(call.protocol) ? "grpc" : "connectrpc",
|
|
52
|
+
"rpc.method" => call.method ? name : "_OTHER" }
|
|
53
|
+
result["connectrpc.protocol"] = "grpc_web" if call.protocol == :grpc_web
|
|
54
|
+
if call.role == :client
|
|
55
|
+
result["server.address"] = call.server_address if call.server_address
|
|
56
|
+
result["server.port"] = call.server_port if call.server_port
|
|
57
|
+
end
|
|
58
|
+
result
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Validate the complete allowlist transaction before retaining any configuration.
|
|
62
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
63
|
+
def allowlist(keys)
|
|
64
|
+
unless keys.is_a?(Array) && keys.size <= 32 && keys.all? do |key|
|
|
65
|
+
key.is_a?(String) && key.bytesize <= 128 && /\A[0-9A-Za-z_.-]+\z/.match?(key) &&
|
|
66
|
+
!Metadata.reserved?(key.downcase)
|
|
67
|
+
end
|
|
68
|
+
raise ArgumentError, "Metadata capture requires at most 32 explicit metadata names of at most 128 bytes"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
keys.map { |key| key.downcase.freeze }.uniq.freeze
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
75
|
+
|
|
76
|
+
class Getter
|
|
77
|
+
def get(carrier, key) # rubocop:disable Metrics/CyclomaticComplexity
|
|
78
|
+
values = Array(carrier[key])
|
|
79
|
+
return unless values.all? { |value| value.is_a?(String) && value.ascii_only? }
|
|
80
|
+
return values.first if key == "traceparent" && values.size == 1
|
|
81
|
+
return if key == "traceparent" || values.empty?
|
|
82
|
+
|
|
83
|
+
values.join(",")
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def keys(carrier) = carrier.keys
|
|
87
|
+
end
|
|
88
|
+
private_constant :Getter
|
|
89
|
+
|
|
90
|
+
class Observer
|
|
91
|
+
def initialize(span, context, role, request_keys, response_keys, protocol) # rubocop:disable Metrics/ParameterLists
|
|
92
|
+
@protocol = protocol
|
|
93
|
+
@span = span
|
|
94
|
+
@context = context
|
|
95
|
+
@role = role
|
|
96
|
+
@request_keys = request_keys
|
|
97
|
+
@response_keys = response_keys
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def attach = OpenTelemetry::Context.attach(@context)
|
|
101
|
+
def detach(token) = OpenTelemetry::Context.detach(token)
|
|
102
|
+
|
|
103
|
+
def processing_finished(outcome) # rubocop:disable Metrics/AbcSize
|
|
104
|
+
@span.set_attribute("rpc.response.status_code", status_code(outcome.code)) if outcome.code
|
|
105
|
+
@span.set_attribute("http.response.status_code", outcome.http_status) if outcome.http_status
|
|
106
|
+
@span.set_attribute("connectrpc.processing.duration", outcome.duration)
|
|
107
|
+
@span.set_attribute("connectrpc.request.wire_bytes", outcome.request_bytes) if outcome.request_bytes
|
|
108
|
+
@span.set_attribute("connectrpc.response.wire_bytes", outcome.response_bytes) if outcome.response_bytes
|
|
109
|
+
capture("request", outcome.request_metadata, @request_keys)
|
|
110
|
+
capture("response", outcome.response_metadata, @response_keys)
|
|
111
|
+
error = processing_error(outcome)
|
|
112
|
+
fail_span(status_code(error)) if error
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def finished(completion) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
116
|
+
@span.set_attribute("connectrpc.processing.state", completion.processing_state.to_s)
|
|
117
|
+
@span.set_attribute("connectrpc.decision", completion.decision.to_s)
|
|
118
|
+
@span.set_attribute("connectrpc.delivery", completion.delivery.to_s)
|
|
119
|
+
@span.set_attribute("http.response.status_code", completion.http_status) if completion.http_status
|
|
120
|
+
if completion.delivery == :aborted
|
|
121
|
+
code = completion.outcome&.code
|
|
122
|
+
type = if %i[grpc
|
|
123
|
+
grpc_web].include?(@protocol) && code && code != "OK"
|
|
124
|
+
status_code(code)
|
|
125
|
+
else
|
|
126
|
+
"delivery_aborted"
|
|
127
|
+
end
|
|
128
|
+
fail_span(type)
|
|
129
|
+
elsif completion.decision != :processed
|
|
130
|
+
fail_span(completion.decision.to_s)
|
|
131
|
+
end
|
|
132
|
+
ensure
|
|
133
|
+
@span.finish
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
private
|
|
137
|
+
|
|
138
|
+
# Keep role-specific wire codes and independent operation failures in one table.
|
|
139
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
140
|
+
def processing_error(outcome)
|
|
141
|
+
if outcome.code && outcome.code != "OK"
|
|
142
|
+
return outcome.code if @role == :client || SERVER_ERRORS.include?(outcome.code)
|
|
143
|
+
|
|
144
|
+
return nil
|
|
145
|
+
end
|
|
146
|
+
return "operation_failed" if outcome.error && !outcome.error.is_a?(ConnectRPC::Error)
|
|
147
|
+
return "operation_failed" if outcome.code.nil? && outcome.error
|
|
148
|
+
return "preflight_rejected" if outcome.http_status && outcome.http_status != 200 && outcome.code.nil?
|
|
149
|
+
|
|
150
|
+
nil
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
154
|
+
|
|
155
|
+
def fail_span(type)
|
|
156
|
+
@span.set_attribute("error.type", type)
|
|
157
|
+
@span.status = OpenTelemetry::Trace::Status.error
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def status_code(code)
|
|
161
|
+
return code unless %i[grpc grpc_web].include?(@protocol)
|
|
162
|
+
|
|
163
|
+
code == "canceled" ? "CANCELLED" : code.upcase
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def capture(direction, metadata, keys) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
167
|
+
keys.each do |key|
|
|
168
|
+
values = metadata[key]
|
|
169
|
+
next unless values
|
|
170
|
+
|
|
171
|
+
values = values.first(8).filter_map do |value|
|
|
172
|
+
value.byteslice(0, 256) if value.is_a?(String) && value.ascii_only? && /\A[\x20-\x7e]*\z/.match?(value)
|
|
173
|
+
end
|
|
174
|
+
prefix = %i[grpc grpc_web].include?(@protocol) ? "rpc" : "connectrpc"
|
|
175
|
+
@span.set_attribute("#{prefix}.#{direction}.metadata.#{key}", values) unless values.empty?
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
private_constant :Observer
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
module ConnectRPC
|
|
2
|
+
module OTel
|
|
3
|
+
VERSION: String
|
|
4
|
+
SEMCONV_VERSION: String
|
|
5
|
+
type attributes = Hash[String, String | Integer | Float | bool | Array[String]]
|
|
6
|
+
interface _TraceFlags
|
|
7
|
+
def sampled?: () -> bool
|
|
8
|
+
end
|
|
9
|
+
interface _Tracestate
|
|
10
|
+
def empty?: () -> bool
|
|
11
|
+
def to_s: () -> String
|
|
12
|
+
end
|
|
13
|
+
# The default W3C propagator consumes these API capabilities. Id contents
|
|
14
|
+
# and lengths remain the provider's runtime responsibility.
|
|
15
|
+
interface _SpanContext
|
|
16
|
+
def valid?: () -> bool
|
|
17
|
+
def trace_id: () -> String
|
|
18
|
+
def span_id: () -> String
|
|
19
|
+
def trace_flags: () -> _TraceFlags
|
|
20
|
+
def tracestate: () -> _Tracestate
|
|
21
|
+
end
|
|
22
|
+
interface _Span
|
|
23
|
+
def context: () -> _SpanContext
|
|
24
|
+
def set_attribute: (String, String | Integer | Float | bool | Array[String]) -> void
|
|
25
|
+
def status=: (Object) -> void
|
|
26
|
+
def finish: () -> void
|
|
27
|
+
end
|
|
28
|
+
interface _Tracer
|
|
29
|
+
def start_span: (String, kind: :client | :server, with_parent: Object, attributes: attributes) -> _Span
|
|
30
|
+
end
|
|
31
|
+
interface _TracerProvider
|
|
32
|
+
def tracer: (String, String, attributes: attributes) -> _Tracer
|
|
33
|
+
end
|
|
34
|
+
interface _Getter
|
|
35
|
+
def get: (Hash[String, String | Array[String]], String) -> String?
|
|
36
|
+
def keys: (Hash[String, String | Array[String]]) -> Array[String]
|
|
37
|
+
end
|
|
38
|
+
interface _Propagator
|
|
39
|
+
def extract: (Hash[String, String | Array[String]], context: Object, getter: _Getter) -> Object
|
|
40
|
+
def inject: (Hash[String, String | Array[String]], context: Object) -> void
|
|
41
|
+
end
|
|
42
|
+
class Subscriber
|
|
43
|
+
def initialize: (?tracer_provider: _TracerProvider, ?propagator: _Propagator?, ?request_metadata: Array[String], ?response_metadata: Array[String]) -> void
|
|
44
|
+
# The adapter's observer is private and discovered by the neutral lifecycle.
|
|
45
|
+
def start: (Instrumentation::Call) -> Object?
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
data/sig/manifest.yaml
ADDED
metadata
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: connectrpc-otel
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- jimeh
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 2026-11-15 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: connectrpc
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: 0.1.0
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: 0.1.0
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: opentelemetry-api
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: 1.11.1
|
|
33
|
+
- - "<"
|
|
34
|
+
- !ruby/object:Gem::Version
|
|
35
|
+
version: '2'
|
|
36
|
+
type: :runtime
|
|
37
|
+
prerelease: false
|
|
38
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
39
|
+
requirements:
|
|
40
|
+
- - ">="
|
|
41
|
+
- !ruby/object:Gem::Version
|
|
42
|
+
version: 1.11.1
|
|
43
|
+
- - "<"
|
|
44
|
+
- !ruby/object:Gem::Version
|
|
45
|
+
version: '2'
|
|
46
|
+
description: Application-owned OpenTelemetry tracing through the ConnectRPC lifecycle.
|
|
47
|
+
Experimental.
|
|
48
|
+
executables: []
|
|
49
|
+
extensions: []
|
|
50
|
+
extra_rdoc_files: []
|
|
51
|
+
files:
|
|
52
|
+
- LICENSE
|
|
53
|
+
- README.md
|
|
54
|
+
- lib/connectrpc/otel.rb
|
|
55
|
+
- lib/connectrpc/otel/subscriber.rb
|
|
56
|
+
- lib/connectrpc/otel/version.rb
|
|
57
|
+
- sig/connectrpc-otel.rbs
|
|
58
|
+
- sig/manifest.yaml
|
|
59
|
+
homepage: https://github.com/jimeh/connectrpc-ruby
|
|
60
|
+
licenses:
|
|
61
|
+
- MIT
|
|
62
|
+
metadata:
|
|
63
|
+
allowed_push_host: https://rubygems.org
|
|
64
|
+
rubygems_mfa_required: 'true'
|
|
65
|
+
source_code_uri: https://github.com/jimeh/connectrpc-ruby
|
|
66
|
+
rdoc_options: []
|
|
67
|
+
require_paths:
|
|
68
|
+
- lib
|
|
69
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
70
|
+
requirements:
|
|
71
|
+
- - ">="
|
|
72
|
+
- !ruby/object:Gem::Version
|
|
73
|
+
version: '3.4'
|
|
74
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
75
|
+
requirements:
|
|
76
|
+
- - ">="
|
|
77
|
+
- !ruby/object:Gem::Version
|
|
78
|
+
version: '0'
|
|
79
|
+
requirements: []
|
|
80
|
+
rubygems_version: 3.6.9
|
|
81
|
+
specification_version: 4
|
|
82
|
+
summary: Optional OpenTelemetry traces for ConnectRPC unary calls
|
|
83
|
+
test_files: []
|