Skip to content

Commit 2793dbd

Browse files
yurkkicursoragent
andcommitted
Add request metadata capture for gRPC exchange attachments
Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 1bd855d commit 2793dbd

3 files changed

Lines changed: 179 additions & 15 deletions

File tree

allure-grpc/README.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ Use this module when your tests call gRPC services and you want method calls, me
88

99
- Allure Java 3.x requires Java 17 or newer.
1010
- This module targets gRPC Java.
11-
- The current build validates against gRPC Java 1.81.0 and Protobuf Java 4.35.0.
11+
- The current build validates against gRPC Java 1.82.0 and Protobuf Java 4.35.1.
1212

1313
## Installation
1414

@@ -42,19 +42,22 @@ ManagedChannel channel = ManagedChannelBuilder.forAddress("localhost", 8080)
4242
.build();
4343
```
4444

45-
For advanced capture policy, use the constructor that accepts an HTTP exchange builder customizer:
45+
Request metadata capture is disabled by default. To enable request and response metadata capture
46+
and apply redaction rules, use the advanced constructor:
4647

4748
```java
4849
ClientInterceptor allure = new AllureGrpc(
4950
Allure.getLifecycle(),
5051
true,
5152
true,
53+
true,
5254
exchange -> exchange.redactHeader("authorization")
5355
);
5456
```
5557

5658
## Report Output
5759

5860
- gRPC method calls as Allure steps.
59-
- Request and response messages, metadata, status, and timing.
61+
- Request and response messages, status, and timing.
62+
- Optional request and response metadata when enabled through constructor flags.
6063
- Stream metadata for unary and streaming calls where available.

allure-grpc/src/main/java/io/qameta/allure/grpc/AllureGrpc.java

Lines changed: 81 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -83,14 +83,15 @@ public class AllureGrpc implements ClientInterceptor {
8383

8484
private final AllureLifecycle lifecycle;
8585
private final boolean markStepFailedOnNonZeroCode;
86+
private final boolean interceptRequestMetadata;
8687
private final boolean interceptResponseMetadata;
8788
private final Consumer<HttpExchange.Builder> exchangeCustomizer;
8889

8990
/**
9091
* Creates an Allure grpc with default configuration.
9192
*/
9293
public AllureGrpc() {
93-
this(Allure.getLifecycle(), true, false);
94+
this(Allure.getLifecycle(), true, false, false);
9495
}
9596

9697
/**
@@ -104,25 +105,72 @@ public AllureGrpc(
104105
final AllureLifecycle lifecycle,
105106
final boolean markStepFailedOnNonZeroCode,
106107
final boolean interceptResponseMetadata) {
107-
this(lifecycle, markStepFailedOnNonZeroCode, interceptResponseMetadata, builder -> {
108-
});
108+
this(lifecycle, markStepFailedOnNonZeroCode, false, interceptResponseMetadata);
109109
}
110110

111111
/**
112112
* Creates an Allure grpc with the supplied values.
113113
*
114114
* @param lifecycle the Allure lifecycle to use
115115
* @param markStepFailedOnNonZeroCode the mark step failed on non zero code
116+
* @param interceptRequestMetadata the intercept request metadata
117+
* @param interceptResponseMetadata the intercept response metadata
118+
*/
119+
public AllureGrpc(
120+
final AllureLifecycle lifecycle,
121+
final boolean markStepFailedOnNonZeroCode,
122+
final boolean interceptRequestMetadata,
123+
final boolean interceptResponseMetadata) {
124+
this(
125+
lifecycle,
126+
markStepFailedOnNonZeroCode,
127+
interceptRequestMetadata,
128+
interceptResponseMetadata,
129+
builder -> {
130+
}
131+
);
132+
}
133+
134+
/**
135+
* Creates an Allure grpc with the supplied values.
136+
*
137+
* @param lifecycle the Allure lifecycle to use
138+
* @param markStepFailedOnNonZeroCode the mark step failed on non zero code
139+
* @param interceptResponseMetadata the intercept response metadata
140+
* @param exchangeCustomizer the HTTP exchange builder customizer
141+
*/
142+
public AllureGrpc(
143+
final AllureLifecycle lifecycle,
144+
final boolean markStepFailedOnNonZeroCode,
145+
final boolean interceptResponseMetadata,
146+
final Consumer<HttpExchange.Builder> exchangeCustomizer) {
147+
this(
148+
lifecycle,
149+
markStepFailedOnNonZeroCode,
150+
false,
151+
interceptResponseMetadata,
152+
exchangeCustomizer
153+
);
154+
}
155+
156+
/**
157+
* Creates an Allure grpc with the supplied values.
158+
*
159+
* @param lifecycle the Allure lifecycle to use
160+
* @param markStepFailedOnNonZeroCode the mark step failed on non zero code
161+
* @param interceptRequestMetadata the intercept request metadata
116162
* @param interceptResponseMetadata the intercept response metadata
117163
* @param exchangeCustomizer the HTTP exchange builder customizer
118164
*/
119165
public AllureGrpc(
120166
final AllureLifecycle lifecycle,
121167
final boolean markStepFailedOnNonZeroCode,
168+
final boolean interceptRequestMetadata,
122169
final boolean interceptResponseMetadata,
123170
final Consumer<HttpExchange.Builder> exchangeCustomizer) {
124171
this.lifecycle = lifecycle;
125172
this.markStepFailedOnNonZeroCode = markStepFailedOnNonZeroCode;
173+
this.interceptRequestMetadata = interceptRequestMetadata;
126174
this.interceptResponseMetadata = interceptResponseMetadata;
127175
this.exchangeCustomizer = exchangeCustomizer == null ? builder -> {
128176
} : exchangeCustomizer;
@@ -143,6 +191,7 @@ public <T, R> ClientCall<T, R> interceptCall(
143191
final long start = System.currentTimeMillis();
144192
final List<String> clientMessages = new ArrayList<>();
145193
final List<String> serverMessages = new ArrayList<>();
194+
final Map<String, String> capturedRequestHeaders = new LinkedHashMap<>();
146195
final Map<String, String> initialHeaders = new LinkedHashMap<>();
147196
final Map<String, String> trailers = new LinkedHashMap<>();
148197
final String authority = channel.authority();
@@ -164,6 +213,7 @@ public <T, R> ClientCall<T, R> interceptCall(
164213
) {
165214
@Override
166215
public void start(final Listener<R> responseListener, final Metadata requestHeaders) {
216+
handleRequestHeaders(requestHeaders, capturedRequestHeaders);
167217
final Listener<R> forwardingListener = new ForwardingClientCallListener<R>() {
168218
@Override
169219
protected Listener<R> delegate() {
@@ -184,7 +234,7 @@ public void onMessage(final R message) {
184234

185235
@Override
186236
public void onClose(final io.grpc.Status status, final Metadata responseTrailers) {
187-
handleClose(status, responseTrailers, stepContext);
237+
handleClose(status, responseTrailers, stepContext, capturedRequestHeaders);
188238
super.onClose(status, responseTrailers);
189239
}
190240
};
@@ -202,12 +252,13 @@ public void sendMessage(final T message) {
202252
private void handleClose(
203253
final io.grpc.Status status,
204254
final Metadata responseTrailers,
205-
final StepContext<?, ?> stepContext) {
255+
final StepContext<?, ?> stepContext,
256+
final Map<String, String> requestHeaders) {
206257
try {
207258
if (interceptResponseMetadata && responseTrailers != null) {
208-
copyAsciiResponseMetadata(responseTrailers, stepContext.getTrailers());
259+
copyAsciiMetadata(responseTrailers, stepContext.getTrailers());
209260
}
210-
attachExchange(stepContext, status);
261+
attachExchange(stepContext, status, requestHeaders);
211262
stepContext.getLifecycle().updateStep(
212263
stepContext.getStepUuid(),
213264
step -> step.setStatus(convertStatus(status))
@@ -226,13 +277,23 @@ private void handleClose(
226277
private void handleHeaders(final Metadata headers, final Map<String, String> destination) {
227278
try {
228279
if (interceptResponseMetadata && headers != null) {
229-
copyAsciiResponseMetadata(headers, destination);
280+
copyAsciiMetadata(headers, destination);
230281
}
231282
} catch (Throwable throwable) {
232283
LOGGER.warn("Failed to capture response headers", throwable);
233284
}
234285
}
235286

287+
private void handleRequestHeaders(final Metadata headers, final Map<String, String> destination) {
288+
try {
289+
if (interceptRequestMetadata && headers != null) {
290+
copyAsciiMetadata(headers, destination);
291+
}
292+
} catch (Throwable throwable) {
293+
LOGGER.warn("Failed to capture request headers", throwable);
294+
}
295+
}
296+
236297
private <T> void handleClientMessage(final T message, final List<String> destination) {
237298
try {
238299
destination.add(GRPC_TO_JSON_PRINTER.print((MessageOrBuilder) message));
@@ -253,10 +314,14 @@ private <R> void handleServerMessage(final R message, final List<String> destina
253314
}
254315
}
255316

256-
private void attachExchange(final StepContext<?, ?> stepContext, final io.grpc.Status status) {
317+
private void attachExchange(
318+
final StepContext<?, ?> stepContext,
319+
final io.grpc.Status status,
320+
final Map<String, String> requestHeaders) {
257321
final HttpExchangeRequest request = buildRequest(
258322
stepContext.getMethodDescriptor(),
259323
stepContext.getClientMessages(),
324+
requestHeaders,
260325
stepContext.getAuthority()
261326
);
262327
final HttpExchangeResponse response = buildResponse(
@@ -283,6 +348,7 @@ private HttpExchange.Builder exchangeBuilder(final HttpExchangeRequest request)
283348
private HttpExchangeRequest buildRequest(
284349
final MethodDescriptor<?, ?> methodDescriptor,
285350
final List<String> clientMessages,
351+
final Map<String, String> requestHeaders,
286352
final String authority) {
287353
final HttpExchangeRequest.Builder builder = HttpExchangeRequest.builder(
288354
HTTP_METHOD,
@@ -294,6 +360,9 @@ private HttpExchangeRequest buildRequest(
294360
if (authority != null) {
295361
builder.addHeader(":authority", authority);
296362
}
363+
if (interceptRequestMetadata) {
364+
requestHeaders.forEach(builder::addHeader);
365+
}
297366
return builder
298367
.setBody(toHttpBody(clientMessages, isRequestStreaming(methodDescriptor.getType())))
299368
.build();
@@ -429,9 +498,9 @@ private static boolean isResponseStreaming(final MethodDescriptor.MethodType met
429498
|| methodType == MethodDescriptor.MethodType.BIDI_STREAMING;
430499
}
431500

432-
private static void copyAsciiResponseMetadata(
433-
final Metadata source,
434-
final Map<String, String> target) {
501+
private static void copyAsciiMetadata(
502+
final Metadata source,
503+
final Map<String, String> target) {
435504
for (String key : source.keys()) {
436505
if (key == null) {
437506
continue;

allure-grpc/src/test/java/io/qameta/allure/grpc/AllureGrpcTest.java

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,14 @@
1717

1818
import com.fasterxml.jackson.databind.JsonNode;
1919
import com.fasterxml.jackson.databind.ObjectMapper;
20+
import io.grpc.CallOptions;
21+
import io.grpc.Channel;
22+
import io.grpc.ClientCall;
23+
import io.grpc.ForwardingClientCall;
2024
import io.grpc.ManagedChannel;
2125
import io.grpc.ManagedChannelBuilder;
26+
import io.grpc.Metadata;
27+
import io.grpc.MethodDescriptor;
2228
import io.grpc.Status;
2329
import io.grpc.StatusRuntimeException;
2430
import io.grpc.stub.StreamObserver;
@@ -56,6 +62,7 @@ class AllureGrpcTest {
5662

5763
private static final String RESPONSE_MESSAGE = "Hello world!";
5864
private static final String GRPC_EXCHANGE = "gRPC exchange";
65+
private static final Metadata.Key<String> REQUEST_ID_HEADER = Metadata.Key.of("x-request-id", Metadata.ASCII_STRING_MARSHALLER);
5966
private static final ObjectMapper JSON = new ObjectMapper();
6067

6168
private ManagedChannel managedChannel;
@@ -318,6 +325,42 @@ void unaryRequestBodyIsCapturedAsJsonObject() throws Exception {
318325
assertThat(exchange.at("/request/body/contentType").asText()).isEqualTo("application/grpc+json");
319326
}
320327

328+
@Test
329+
void unaryRequestMetadataIsCapturedWhenEnabled() throws Exception {
330+
Request request = Request.newBuilder().setTopic("topic-with-metadata").build();
331+
Metadata requestHeaders = new Metadata();
332+
requestHeaders.put(REQUEST_ID_HEADER, "request-42");
333+
334+
AllureResults allureResults = executeUnaryWithMetadata(
335+
request,
336+
requestHeaders,
337+
true
338+
);
339+
340+
JsonNode exchange = readGrpcExchangeAttachment(allureResults);
341+
342+
assertThat(findValueByName(exchange.at("/request/headers"), REQUEST_ID_HEADER.name()))
343+
.contains("request-42");
344+
}
345+
346+
@Test
347+
void unaryRequestMetadataIsNotCapturedWhenDisabled() throws Exception {
348+
Request request = Request.newBuilder().setTopic("topic-without-metadata").build();
349+
Metadata requestHeaders = new Metadata();
350+
requestHeaders.put(REQUEST_ID_HEADER, "request-42");
351+
352+
AllureResults allureResults = executeUnaryWithMetadata(
353+
request,
354+
requestHeaders,
355+
false
356+
);
357+
358+
JsonNode exchange = readGrpcExchangeAttachment(allureResults);
359+
360+
assertThat(findValueByName(exchange.at("/request/headers"), REQUEST_ID_HEADER.name()))
361+
.isEmpty();
362+
}
363+
321364
@Test
322365
void unaryResponseBodyIsCapturedAsJsonObject() throws Exception {
323366
GrpcMock.stubFor(
@@ -473,4 +516,53 @@ protected final AllureResults executeUnaryExpectingException(Request request) {
473516
);
474517
}
475518

519+
protected final AllureResults executeUnaryWithMetadata(
520+
final Request request,
521+
final Metadata requestHeaders,
522+
final boolean interceptRequestMetadata) {
523+
return Allure.step(
524+
"Execute unary gRPC request with custom metadata and collect Allure results",
525+
() -> runWithinTestContext(() -> {
526+
try {
527+
AllureGrpc interceptor = new RequestMetadataAllureGrpc(
528+
requestHeaders,
529+
interceptRequestMetadata
530+
);
531+
TestServiceGrpc.TestServiceBlockingStub stub = TestServiceGrpc.newBlockingStub(managedChannel)
532+
.withInterceptors(interceptor);
533+
Response response = stub.calculate(request);
534+
assertThat(response.getMessage()).isEqualTo(RESPONSE_MESSAGE);
535+
} catch (Exception exception) {
536+
throw new RuntimeException("Could not execute request " + request, exception);
537+
}
538+
})
539+
);
540+
}
541+
542+
private static final class RequestMetadataAllureGrpc extends AllureGrpc {
543+
544+
private final Metadata requestHeaders;
545+
546+
RequestMetadataAllureGrpc(final Metadata requestHeaders, final boolean interceptRequestMetadata) {
547+
super(Allure.getLifecycle(), true, interceptRequestMetadata, false);
548+
this.requestHeaders = new Metadata();
549+
this.requestHeaders.merge(requestHeaders);
550+
}
551+
552+
@Override
553+
public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(
554+
final MethodDescriptor<ReqT, RespT> method,
555+
final CallOptions callOptions,
556+
final Channel next) {
557+
final ClientCall<ReqT, RespT> delegate = super.interceptCall(method, callOptions, next);
558+
return new ForwardingClientCall.SimpleForwardingClientCall<ReqT, RespT>(delegate) {
559+
@Override
560+
public void start(final Listener<RespT> responseListener, final Metadata headers) {
561+
headers.merge(requestHeaders);
562+
super.start(responseListener, headers);
563+
}
564+
};
565+
}
566+
}
567+
476568
}

0 commit comments

Comments
 (0)