|
17 | 17 | package io.grpc.opentelemetry; |
18 | 18 |
|
19 | 19 | import static io.grpc.ClientStreamTracer.NAME_RESOLUTION_DELAYED; |
| 20 | +import static io.grpc.opentelemetry.internal.OpenTelemetryConstants.BAGGAGE_KEY; |
20 | 21 | import static io.grpc.opentelemetry.internal.OpenTelemetryConstants.LOCALITY_KEY; |
21 | 22 | import static io.grpc.opentelemetry.internal.OpenTelemetryConstants.METHOD_KEY; |
22 | 23 | import static io.grpc.opentelemetry.internal.OpenTelemetryConstants.STATUS_KEY; |
|
42 | 43 | import io.grpc.ClientInterceptor; |
43 | 44 | import io.grpc.ClientInterceptors; |
44 | 45 | import io.grpc.ClientStreamTracer; |
| 46 | +import io.grpc.Contexts; |
45 | 47 | import io.grpc.ForwardingClientCall; |
46 | 48 | import io.grpc.KnownLength; |
47 | 49 | import io.grpc.Metadata; |
48 | 50 | import io.grpc.MethodDescriptor; |
49 | 51 | import io.grpc.ServerCall; |
50 | 52 | import io.grpc.ServerCallHandler; |
| 53 | +import io.grpc.ServerInterceptor; |
51 | 54 | import io.grpc.ServerServiceDefinition; |
52 | 55 | import io.grpc.ServerStreamTracer; |
53 | 56 | import io.grpc.ServerStreamTracer.ServerCallInfo; |
|
59 | 62 | import io.grpc.opentelemetry.GrpcOpenTelemetry.TargetFilter; |
60 | 63 | import io.grpc.opentelemetry.OpenTelemetryMetricsModule.CallAttemptsTracerFactory; |
61 | 64 | import io.grpc.opentelemetry.internal.OpenTelemetryConstants; |
| 65 | +import io.grpc.stub.ClientCalls; |
62 | 66 | import io.grpc.stub.MetadataUtils; |
| 67 | +import io.grpc.stub.ServerCalls; |
63 | 68 | import io.grpc.stub.StreamObserver; |
64 | 69 | import io.grpc.testing.GrpcServerRule; |
65 | 70 | import io.grpc.testing.protobuf.SimpleRequest; |
|
83 | 88 | import java.io.IOException; |
84 | 89 | import java.io.InputStream; |
85 | 90 | import java.util.Arrays; |
| 91 | +import java.util.Collections; |
86 | 92 | import java.util.List; |
87 | 93 | import java.util.Map; |
88 | 94 | import java.util.Optional; |
@@ -2091,4 +2097,245 @@ public void set(Metadata carrier, String key, String value) { |
2091 | 2097 | carrier.put(Metadata.Key.of(key, Metadata.ASCII_STRING_MARSHALLER), value); |
2092 | 2098 | } |
2093 | 2099 | } |
| 2100 | + |
| 2101 | + @Test |
| 2102 | + public void clientMetric_baggagePropagation_externalExecutor() throws Exception { |
| 2103 | + String target = "target:///"; |
| 2104 | + ExecutorService executor = Executors.newSingleThreadExecutor(); |
| 2105 | + try { |
| 2106 | + // Mock the instrument to verify Context |
| 2107 | + DoubleHistogram mockHistogram = mock(DoubleHistogram.class); |
| 2108 | + OpenTelemetryMetricsResource resource = OpenTelemetryMetricsResource.builder() |
| 2109 | + .clientAttemptDurationCounter(mockHistogram) |
| 2110 | + .build(); |
| 2111 | + |
| 2112 | + OpenTelemetryMetricsModule module = newOpenTelemetryMetricsModule(resource); |
| 2113 | + |
| 2114 | + MethodDescriptor<String, String> methodDescriptor = |
| 2115 | + MethodDescriptor.<String, String>newBuilder() |
| 2116 | + .setType(MethodDescriptor.MethodType.UNARY) |
| 2117 | + .setFullMethodName("service/method") |
| 2118 | + .setRequestMarshaller(MARSHALLER) |
| 2119 | + .setResponseMarshaller(MARSHALLER) |
| 2120 | + .setSampledToLocalTracing(true) |
| 2121 | + .build(); |
| 2122 | + |
| 2123 | + ServerServiceDefinition serviceDef = ServerServiceDefinition.builder("service") |
| 2124 | + .addMethod(methodDescriptor, ServerCalls.asyncUnaryCall( |
| 2125 | + new ServerCalls.UnaryMethod<String, String>() { |
| 2126 | + @Override |
| 2127 | + public void invoke(String req, StreamObserver<String> responseObserver) { |
| 2128 | + responseObserver.onNext("Response"); |
| 2129 | + responseObserver.onCompleted(); |
| 2130 | + } |
| 2131 | + })) |
| 2132 | + .build(); |
| 2133 | + |
| 2134 | + server = InProcessServerBuilder.forName("client-baggage-test") |
| 2135 | + .directExecutor() |
| 2136 | + .addService(serviceDef) |
| 2137 | + .build().start(); |
| 2138 | + |
| 2139 | + InProcessChannelBuilder channelBuilder = |
| 2140 | + InProcessChannelBuilder.forName("client-baggage-test") |
| 2141 | + .executor(executor); |
| 2142 | + |
| 2143 | + // Use the module's interceptor |
| 2144 | + ClientInterceptor interceptor = module.getClientInterceptor(target); |
| 2145 | + channel = channelBuilder.intercept(interceptor).build(); |
| 2146 | + |
| 2147 | + Baggage baggage = Baggage.builder().put("client_key", "client_value").build(); |
| 2148 | + try (io.opentelemetry.context.Scope scope = Context.current().with(baggage).makeCurrent()) { |
| 2149 | + ClientCalls.blockingUnaryCall(channel, methodDescriptor, CallOptions.DEFAULT, "Request"); |
| 2150 | + } |
| 2151 | + |
| 2152 | + ArgumentCaptor<Context> contextCaptor = ArgumentCaptor.forClass(Context.class); |
| 2153 | + |
| 2154 | + // Use atLeastOnce() and timeout to allow for async execution |
| 2155 | + verify(mockHistogram, timeout(1000).atLeastOnce()) |
| 2156 | + .record(anyDouble(), any(io.opentelemetry.api.common.Attributes.class), |
| 2157 | + contextCaptor.capture()); |
| 2158 | + |
| 2159 | + boolean found = false; |
| 2160 | + for (Context ctx : contextCaptor.getAllValues()) { |
| 2161 | + Baggage b = Baggage.fromContext(ctx); |
| 2162 | + if ("client_value".equals(b.getEntryValue("client_key"))) { |
| 2163 | + found = true; |
| 2164 | + break; |
| 2165 | + } |
| 2166 | + } |
| 2167 | + assertTrue("Client baggage not found in metrics context", found); |
| 2168 | + } finally { |
| 2169 | + executor.shutdown(); |
| 2170 | + } |
| 2171 | + } |
| 2172 | + |
| 2173 | + @Test |
| 2174 | + public void serverMetric_baggagePropagation_externalExecutor() throws Exception { |
| 2175 | + ExecutorService executor = Executors.newSingleThreadExecutor(); |
| 2176 | + try { |
| 2177 | + // Mock the instrument |
| 2178 | + DoubleHistogram mockHistogram = mock(DoubleHistogram.class); |
| 2179 | + OpenTelemetryMetricsResource resource = OpenTelemetryMetricsResource.builder() |
| 2180 | + .serverCallDurationCounter(mockHistogram) |
| 2181 | + .build(); |
| 2182 | + |
| 2183 | + // Configure module with propagation |
| 2184 | + ContextPropagators propagators = |
| 2185 | + ContextPropagators.create(W3CBaggagePropagator.getInstance()); |
| 2186 | + |
| 2187 | + OpenTelemetryMetricsModule module = new OpenTelemetryMetricsModule( |
| 2188 | + fakeClock.getStopwatchSupplier(), resource, emptyList(), emptyList(), propagators); |
| 2189 | + |
| 2190 | + MethodDescriptor<String, String> methodDescriptor = |
| 2191 | + MethodDescriptor.<String, String>newBuilder() |
| 2192 | + .setType(MethodDescriptor.MethodType.UNARY) |
| 2193 | + .setFullMethodName("service/method") |
| 2194 | + .setRequestMarshaller(MARSHALLER) |
| 2195 | + .setResponseMarshaller(MARSHALLER) |
| 2196 | + .setSampledToLocalTracing(true) |
| 2197 | + .build(); |
| 2198 | + |
| 2199 | + ServerServiceDefinition serviceDef = ServerServiceDefinition.builder("service") |
| 2200 | + .addMethod(methodDescriptor, ServerCalls.asyncUnaryCall( |
| 2201 | + new ServerCalls.UnaryMethod<String, String>() { |
| 2202 | + @Override |
| 2203 | + public void invoke(String req, StreamObserver<String> responseObserver) { |
| 2204 | + responseObserver.onNext("Response"); |
| 2205 | + responseObserver.onCompleted(); |
| 2206 | + } |
| 2207 | + })) |
| 2208 | + .build(); |
| 2209 | + |
| 2210 | + // Use external executor by setting it on builder |
| 2211 | + InProcessServerBuilder serverBuilder = InProcessServerBuilder.forName("server-baggage-test") |
| 2212 | + .executor(executor) |
| 2213 | + .addService(serviceDef) |
| 2214 | + .addStreamTracerFactory(module.getServerTracerFactory()); |
| 2215 | + |
| 2216 | + server = serverBuilder.build().start(); |
| 2217 | + |
| 2218 | + channel = InProcessChannelBuilder.forName("server-baggage-test") |
| 2219 | + .directExecutor() |
| 2220 | + .build(); |
| 2221 | + |
| 2222 | + // We need to inject Baggage into the call. |
| 2223 | + ClientInterceptor baggageInjector = new ClientInterceptor() { |
| 2224 | + @Override |
| 2225 | + public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall( |
| 2226 | + MethodDescriptor<ReqT, RespT> method, CallOptions callOptions, Channel next) { |
| 2227 | + return new ForwardingClientCall.SimpleForwardingClientCall<ReqT, RespT>( |
| 2228 | + next.newCall(method, callOptions)) { |
| 2229 | + @Override |
| 2230 | + public void start(Listener<RespT> responseListener, Metadata headers) { |
| 2231 | + // Inject baggage manually into headers |
| 2232 | + Baggage baggage = Baggage.builder().put("server_key", "server_value").build(); |
| 2233 | + propagators.getTextMapPropagator().inject(Context.current().with(baggage), headers, |
| 2234 | + new MetadataSetter()); |
| 2235 | + super.start(responseListener, headers); |
| 2236 | + } |
| 2237 | + }; |
| 2238 | + } |
| 2239 | + }; |
| 2240 | + |
| 2241 | + ClientCalls.blockingUnaryCall( |
| 2242 | + ClientInterceptors.intercept(channel, baggageInjector), |
| 2243 | + methodDescriptor, CallOptions.DEFAULT, "Request"); |
| 2244 | + |
| 2245 | + ArgumentCaptor<Context> contextCaptor = ArgumentCaptor.forClass(Context.class); |
| 2246 | + |
| 2247 | + verify(mockHistogram, timeout(1000).atLeastOnce()) |
| 2248 | + .record(anyDouble(), any(io.opentelemetry.api.common.Attributes.class), |
| 2249 | + contextCaptor.capture()); |
| 2250 | + |
| 2251 | + boolean found = false; |
| 2252 | + for (Context ctx : contextCaptor.getAllValues()) { |
| 2253 | + Baggage b = Baggage.fromContext(ctx); |
| 2254 | + if ("server_value".equals(b.getEntryValue("server_key"))) { |
| 2255 | + found = true; |
| 2256 | + break; |
| 2257 | + } |
| 2258 | + } |
| 2259 | + assertTrue("Server baggage not found in metrics context", found); |
| 2260 | + } finally { |
| 2261 | + executor.shutdown(); |
| 2262 | + } |
| 2263 | + } |
| 2264 | + |
| 2265 | + @Test |
| 2266 | + public void serverMetric_interceptedBaggage() throws Exception { |
| 2267 | + // This test verifies if baggage added by a ServerInterceptor is visible to |
| 2268 | + // OpenTelemetry metrics Context. |
| 2269 | + |
| 2270 | + OpenTelemetryMetricsModule module = new OpenTelemetryMetricsModule( |
| 2271 | + () -> Stopwatch.createUnstarted(), |
| 2272 | + resource, |
| 2273 | + Collections.emptyList(), /* optionalLabels */ |
| 2274 | + Collections.emptyList(), /* plugins */ |
| 2275 | + ContextPropagators.noop()); |
| 2276 | + |
| 2277 | + ServerInterceptor baggageInterceptor = new ServerInterceptor() { |
| 2278 | + @Override |
| 2279 | + public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall( |
| 2280 | + ServerCall<ReqT, RespT> call, Metadata headers, ServerCallHandler<ReqT, RespT> next) { |
| 2281 | + // Add baggage to the context |
| 2282 | + Baggage baggage = Baggage.builder() |
| 2283 | + .put("interceptor_key", "interceptor_value") |
| 2284 | + .build(); |
| 2285 | + io.grpc.Context ctx = io.grpc.Context.current().withValue(BAGGAGE_KEY, baggage); |
| 2286 | + return Contexts.interceptCall(ctx, call, headers, next); |
| 2287 | + } |
| 2288 | + }; |
| 2289 | + |
| 2290 | + MethodDescriptor<String, String> methodDescriptor = |
| 2291 | + MethodDescriptor.<String, String>newBuilder() |
| 2292 | + .setType(MethodDescriptor.MethodType.UNARY) |
| 2293 | + // Matching existing method name in test class |
| 2294 | + .setFullMethodName("package1.service2/method3") |
| 2295 | + .setRequestMarshaller(MARSHALLER) // Use existing MARSHALLER |
| 2296 | + .setResponseMarshaller(MARSHALLER) |
| 2297 | + .setSampledToLocalTracing(true) |
| 2298 | + .build(); |
| 2299 | + |
| 2300 | + ServerServiceDefinition serviceDef = ServerServiceDefinition.builder("package1.service2") |
| 2301 | + .addMethod(methodDescriptor, ServerCalls.asyncUnaryCall( |
| 2302 | + new ServerCalls.UnaryMethod<String, String>() { |
| 2303 | + @Override |
| 2304 | + public void invoke(String req, StreamObserver<String> responseObserver) { |
| 2305 | + responseObserver.onNext("Response"); |
| 2306 | + responseObserver.onCompleted(); |
| 2307 | + } |
| 2308 | + })) |
| 2309 | + .build(); |
| 2310 | + |
| 2311 | + InProcessServerBuilder serverBuilder = InProcessServerBuilder.forName("interceptor-test") |
| 2312 | + .addService(serviceDef) |
| 2313 | + .intercept(baggageInterceptor) |
| 2314 | + .addStreamTracerFactory(module.getServerTracerFactory()); |
| 2315 | + |
| 2316 | + server = serverBuilder.build().start(); |
| 2317 | + |
| 2318 | + // Use a real channel but we don't need metrics on client side for this test |
| 2319 | + InProcessChannelBuilder channelBuilder = InProcessChannelBuilder.forName("interceptor-test"); |
| 2320 | + channel = channelBuilder.build(); |
| 2321 | + |
| 2322 | + ClientCalls.blockingUnaryCall(channel, methodDescriptor, CallOptions.DEFAULT, "Request"); |
| 2323 | + |
| 2324 | + // Verify that record was called with a Context containing the baggage |
| 2325 | + // Reusing contextCaptor from the class |
| 2326 | + verify(mockServerCallDurationHistogram, timeout(1000).atLeastOnce()) |
| 2327 | + .record(anyDouble(), any(io.opentelemetry.api.common.Attributes.class), |
| 2328 | + contextCaptor.capture()); |
| 2329 | + |
| 2330 | + boolean found = false; |
| 2331 | + for (io.opentelemetry.context.Context ctx : contextCaptor.getAllValues()) { |
| 2332 | + // Baggage from OTEL Context |
| 2333 | + Baggage otelBaggage = Baggage.fromContext(ctx); |
| 2334 | + if ("interceptor_value".equals(otelBaggage.getEntryValue("interceptor_key"))) { |
| 2335 | + found = true; |
| 2336 | + break; |
| 2337 | + } |
| 2338 | + } |
| 2339 | + assertThat(found).isTrue(); |
| 2340 | + } |
2094 | 2341 | } |
0 commit comments