springtomcatspring-bootgelf

How to send spring boot access logs to a remote server via GELF?


For our spring boot applications we use logback + GELF to send the application logs to our central log server where we can analyze them. Is it possible to do the same with spring boot's access log?

And if not, are there any other suggestions or best practices for collecting the access logs from multiple spring boot applications on a central server?


Solution

  • OK I found out after some research. You need to add a custom valve to tomcat:

    @Bean
    public TomcatEmbeddedServletContainerFactory tomcatEmbeddedServletContainerFactory() {
        TomcatEmbeddedServletContainerFactory factory 
            = new TomcatEmbeddedServletContainerFactory();
        GelfAccessLogValve gelfAccessLogValve = new GelfAccessLogValve();
        gelfAccessLogValve.setPattern("%h %m %U %I %l %u %t "%r" %s %b");
        factory.addContextValves(gelfAccessLogValve);
        return factory;
    }
    

    The valve I wrote is based on paluch.biz's valve but is different in that it depends on graylog's gelf client only. Therfore you need to add this dependency to your pom:

    <dependency>
        <groupId>org.graylog2</groupId>
        <artifactId>gelfclient</artifactId>
        <version>1.4.0</version>
    </dependency>
    

    This is the valve code:

    public class GelfAccessLogValve extends AccessLogValve {
    
        private final static Map<Class, String> names = Collections.unmodifiableMap(new HashMap<Class, String>() {
            {
                put(HeaderElement.class, "Header");
                put(CookieElement.class, "Cookie");
                put(ResponseHeaderElement.class, "ResponseHeader");
                put(SessionAttributeElement.class, "SessionAttribute");
                put(RemoteAddrElement.class, "RemoteAddr");
                put(LocalAddrElement.class, "LocalAddr");
                put(ByteSentElement.class, "ByteSent");
                put(ElapsedTimeElement.class, "ElapsedTime");
                put(HostElement.class, "Host");
                put(ProtocolElement.class, "Protocol");
                put(MethodElement.class, "Method");
                put(PortElement.class, "LocalPort");
                put(QueryElement.class, "Query");
                put(RequestElement.class, "Request");
                put(FirstByteTimeElement.class, "FirstByteTime");
                put(HttpStatusCodeElement.class, "HttpStatusCode");
                put(SessionIdElement.class, "SessionId");
                put(DateAndTimeElement.class, "DateAndTime");
                put(UserElement.class, "User");
                put(RequestURIElement.class, "RequestURI");
                put(LocalServerNameElement.class, "LocalServerName");
                put(ThreadNameElement.class, "ThreadName");
            }
        });
    
        private String host = "localhost";
        private int port = 1234;
        private GelfTransport gelfSender;
    
        @Override
        public void log(Request request, Response response, long time) {
    
            if (gelfSender == null || !getState().isAvailable() || !getEnabled() || logElements == null || condition != null
                    && null != request.getRequest().getAttribute(condition) || conditionIf != null
                    && null == request.getRequest().getAttribute(conditionIf)) {
                return;
            }
    
            /**
             * XXX This is a bit silly, but we want to have start and stop time and duration consistent. It would be better to keep
             * start and stop simply in the request and/or response object and remove time (duration) from the interface.
             */
            long start = request.getCoyoteRequest().getStartTime();
            Date date = new Date(start + time);
    
            GelfMessage message = new GelfMessage(request.getMethod() + " " + request.getRequestURI());
            message.addAdditionalField("facility", getClass().getSimpleName());
            message.setFullMessage(request.getMethod() + " " + request.getRequestURI());
            message.setTimestamp(start + time);
            message.setLevel(GelfMessageLevel.INFO);
    
            for (int i = 0; i < logElements.length; i++) {
    
                String name = names.get(logElements[i].getClass());
                if (name == null) {
                    continue;
                }
    
                CharArrayWriter result = new CharArrayWriter(128);
                logElements[i].addElement(result, date, request, response, time);
                message.addAdditionalField(name, result.toString());
            }
    
            try {
                gelfSender.send(message);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    
        private void createSender() {
            GelfConfiguration configuration = new GelfConfiguration(host, port);
            gelfSender = GelfTransports.create(configuration);
        }
    
        @Override
        protected synchronized void startInternal() throws LifecycleException {
            createSender();
    
            super.startInternal();
        }
    
        @Override
        protected synchronized void stopInternal() throws LifecycleException {
            if (gelfSender != null) {
                gelfSender.stop();
                gelfSender = null;
            }
            super.stopInternal();
        }
    
        public String getHost() {
            return host;
        }
    
        public void setHost(String host) {
            this.host = host;
        }
    
        public int getPort() {
            return port;
        }
    
        public void setPort(int port) {
            this.port = port;
        }
    }