|
| 1 | +package io.github.simbo1905.json.schema; |
| 2 | + |
| 3 | +import java.util.Map; |
| 4 | +import java.util.Objects; |
| 5 | +import java.util.concurrent.ConcurrentHashMap; |
| 6 | +import java.util.concurrent.atomic.AtomicLong; |
| 7 | +import java.util.logging.Level; |
| 8 | +import java.util.logging.Logger; |
| 9 | + |
| 10 | +/// Package-private helper for structured JUL logging with simple sampling. |
| 11 | +/// Produces concise key=value pairs prefixed by event=NAME. |
| 12 | +final class StructuredLog { |
| 13 | + private static final Map<String, AtomicLong> COUNTERS = new ConcurrentHashMap<>(); |
| 14 | + |
| 15 | + static void fine(Logger log, String event, Object... kv) { |
| 16 | + if (log.isLoggable(Level.FINE)) log.fine(() -> ev(event, kv)); |
| 17 | + } |
| 18 | + |
| 19 | + static void finer(Logger log, String event, Object... kv) { |
| 20 | + if (log.isLoggable(Level.FINER)) log.finer(() -> ev(event, kv)); |
| 21 | + } |
| 22 | + |
| 23 | + static void finest(Logger log, String event, Object... kv) { |
| 24 | + if (log.isLoggable(Level.FINEST)) log.finest(() -> ev(event, kv)); |
| 25 | + } |
| 26 | + |
| 27 | + /// Log at FINEST but only every Nth occurrence per event key. |
| 28 | + static void finestSampled(Logger log, String event, int everyN, Object... kv) { |
| 29 | + if (!log.isLoggable(Level.FINEST)) return; |
| 30 | + if (everyN <= 1) { |
| 31 | + log.finest(() -> ev(event, kv)); |
| 32 | + return; |
| 33 | + } |
| 34 | + long n = COUNTERS.computeIfAbsent(event, k -> new AtomicLong()).incrementAndGet(); |
| 35 | + if (n % everyN == 0L) { |
| 36 | + log.finest(() -> ev(event, kv("sample", n, kv))); |
| 37 | + } |
| 38 | + } |
| 39 | + |
| 40 | + private static Object[] kv(String k, Object v, Object... rest) { |
| 41 | + Object[] out = new Object[2 + rest.length]; |
| 42 | + out[0] = k; out[1] = v; |
| 43 | + System.arraycopy(rest, 0, out, 2, rest.length); |
| 44 | + return out; |
| 45 | + } |
| 46 | + |
| 47 | + static String ev(String event, Object... kv) { |
| 48 | + StringBuilder sb = new StringBuilder(64); |
| 49 | + sb.append("event=").append(sanitize(event)); |
| 50 | + for (int i = 0; i + 1 < kv.length; i += 2) { |
| 51 | + Object key = kv[i]; |
| 52 | + Object val = kv[i + 1]; |
| 53 | + if (key == null) continue; |
| 54 | + String k = key.toString(); |
| 55 | + String v = val == null ? "null" : sanitize(val.toString()); |
| 56 | + sb.append(' ').append(k).append('='); |
| 57 | + // quote if contains whitespace |
| 58 | + if (needsQuotes(v)) sb.append('"').append(v).append('"'); else sb.append(v); |
| 59 | + } |
| 60 | + return sb.toString(); |
| 61 | + } |
| 62 | + |
| 63 | + private static boolean needsQuotes(String s) { |
| 64 | + for (int i = 0; i < s.length(); i++) { |
| 65 | + char c = s.charAt(i); |
| 66 | + if (Character.isWhitespace(c)) return true; |
| 67 | + if (c == '"') return true; |
| 68 | + } |
| 69 | + return false; |
| 70 | + } |
| 71 | + |
| 72 | + private static String sanitize(String s) { |
| 73 | + if (s == null) return "null"; |
| 74 | + // Trim overly long payloads to keep logs readable |
| 75 | + final int MAX = 256; |
| 76 | + String trimmed = s.length() > MAX ? s.substring(0, MAX) + "…" : s; |
| 77 | + // Collapse newlines and tabs |
| 78 | + return trimmed.replace('\n', ' ').replace('\r', ' ').replace('\t', ' '); |
| 79 | + } |
| 80 | +} |
| 81 | + |
0 commit comments