• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

jhannes / logevents / #125

30 Sep 2025 08:36PM UTC coverage: 90.881% (-0.03%) from 90.912%
#125

push

jhannes-test
add name to all project to appease maven-release-plugin

5840 of 6426 relevant lines covered (90.88%)

0.91 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

87.94
/logevents/src/main/java/org/logevents/optional/servlets/LogEventsServlet.java
1
package org.logevents.optional.servlets;
2

3
import org.logevents.LogEventFactory;
4
import org.logevents.LogEventObserver;
5
import org.logevents.LogEventLogger;
6
import org.logevents.observers.LogEventSource;
7
import org.logevents.observers.WebLogEventObserver;
8
import org.logevents.query.LogEventQuery;
9
import org.logevents.query.LogEventQueryResult;
10
import org.logevents.status.LogEventStatus;
11
import org.logevents.util.JsonParser;
12
import org.logevents.util.JsonUtil;
13
import org.logevents.util.openid.OpenIdConfiguration;
14
import org.logevents.observers.web.CryptoVault;
15
import org.slf4j.Logger;
16
import org.slf4j.LoggerFactory;
17
import org.slf4j.Marker;
18
import org.slf4j.MarkerFactory;
19

20
import javax.servlet.http.Cookie;
21
import javax.servlet.http.HttpServlet;
22
import javax.servlet.http.HttpServletRequest;
23
import javax.servlet.http.HttpServletResponse;
24
import java.io.IOException;
25
import java.io.InputStream;
26
import java.io.InputStreamReader;
27
import java.io.Reader;
28
import java.security.GeneralSecurityException;
29
import java.time.Instant;
30
import java.util.ArrayList;
31
import java.util.Collections;
32
import java.util.HashMap;
33
import java.util.LinkedHashMap;
34
import java.util.List;
35
import java.util.Map;
36
import java.util.Optional;
37
import java.util.stream.Collectors;
38
import java.util.stream.Stream;
39

40
/**
41
 * A servlet that exposes log information to administrative users via a built in web page. To use, you need to:
42
 * <ul>
43
 *     <li>
44
 *         Run your application in a servlet container: Add LogEventsServlet as a servlet in <code>web.xml</code>, add in a ServletContextListener or in Spring, add a <code>ServletRegistrationBean</code>
45
 *     </li>
46
 *     <li>
47
 *         You need an Identity Provider that supports OpenID Connect to authorize administrative users.
48
 *         If you don't have any existing options, I suggest creating a (free!) Azure Active Directory
49
 *         and adding users that should have access as guest users. See {@link OpenIdConfiguration}
50
 *         to learn how to set this up.
51
 *     </li>
52
 *     <li>
53
 *         In order to run LogEventsServlet needs security configuration in your logevents*.properties.
54
 *         You need to set <code>observer.servlet.openIdIssuer</code>, <code>observer.servlet.clientId</code>
55
 *         and <code>observer.servlet.clientSecret</code>. See {@link WebLogEventObserver}
56
 *     </li>
57
 *     <li>
58
 *         If you mount LogEventsServlet on "/logs", the API will be at "/logs/events", the OpenAPI documentation
59
 *         will be at "/logs/openapi.json" and a simple client web page will be at "/logs/"".
60
 *     </li>
61
 * </ul>
62
 *
63
 * <h2>Example configuration:</h2>
64
 *
65
 * <pre>
66
 * observer.servlet=WebLogEventObserver
67
 * observer.servlet.openIdIssuer=https://login.microsoftonline.com/common
68
 * observer.servlet.clientId=12345678-abcd-pqrs-9876-9abcdef01234
69
 * observer.servlet.clientSecret=3¤..¤!?qwer
70
 * observer.servlet.redirectUri=https://my-server.example.com/logs/oauth2callback
71
 * observer.servlet.requiredClaim.username=johannes@brodwall.com,someone@brodwall.com
72
 * observer.servlet.requiredClaim.roles=admin
73
 * </pre>
74
 *
75
 * <h2>Register LogEventsServlet in your servlet container</h2>
76
 *
77
 * <h3>Example web.xml-file</h3>
78
 *
79
 * <pre>
80
 * &lt;servlet&gt;
81
 *     &lt;servlet-name&gt;LogEvents&lt;/servlet-name&gt;
82
 *     &lt;servlet-class&gt;org.logevents.extend.servlets.LogEventsServlet&lt;/servlet-class&gt;
83
 * &lt;/servlet&gt;
84
 * &lt;servlet-mapping&gt;
85
 *   &lt;servlet-name&gt;LogEvents&lt;/servlet-name&gt;
86
 *   &lt;url-pattern&gt;/*&lt;/url-pattern&gt;
87
 * &lt;/servlet-mapping&gt;
88
 * </pre>
89
 *
90
 * <h3>Example ServletContextListener</h3>
91
 *
92
 * <pre>
93
 * public class ApplicationContext implements ServletContextListener {
94
 *     public void contextInitialized(ServletContextEvent sce) {
95
 *        sce.getServletContext().addServlet("logs", new LogEventsServlet()).addMapping("/logs/*");
96
 *    }
97
 *    public void contextDestroyed(ServletContextEvent sce) {
98
 *    }
99
 * }
100
 * </pre>
101
 *
102
 * <h3>Example Spring ServletRegistrationBean</h3>
103
 *
104
 * <pre>
105
 * &#064;Bean
106
 * public ServletRegistrationBean servletRegistrationBean(){
107
 *     return new ServletRegistrationBean(new LogEventsServlet(), "/logs/*");
108
 * }
109
 * </pre>
110
 *
111
 * @see WebLogEventObserver
112
 * @see OpenIdConfiguration
113
 * @see LogEventQuery
114
 *
115
 */
116
public class LogEventsServlet extends HttpServlet {
1✔
117

118
    private final static Logger logger = LoggerFactory.getLogger(LogEventsServlet.class);
1✔
119
    private static final Marker AUDIT = MarkerFactory.getMarker("AUDIT");
1✔
120
    private static final String LOGEVENTS_API = "/org/logevents/openapi.json";
121

122
    @Override
123
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
124
        String path = req.getPathInfo();
1✔
125
        String contextPath = req.getContextPath() != null ? req.getContextPath() : "";
1✔
126
        if (path == null) {
1✔
127
            resp.sendRedirect(contextPath + req.getServletPath() + "/" +
×
128
                    (req.getQueryString() != null ? "?" + req.getQueryString() : ""));
×
129
        } else if (path.equals("/")) {
1✔
130
            resp.setContentType("text/html");
1✔
131
            copyResource(resp, getLogEventsHtml());
1✔
132
        } else if (path.matches("/[a-zA-Z._-]+\\.css")) {
1✔
133
            resp.setContentType("text/css");
1✔
134
            copyResource(resp, "/org/logevents" + path);
1✔
135
        } else if (path.matches("/[a-zA-Z._-]+\\.js")) {
1✔
136
            resp.setContentType("text/javascript");
1✔
137
            copyResource(resp, "/org/logevents" + path);
1✔
138
        } else if (path.equals("/openapi.json")) {
1✔
139
            resp.setContentType("application/json");
1✔
140
            Map<String, Object> api = JsonParser.parseObject(getClass().getResourceAsStream(LOGEVENTS_API));
1✔
141
            HashMap<Object, Object> localServer = new HashMap<>();
1✔
142
            localServer.put("url", contextPath + req.getServletPath());
1✔
143
            api.put("servers", Collections.singletonList(localServer));
1✔
144
            resp.getWriter().write(JsonUtil.toIndentedJson(api));
1✔
145
        } else if (path.equals("/login")) {
1✔
146
            String state = OpenIdConfiguration.randomString(50);
1✔
147
            resp.sendRedirect(getOpenIdConfiguration().getAuthorizationUrl(
1✔
148
                    state, getServletUrl(req) + "/oauth2callback"
1✔
149
            ));
150
        } else if (path.equals("/oauth2callback")) {
1✔
151
            establishSession(req, resp);
1✔
152
        } else if (!authenticated(resp, req.getCookies())) {
1✔
153
            resp.sendError(401, "Please log in");
×
154
        } else if (path.equals("/events")) {
1✔
155
            LogEventQuery query = new LogEventQuery(req.getParameterMap());
1✔
156
            LogEventQueryResult queryResult = getLogEventSource().query(query);
1✔
157

158
            Map<String, Object> result = new LinkedHashMap<>();
1✔
159
            result.put("facets", queryResult.getSummary().toJson());
1✔
160
            result.put("events", queryResult.getEventsAsJson());
1✔
161

162
            resp.setContentType("application/json");
1✔
163
            resp.getWriter().write(JsonUtil.toIndentedJson(result));
1✔
164
        } else if (path.equals("/loggers")) {
1✔
165
            Map<String, Object> result = loggersAsJson(LogEventFactory.getInstance());
×
166
            resp.setContentType("application/json");
×
167
            resp.getWriter().write(JsonUtil.toIndentedJson(result));
×
168
        } else {
×
169
            resp.sendError(404, "Not found " + path);
×
170
        }
171
    }
1✔
172

173
    protected String getLogEventsHtml() {
174
        return getObserver().getLogEventsHtml();
1✔
175
    }
176

177
    Map<String, Object> loggersAsJson(LogEventFactory factory) {
178
        Map<String, Object> configuration = new HashMap<>();
1✔
179
        List<Map<String, Object>> loggers = new ArrayList<>();
1✔
180

181
        List<String> loggerNames = new ArrayList<>();
1✔
182
        loggerNames.add(Logger.ROOT_LOGGER_NAME);
1✔
183
        factory.getLoggers().entrySet().stream()
1✔
184
                .filter(entry -> entry.getValue().isConfigured())
1✔
185
                .map(Map.Entry::getKey)
1✔
186
                .sorted()
1✔
187
                .forEach(loggerNames::add);
1✔
188

189
        for (String loggerName : loggerNames) {
1✔
190
            Map<String, Object> loggerJson = new LinkedHashMap<>();
1✔
191
            loggerJson.put("loggerName", loggerName);
1✔
192
            LogEventLogger logger = factory.getLogger(loggerName);
1✔
193
            loggerJson.put("trace", observersAsJson(logger.getTraceObservers()));
1✔
194
            loggerJson.put("debug", observersAsJson(logger.getDebugObservers()));
1✔
195
            loggerJson.put("info", observersAsJson(logger.getInfoObservers()));
1✔
196
            loggerJson.put("warn", observersAsJson(logger.getWarnObservers()));
1✔
197
            loggerJson.put("error", observersAsJson(logger.getErrorObservers()));
1✔
198
            loggers.add(loggerJson);
1✔
199
        }
1✔
200

201
        configuration.put("loggers", loggers);
1✔
202
        return configuration;
1✔
203
    }
204

205
    private List<Map<String, Object>> observersAsJson(LogEventObserver observers) {
206
        return observers.stream()
1✔
207
                .map(o -> {
1✔
208
                    Map<String, Object> observer = new HashMap<>();
1✔
209
                    observer.put("observerClass", o.getClass().getName());
1✔
210
                    observer.put("observerDescription", o.toString());
1✔
211
                    return observer;
1✔
212
                })
213
                .collect(Collectors.toList());
1✔
214
    }
215

216
    protected void establishSession(HttpServletRequest req, HttpServletResponse resp) throws IOException {
217
        if (req.getParameter("error_description") != null) {
1✔
218
            resp.getWriter().write("Login failed\n\n");
×
219
            resp.getWriter().write(req.getParameter("error_description"));
×
220
            return;
×
221
        }
222

223
        Map<String, Object> idToken = getOpenIdConfiguration()
1✔
224
                .fetchIdToken(req.getParameter("code"), getServletUrl(req) + "/oauth2callback");
1✔
225

226
        if (!getOpenIdConfiguration().isAuthorizedToken(idToken)) {
1✔
227
            logger.warn(AUDIT, "Unknown user tried to log in {}", idToken);
1✔
228
            resp.sendError(403, "Unauthorized");
1✔
229
            return;
1✔
230
        }
231

232
        logger.warn(AUDIT, "User logged in {}", idToken);
1✔
233
        LogEventStatus.getInstance().addConfig(this, "User logged in " + idToken);
1✔
234

235
        resp.addCookie(createSessionCookie(idToken));
1✔
236
        String location = req.getContextPath() + req.getServletPath() + "/";
1✔
237
        String redirectTo = findCookie(req.getCookies(), "logevents.query")
1✔
238
            .map(query -> location + "?" + query)
1✔
239
            .orElse(location);
1✔
240
        resp.sendRedirect(redirectTo);
1✔
241
    }
1✔
242

243
    protected LogEventSource getLogEventSource() {
244
        return getObserver().getLogEventSource();
1✔
245
    }
246

247
    protected OpenIdConfiguration getOpenIdConfiguration() {
248
        return getObserver().getOpenIdConfiguration();
1✔
249
    }
250

251
    protected Optional<String> findCookie(Cookie[] reqCookies, String name) {
252
        return Optional.ofNullable(reqCookies)
1✔
253
                .flatMap(cookies -> Stream.of(cookies)
1✔
254
                        .filter(c -> c.getName().equals(name))
×
255
                        .map(Cookie::getValue)
×
256
                        .findAny()
×
257
                );
258
    }
259

260
    private String getServletUrl(HttpServletRequest req) {
261
        return getServerUrl(req) + req.getContextPath() + req.getServletPath();
1✔
262
    }
263

264
    protected Cookie createSessionCookie(Map<String, Object> idToken) {
265
        String session = "subject=" + idToken.get("sub") + "\n"
1✔
266
                + "sessionTime=" + Instant.ofEpochSecond(Long.parseLong(idToken.get("iat").toString()));
1✔
267
        return new Cookie("logevents.session", encrypt(session));
1✔
268
    }
269

270
    private String encrypt(String session) {
271
        return getCookieVault().encrypt(session);
1✔
272
    }
273

274
    protected String decrypt(String value) throws GeneralSecurityException {
275
        return getCookieVault().decrypt(value);
1✔
276
    }
277

278
    protected synchronized CryptoVault getCookieVault() {
279
        return getObserver().getCookieVault();
1✔
280
    }
281

282
    protected boolean authenticated(HttpServletResponse resp, Cookie[] cookies) {
283
        if (cookies != null) {
1✔
284
            for (Cookie cookie : cookies) {
1✔
285
                if (cookie.getName().equals("logevents.session")) {
1✔
286
                    try {
287
                        Map<String, String> session = Stream.of(decrypt(cookie.getValue()).split("\n"))
1✔
288
                                .collect(Collectors.toMap(
1✔
289
                                        s -> s.split("=")[0],
1✔
290
                                        s -> s.split("=")[1]
1✔
291
                                ));
292
                        if (session.containsKey("sessionTime")) {
1✔
293
                            Instant sessionTime = Instant.parse(session.get("sessionTime"));
1✔
294
                            if (Instant.now().isBefore(sessionTime.plusSeconds(60*60))) {
1✔
295
                                return true;
1✔
296
                            }
297
                        }
298
                    } catch (GeneralSecurityException|IllegalArgumentException|ArrayIndexOutOfBoundsException e) {
×
299
                        LogEventStatus.getInstance().addInfo(this, "Decoding session failed, invalidating session " + e);
×
300
                    }
1✔
301
                    cookie.setValue("");
1✔
302
                    cookie.setMaxAge(0);
1✔
303
                    resp.addCookie(cookie);
1✔
304
                    return false;
1✔
305
                }
306
            }
307
        }
308
        return false;
×
309
    }
310

311
    protected void copyResource(HttpServletResponse resp, String resource) throws IOException {
312
        InputStream resourceAsStream = getClass().getResourceAsStream(resource);
1✔
313
        if (resourceAsStream == null) {
1✔
314
            resp.sendError(404);
1✔
315
            return;
1✔
316
        }
317
        try (Reader html = new InputStreamReader(resourceAsStream)) {
1✔
318
            int c;
319
            while ((c = html.read()) != -1) {
1✔
320
                resp.getWriter().write((char) c);
1✔
321
            }
322
        }
323
    }
1✔
324

325
    protected String getServerUrl(HttpServletRequest req) {
326
        String scheme = Optional.ofNullable(req.getHeader("X-Forwarded-Proto")).orElse(req.getScheme());
1✔
327
        String host = Optional.ofNullable(req.getHeader("X-Forwarded-Host")).orElse(req.getHeader("Host"));
1✔
328
        return scheme + "://" + host;
1✔
329
    }
330

331
    public WebLogEventObserver getObserver() {
332
        return (WebLogEventObserver) LogEventFactory.getInstance().tryGetObserver("servlet");
1✔
333
    }
334

335
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc