/* * Copyright (c) 2016 Savoir Technologies * * 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.savoirtech.logging.slf4j.json.logger; import com.google.gson.Gson; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import org.apache.commons.lang3.exception.ExceptionUtils; import org.apache.commons.lang3.time.FastDateFormat; import org.slf4j.MDC; import org.slf4j.Marker; import java.text.Format; import java.util.List; import java.util.Map; import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.function.Supplier; public class StandardJsonLogger implements JsonLogger { private final org.slf4j.Logger slf4jLogger; private final FastDateFormat formatter; private final Gson gson; private final JsonObject jsonObject; // LEVEL-Specific Settings private final Consumer<String> logOperation; private final BiConsumer<Marker, String> logWithMarkerOperation; private final String levelName; private Marker marker; private boolean includeLoggerName = true; private boolean includeThreadName = true ; private boolean includeClassName = true ; public StandardJsonLogger(org.slf4j.Logger slf4jLogger, FastDateFormat formatter, Gson gson, String levelName, Consumer<String> logOperation, BiConsumer<Marker, String> logWithMarkerOperation) { this.slf4jLogger = slf4jLogger; this.formatter = formatter; this.gson = gson; this.levelName = levelName; this.logOperation = logOperation; this.logWithMarkerOperation = logWithMarkerOperation; this.jsonObject = new JsonObject(); } //======================================== // Getters and Setters //---------------------------------------- public boolean isIncludeLoggerName() { return includeLoggerName; } public void setIncludeLoggerName(boolean includeLoggerName) { this.includeLoggerName = includeLoggerName; } public boolean isIncludeThreadName() { return includeThreadName; } public void setIncludeThreadName(boolean includeThreadName) { this.includeThreadName = includeThreadName; } public boolean isIncludeClassName() { return includeClassName; } public void setIncludeClassName(boolean includeClassName) { this.includeClassName = includeClassName; } //======================================== // Public API //---------------------------------------- @Override public JsonLogger message(String message) { try { jsonObject.add("message", gson.toJsonTree(message)); } catch (Exception e) { jsonObject.add("message", gson.toJsonTree(formatException(e))); } return this; } @Override public JsonLogger message(Supplier<String> message) { try { jsonObject.add("message", gson.toJsonTree(message.get())); } catch (Exception e) { jsonObject.add("message", gson.toJsonTree(formatException(e))); } return this; } @Override public JsonLogger map(String key, Map map) { try { jsonObject.add(key, gson.toJsonTree(map)); } catch (Exception e) { jsonObject.add(key, gson.toJsonTree(formatException(e))); } return this; } @Override public JsonLogger map(String key, Supplier<Map> map) { try { jsonObject.add(key, gson.toJsonTree(map.get())); } catch (Exception e) { jsonObject.add(key, gson.toJsonTree(formatException(e))); } return this; } @Override public JsonLogger list(String key, List list) { try { jsonObject.add(key, gson.toJsonTree(list)); } catch (Exception e) { jsonObject.add(key, gson.toJsonTree(formatException(e))); } return this; } @Override public JsonLogger list(String key, Supplier<List> list) { try { jsonObject.add(key, gson.toJsonTree(list.get())); } catch (Exception e) { jsonObject.add(key, gson.toJsonTree(formatException(e))); } return this; } @Override public JsonLogger field(String key, Object value) { try { jsonObject.add(key, gson.toJsonTree(value)); } catch (Exception e) { jsonObject.add(key, gson.toJsonTree(formatException(e))); } return this; } @Override public JsonLogger field(String key, Supplier value) { try { // in the rare case that the value passed is null, this method will be selected as more specific than the Object // method. Have to handle it here or the value.get() will NullPointer if (value == null) { jsonObject.add(key, null); } else { jsonObject.add(key, gson.toJsonTree(value.get())); } } catch (Exception e) { jsonObject.add(key, gson.toJsonTree(formatException(e))); } return this; } @Override public JsonLogger json(String key, JsonElement jsonElement) { try { jsonObject.add(key, jsonElement); } catch (Exception e) { jsonObject.add(key, gson.toJsonTree(formatException(e))); } return this; } @Override public JsonLogger json(String key, Supplier<JsonElement> jsonElement) { try { jsonObject.add(key, jsonElement.get()); } catch (Exception e) { jsonObject.add(key, gson.toJsonTree(formatException(e))); } return this; } @Override public JsonLogger exception(String key, Exception exception) { try { jsonObject.add(key, gson.toJsonTree(formatException(exception))); } catch (Exception e) { jsonObject.add(key, gson.toJsonTree(formatException(e))); } return this; } @Override public JsonLogger stack() { try { jsonObject.add("stacktrace", gson.toJsonTree(formatStack())); } catch (Exception e) { jsonObject.add("stacktrace", gson.toJsonTree(formatException(e))); } return this; } @Override public JsonLogger marker(Marker marker) { this.marker = marker; jsonObject.add("marker", gson.toJsonTree(marker.getName())); return this; } @Override public void log() { String message = this.formatMessage(levelName); if (this.marker == null) { this.logOperation.accept(message); } else { this.logWithMarkerOperation.accept(marker, message); } } //======================================== // Internals //---------------------------------------- protected String formatMessage(String level) { jsonObject.add("level", gson.toJsonTree(level)); if (includeThreadName) { jsonObject.add("thread_name", gson.toJsonTree(Thread.currentThread().getName())); } if (includeClassName) { try { jsonObject.add("class", gson.toJsonTree(getCallingClass())); } catch (Exception e) { jsonObject.add("class", gson.toJsonTree(formatException(e))); } } if (includeLoggerName) { jsonObject.add("logger_name", gson.toJsonTree(slf4jLogger.getName())); } try { jsonObject.add("@timestamp", gson.toJsonTree(getCurrentTimestamp(formatter))); } catch (Exception e) { jsonObject.add("@timestamp", gson.toJsonTree(formatException(e))); } Map mdc = MDC.getCopyOfContextMap(); if (mdc != null && !mdc.isEmpty()) { try { jsonObject.add("mdc", gson.toJsonTree(mdc)); } catch (Exception e) { jsonObject.add("mdc", gson.toJsonTree(formatException(e))); } } return gson.toJson(jsonObject); } private String getCallingClass() { StackTraceElement[] stackTraceElements = (new Exception()).getStackTrace(); return stackTraceElements[3].getClassName(); } private String getCurrentTimestamp(Format formatter) { return formatter.format(System.currentTimeMillis()); } private String formatException(Exception e) { return ExceptionUtils.getStackTrace(e); } /** * Some contention over performance of Thread.currentThread.getStackTrace() vs (new Exception()).getStackTrace() * Code in Thread.java actually uses the latter if 'this' is the current thread so we do the same * * Remove the top two elements as those are the elements from this logging class */ private String formatStack() { StringBuilder output = new StringBuilder(); StackTraceElement[] stackTraceElements = (new Exception()).getStackTrace(); output.append(stackTraceElements[2]); for (int index = 3; index < stackTraceElements.length; index++) { output.append("\n\tat ") .append(stackTraceElements[index]); } return output.toString(); } }