Skip to content

Commit 0c752f5

Browse files
committed
Utility for access logging.
1 parent 7123703 commit 0c752f5

File tree

6 files changed

+228
-34
lines changed

6 files changed

+228
-34
lines changed

vpro-shared-logging/src/main/java/nl/vpro/logging/mdc/MDCConstants.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ public class MDCConstants {
1717
public static final String REMOTE_ADDR = "remoteAddr";
1818
public static final String USER_AGENT = "userAgent";
1919
public static final String BODY = "body";
20+
2021
public static final String HEADERS = "headers";
22+
public static final String USER_COUNT = "userCount";
2123

2224

2325
public static void onBehalfOf(String user) {

vpro-shared-rs/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,5 +62,10 @@
6262
<scope>compile</scope>
6363
<optional>true</optional>
6464
</dependency>
65+
<dependency>
66+
<groupId>org.springframework</groupId>
67+
<artifactId>spring-context</artifactId>
68+
<optional>true</optional>
69+
</dependency>
6570
</dependencies>
6671
</project>
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
package nl.vpro.rs.interceptors;
2+
3+
import lombok.extern.slf4j.Slf4j;
4+
5+
import java.io.IOException;
6+
import java.nio.file.Files;
7+
import java.nio.file.Path;
8+
import java.util.Map;
9+
import java.util.concurrent.ConcurrentHashMap;
10+
import java.util.concurrent.atomic.AtomicLong;
11+
import java.util.regex.Pattern;
12+
13+
import jakarta.ws.rs.container.ContainerRequestContext;
14+
import jakarta.ws.rs.container.ContainerRequestFilter;
15+
import jakarta.ws.rs.ext.Provider;
16+
17+
import org.slf4j.MDC;
18+
import org.springframework.jmx.export.annotation.ManagedAttribute;
19+
import org.springframework.jmx.export.annotation.ManagedResource;
20+
import org.springframework.stereotype.Component;
21+
22+
import nl.vpro.logging.mdc.MDCConstants;
23+
import nl.vpro.logging.simple.Slf4jSimpleLogger;
24+
import nl.vpro.util.*;
25+
26+
27+
/**
28+
* TODO: seems a more generic utility. Move it to shared.
29+
*/
30+
@Provider
31+
@Component
32+
@Slf4j
33+
@ManagedResource
34+
public class AccessLogInterceptor implements ContainerRequestFilter {
35+
36+
private Pattern forUser = Pattern.compile("^(rcrs|npo-sourcing-service.*|functional-tests.*|nep-backend)$");
37+
38+
private Pattern forContentType = Pattern.compile("^application/(xml|json)");
39+
40+
private int truncateAfter = 2048;
41+
42+
private final Map<String, AtomicLong> counters = new ConcurrentHashMap<>();
43+
44+
private Path filesPath = null;
45+
46+
47+
@Override
48+
public void filter(ContainerRequestContext requestContext) throws IOException {
49+
50+
if ("POST".equals(requestContext.getMethod())) {
51+
String contentType = requestContext.getHeaderString("content-type");
52+
String user = MDC.get(MDCConstants.USER_NAME);
53+
if ((user != null && forUser.matcher(user).matches()) &&
54+
(contentType != null && forContentType.matcher(contentType).matches())
55+
) {
56+
long count = counters.computeIfAbsent(user, k -> new AtomicLong()).incrementAndGet();
57+
MDC.put(MDCConstants.USER_COUNT, String.valueOf(count));
58+
TruncatedObservableInputStream inputStream;
59+
if (filesPath == null) {
60+
inputStream = new LoggingInputStream(Slf4jSimpleLogger.slf4j(log), requestContext.getEntityStream());
61+
} else {
62+
Path file = filesPath.resolve(user + "-" + count + ".log");
63+
inputStream = new FileInputStreamTee(Files.newOutputStream(file), requestContext.getEntityStream());
64+
}
65+
inputStream.setTruncateAfter(truncateAfter);
66+
requestContext.setEntityStream(inputStream);
67+
} else {
68+
log.trace("Not logging body for {} {}", user, contentType);
69+
}
70+
}
71+
}
72+
73+
@ManagedAttribute
74+
public String getForUser() {
75+
return forUser.pattern();
76+
}
77+
78+
@ManagedAttribute
79+
public void setForUser(String pattern) {
80+
this.forUser = Pattern.compile(pattern);
81+
}
82+
83+
@ManagedAttribute
84+
public String getForContentType() {
85+
return forContentType.pattern();
86+
}
87+
88+
@ManagedAttribute
89+
public void setForContentType(String forContentType) {
90+
this.forContentType = Pattern.compile(forContentType);
91+
}
92+
93+
@ManagedAttribute
94+
public int getTruncateAfter() {
95+
return truncateAfter;
96+
}
97+
98+
@ManagedAttribute
99+
public void setTruncateAfter(int truncateAfter) {
100+
this.truncateAfter = truncateAfter;
101+
}
102+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package nl.vpro.util;
2+
3+
import lombok.Getter;
4+
import lombok.Setter;
5+
6+
import java.io.*;
7+
8+
9+
/**
10+
* A wrapper for an {@link InputStream} that logs it's first bytes.
11+
*/
12+
@Setter
13+
@Getter
14+
public class FileInputStreamTee extends TruncatedObservableInputStream {
15+
16+
private final OutputStream fileOutputStream;
17+
18+
public FileInputStreamTee(OutputStream fileOutputStream, InputStream wrapped) {
19+
super(wrapped);
20+
this.fileOutputStream = fileOutputStream;
21+
}
22+
23+
@Override
24+
void write(byte[] buffer, int offset, int effectiveLength) throws IOException {
25+
fileOutputStream.write(buffer, offset, effectiveLength);
26+
27+
}
28+
29+
@Override
30+
void write(int value) throws IOException {
31+
fileOutputStream.write(value);
32+
}
33+
34+
@Override
35+
void closed(long count, boolean truncated) throws IOException {
36+
fileOutputStream.close();
37+
}
38+
39+
}

vpro-shared-util/src/main/java/nl/vpro/util/LoggingInputStream.java

Lines changed: 19 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@
66
import java.io.*;
77
import java.nio.charset.StandardCharsets;
88

9-
import org.apache.commons.io.input.ObservableInputStream;
10-
119
import nl.vpro.logging.simple.SimpleLogger;
1210

1311

@@ -16,42 +14,29 @@
1614
*/
1715
@Setter
1816
@Getter
19-
public class LoggingInputStream extends ObservableInputStream {
17+
public class LoggingInputStream extends TruncatedObservableInputStream {
18+
2019

21-
private int truncateAfter = 2048;
2220
private final ByteArrayOutputStream bytes = new ByteArrayOutputStream();
21+
private final SimpleLogger logger;
2322

2423
public LoggingInputStream(SimpleLogger log, InputStream wrapped) {
2524
super(wrapped);
26-
add(new ObservableInputStream.Observer() {
27-
28-
private boolean truncated = false;
29-
private long count = 0;
30-
31-
@Override
32-
public void data(final byte[] buffer, final int offset, final int length) {
33-
if (bytes.size() < truncateAfter) {
34-
int effectiveLength = Math.min(truncateAfter - bytes.size(), length);
35-
truncated = effectiveLength < length;
36-
bytes.write(buffer, offset, effectiveLength);
37-
}
38-
count += length;
39-
}
40-
41-
@Override
42-
public void data(final int value) {
43-
if (bytes.size() < truncateAfter) {
44-
bytes.write(value);
45-
} else {
46-
truncated = true;
47-
}
48-
count++;
49-
}
50-
51-
@Override
52-
public void closed() throws IOException {
53-
log.info("body of {} bytes{}:\n{}{}\n", count, truncated ? " (truncated)" : "", bytes.toString(StandardCharsets.UTF_8), truncated ? "..." : "");
54-
}
55-
});
25+
this.logger = log;
26+
}
27+
28+
@Override
29+
void write(byte[] buffer, int offset, int length) throws IOException {
30+
bytes.write(buffer, offset, length);
31+
}
32+
33+
@Override
34+
void write(int value) throws IOException {
35+
bytes.write(value);
36+
5637
}
38+
@Override
39+
void closed(long count, boolean truncated) throws IOException {
40+
logger.info("body of {} bytes{}:\n{}{}\n", count, truncated ? " (truncated)" : "", bytes.toString(StandardCharsets.UTF_8), truncated ? "..." : "");
41+
}
5742
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package nl.vpro.util;
2+
3+
import lombok.Getter;
4+
import lombok.Setter;
5+
6+
import java.io.IOException;
7+
import java.io.InputStream;
8+
9+
import org.apache.commons.io.input.ObservableInputStream;
10+
11+
12+
/**
13+
* A wrapper for an {@link InputStream} that observes it's first bytes.
14+
* @since 5.5
15+
* @author Michiel Meeuwissen
16+
*/
17+
@Setter
18+
@Getter
19+
public abstract class TruncatedObservableInputStream extends ObservableInputStream {
20+
21+
private int truncateAfter = 2048;
22+
protected TruncatedObservableInputStream(InputStream wrapped) {
23+
super(wrapped);
24+
add(new Observer() {
25+
private boolean truncated = false;
26+
private long count = 0;
27+
28+
@Override
29+
public void data(final byte[] buffer, final int offset, final int length) throws IOException{
30+
if (count < truncateAfter) {
31+
int effectiveLength = Math.min(truncateAfter - (int) count, length);
32+
truncated = effectiveLength < length;
33+
write(buffer, offset, effectiveLength);
34+
}
35+
count += length;
36+
}
37+
38+
@Override
39+
public void data(final int value) throws IOException{
40+
if (count < truncateAfter) {
41+
write(value);
42+
} else {
43+
truncated = true;
44+
}
45+
count++;
46+
}
47+
48+
@Override
49+
public void closed() throws IOException {
50+
TruncatedObservableInputStream.this.closed(count, true);
51+
}
52+
});
53+
}
54+
55+
abstract void write(byte[] buffer, int offset, int length) throws IOException;
56+
abstract void write(int value) throws IOException;
57+
58+
void closed(long count, boolean truncated) throws IOException {
59+
60+
}
61+
}

0 commit comments

Comments
 (0)