aboutsummaryrefslogtreecommitdiffstats
path: root/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/AccessLoggingRequestHandler.java
blob: 3b1db55defd11835b2dfea01f9bf1604f09e2185 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.jdisc.http.server.jetty;

import com.yahoo.container.logging.AccessLogEntry;
import com.yahoo.jdisc.Request;
import com.yahoo.jdisc.handler.AbstractRequestHandler;
import com.yahoo.jdisc.handler.CompletionHandler;
import com.yahoo.jdisc.handler.ContentChannel;
import com.yahoo.jdisc.handler.DelegatedRequestHandler;
import com.yahoo.jdisc.handler.RequestHandler;
import com.yahoo.jdisc.handler.ResponseHandler;
import com.yahoo.jdisc.http.HttpHeaders;
import com.yahoo.jdisc.http.HttpRequest;

import java.io.ByteArrayOutputStream;
import java.nio.ByteBuffer;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Random;
import java.util.TreeMap;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

import static com.yahoo.jdisc.http.server.jetty.RequestUtils.getConnector;

/**
 * A wrapper RequestHandler that enables access logging. By wrapping the request handler, we are able to wrap the
 * response handler as well. Hence, we can populate the access log entry with information from both the request
 * and the response. This wrapper also adds the access log entry to the request context, so that request handlers
 * may add information to it.
 *
 * Does not otherwise interfere with the request processing of the delegate request handler.
 *
 * @author bakksjo
 * @author bjorncs
 */
public class AccessLoggingRequestHandler extends AbstractRequestHandler implements DelegatedRequestHandler {
    public static final String CONTEXT_KEY_ACCESS_LOG_ENTRY
            = AccessLoggingRequestHandler.class.getName() + "_access-log-entry";

    public static Optional<AccessLogEntry> getAccessLogEntry(final HttpRequest jdiscRequest) {
        final Map<String, Object> requestContextMap = jdiscRequest.context();
        return getAccessLogEntry(requestContextMap);
    }

    public static Optional<AccessLogEntry> getAccessLogEntry(final Map<String, Object> requestContextMap) {
        return Optional.ofNullable(
                (AccessLogEntry) requestContextMap.get(CONTEXT_KEY_ACCESS_LOG_ENTRY));
    }

    private final org.eclipse.jetty.server.Request jettyRequest;
    private final RequestHandler delegateRequestHandler;
    private final AccessLogEntry accessLogEntry;
    private final List<String> pathPrefixes;
    private final List<Double> samplingRate;
    private final Random rng = new Random();

    public AccessLoggingRequestHandler(
            org.eclipse.jetty.server.Request jettyRequest,
            RequestHandler delegateRequestHandler,
            AccessLogEntry accessLogEntry) {
        this.jettyRequest = jettyRequest;
        this.delegateRequestHandler = delegateRequestHandler;
        this.accessLogEntry = accessLogEntry;
        var contentPathPrefixes = getConnector(jettyRequest).connectorConfig().accessLog().contentPathPrefixes();
        this.pathPrefixes = contentPathPrefixes.stream()
                .map(s -> {
                    var separatorIndex = s.lastIndexOf(':');
                    return s.substring(0, separatorIndex == -1 ? s.length() : separatorIndex);
                })
                .toList();
        this.samplingRate = contentPathPrefixes.stream()
                .map(s -> {
                    return Double.parseDouble(s.substring(s.lastIndexOf(':') + 1));
                })
                .toList();
    }

    @Override
    public ContentChannel handleRequest(final Request request, final ResponseHandler handler) {
        final HttpRequest httpRequest = (HttpRequest) request;
        httpRequest.context().put(CONTEXT_KEY_ACCESS_LOG_ENTRY, accessLogEntry);
        var methodsWithEntity = List.of(HttpRequest.Method.POST, HttpRequest.Method.PUT, HttpRequest.Method.PATCH);
        var originalContentChannel = delegateRequestHandler.handleRequest(request, handler);
        var uriPath = request.getUri().getPath();
        if (methodsWithEntity.contains(httpRequest.getMethod())) {
            for (int i = 0; i < pathPrefixes.size(); i++) {
                if (uriPath.startsWith(pathPrefixes.get(i))) {
                    if (samplingRate.get(i) > rng.nextDouble()) {
                        return new ContentLoggingContentChannel(originalContentChannel);
                    }
                }
            }
        }
        return originalContentChannel;
    }

    @Override
    public RequestHandler getDelegate() {
        return delegateRequestHandler;
    }

    private class ContentLoggingContentChannel implements ContentChannel {
        private static final int CONTENT_LOGGING_MAX_SIZE = 16 * 1024 * 1024;

        final AtomicLong length = new AtomicLong();
        final ByteArrayOutputStream accumulatedRequestContent;
        final ContentChannel originalContentChannel;

        public ContentLoggingContentChannel(ContentChannel originalContentChannel) {
            this.originalContentChannel = originalContentChannel;
            var contentLength = jettyRequest.getContentLength();
            this.accumulatedRequestContent = new ByteArrayOutputStream(contentLength == -1 ? 128 : contentLength);
        }

        @Override
        public void write(ByteBuffer buf, CompletionHandler handler) {
            length.addAndGet(buf.remaining());
            var bytesToLog = Math.min(buf.remaining(), CONTENT_LOGGING_MAX_SIZE - accumulatedRequestContent.size());
            if (bytesToLog > 0) accumulatedRequestContent.write(buf.array(), buf.arrayOffset() + buf.position(), bytesToLog);
            if (originalContentChannel != null) originalContentChannel.write(buf, handler);
        }

        @Override
        public void close(CompletionHandler handler) {
            var bytes = accumulatedRequestContent.toByteArray();
            accessLogEntry.setContent(new AccessLogEntry.Content(
                    Objects.requireNonNullElse(jettyRequest.getHeader(HttpHeaders.Names.CONTENT_TYPE), ""),
                    length.get(),
                    bytes));
            accumulatedRequestContent.reset();
            length.set(0);
            if (originalContentChannel != null) originalContentChannel.close(handler);
        }

        @Override
        public void onError(Throwable error) {
            if (originalContentChannel != null) originalContentChannel.onError(error);
        }
    }
}