/* * Copyright (c) 2016 Network New Technologies Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * You may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.networknt.audit; import com.fasterxml.jackson.core.JsonProcessingException; import com.networknt.config.Config; import com.networknt.handler.MiddlewareHandler; import com.networknt.utility.Constants; import com.networknt.utility.ModuleRegistry; import io.undertow.Handlers; import io.undertow.server.HttpHandler; import io.undertow.server.HttpServerExchange; import io.undertow.util.AttachmentKey; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; /** * This is a simple audit handler that dump most important info per request basis. The following * elements will be logged if it's available in auditInfo object attached to exchange. This object * wil be populated by other upstream handlers like swagger-meta and swagger-security for * light-rest-4j framework. * * This handler can be used on production but be aware that it will impact the overall performance. * Turning off statusCode and responseTime can make it faster as these have to be captured on the * response chain instead of request chain. * * For most business and majority of microservices, you don't need to enable this handler due to * performance reason. The default audit log will be audit.log config in the default logback.xml; * however, it can be changed to syslog or Kafka with customized appender. * * Majority of the fields in audit log are collected in request and response; however, to allow * user to customize it, we have put an attachment into the exchange to allow other handlers to * write important info into it. The audit.yml can control which fields should be included in the * final log. * * By default, the following fields are included: * * timestamp * serviceId (from server.yml) * correlationId * traceabilityId (if available) * clientId * userId (if available) * scopeClientId (available if called by another API) * endpoint (uriPattern@method) * statusCode * responseTime * * Created by steve on 17/09/16. */ public class AuditHandler implements MiddlewareHandler { public static final String CONFIG_NAME = "audit"; public static final String ENABLED = "enabled"; static final String HEADERS = "headers"; static final String AUDIT = "audit"; static final String STATUS_CODE = "statusCode"; static final String RESPONSE_TIME = "responseTime"; static final String TIMESTAMP = "timestamp"; public static final Map<String, Object> config; private static final List<String> headerList; private static final List<String> auditList; private static boolean statusCode = false; private static boolean responseTime = false; // A customized logger appender defined in default logback.xml static final Logger audit = LoggerFactory.getLogger(Constants.AUDIT_LOGGER); // The key to the audit info attachment in exchange. Allow other handlers to set values. public static final AttachmentKey<Map> AUDIT_INFO = AttachmentKey.create(Map.class); private volatile HttpHandler next; static { config = Config.getInstance().getJsonMapConfigNoCache(CONFIG_NAME); headerList = (List<String>)config.get(HEADERS); auditList = (List<String>)config.get(AUDIT); Object object = config.get(STATUS_CODE); if(object != null && (Boolean) object) { statusCode = true; } object = config.get(RESPONSE_TIME); if(object != null && (Boolean) object) { responseTime = true; } } public AuditHandler() { } @Override public void handleRequest(final HttpServerExchange exchange) throws Exception { Map<String, Object> auditInfo = exchange.getAttachment(AuditHandler.AUDIT_INFO); Map<String, Object> auditMap = new LinkedHashMap<>(); final long start = System.currentTimeMillis(); auditMap.put(TIMESTAMP, System.currentTimeMillis()); // dump audit info fields according to config if(auditInfo != null) { if(auditList != null && auditList.size() > 0) { for(String name: auditList) { auditMap.put(name, auditInfo.get(name)); } } } // dump headers field according to config if(headerList != null && headerList.size() > 0) { for(String name: headerList) { auditMap.put(name, exchange.getRequestHeaders().getFirst(name)); } } if(statusCode || responseTime) { exchange.addExchangeCompleteListener((exchange1, nextListener) -> { if(statusCode) { auditMap.put(STATUS_CODE, exchange1.getStatusCode()); } if(responseTime) { auditMap.put(RESPONSE_TIME, System.currentTimeMillis() - start); } try { audit.info(Config.getInstance().getMapper().writeValueAsString(auditMap)); } catch (JsonProcessingException e) { throw new RuntimeException(e); } nextListener.proceed(); }); } else { audit.info(Config.getInstance().getMapper().writeValueAsString(auditMap)); } next.handleRequest(exchange); } @Override public HttpHandler getNext() { return next; } @Override public MiddlewareHandler setNext(final HttpHandler next) { Handlers.handlerNotNull(next); this.next = next; return this; } @Override public boolean isEnabled() { Object object = config.get(ENABLED); return object != null && (Boolean) object; } @Override public void register() { ModuleRegistry.registerModule(AuditHandler.class.getName(), config, null); } }