Skip to content

Commit 227146d

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 227146d

File tree

20 files changed

+575
-275
lines changed

20 files changed

+575
-275
lines changed
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
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.Objects;
22+
import java.util.function.Predicate;
23+
import java.util.function.Supplier;
24+
import java.util.regex.Pattern;
25+
import java.util.regex.PatternSyntaxException;
26+
import java.util.stream.Stream;
27+
import lombok.Getter;
28+
import lombok.RequiredArgsConstructor;
29+
import lombok.Setter;
30+
import org.jspecify.annotations.Nullable;
31+
32+
@RequiredArgsConstructor
33+
public class LoggableContentType {
34+
35+
private static final String DEFAULT_EXCLUDED_CONTENT_TYPES =
36+
"video.*|audio.*|image.*|application/octet-stream|application/pdf|text/event-stream";
37+
private Predicate<String> defaultPattern;
38+
39+
@Setter
40+
@Getter
41+
private String excludedResponseTypes;
42+
43+
private Predicate<String> matcher(Stream<Supplier<String>> regex, @Nullable BaseExecutionContext ctx) {
44+
Pattern overridePattern = ctx != null
45+
? ctx.getInternalAttribute(InternalContextAttributes.ATTR_INTERNAL_OVERRIDE_LOGGABLE_CONTENT_TYPE_PATTERN)
46+
: null;
47+
return overridePattern != null ? overridePattern.asPredicate() : buildDefault(regex, ctx);
48+
}
49+
50+
private Predicate<String> buildDefault(Stream<Supplier<String>> regex, @Nullable BaseExecutionContext ctx) {
51+
if (defaultPattern == null) {
52+
defaultPattern = regex
53+
.filter(Objects::nonNull)
54+
.map(Supplier::get)
55+
.filter(pattern -> pattern != null && !pattern.isEmpty())
56+
.flatMap(pattern -> Stream.ofNullable(buildPattern(pattern, ctx)))
57+
.findFirst()
58+
.orElse(contentType -> true);
59+
}
60+
return defaultPattern;
61+
}
62+
63+
private Predicate<String> buildPattern(String pattern, @Nullable BaseExecutionContext ctx) {
64+
try {
65+
return Pattern.compile(pattern).asPredicate();
66+
} catch (PatternSyntaxException e) {
67+
if (ctx != null) {
68+
ctx.warnWith(
69+
new ExecutionWarn("BAD_REGEX_CONTENT_TYPE")
70+
.cause(e)
71+
.message("Invalid Content-Type filter regex provided ('%s'). Default one will be used.".formatted(pattern))
72+
);
73+
}
74+
return null;
75+
}
76+
}
77+
78+
/**
79+
* Determines if body can be logged for APIv4
80+
*/
81+
public boolean isContentTypeLoggable(@Nullable final String contentType, BaseExecutionContext ctx) {
82+
return (
83+
contentType == null ||
84+
!matcher(Stream.of(() -> excludedResponseTypes, () -> DEFAULT_EXCLUDED_CONTENT_TYPES), ctx).test(contentType)
85+
);
86+
}
87+
88+
/**
89+
* Determines if body can be logged for APIv2
90+
*/
91+
public boolean isContentTypeLoggable(@Nullable final String contentType, @Nullable final LoggingContext loggingContext) {
92+
Stream<Supplier<String>> getExcludedResponseTypes = loggingContext != null
93+
? Stream.of(loggingContext::getExcludedResponseTypes)
94+
: Stream.empty();
95+
Stream<Supplier<String>> defaultPattern = Stream.of(() -> DEFAULT_EXCLUDED_CONTENT_TYPES);
96+
return (contentType == null || !matcher(Stream.concat(getExcludedResponseTypes, defaultPattern), null).test(contentType));
97+
}
98+
99+
public void reset() {
100+
defaultPattern = null;
101+
}
102+
}

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

Lines changed: 7 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) {
@@ -141,4 +128,8 @@ public static boolean isProxyLoggable(final ExecutionContext executionContext) {
141128
final LoggingContext context = getLoggingContext(executionContext);
142129
return context != null && context.proxyMode();
143130
}
131+
132+
public static void reset() {
133+
loggableContentType.reset();
134+
}
144135
}

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/core/logging/utils/LoggingUtilsTest.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@
3232
import org.junit.jupiter.params.provider.ValueSource;
3333
import org.mockito.Mock;
3434
import org.mockito.junit.jupiter.MockitoExtension;
35-
import org.springframework.test.util.ReflectionTestUtils;
3635

3736
/**
3837
* @author Nicolas GERAUD (nicolas.geraud at graviteesource.com)
@@ -49,7 +48,7 @@ class LoggingUtilsTest {
4948

5049
@BeforeEach
5150
void init() {
52-
ReflectionTestUtils.setField(LoggingUtils.class, "EXCLUDED_CONTENT_TYPES_PATTERN", null);
51+
LoggingUtils.reset();
5352
}
5453

5554
@ParameterizedTest

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
}

0 commit comments

Comments
 (0)