Skip to content

Commit 8a9865c

Browse files
committed
feat: add parameter to allow to log SSE trafic
Allow APIs to log setup custom Content-Type filter https://gravitee.atlassian.net/browse/APIM-11603
1 parent 94c05eb commit 8a9865c

File tree

19 files changed

+564
-273
lines changed

19 files changed

+564
-273
lines changed
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/*
2+
* Copyright © 2015 The Gravitee team (http://gravitee.io)
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.gravitee.gateway.core.logging;
17+
18+
import io.gravitee.gateway.reactive.api.ExecutionWarn;
19+
import io.gravitee.gateway.reactive.api.context.InternalContextAttributes;
20+
import io.gravitee.gateway.reactive.api.context.base.BaseExecutionContext;
21+
import java.util.Collection;
22+
import java.util.List;
23+
import java.util.function.Predicate;
24+
import java.util.function.Supplier;
25+
import java.util.regex.Pattern;
26+
import java.util.regex.PatternSyntaxException;
27+
import java.util.stream.Stream;
28+
import lombok.Getter;
29+
import lombok.RequiredArgsConstructor;
30+
import lombok.Setter;
31+
import org.jspecify.annotations.Nullable;
32+
33+
@RequiredArgsConstructor
34+
public class LoggableContentType {
35+
36+
private static final String DEFAULT_EXCLUDED_CONTENT_TYPES =
37+
"video.*|audio.*|image.*|application/octet-stream|application/pdf|text/event-stream";
38+
private Predicate<String> defaultPattern;
39+
40+
@Setter
41+
@Getter
42+
private String excludedResponseTypes;
43+
44+
private Predicate<String> matcher(Collection<Supplier<String>> regex, @Nullable BaseExecutionContext ctx) {
45+
Pattern overridePattern = ctx.getInternalAttribute(InternalContextAttributes.ATTR_INTERNAL_OVERRIDE_LOGGABLE_CONTENT_TYPE_PATTERN);
46+
return overridePattern != null ? overridePattern.asPredicate() : buildDefault(regex, ctx);
47+
}
48+
49+
private Predicate<String> buildDefault(Collection<Supplier<String>> regex, @Nullable BaseExecutionContext ctx) {
50+
if (defaultPattern == null) {
51+
defaultPattern = regex
52+
.stream()
53+
.map(Supplier::get)
54+
.filter(pattern -> pattern != null && !pattern.isEmpty())
55+
.flatMap(pattern -> Stream.ofNullable(buildPattern(pattern, ctx)))
56+
.findFirst()
57+
.orElse(contentType -> true);
58+
}
59+
return defaultPattern;
60+
}
61+
62+
private Predicate<String> buildPattern(String pattern, @Nullable BaseExecutionContext ctx) {
63+
try {
64+
return Pattern.compile(pattern).asPredicate();
65+
} catch (PatternSyntaxException e) {
66+
if (ctx != null) {
67+
ctx.warnWith(
68+
new ExecutionWarn("BAD_REGEX_CONTENT_TYPE")
69+
.cause(e)
70+
.message("Invalid Content-Type filter regex provided ('%s'). Default one will be used.".formatted(pattern))
71+
);
72+
}
73+
return null;
74+
}
75+
}
76+
77+
/**
78+
* Determines if body can be logged for APIv4
79+
*/
80+
public boolean isContentTypeLoggable(@Nullable final String contentType, BaseExecutionContext ctx) {
81+
return (
82+
contentType == null ||
83+
!matcher(List.of(() -> excludedResponseTypes, () -> DEFAULT_EXCLUDED_CONTENT_TYPES), ctx).test(contentType)
84+
);
85+
}
86+
87+
/**
88+
* Determines if body can be logged for APIv2
89+
*/
90+
public boolean isContentTypeLoggable(@Nullable final String contentType, final LoggingContext loggingContext) {
91+
return (
92+
contentType == null ||
93+
!matcher(List.of(loggingContext::getExcludedResponseTypes, () -> DEFAULT_EXCLUDED_CONTENT_TYPES), null).test(contentType)
94+
);
95+
}
96+
}

gravitee-apim-gateway/gravitee-apim-gateway-core/src/main/java/io/gravitee/gateway/core/logging/utils/LoggingUtils.java

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,11 @@
2020
import io.gravitee.definition.model.LoggingMode;
2121
import io.gravitee.gateway.api.ExecutionContext;
2222
import io.gravitee.gateway.api.buffer.Buffer;
23+
import io.gravitee.gateway.core.logging.LoggableContentType;
2324
import io.gravitee.gateway.core.logging.LoggingContext;
2425
import io.gravitee.gateway.reactive.core.v4.analytics.BufferUtils;
2526
import jakarta.annotation.Nonnull;
2627
import jakarta.annotation.Nullable;
27-
import java.util.regex.Pattern;
2828

2929
/**
3030
* @author Titouan COMPIEGNE (titouan.compiegne at graviteesource.com)
@@ -34,10 +34,7 @@
3434
*/
3535
public final class LoggingUtils {
3636

37-
private static final String DEFAULT_EXCLUDED_CONTENT_TYPES =
38-
"video.*|audio.*|image.*|application\\/octet-stream|application\\/pdf|text\\/event-stream";
39-
40-
private static Pattern EXCLUDED_CONTENT_TYPES_PATTERN;
37+
private static final LoggableContentType loggableContentType = new LoggableContentType();
4138

4239
@Nullable
4340
public static LoggingContext getLoggingContext(final Logging logging) {
@@ -76,17 +73,7 @@ public static boolean isContentTypeLoggable(final String contentType, final Exec
7673
}
7774

7875
public static boolean isContentTypeLoggable(final String contentType, final LoggingContext loggingContext) {
79-
// init pattern
80-
if (EXCLUDED_CONTENT_TYPES_PATTERN == null) {
81-
try {
82-
final String responseTypes = loggingContext.getExcludedResponseTypes();
83-
EXCLUDED_CONTENT_TYPES_PATTERN = Pattern.compile(responseTypes);
84-
} catch (Exception e) {
85-
EXCLUDED_CONTENT_TYPES_PATTERN = Pattern.compile(DEFAULT_EXCLUDED_CONTENT_TYPES);
86-
}
87-
}
88-
89-
return contentType == null || !EXCLUDED_CONTENT_TYPES_PATTERN.matcher(contentType).find();
76+
return loggableContentType.isContentTypeLoggable(contentType, loggingContext);
9077
}
9178

9279
public static boolean isRequestHeadersLoggable(final ExecutionContext executionContext) {

gravitee-apim-gateway/gravitee-apim-gateway-core/src/main/java/io/gravitee/gateway/reactive/core/v4/analytics/LoggingContext.java

Lines changed: 23 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,12 @@
1818
import io.gravitee.common.utils.SizeUtils;
1919
import io.gravitee.definition.model.ConditionSupplier;
2020
import io.gravitee.definition.model.v4.analytics.logging.Logging;
21+
import io.gravitee.gateway.core.logging.LoggableContentType;
22+
import io.gravitee.gateway.reactive.api.context.base.BaseExecutionContext;
2123
import io.gravitee.gateway.report.guard.LogGuardService;
22-
import java.util.regex.Pattern;
24+
import lombok.Getter;
2325
import lombok.RequiredArgsConstructor;
26+
import lombok.Setter;
2427
import lombok.extern.slf4j.Slf4j;
2528

2629
/**
@@ -31,15 +34,21 @@
3134
@RequiredArgsConstructor
3235
public class LoggingContext implements ConditionSupplier {
3336

34-
private static final String DEFAULT_EXCLUDED_CONTENT_TYPES =
35-
"video.*|audio.*|image.*|application\\/octet-stream|application\\/pdf|text\\/event-stream";
36-
3737
protected final Logging logging;
38+
39+
@Getter
3840
private int maxSizeLogMessage = -1;
39-
private String excludedResponseTypes;
40-
private Pattern excludedContentTypesPattern;
41+
42+
@Setter
4143
private LogGuardService logGuardService;
4244

45+
private final LoggableContentType loggableContentType;
46+
47+
public LoggingContext(Logging logging) {
48+
this.logging = logging;
49+
loggableContentType = new LoggableContentType();
50+
}
51+
4352
@Override
4453
public String getCondition() {
4554
return logging.getCondition();
@@ -93,8 +102,12 @@ public boolean endpointResponsePayload() {
93102
return logging.getMode().isEndpoint() && logging.getPhase().isResponse() && logging.getContent().isPayload();
94103
}
95104

96-
public int getMaxSizeLogMessage() {
97-
return maxSizeLogMessage;
105+
public String getExcludedResponseTypes() {
106+
return loggableContentType.getExcludedResponseTypes();
107+
}
108+
109+
public void setExcludedResponseTypes(String excludedResponseTypes) {
110+
loggableContentType.setExcludedResponseTypes(excludedResponseTypes);
98111
}
99112

100113
/**
@@ -115,25 +128,8 @@ public void setMaxSizeLogMessage(String maxSizeLogMessage) {
115128
}
116129
}
117130

118-
public String getExcludedResponseTypes() {
119-
return excludedResponseTypes;
120-
}
121-
122-
public void setExcludedResponseTypes(final String excludedResponseTypes) {
123-
this.excludedResponseTypes = excludedResponseTypes;
124-
}
125-
126-
public boolean isContentTypeLoggable(final String contentType) {
127-
// init pattern
128-
if (excludedContentTypesPattern == null) {
129-
try {
130-
excludedContentTypesPattern = Pattern.compile(excludedResponseTypes);
131-
} catch (Exception e) {
132-
excludedContentTypesPattern = Pattern.compile(DEFAULT_EXCLUDED_CONTENT_TYPES);
133-
}
134-
}
135-
136-
return contentType == null || !excludedContentTypesPattern.matcher(contentType).find();
131+
public boolean isContentTypeLoggable(final String contentType, BaseExecutionContext ctx) {
132+
return loggableContentType.isContentTypeLoggable(contentType, ctx);
137133
}
138134

139135
/**
@@ -145,8 +141,4 @@ public boolean isContentTypeLoggable(final String contentType) {
145141
public boolean isBodyLoggable() {
146142
return logGuardService == null || !logGuardService.isLogGuardActive();
147143
}
148-
149-
public void setLogGuardService(LogGuardService logGuardService) {
150-
this.logGuardService = logGuardService;
151-
}
152144
}

gravitee-apim-gateway/gravitee-apim-gateway-core/src/test/java/io/gravitee/gateway/reactive/core/v4/analytics/AnalyticsContextTest.java

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,13 @@
1616
package io.gravitee.gateway.reactive.core.v4.analytics;
1717

1818
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
19-
import static org.mockito.ArgumentMatchers.any;
20-
import static org.mockito.ArgumentMatchers.eq;
21-
import static org.mockito.Mockito.when;
2219

2320
import io.gravitee.definition.model.v4.analytics.Analytics;
2421
import io.gravitee.gateway.opentelemetry.TracingContext;
25-
import io.gravitee.node.api.configuration.Configuration;
2622
import org.junit.jupiter.api.DisplayNameGeneration;
2723
import org.junit.jupiter.api.DisplayNameGenerator;
2824
import org.junit.jupiter.api.Test;
2925
import org.junit.jupiter.api.extension.ExtendWith;
30-
import org.mockito.Mock;
3126
import org.mockito.junit.jupiter.MockitoExtension;
3227

3328
/**

gravitee-apim-gateway/gravitee-apim-gateway-handlers/gravitee-apim-gateway-handlers-api/src/main/java/io/gravitee/gateway/reactive/handlers/api/v4/AbstractApiReactor.java

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -15,47 +15,30 @@
1515
*/
1616
package io.gravitee.gateway.reactive.handlers.api.v4;
1717

18-
import static io.gravitee.gateway.reactive.api.context.InternalContextAttributes.ATTR_INTERNAL_ENDPOINT_CONNECTOR_ID;
1918
import static io.gravitee.gateway.reactive.api.context.InternalContextAttributes.ATTR_INTERNAL_ENTRYPOINT_CONNECTOR;
2019
import static io.gravitee.gateway.reactive.api.context.InternalContextAttributes.ATTR_INTERNAL_EXECUTION_FAILURE;
21-
import static io.gravitee.gateway.reactive.api.context.InternalContextAttributes.ATTR_INTERNAL_INVOKER;
22-
import static io.gravitee.gateway.reactive.api.context.InternalContextAttributes.ATTR_INTERNAL_INVOKER_SKIP;
23-
import static io.reactivex.rxjava3.core.Completable.defer;
2420
import static io.reactivex.rxjava3.core.Observable.interval;
25-
import static java.lang.Boolean.TRUE;
2621

2722
import io.gravitee.common.component.AbstractLifecycleComponent;
2823
import io.gravitee.gateway.env.RequestTimeoutConfiguration;
2924
import io.gravitee.gateway.opentelemetry.TracingContext;
3025
import io.gravitee.gateway.reactive.api.ExecutionFailure;
3126
import io.gravitee.gateway.reactive.api.ExecutionPhase;
3227
import io.gravitee.gateway.reactive.api.connector.entrypoint.BaseEntrypointConnector;
33-
import io.gravitee.gateway.reactive.api.connector.entrypoint.EntrypointConnector;
34-
import io.gravitee.gateway.reactive.api.connector.entrypoint.HttpEntrypointConnector;
3528
import io.gravitee.gateway.reactive.api.context.ContextAttributes;
36-
import io.gravitee.gateway.reactive.api.context.InternalContextAttributes;
3729
import io.gravitee.gateway.reactive.api.context.base.BaseExecutionContext;
38-
import io.gravitee.gateway.reactive.api.context.tcp.TcpExecutionContext;
3930
import io.gravitee.gateway.reactive.api.hook.InvokerHook;
40-
import io.gravitee.gateway.reactive.api.invoker.BaseInvoker;
4131
import io.gravitee.gateway.reactive.api.invoker.HttpInvoker;
42-
import io.gravitee.gateway.reactive.api.invoker.Invoker;
43-
import io.gravitee.gateway.reactive.api.invoker.TcpInvoker;
4432
import io.gravitee.gateway.reactive.core.context.MutableExecutionContext;
45-
import io.gravitee.gateway.reactive.core.hook.HookHelper;
4633
import io.gravitee.gateway.reactive.core.v4.entrypoint.DefaultEntrypointConnectorResolver;
47-
import io.gravitee.gateway.reactive.handlers.api.adapter.invoker.InvokerAdapter;
4834
import io.gravitee.gateway.reactive.reactor.ApiReactor;
4935
import io.gravitee.gateway.reactor.handler.Acceptor;
5036
import io.gravitee.gateway.reactor.handler.ReactorHandler;
5137
import io.gravitee.node.api.configuration.Configuration;
52-
import io.gravitee.node.api.opentelemetry.Tracer;
53-
import io.gravitee.node.opentelemetry.OpenTelemetryFactory;
5438
import io.reactivex.rxjava3.core.Completable;
5539
import io.reactivex.rxjava3.schedulers.Schedulers;
5640
import java.util.ArrayList;
5741
import java.util.List;
58-
import java.util.Optional;
5942
import java.util.concurrent.TimeUnit;
6043
import java.util.concurrent.atomic.AtomicLong;
6144
import lombok.extern.slf4j.Slf4j;

gravitee-apim-gateway/gravitee-apim-gateway-handlers/gravitee-apim-gateway-handlers-api/src/main/java/io/gravitee/gateway/reactive/handlers/api/v4/analytics/logging/LoggingHook.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ public Completable pre(String id, HttpExecutionContext ctx, @Nullable ExecutionP
5050

5151
if (log != null && loggingContext != null) {
5252
if (loggingContext.endpointRequest()) {
53-
((LogEndpointRequest) log.getEndpointRequest()).capture();
53+
((LogEndpointRequest) log.getEndpointRequest()).capture(ctx);
5454
}
5555

5656
if (loggingContext.endpointResponseHeaders() || loggingContext.endpointResponse()) {
@@ -81,7 +81,7 @@ public Completable post(String id, HttpExecutionContext ctx, @Nullable Execution
8181
InternalContextAttributes.ATTR_INTERNAL_EXECUTION_FAILURE
8282
);
8383
if (executionFailure == null) {
84-
((LogEndpointResponse) log.getEndpointResponse()).capture();
84+
((LogEndpointResponse) log.getEndpointResponse()).capture(ctx);
8585
}
8686

8787
final HttpResponseInternal response = ((HttpExecutionContextInternal) ctx).response();

gravitee-apim-gateway/gravitee-apim-gateway-handlers/gravitee-apim-gateway-handlers-api/src/main/java/io/gravitee/gateway/reactive/handlers/api/v4/analytics/logging/request/LogRequest.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import io.gravitee.gateway.api.buffer.Buffer;
1919
import io.gravitee.gateway.api.http.HttpHeaderNames;
2020
import io.gravitee.gateway.api.http.HttpHeaders;
21+
import io.gravitee.gateway.reactive.api.context.base.BaseExecutionContext;
2122
import io.gravitee.gateway.reactive.api.context.http.HttpPlainRequest;
2223
import io.gravitee.gateway.reactive.core.v4.analytics.BufferUtils;
2324
import io.gravitee.gateway.reactive.core.v4.analytics.LoggingContext;
@@ -38,8 +39,8 @@ protected LogRequest(LoggingContext loggingContext, HttpPlainRequest request) {
3839
this.setMethod(request.method());
3940
}
4041

41-
public void capture() {
42-
if (isLogPayload() && loggingContext.isContentTypeLoggable(request.headers().get(HttpHeaderNames.CONTENT_TYPE))) {
42+
public void capture(BaseExecutionContext ctx) {
43+
if (isLogPayload() && loggingContext.isContentTypeLoggable(request.headers().get(HttpHeaderNames.CONTENT_TYPE), ctx)) {
4344
final Buffer buffer = Buffer.buffer();
4445

4546
if (loggingContext.isBodyLoggable()) {

gravitee-apim-gateway/gravitee-apim-gateway-handlers/gravitee-apim-gateway-handlers-api/src/main/java/io/gravitee/gateway/reactive/handlers/api/v4/analytics/logging/response/LogResponse.java

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import io.gravitee.gateway.api.buffer.Buffer;
1919
import io.gravitee.gateway.api.http.HttpHeaderNames;
2020
import io.gravitee.gateway.api.http.HttpHeaders;
21+
import io.gravitee.gateway.reactive.api.context.base.BaseExecutionContext;
2122
import io.gravitee.gateway.reactive.api.context.http.HttpPlainResponse;
2223
import io.gravitee.gateway.reactive.core.v4.analytics.BufferUtils;
2324
import io.gravitee.gateway.reactive.core.v4.analytics.LoggingContext;
@@ -37,8 +38,8 @@ protected LogResponse(LoggingContext loggingContext, HttpPlainResponse response)
3738
this.response = response;
3839
}
3940

40-
public void capture() {
41-
if (isLogPayload() && loggingContext.isContentTypeLoggable(response.headers().get(HttpHeaderNames.CONTENT_TYPE))) {
41+
public void capture(BaseExecutionContext ctx) {
42+
if (isLogPayload() && loggingContext.isContentTypeLoggable(response.headers().get(HttpHeaderNames.CONTENT_TYPE), ctx)) {
4243
final Buffer buffer = Buffer.buffer();
4344
if (loggingContext.isBodyLoggable()) {
4445
response.chunks(
@@ -61,8 +62,8 @@ public void capture() {
6162

6263
@Override
6364
public void setHeaders(HttpHeaders headers) {
64-
if (headers instanceof LogHeadersCaptor) {
65-
super.setHeaders(((LogHeadersCaptor) headers).getCaptured());
65+
if (headers instanceof LogHeadersCaptor logHeadersCaptor) {
66+
super.setHeaders(logHeadersCaptor.getCaptured());
6667
} else {
6768
super.setHeaders(HttpHeaders.create(headers));
6869
}

gravitee-apim-gateway/gravitee-apim-gateway-handlers/gravitee-apim-gateway-handlers-api/src/main/java/io/gravitee/gateway/reactive/handlers/api/v4/processor/logging/LogRequestProcessor.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ public Completable execute(final HttpExecutionContextInternal ctx) {
5151
LoggingContext loggingContext = analyticsContext.getLoggingContext();
5252

5353
if (log != null && loggingContext.entrypointRequest()) {
54-
((LogEntrypointRequest) log.getEntrypointRequest()).capture();
54+
((LogEntrypointRequest) log.getEntrypointRequest()).capture(ctx);
5555
}
5656
});
5757
}

gravitee-apim-gateway/gravitee-apim-gateway-handlers/gravitee-apim-gateway-handlers-api/src/main/java/io/gravitee/gateway/reactive/handlers/api/v4/processor/logging/LogResponseProcessor.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ public Completable execute(final HttpExecutionContextInternal ctx) {
5151
LoggingContext loggingContext = analyticsContext.getLoggingContext();
5252

5353
if (log != null && loggingContext.entrypointResponse()) {
54-
((LogEntrypointResponse) log.getEntrypointResponse()).capture();
54+
((LogEntrypointResponse) log.getEntrypointResponse()).capture(ctx);
5555
}
5656
});
5757
}

0 commit comments

Comments
 (0)