001/**
002 * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com).
003 * <p>
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 * <p>
008 * http://www.apache.org/licenses/LICENSE-2.0
009 * <p>
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package io.jboot.support.metric.request;
017
018import com.codahale.metrics.Counter;
019import com.codahale.metrics.Meter;
020import com.codahale.metrics.MetricRegistry;
021import com.codahale.metrics.Timer;
022import com.jfinal.handler.Handler;
023import io.jboot.Jboot;
024import io.jboot.support.metric.JbootMetricConfig;
025
026import javax.servlet.*;
027import javax.servlet.http.HttpServletRequest;
028import javax.servlet.http.HttpServletResponse;
029import javax.servlet.http.HttpServletResponseWrapper;
030import java.io.IOException;
031import java.util.Map;
032import java.util.Map.Entry;
033import java.util.concurrent.ConcurrentHashMap;
034import java.util.concurrent.ConcurrentMap;
035
036import static com.codahale.metrics.MetricRegistry.name;
037
038/**
039 * {@link Filter} implementation which captures request information and a breakdown of the response
040 * codes being returned.
041 */
042public abstract class AbstractInstrumentedFilter extends Handler {
043
044    private final Map<Integer, String> meterNamesByStatusCode;
045
046    // initialized after call of init method
047    private ConcurrentMap<Integer, Meter> metersByStatusCode;
048    private Meter otherMeter;
049    private Meter timeoutsMeter;
050    private Meter errorsMeter;
051    private Counter activeRequests;
052    private Timer requestTimer;
053
054    private JbootMetricConfig jbootMetricConfig = Jboot.config(JbootMetricConfig.class);
055
056
057    /**
058     * Creates a new instance of the filter.
059     *
060     * @param meterNamesByStatusCode A map, keyed by status code, of meter names that we are
061     *                               interested in.
062     * @param otherMetricName        The name used for the catch-all meter.
063     */
064    protected AbstractInstrumentedFilter(Map<Integer, String> meterNamesByStatusCode, String otherMetricName) {
065        this.meterNamesByStatusCode = meterNamesByStatusCode;
066
067
068        final MetricRegistry metricsRegistry = Jboot.getMetric();
069
070        String metricName = jbootMetricConfig.getRequestMetricName();
071        if (metricName == null || metricName.isEmpty()) {
072            metricName = getClass().getName();
073        }
074
075        this.metersByStatusCode = new ConcurrentHashMap<>(meterNamesByStatusCode.size());
076        for (Entry<Integer, String> entry : meterNamesByStatusCode.entrySet()) {
077            metersByStatusCode.put(entry.getKey(),
078                    metricsRegistry.meter(name(metricName, entry.getValue())));
079        }
080        this.otherMeter = metricsRegistry.meter(name(metricName, otherMetricName));
081        this.timeoutsMeter = metricsRegistry.meter(name(metricName, "timeouts"));
082        this.errorsMeter = metricsRegistry.meter(name(metricName, "errors"));
083        this.activeRequests = metricsRegistry.counter(name(metricName, "activeRequests"));
084        this.requestTimer = metricsRegistry.timer(name(metricName, "requests"));
085
086    }
087
088
089    @Override
090    public void handle(String target, HttpServletRequest request, HttpServletResponse response, boolean[] isHandled) {
091
092        final StatusExposingServletResponse wrappedResponse = new StatusExposingServletResponse(response);
093        activeRequests.inc();
094        final Timer.Context context = requestTimer.time();
095        boolean error = false;
096        try {
097            next.handle(target, request, wrappedResponse, isHandled);
098        } catch (Exception e) {
099            error = true;
100            throw e;
101        } finally {
102            if (!error && request.isAsyncStarted()) {
103                request.getAsyncContext().addListener(new AsyncResultListener(context));
104            } else {
105                context.stop();
106                activeRequests.dec();
107                if (error) {
108                    errorsMeter.mark();
109                } else {
110                    markMeterForStatusCode(wrappedResponse.getStatus());
111                }
112            }
113        }
114    }
115
116    private void markMeterForStatusCode(int status) {
117        final Meter metric = metersByStatusCode.get(status);
118        if (metric != null) {
119            metric.mark();
120        } else {
121            otherMeter.mark();
122        }
123    }
124
125    private static class StatusExposingServletResponse extends HttpServletResponseWrapper {
126        // The Servlet spec says: calling setStatus is optional, if no status is set, the default is 200.
127        private int httpStatus = 200;
128
129        public StatusExposingServletResponse(HttpServletResponse response) {
130            super(response);
131        }
132
133        @Override
134        public void sendError(int sc) throws IOException {
135            httpStatus = sc;
136            super.sendError(sc);
137        }
138
139        @Override
140        public void sendError(int sc, String msg) throws IOException {
141            httpStatus = sc;
142            super.sendError(sc, msg);
143        }
144
145        @Override
146        public void setStatus(int sc) {
147            httpStatus = sc;
148            super.setStatus(sc);
149        }
150
151        @Override
152        @SuppressWarnings("deprecation")
153        public void setStatus(int sc, String sm) {
154            httpStatus = sc;
155            super.setStatus(sc, sm);
156        }
157
158        @Override
159        public int getStatus() {
160            return httpStatus;
161        }
162    }
163
164    private class AsyncResultListener implements AsyncListener {
165        private Timer.Context context;
166        private boolean done = false;
167
168        public AsyncResultListener(Timer.Context context) {
169            this.context = context;
170        }
171
172        @Override
173        public void onComplete(AsyncEvent event) throws IOException {
174            if (!done) {
175                HttpServletResponse suppliedResponse = (HttpServletResponse) event.getSuppliedResponse();
176                context.stop();
177                activeRequests.dec();
178                markMeterForStatusCode(suppliedResponse.getStatus());
179            }
180        }
181
182        @Override
183        public void onTimeout(AsyncEvent event) throws IOException {
184            context.stop();
185            activeRequests.dec();
186            timeoutsMeter.mark();
187            done = true;
188        }
189
190        @Override
191        public void onError(AsyncEvent event) throws IOException {
192            context.stop();
193            activeRequests.dec();
194            errorsMeter.mark();
195            done = true;
196        }
197
198        @Override
199        public void onStartAsync(AsyncEvent event) throws IOException {
200
201        }
202    }
203}