From e8f728ecd6517d6f568f27efcbb51db7ab1df8c6 Mon Sep 17 00:00:00 2001
From: Lars Johansson <lars.johansson@ess.eu>
Date: Thu, 24 Feb 2022 09:13:45 +0100
Subject: [PATCH] Added REST log filter

Purpose to intercept rest calls and handle logging in uniformed manner.
---
 .../names/rest/filter/RestLogFilter.java      | 219 ++++++++++++++++++
 .../org/openepics/names/util/RequestUtil.java |  76 ++++++
 2 files changed, 295 insertions(+)
 create mode 100644 src/main/java/org/openepics/names/rest/filter/RestLogFilter.java
 create mode 100644 src/main/java/org/openepics/names/util/RequestUtil.java

diff --git a/src/main/java/org/openepics/names/rest/filter/RestLogFilter.java b/src/main/java/org/openepics/names/rest/filter/RestLogFilter.java
new file mode 100644
index 0000000..1a5269a
--- /dev/null
+++ b/src/main/java/org/openepics/names/rest/filter/RestLogFilter.java
@@ -0,0 +1,219 @@
+/*
+ * Copyright (C) 2020 European Spallation Source ERIC.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+ */
+
+package org.openepics.names.rest.filter;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+
+import org.apache.commons.lang3.StringUtils;
+import org.openepics.names.util.RequestUtil;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Purpose of class to intercept rest calls and handle logging in uniformed manner.
+ *
+ * @author <a href="mailto:zoltan.runyo@ess.eu">Zoltan Runyo</a>
+ * @author Lars Johansson
+ */
+@Component
+public class RestLogFilter implements Filter {
+
+    private static final Logger LOGGER = Logger.getLogger(RestLogFilter.class.getName());
+
+    @JsonInclude(JsonInclude.Include.NON_NULL)
+    private static class LogEntry {
+        // fields to be public for logging to work
+        // com.fasterxml.jackson.databind.ObjectMapper#writeValueAsString
+
+        /**
+         * Method for rest call.
+         */
+        @SuppressFBWarnings("URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD")
+        public String method;
+        /**
+         * Path for rest call.
+         */
+        @SuppressFBWarnings("URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD")
+        public String path;
+        /**
+         * Path info for rest call.
+         */
+        @SuppressFBWarnings("URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD")
+        public String pathInfo;
+        /**
+         * Query string for rest call.
+         */
+        @SuppressFBWarnings("URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD")
+        public String queryString;
+        /**
+         * Remote address for rest call.
+         */
+        @SuppressFBWarnings("URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD")
+        public String remoteAddress;
+        /**
+         * Status code for rest call.
+         */
+        @SuppressFBWarnings("URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD")
+        public int statusCode;
+        /**
+         * Time to complete rest call, in milliseconds.
+         */
+        @SuppressFBWarnings("URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD")
+        public long time;
+
+        /**
+         * Store method for rest call in log data.
+         *
+         * @param method method for rest call
+         */
+        public void setMethod(String method) {
+            this.method = method;
+        }
+
+        /**
+         * Store path for rest call in log data.
+         *
+         * @param path path for rest call
+         */
+        public void setPath(String path) {
+            this.path = path;
+        }
+
+        /**
+         * Store path info for rest call in log data.
+         *
+         * @param pathInfo path info for rest call
+         */
+        public void setPathInfo(String pathInfo) {
+            this.pathInfo = pathInfo;
+        }
+
+        /**
+         * Store query string for rest call in log data.
+         *
+         * @param queryString query string for rest call
+         */
+        public void setQueryString(String queryString) {
+            this.queryString = queryString;
+        }
+
+        /**
+         * Store remote address for rest call in log data.
+         *
+         * @param remoteAddress remote address for rest call
+         */
+        public void setRemoteAddress(String remoteAddress) {
+            this.remoteAddress = remoteAddress;
+        }
+
+        /**
+         * Store status code for rest call in log data.
+         *
+         * @param statusCode status code for rest call
+         */
+        public void setStatusCode(int statusCode) {
+            this.statusCode = statusCode;
+        }
+
+        /**
+         * Store time to complete rest call in log data.
+         *
+         * @param time time to complete rest call, in milliseconds
+         */
+        public void setTime(long time) {
+            this.time = time;
+        }
+    }
+
+    @Autowired
+    private ObjectMapper mapper;
+
+    @Override
+    public void init(FilterConfig filterConfig) {}
+
+    @Override
+    public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain filterChain)
+            throws IOException, ServletException {
+        HttpServletRequest req = null;
+        if (request instanceof HttpServletRequest) {
+            req = (HttpServletRequest)request;
+        }
+
+        LogEntry logLine = new LogEntry();
+        boolean restRequest = false;
+
+        if(req != null) {
+            restRequest = isRestRequest(req.getServletPath());
+
+            logLine.setMethod(req.getMethod());
+            logLine.setPath(req.getServletPath());
+            logLine.setPathInfo(req.getPathInfo());
+            logLine.setQueryString(req.getQueryString());
+            logLine.setRemoteAddress(RequestUtil.getIP(req));
+        }
+
+        try {
+            long startTime = System.currentTimeMillis();
+            filterChain.doFilter(request, response);
+            long endTime = System.currentTimeMillis();
+            logLine.setTime(endTime - startTime);
+
+            HttpServletResponse resp = null;
+            if (response instanceof HttpServletResponse) {
+                resp = (HttpServletResponse) response;
+            }
+
+            if (resp != null) {
+                logLine.setStatusCode(resp.getStatus());
+            }
+        } finally {
+            if (restRequest) {
+                LOGGER.log(Level.INFO, mapper.writeValueAsString(logLine));
+            }
+        }
+    }
+
+    @Override
+    public void destroy() { }
+
+    private boolean isRestRequest(String pathInfo) {
+        return StringUtils.isNotEmpty(pathInfo)
+                && !StringUtils.endsWithIgnoreCase(pathInfo,".ico")
+                && !StringUtils.startsWithIgnoreCase(pathInfo,"/swagger")
+                && !StringUtils.endsWithIgnoreCase(pathInfo,".png")
+                && !StringUtils.equals(pathInfo.trim(), "/");
+    }
+
+}
+
diff --git a/src/main/java/org/openepics/names/util/RequestUtil.java b/src/main/java/org/openepics/names/util/RequestUtil.java
new file mode 100644
index 0000000..eecb757
--- /dev/null
+++ b/src/main/java/org/openepics/names/util/RequestUtil.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2021 European Spallation Source ERIC.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+ */
+
+package org.openepics.names.util;
+
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * Utility class for extracting specific information from a {@link HttpServletRequest}.
+ *
+ * @author Lars Johansson
+ */
+public class RequestUtil {
+
+    /**
+     * The name of header set by proxy server, alternative source of IP address
+     */
+    public static final String HEADER_X_FORWARDED_FOR = "X-Forwarded-For";
+
+    /**
+     * This class is not to be instantiated.
+     */
+    private RequestUtil() {
+        throw new IllegalStateException("Utility class");
+    }
+
+    /**
+     * Retrieve ip address for (origin of) request.
+     *
+     * <p>
+     * In descending order:
+     * <ol>
+     * <li>IP address of request header {@link RequestUtil#HEADER_X_FORWARDED_FOR}
+     * <li>IP address of the client or last proxy that sent the request
+     * </ol>
+     *
+     * @param httpServletRequest request
+     * @return ip address for (origin of) request
+     */
+    public static String getIP(HttpServletRequest httpServletRequest) {
+        if (httpServletRequest != null) {
+            // retrieve ip address from, in descending order
+            //     1. HEADER_X_FORWARDED_FOR
+            //     2. client or last proxy that sent request
+
+            String xForwardedFor = httpServletRequest.getHeader(RequestUtil.HEADER_X_FORWARDED_FOR);
+            String remoteAddr = httpServletRequest.getRemoteAddr();
+
+            final String ip;
+            if (xForwardedFor != null) {
+                ip = xForwardedFor;
+            } else {
+                ip = remoteAddr;
+            }
+
+            return ip;
+        }
+        return null;
+    }
+
+}
-- 
GitLab