package core.framework.impl.log; import core.framework.api.util.Exceptions; import core.framework.api.util.Maps; import java.lang.management.ManagementFactory; import java.lang.management.ThreadMXBean; import java.time.Instant; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.UUID; /** * @author neo */ public final class ActionLog { private static final ThreadMXBean THREAD = ManagementFactory.getThreadMXBean(); private static final int MAX_TRACE_HOLD_SIZE = 3000; // normal trace 3000 lines is about 350k private static final String LOGGER = LoggerImpl.abbreviateLoggerName(ActionLog.class.getCanonicalName()); public final String id; final Instant date; final Map<String, String> context; final Map<String, Double> stats; final Map<String, PerformanceStat> performanceStats; final List<LogEvent> events; private final long startCPUTime; private final long startElapsed; public boolean trace; // whether flush trace log for all subsequent actions public String action = "unassigned"; String refId; String errorMessage; long elapsed; long cpuTime; private LogLevel result = LogLevel.INFO; private String errorCode; ActionLog(String message) { startElapsed = System.nanoTime(); startCPUTime = THREAD.getCurrentThreadCpuTime(); date = Instant.now(); events = new LinkedList<>(); context = Maps.newLinkedHashMap(); stats = Maps.newLinkedHashMap(); performanceStats = Maps.newHashMap(); id = UUID.randomUUID().toString(); log(message); log("[context] id={}", id); } void end(String message) { cpuTime = THREAD.getCurrentThreadCpuTime() - startCPUTime; elapsed = elapsedTime(); log("[context] elapsed={}", elapsed); log(message); } public long elapsedTime() { return System.nanoTime() - startElapsed; } void process(LogEvent event) { if (event.level.value > result.value) { result = event.level; errorCode = event.errorCode(); // only update error type/message if level raised, so error type will be first WARN or first ERROR errorMessage = errorMessage(event); } if (events.size() < MAX_TRACE_HOLD_SIZE || event.level.value >= LogLevel.WARN.value) { // after reach max holding lines, only add warning/error events add(event); } } private void log(String message, Object... argument) { // add log event directly, so internal message and won't be suspended add(new LogEvent(LOGGER, null, LogLevel.DEBUG, message, argument, null)); } private void add(LogEvent event) { events.add(event); if (events.size() == MAX_TRACE_HOLD_SIZE) { events.add(new LogEvent(LOGGER, null, LogLevel.DEBUG, "reached max trace log holding size, only collect critical log event from now on", null, null)); } } private String errorMessage(LogEvent event) { String message = event.message(); if (message != null && message.length() > 200) return message.substring(0, 200); // limit 200 chars in action log return message; } String result() { if (result == LogLevel.INFO) return trace ? "TRACE" : "OK"; return result.name(); } boolean flushTraceLog() { return trace || result.value >= LogLevel.WARN.value; } String errorCode() { if (errorCode != null) return errorCode; if (result.value >= LogLevel.WARN.value) return "UNASSIGNED"; return null; } public Optional<String> context(String key) { return Optional.ofNullable(context.get(key)); } public void context(String key, Object value) { String previous = context.put(key, String.valueOf(value)); // put context can be called by application code, check duplication to avoid producing huge trace log by accident if (previous != null) throw Exceptions.error("context key must only be set once, key={}, value={}, previous={}", key, value, previous); log("[context] {}={}", key, value); } public void stats(String key, Number value) { Double previous = stats.put(key, value.doubleValue()); if (previous != null) throw Exceptions.error("stats key must only be set once, key={}, value={}, previous={}", key, value, previous); log("[stats] {}={}", key, value); } public void track(String action, long elapsedTime) { PerformanceStat tracking = performanceStats.computeIfAbsent(action, key -> new PerformanceStat()); tracking.count++; tracking.totalElapsed += elapsedTime; log("[perf_stats] {}={}", action, elapsedTime); } public String refId() { if (refId == null) return id; return refId; } public void refId(String refId) { if (refId != null) { log("[context] refId={}", refId); this.refId = refId; } } public void action(String action) { log("[context] action={}", action); this.action = action; } }