LogMetrics.java
package io.jstach.rainbowgum;
import java.lang.System.Logger.Level;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.LongAdder;
import io.jstach.rainbowgum.annotation.CaseChanging;
/**
* Numeric metrics about the logging system itself. Counters track running totals such as
* dropped events; gauges track values that can rise and fall, such as queued events.
* Unlike {@link LogAlerts}, updating a metric does not create an alert or notify
* listeners. Retain a {@link Gauge} for frequent updates without a name lookup.
* <p>
* An instance is available from every {@link LogConfig#metrics()}. Components that are
* {@linkplain LogProvider provided} config, or that are
* {@linkplain LogLifecycle#start( LogConfig) started} with config, should prefer
* capturing {@code config.metrics()} over reaching for global state.
* <p>
* Deliberately a separate type from {@link LogAlerts} rather than more methods on it -
* the two are meant to be independently replaceable (e.g. a future Micrometer-backed
* {@code LogMetrics} without needing to also replace how alerts are recorded).
*
* @see LogConfig#metrics()
*/
public sealed interface LogMetrics permits DefaultLogMetrics {
/**
* Counter name for the global count of log events dropped without ever being written
* anywhere - for example an appender dropping events on reentry. Incremented whenever
* a drop happens regardless of whether that particular drop is also logged/alerted,
* since counting and alerting/logging are separate concerns.
*/
static final String EVENTS_DROPPED_METRIC = "events.dropped";
/**
* Gauge name for the aggregate number of events waiting in async publisher buffers.
* Events removed for processing are no longer counted, even while their output is
* being written. Concurrent snapshots can briefly lead or lag queue activity.
*/
static final String EVENTS_QUEUED_METRIC = "events.queued";
/**
* Counter name for the global count of times a reused encoder buffer had its backing
* storage shrunk back down after growing past its configured max size (see
* {@link LogEncoder.Buffer#isOversized()}). An occasional trim is normal and expected
* once in a while, but resizing <em>often</em> is a sign the configured max size (or
* the initial size) doesn't match the actual event sizes being logged - worth
* watching, not erroring on, hence {@link #warnCounter(String, long)} rather than
* {@link #errorCounter(String, long)}.
*/
static final String BUFFER_TRIMMED_METRIC = "buffer.trimmed";
/**
* Counter name for the global count of log events an appender failed to write - the
* encoder or output threw while appending, so the event was caught, alerted (see
* {@link LogAlerts#error(Class, String, Throwable)}), and lost rather than retried.
* Kept separate from {@link #EVENTS_DROPPED_METRIC}: a drop is a deliberate skip (the
* appender chose not to write), while this is an unexpected failure partway through
* actually trying to - different root causes worth distinguishing when triaging.
*/
static final String EVENTS_FAILED_METRIC = "events.failed";
/**
* Counter name for the global count of failed appender output reopen attempts. Each
* failed attempt increments this counter in addition to recording an alert,
* regardless of the appender or output type.
*/
static final String REOPEN_FAIL_METRIC = "reopen.fail";
/**
* Counter name for the global count of appender output reopen attempts, whether
* successful or failed. Subtract {@link #REOPEN_FAIL_METRIC} to obtain the number of
* successful attempts.
*/
static final String REOPEN_METRIC = "reopen";
/**
* Counter name for failed automatic file rotation attempts, including closing the
* active output, rotating archives, and opening the replacement output. Recovery
* attempts that fail also increment this counter. External rotation followed by an
* explicit reopen is counted by {@link #REOPEN_FAIL_METRIC} instead. Events lost
* because rotation failed are also counted by {@link #EVENTS_FAILED_METRIC}.
*/
static final String ROLL_FAIL_METRIC = "roll.fail";
/**
* Counter name for the running count of distinct logger names registered via
* {@link LogConfig.LoggerRegistry#registerLoggerName(LoggerAPI, String)}. Not itself
* a problem, hence {@link #infoCounter(String, long)} rather than
* {@link #warnCounter(String, long)}, but worth watching: a count that keeps climbing
* without bound is a sign something is using per-request/per-entity data (e.g. a
* request id) as a logger name instead of a fixed, bounded set of names.
*/
static final String LOGGER_NAMES_METRIC = "logger.names";
/**
* Increments a counter for something worth tracking as "this happens and it matters".
* @param name counter name, e.g. {@link #EVENTS_DROPPED_METRIC} or a logger name.
* @param increment amount to add, usually {@code 1}.
*/
public void errorCounter(String name, long increment);
/**
* Like {@link #errorCounter(String, long)} but for something worth tracking yet less
* significant than an error - a trend worth watching rather than something that, by
* itself, indicates a problem. Kept as a separate counter namespace from
* {@link #errorCounter(String, long)}: the same {@code name} passed to both is two
* distinct counters, not one shared one.
* @param name counter name, e.g. {@link #BUFFER_TRIMMED_METRIC}.
* @param increment amount to add, usually {@code 1}.
*/
public void warnCounter(String name, long increment);
/**
* Like {@link #errorCounter(String, long)} but for a plain running total, not
* inherently a problem or even a trend worth watching. Kept as a separate counter
* namespace from
* {@link #errorCounter(String, long)}/{@link #warnCounter(String, long)}: the same
* {@code name} passed to more than one of these is a distinct counter per method, not
* one shared one.
* @param name counter name, e.g. {@link #LOGGER_NAMES_METRIC}.
* @param increment amount to add, usually {@code 1}.
*/
public void infoCounter(String name, long increment);
/**
* Creates a handle for updating a named value directly. For INFO, WARNING, and ERROR,
* each call returns a new handle sharing the value for the same name and level;
* retain the handle for frequent updates without repeated name lookups. The value is
* initially zero and appears in {@link #snapshot()} as soon as the handle is created.
* Counter methods for the same name and level share this value too. Levels below INFO
* and OFF return a shared no-op handle and do not register a metric.
* @param name metric name.
* @param level significance of the metric.
* @return a handle to the shared value, or a no-op handle.
*/
public Gauge gauge(String name, Level level);
/**
* A retained handle for incrementing or decrementing a metric. Updates are thread
* safe and do not perform a name lookup. Read the value through {@link #snapshot()}.
*/
sealed interface Gauge permits DefaultGauge, NoopGauge {
/**
* Returns a shared gauge that ignores all updates. Use it until a metric is
* available, or when metric collection is disabled.
* @return no-op gauge.
*/
public static Gauge noop() {
return NoopGauge.NOOP;
}
/**
* Increments the value by one.
*/
public void increment();
/**
* Adds the supplied amount.
* @param increment amount to add.
*/
public void increment(long increment);
/**
* Decrements the value by one.
*/
public void decrement();
/**
* Subtracts the supplied amount.
* @param decrement amount to subtract.
*/
public void decrement(long decrement);
}
/**
* A snapshot of every metric recorded via {@link #errorCounter(String, long)},
* {@link #warnCounter(String, long)}, {@link #infoCounter(String, long)}, and
* {@link #gauge(String, Level)}. Counter values increase over time; gauge values can
* also decrease. Snapshots are approximate while updates are concurrent.
* @return immutable snapshot.
*/
public List<Metric> snapshot();
/**
* A single named metric's current value, as returned by {@link #snapshot()}.
*
* @param name metric name, as passed to a counter method or
* {@link #gauge(String, Level)}.
* @param level significance of the metric. Counter methods use {@link Level#ERROR},
* {@link Level#WARNING}, or {@link Level#INFO}; gauges use the level supplied to
* {@link #gauge(String, Level)}.
* @param value current value, which can decrease for a gauge.
*/
record Metric(String name, Level level, long value) {
}
/**
* The fixed, well known set of metrics RainbowGum itself records, as opposed to the
* open ended, per logger name counters {@link LogAlerts#alert(LogEvent)} also drives
* into the counter matching the alert's level. Enumerable on purpose: consumers that
* want to bind every well known counter to something else (a Micrometer counter or
* gauge per constant, for example) can loop over {@link #values()} instead of hand
* listing each {@code String}/{@link Level} pair themselves.
*/
@CaseChanging
enum StandardMetric {
/**
* See {@link #EVENTS_DROPPED_METRIC}.
*/
EVENTS_DROPPED(EVENTS_DROPPED_METRIC, Level.ERROR),
/**
* See {@link #EVENTS_QUEUED_METRIC}.
*/
EVENTS_QUEUED(EVENTS_QUEUED_METRIC, Level.INFO, true),
/**
* See {@link #BUFFER_TRIMMED_METRIC}.
*/
BUFFER_TRIMMED(BUFFER_TRIMMED_METRIC, Level.WARNING),
/**
* See {@link #EVENTS_FAILED_METRIC}.
*/
EVENTS_FAILED(EVENTS_FAILED_METRIC, Level.ERROR),
/**
* See {@link #LOGGER_NAMES_METRIC}.
*/
LOGGER_NAMES(LOGGER_NAMES_METRIC, Level.INFO),
/**
* See {@link #REOPEN_FAIL_METRIC}.
*/
REOPEN_FAIL(REOPEN_FAIL_METRIC, Level.ERROR),
/**
* See {@link #REOPEN_METRIC}.
*/
REOPEN(REOPEN_METRIC, Level.INFO),
/**
* See {@link #ROLL_FAIL_METRIC}.
*/
ROLL_FAIL(ROLL_FAIL_METRIC, Level.ERROR);
private final String metricName;
private final Level level;
private final boolean gauge;
StandardMetric(String metricName, Level level) {
this(metricName, level, false);
}
StandardMetric(String metricName, Level level, boolean gauge) {
this.metricName = metricName;
this.level = level;
this.gauge = gauge;
}
/**
* The metric name, as passed to a counter method or {@link #gauge(String, Level)}
* and matched against {@link Metric#name()}.
* @return metric name.
*/
public String metricName() {
return metricName;
}
/**
* The significance of this metric, also exposed through {@link Metric#level()} in
* {@link #snapshot()}.
* @return level.
*/
public Level level() {
return level;
}
/**
* Whether this metric can decrease and should be bound as a gauge instead of an
* increasing counter.
* @return true for a gauge.
*/
public boolean isGauge() {
return gauge;
}
}
}
final class DefaultLogMetrics implements LogMetrics {
private final ConcurrentHashMap<String, LongAdder> errorCounters = new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, LongAdder> warnCounters = new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, LongAdder> infoCounters = new ConcurrentHashMap<>();
@Override
public Gauge gauge(String name, Level level) {
return switch (level) {
case ERROR -> new DefaultGauge(errorCounters.computeIfAbsent(name, k -> new LongAdder()));
case WARNING -> new DefaultGauge(warnCounters.computeIfAbsent(name, k -> new LongAdder()));
case INFO -> new DefaultGauge(infoCounters.computeIfAbsent(name, k -> new LongAdder()));
case ALL, TRACE, DEBUG, OFF -> NoopGauge.NOOP;
};
}
@Override
public void errorCounter(String name, long increment) {
errorCounters.computeIfAbsent(name, k -> new LongAdder()).add(increment);
}
@Override
public void warnCounter(String name, long increment) {
warnCounters.computeIfAbsent(name, k -> new LongAdder()).add(increment);
}
@Override
public void infoCounter(String name, long increment) {
infoCounters.computeIfAbsent(name, k -> new LongAdder()).add(increment);
}
@Override
public List<Metric> snapshot() {
List<Metric> list = new ArrayList<>(errorCounters.size() + warnCounters.size() + infoCounters.size());
for (var e : errorCounters.entrySet()) {
list.add(new Metric(e.getKey(), Level.ERROR, e.getValue().sum()));
}
for (var e : warnCounters.entrySet()) {
list.add(new Metric(e.getKey(), Level.WARNING, e.getValue().sum()));
}
for (var e : infoCounters.entrySet()) {
list.add(new Metric(e.getKey(), Level.INFO, e.getValue().sum()));
}
return List.copyOf(list);
}
}
final class DefaultGauge implements LogMetrics.Gauge {
private final LongAdder value;
DefaultGauge(LongAdder value) {
this.value = value;
}
@Override
public void increment() {
value.increment();
}
@Override
public void increment(long increment) {
value.add(increment);
}
@Override
public void decrement() {
value.decrement();
}
@Override
public void decrement(long decrement) {
value.add(-decrement);
}
}
enum NoopGauge implements LogMetrics.Gauge {
NOOP;
@Override
public void increment() {
}
@Override
public void increment(long increment) {
}
@Override
public void decrement() {
}
@Override
public void decrement(long decrement) {
}
}