LogConfig.java
package io.jstach.rainbowgum;
import static io.jstach.rainbowgum.spi.RainbowGumServiceProvider.findProviders;
import java.lang.System.Logger.Level;
import java.util.ArrayList;
import java.util.Collection;
import java.util.EnumSet;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.ServiceLoader;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import org.jspecify.annotations.Nullable;
import io.jstach.rainbowgum.LevelResolver.LevelConfig;
import io.jstach.rainbowgum.LogConfig.ChangePublisher;
import io.jstach.rainbowgum.LogEvent.Caller.CallerType;
import io.jstach.rainbowgum.annotation.EnumAlias;
import io.jstach.rainbowgum.spi.RainbowGumServiceProvider;
import io.jstach.rainbowgum.spi.RainbowGumServiceProvider.Configurator;
import io.jstach.rainbowgum.spi.RainbowGumServiceProvider.PropertiesProvider;
/**
* The configuration of a RainbowGum. In some other logging implementations this is called
* "context".
* <p>
* A config can be used by only one {@link RainbowGum}, even after that RainbowGum is
* closed. Using it for another {@link RainbowGum.Builder} throws
* {@link IllegalStateException}; build a new config instead.
*/
public sealed interface LogConfig extends LogProperty.PropertySupport {
/**
* When to dump bootstrap alerts to the fail-safe output.
*/
public enum DebugModeType {
/**
* Do not dump alerts beyond the normal alert startup policy.
*/
@EnumAlias("false")
OFF,
/**
* Dump the failure and collected alerts if building the config fails.
*/
ERROR,
/**
* Dump collected alerts after every successful config build and on build failure.
*/
INFO,
/**
* Dump collected alerts as in {@link #INFO}, and print a {@link LogReporter}
* report after Rainbow Gum starts.
*/
@EnumAlias("true")
ALL,
/**
* Everything {@link #ALL} does, then fails after Rainbow Gum starts if a property
* key set in a source that can list its keys (system properties, property files)
* was never read and is close to a key that was, which is almost always a typo.
* Meant for trying configuration on the command line and in CI.
*/
HELP;
boolean checksUnusedKeys() {
return this == ALL || this == HELP;
}
static DebugModeType parse(String value) {
String v = value.toUpperCase(Locale.ROOT);
return switch (v) {
case "FALSE" -> OFF;
case "TRUE" -> ALL;
default -> LogProperty.enumValue(DebugModeType.class, value, "true", "false");
};
}
}
/**
* String key value properties.
* @return properties.
*/
@Override
public LogProperties properties();
/**
* Effective debug mode collected while this configuration was built. A system
* property value, when present, takes precedence over the builder setting.
* @return debug mode.
*/
public DebugModeType debugMode();
/**
* Level resolver for resolving levels from logger names.
* @return level resolver.
*/
public LevelConfig levelResolver();
/**
* Output provider that uses URI to find output.
* @return output provider.
*/
public LogOutputRegistry outputRegistry();
/**
* Provides encoders by URI scheme.
* @return encoder registry.
*/
public LogEncoderRegistry encoderRegistry();
/**
* Provides publishers by URI scheme.
* @return changePublisher registry.
*/
public LogPublisherRegistry publisherRegistry();
/**
* Creates a builder for making LogConfig.
* @return builder.
*/
public static Builder builder() {
return new Builder();
}
/**
* An event changePublisher to publish configuration changes.
* @return changePublisher.
*/
public ChangePublisher changePublisher();
/**
* Service registry are custom services needed by plugins particularly during the
* initialization process.
* @return registry.
*/
public ServiceRegistry serviceRegistry();
/**
* Alerts about problems with the logging system itself (as opposed to application
* logging routed through the {@link LogRouter}).
* @return alerts.
*/
public LogAlerts alerts();
/**
* Metrics about the logging system itself (as opposed to application logging routed
* through the {@link LogRouter}).
* @return metrics.
*/
public LogMetrics metrics();
/**
* Accounts for every logger name a facade implementation has vended.
* @return logger registry.
*/
public LoggerRegistry loggerRegistry();
/**
* Accounts for logger names, and which {@link LoggerAPI} vended them, registered via
* {@link #registerLoggerName(LoggerAPI, String)}, deliberately without exposing the
* accumulated names/APIs themselves: the one supported way to see them is
* {@link LogReporter.Section#LOGGERS}/{@link LogReporter.Section#FACADES}.
*/
interface LoggerRegistry {
/**
* Registers that a logger with this name exists. Idempotent: registering the same
* name more than once has no additional effect. The first time a given name is
* registered this also increments {@link LogMetrics#LOGGER_NAMES_METRIC} on
* {@link LogConfig#metrics()}, so the running total is cheap to observe (e.g. to
* catch unbounded logger name usage) without ever having to size the accumulated
* names themselves.
* @param api which facade this name came through.
* @param loggerName logger name.
*/
void registerLoggerName(LoggerAPI api, String loggerName);
}
/**
* Mixin for config support.
*/
interface ConfigSupport extends LogProperty.PropertySupport {
/**
* Provides config.
* @return config.
*/
public LogConfig config();
@Override
default LogProperties properties() {
return config().properties();
}
}
/**
* Config Change Publisher. By default this is enabled with
* {@value LogProperties#GLOBAL_CHANGE_PROPERTY} set to <code>true</code>, which then
* gates two independent, per-logger-name properties:
* {@value LogProperties#CHANGE_PREFIX} + {@value LogProperties#SEP} + logger name,
* set to a list of {@link ChangeType} or <code>true</code>/<code>false</code> to
* enable or disable all changes, and {@value LogProperties#CALLER_PREFIX} +
* {@value LogProperties#SEP} + logger name, set to a {@link CallerType} (or
* <code>true</code>/<code>false</code>) to control caller info. The two are resolved
* independently - one does not imply or override the other, even for overlapping
* logger name prefixes.
*/
interface ChangePublisher {
/**
* Subscribe to changes.
* @param consumer consumer.
*/
public void subscribe(Consumer<? super LogConfig> consumer);
/**
* Publish that there has been changes.
*/
public void publish();
/**
* Test to see if <strong>any</strong> changes are enabled for a logger - either
* {@link #allowedChanges(String)} is non-empty or {@link #callerType(String)} is
* not {@link CallerType#NONE}.
* @param loggerName logger name.
* @return true if enabled.
*/
public boolean isEnabled(String loggerName);
/**
* Returns what the logger is allowed to change.
* @param loggerName logger name.
* @return which things are allowed to change in the logger
*/
public Set<ChangeType> allowedChanges(String loggerName);
/**
* The caller info strategy for a logger, resolved from
* {@value LogProperties#CALLER_PREFIX}, independently of
* {@link #allowedChanges(String)} /{@value LogProperties#CHANGE_PREFIX}. Unlike
* {@link ChangeType#LEVEL}, which is genuinely re-evaluated on every
* {@link #publish()}, this is resolved once when a logger is first created and
* never revisited afterward - it reflects a static per-logger capability, not
* something that changes at runtime.
* @param loggerName logger name.
* @return caller type, {@link CallerType#NONE} if not configured.
*/
public CallerType callerType(String loggerName);
/**
* Whether caller info should be computed for a logger at all, regardless of which
* non-{@link CallerType#NONE} strategy.
* @param loggerName logger name.
* @return true if caller info is enabled for this logger.
*/
default boolean callerInfoEnabled(String loggerName) {
return callerType(loggerName) != CallerType.NONE;
}
/**
* Changing type options.
*/
public enum ChangeType {
/**
* No changes are allowed. Synonymous with <code>false</code> - never actually
* present in a {@link #allowedChanges(String)} result (that case is just the
* empty set); this exists so a single malformed-vs-explicitly-off token can
* be told apart while parsing, and so <code>false</code> is a real,
* documented enum member rather than only a magic string.
*/
@EnumAlias("false")
NONE,
/**
* The logger is allowed to change levels.
*/
LEVEL;
static Set<ChangeType> parse(List<String> value) {
if (value.isEmpty()) {
return Set.of();
}
var s = EnumSet.noneOf(ChangeType.class);
for (var v : value) {
if (v.equalsIgnoreCase("true")) {
return EnumSet.complementOf(EnumSet.of(ChangeType.NONE));
}
var ct = ChangeType.parse(v);
if (ct == NONE) {
return EnumSet.noneOf(ChangeType.class);
}
s.add(ct);
}
return s;
}
static ChangeType parse(String value) {
String v = value.toUpperCase(Locale.ROOT);
return switch (v) {
case "FALSE" -> NONE;
default -> LogProperty.enumValue(ChangeType.class, value, "true", "false");
};
}
}
}
/**
* Builder for LogConfig.
* <p>
* <strong>NOTE:</strong> The service loader is not used by default with this builder.
* If the automatic discovery of components is desired call
* {@link #serviceLoader(ServiceLoader)}.
*/
public static final class Builder extends LevelResolver.AbstractBuilder<Builder> {
private @Nullable ServiceRegistry serviceRegistry;
private @Nullable LogProperties logProperties;
private @Nullable ServiceLoader<RainbowGumServiceProvider> serviceLoader;
private final List<RainbowGumServiceProvider.Configurator> configurators = new ArrayList<>();
private final List<RainbowGumServiceProvider.PropertiesProvider> propertiesProviders = new ArrayList<>();
private DebugModeType debugMode = DebugModeType.OFF;
private LogAlerts.FailLevel alertsFail = LogAlerts.FailLevel.OFF;
private LogAlerts.UnusedKeyLevel alertsUnusedKey = LogAlerts.UnusedKeyLevel.INFO;
/**
* Default constructor
*/
private Builder() {
}
/**
* Sets log properties
* @param logProperties log properties.
* @return this.
*/
public Builder properties(LogProperties logProperties) {
this.logProperties = logProperties;
return this;
}
/**
* Sets the debug mode used when the system property
* {@value LogProperties#DEBUG_PROPERTY} is absent.
* @param debugMode debug mode.
* @return this.
*/
public Builder debug(DebugModeType debugMode) {
this.debugMode = Objects.requireNonNull(debugMode);
return this;
}
/**
* Sets the alert level at which starting fails, used when the property
* {@value LogProperties#ALERTS_FAIL_PROPERTY} is absent.
* @param failLevel fail level, default {@link LogAlerts.FailLevel#OFF}.
* @return this.
*/
public Builder alertsFail(LogAlerts.FailLevel failLevel) {
this.alertsFail = Objects.requireNonNull(failLevel);
return this;
}
/**
* Sets the alert level of a property key nothing read, used when the property
* {@value LogProperties#ALERTS_UNUSED_KEY_PROPERTY} is absent.
* @param unusedKeyLevel unused key level, default
* {@link LogAlerts.UnusedKeyLevel#INFO}.
* @return this.
*/
public Builder alertsUnusedKey(LogAlerts.UnusedKeyLevel unusedKeyLevel) {
this.alertsUnusedKey = Objects.requireNonNull(unusedKeyLevel);
return this;
}
/**
* Sets service registry
* @param serviceRegistry service registry.
* @return this.
*/
public Builder serviceRegistry(ServiceRegistry serviceRegistry) {
this.serviceRegistry = serviceRegistry;
return this;
}
/**
* Add to configure LogConfig and ServiceRegistry.
* @param configurator will run on build.
* @return this.
*/
public Builder configurator(RainbowGumServiceProvider.Configurator configurator) {
configurators.add(configurator);
return this;
}
/**
* Add properties.
* @param propertiesProvider will run on build.
* @return this.
*/
public Builder propertiesProvider(RainbowGumServiceProvider.PropertiesProvider propertiesProvider) {
propertiesProviders.add(propertiesProvider);
return this;
}
/**
* Sets the service loader to use for loading components that were not set.
* @param serviceLoader loader to use for missing components.
* @return this.
*/
public Builder serviceLoader(ServiceLoader<RainbowGumServiceProvider> serviceLoader) {
this.serviceLoader = serviceLoader;
return this;
}
/**
* Sets a default service loader to use for loading components that were not set.
* This call will just call the {@link ServiceLoader} without providing a class
* loader.
* @return this.
*/
public Builder serviceLoader() {
return this.serviceLoader(ServiceLoader.load(RainbowGumServiceProvider.class));
}
/**
* Configures the builder with a function for ergonomics.
* @param consumer passed the builder.
* @return this.
*/
public Builder with(Consumer<? super Builder> consumer) {
consumer.accept(this);
return this;
}
/**
* Builds LogConfig which will use the {@link ServiceLoader} if set to load
* missing components and if not set will use static defaults. Registered
* closeables are closed if configuration construction fails.
* @return log config
*/
public LogConfig build() {
var prePropertiesAlerts = new SwappableLogAlerts();
LogAlerts dumpAlerts = prePropertiesAlerts;
@Nullable ServiceRegistry startupRegistry = null;
boolean startingAlerts = false;
DebugModeType debug = LogProperties.StandardProperties.SYSTEM_PROPERTIES
.forKey(LogProperties.DEBUG_PROPERTY)
.ofString()
.map(DebugModeType::parse)
.or(this.debugMode)
.validateNow(Builder.class);
try {
ServiceRegistry serviceRegistry = this.serviceRegistry;
LogProperties logProperties = this.logProperties;
var serviceLoader = this.serviceLoader;
var configurators = this.configurators;
if (serviceRegistry == null) {
serviceRegistry = ServiceRegistry.of();
}
startupRegistry = serviceRegistry;
if (logProperties == null) {
List<LogProperties> props = new ArrayList<>();
for (var pp : propertiesProviders) {
prePropertiesAlerts.info(LogConfig.class, "Loading properties from "
+ LogReporter.Reportable.toString(pp, "unknown PropertiesProvider"));
props.addAll(pp.provideProperties(serviceRegistry, prePropertiesAlerts));
}
if (props.isEmpty() && serviceLoader != null) {
props.addAll(provideProperties(serviceRegistry, serviceLoader, prePropertiesAlerts));
}
/*
* LogProperties.of(List, LogProperties)'s fallback argument is only
* used when the list is empty, so SYSTEM_PROPERTIES has to be a real
* member of the list itself to always be included, not passed as that
* fallback: otherwise it silently disappears the moment any
* PropertiesProvider contributes anything at all (e.g.
* rainbowgum-micronaut5's own GLOBAL_CHANGE_PROPERTY layer), breaking
* every -D system property override for such an application with no
* warning.
*/
props.add(LogProperties.StandardProperties.SYSTEM_PROPERTIES);
logProperties = LogProperties.of(props);
}
@Nullable UnusedKeyCheck unusedKeyCheck = null;
/*
* Reads are recorded for the debug modes that report unused keys, and
* when logging.alerts.fail fails on a misspelled key's alert: a warning,
* or an error with logging.alerts.unusedKey=error.
*/
var suggestedKeyLevel = DefaultLogAlerts.unusedKeyLevelOr(logProperties, alertsUnusedKey).level(true);
if (debug.checksUnusedKeys()
|| DefaultLogAlerts.failLevelOr(logProperties, alertsFail).fails(suggestedKeyLevel)) {
unusedKeyCheck = unusedKeyCheck(logProperties);
logProperties = recording(logProperties, unusedKeyCheck);
}
/*
* Built before DefaultLogConfig itself (rather than left for
* DefaultLogConfig's constructor to create, as before) specifically so
* buildGlobalResolver below can hand LogAlerts to the global level
* resolver's alerting wrapper - the global resolver is built before a
* full LogConfig exists to pull config.alerts() from, so alerts (and
* metrics, via alerts' own listener wiring) are constructed directly here
* instead. Capacity and logging.alerts.unobservedErrorsAction both come
* from logProperties as already resolved above - before any configurator
* runs, the same as everything else built directly in this method - see
* DefaultLogAlerts.of(...) for how the two are validated together.
*/
var alerts = DefaultLogAlerts.of(logProperties, alertsFail, alertsUnusedKey,
debug == DebugModeType.HELP);
LogMetrics metrics = new DefaultLogMetrics();
var levelResolver = this.buildGlobalResolver(logProperties, alerts);
var config = new DefaultLogConfig(serviceRegistry, logProperties, levelResolver, alerts, metrics, debug,
unusedKeyCheck);
// The config constructor installs the metrics listener before replay.
prePropertiesAlerts.drainTo(alerts);
dumpAlerts = alerts;
if (serviceLoader != null) {
configurators = new ArrayList<>(configurators);
findProviders(serviceLoader, Configurator.class).forEach(configurators::add);
}
if (!configurators.isEmpty()) {
for (var configurator : configurators) {
alerts.info(LogConfig.class, "Adding configurator: "
+ LogReporter.Reportable.toString(configurator, "unknown Configurator"));
}
/*
* TODO two-pass config: configurators run after config (and therefore
* the global level resolver above) already exists, so a configurator
* that contributes additional property sources or otherwise changes
* what the level resolver should have seen is invisible to it - the
* resolver was already built from logProperties as it stood before
* any configurator ran. A more correct design would run configurators
* first, then rebuild whatever is purely derived from properties
* (starting with the level resolver) a second time against the
* now-fully-configured LogConfig. Not done here - see todo.md.
*/
RainbowGumServiceProvider.Configurator.runConfigurators(configurators.stream(), config);
}
/*
* Deliberately last: alerts already resolved
* LogAlerts.UnobservedErrorsAction at construction above, but only now,
* once configurators have had their chance to record alerts and/or
* register a real Listener, does "should I report/refuse to start"
* actually mean anything - by construction nothing has had a chance to
* register one before this point.
*/
// Failure here uses the builder's debug dump, including informational
// context.
alerts.failIfAlerted("building the configuration");
// start() itself reports the backlog before throwing for unobserved
// errors.
startingAlerts = true;
alerts.start(config);
if (debug == DebugModeType.INFO || debug.checksUnusedKeys()) {
dumpSuccessfulBuild(alerts);
}
return config;
}
catch (RuntimeException | Error e) {
if (debug != DebugModeType.OFF && !startingAlerts) {
dumpBuildFailure(dumpAlerts, e);
}
if (startupRegistry != null) {
closeOnFailure(startupRegistry, e);
}
closeOnFailure(dumpAlerts, e);
throw e;
}
}
@SuppressWarnings("ReferenceEquality") // Throwable rejects self-suppression by
// identity.
private static void closeOnFailure(AutoCloseable closeable, Throwable failure) {
try {
closeable.close();
}
catch (Exception | Error closeFailure) {
if (closeFailure != failure) {
failure.addSuppressed(closeFailure);
}
}
}
/*
* Layers that can list their keys, highest priority first. System properties are
* always listed, even when the properties given do not include them, since a -D
* that nothing reads is exactly what this is meant to catch.
*/
private static UnusedKeyCheck unusedKeyCheck(LogProperties properties) {
var sources = new ArrayList<UnusedKeyCheck.KeySource>();
var system = LogProperties.StandardProperties.SYSTEM_PROPERTIES;
sources.add(new UnusedKeyCheck.KeySource(system,
() -> new TreeSet<>(System.getProperties().stringPropertyNames())));
for (var layer : layers(properties)) {
if (layer instanceof LogProperties.Listable listable) {
sources.add(new UnusedKeyCheck.KeySource(listable, listable::keys));
}
}
return new UnusedKeyCheck(sources);
}
/*
* Preserve composite precedence and mutability while recording every layer.
*/
private static LogProperties recording(LogProperties properties, UnusedKeyCheck check) {
if (properties instanceof ListLogProperties list) {
var wrapped = java.util.Arrays.stream(list.properties())
.map(layer -> recording(layer, check))
.toArray(LogProperties[]::new);
return properties instanceof LogProperties.MutableLogProperties
? new CompositeMutableLogProperties(wrapped) : new CompositeLogProperties(wrapped);
}
return check.wrap(properties);
}
private static List<LogProperties> layers(LogProperties properties) {
var layers = new ArrayList<LogProperties>();
if (properties instanceof ListLogProperties list) {
for (var layer : list.properties()) {
layers.addAll(layers(layer));
}
}
else {
layers.add(properties);
}
return layers;
}
private static void dumpSuccessfulBuild(LogAlerts alerts) {
var events = alerts.dump();
MetaLog.error(LogEventFactory.of(LogConfig.class.getName())
.eventNoArg(Level.INFO, "LogConfig built; dumping " + events.size() + " alert(s)", null));
for (var event : events) {
MetaLog.error(event);
}
}
/* Throwable.addSuppressed rejects self-suppression. */
@SuppressWarnings("ReferenceEquality")
private static void dumpBuildFailure(LogAlerts alerts, Throwable failure) {
try {
var events = alerts.dump();
MetaLog.error(LogConfig.class, "LogConfig build failed; dumping " + events.size() + " alert(s)",
failure);
for (var event : events) {
MetaLog.error(event);
}
}
catch (Throwable dumpFailure) {
if (dumpFailure != failure) {
failure.addSuppressed(dumpFailure);
}
}
}
LevelConfig buildGlobalResolver(LogProperties logProperties, LogAlerts alerts) {
LevelConfig levelResolver = LevelConfig
.of(List.of(ConfigLevelResolver.of(logProperties), GroupLevelResolver.of(logProperties)));
var config = buildLevelConfigOrNull();
if (config != null) {
levelResolver = LevelConfig.of(List.<LevelConfig>of(config, levelResolver));
}
return new AlertingLevelConfig(levelResolver, alerts);
}
private static List<LogProperties> provideProperties(ServiceRegistry registry,
ServiceLoader<RainbowGumServiceProvider> loader, LogAlerts alerts) {
List<LogProperties> props = new ArrayList<>();
for (var provider : findProviders(loader, PropertiesProvider.class).toList()) {
alerts.info(LogConfig.class, "Loading properties from "
+ LogReporter.Reportable.toString(provider, "unknown PropertiesProvider"));
props.addAll(provider.provideProperties(registry, alerts));
}
return props;
}
@Override
protected Builder self() {
return this;
}
}
}
abstract class AbstractChangePublisher implements ChangePublisher {
/*
* TODO Ideally this would be a concurrent weak hashmap. The reasoning is we may want
* a configuration where SLF4J loggers are not stored in concurrent hash map but some
* sort of GC friendly cache or not cached at all. If they subscribe to changes the
* loggers will never be GCed.
*
* Consequently the functional Consumer interface might not be the best choice.
*/
private final Collection<Consumer<? super LogConfig>> consumers = new CopyOnWriteArrayList<Consumer<? super LogConfig>>();
/*
* Same shape as LevelResolver.java's CachedLevelResolver: allowedChanges(name) is
* only ever called once per never-before-seen logger name (RainbowGumLoggerFactory
* caches the resulting Logger forever afterward), so a ConcurrentHashMap here matches
* the exact call pattern that made a ConcurrentHashMap the right tradeoff there too.
*/
private final ConcurrentHashMap<String, Set<ChangeType>> changesCache = new ConcurrentHashMap<>();
/*
* Sibling to changesCache, not folded into it - CallerType is resolved from its own
* logging.caller.<name> property, independent of logging.change.<name>, so it gets
* its own map rather than a combined per-name struct (one map per concern).
*/
private final ConcurrentHashMap<String, CallerType> callerTypeCache = new ConcurrentHashMap<>();
protected abstract LogConfig reload();
protected abstract LogConfig config();
@Override
public void publish() {
changesCache.clear();
callerTypeCache.clear();
LogConfig config = reload();
for (var c : consumers) {
c.accept(config);
}
}
@Override
public void subscribe(Consumer<? super LogConfig> consumer) {
consumers.add(consumer);
}
@Override
public boolean isEnabled(String loggerName) {
return !allowedChanges(loggerName).isEmpty() || callerType(loggerName) != CallerType.NONE;
}
@Override
public Set<ChangeType> allowedChanges(String loggerName) {
return changesCache.computeIfAbsent(loggerName, n -> {
/*
* findOrNull walks the logger-name hierarchy (closest match wins, see its own
* javadoc) trying each candidate key in turn - per candidate, resolve it as a
* LogProperty and only return null (telling findOrNull to keep walking up)
* when the key is genuinely Missing there. A present-but-malformed value is a
* Result.Error, not a Missing - it must stop the walk right there rather than
* silently falling through to a less specific ancestor's value.
*/
LogProperty.Result<Set<ChangeType>> result = config().properties()
.findOrNull(LogProperties.CHANGE_PREFIX, n, (props, fullKey) -> {
LogProperty.Result<Set<ChangeType>> r = props.forKey(fullKey).ofList().map(ChangeType::parse);
return r instanceof LogProperty.Result.Missing ? null : r;
});
if (result == null) {
return Set.of();
}
try {
return result.validateNow(ChangePublisher.class);
}
catch (LogProperty.ValidationException e) {
/*
* A malformed logging.change.<name> value must not be able to break
* logging itself. ConcurrentHashMap#computeIfAbsent leaves nothing cached
* when the mapping function throws, so without catching this a bad
* property would re-parse and re-alert on every single allowedChanges(n)
* call for this logger name, not just once - same amplification risk
* CachedLevelResolver guards against for level properties. Fall back to
* "nothing allowed to change" and alert exactly once per logger name
* instead, since the fallback value is itself cached above just like a
* successful parse would be. validateNow's own Missing = failure
* semantics are moot here - the lambda above never returns a Missing
* result to it in the first place.
*/
config().alerts().error(ChangePublisher.class, e);
return Set.of();
}
});
}
@Override
public CallerType callerType(String loggerName) {
return callerTypeCache.computeIfAbsent(loggerName, n -> {
LogProperty.Result<CallerType> result = config().properties()
.findOrNull(LogProperties.CALLER_PREFIX, n, (props, fullKey) -> {
LogProperty.Result<CallerType> r = props.forKey(fullKey).ofString().map(CallerType::parse);
return r instanceof LogProperty.Result.Missing ? null : r;
});
if (result == null) {
return CallerType.NONE;
}
try {
return result.validateNow(ChangePublisher.class);
}
catch (LogProperty.ValidationException e) {
/*
* Same reasoning as allowedChanges()'s catch above: a malformed (or
* pre-split, e.g. the old "caller" token that used to live in
* logging.change.<name>) logging.caller.<name> value alerts once and
* falls back to CallerType.NONE, cached, rather than re-parsing and
* re-alerting on every call.
*/
config().alerts().error(ChangePublisher.class, e);
return CallerType.NONE;
}
});
}
}
enum IgnoreChangePublisher implements ChangePublisher {
INSTANT;
@Override
public void subscribe(Consumer<? super LogConfig> consumer) {
}
@Override
public void publish() {
}
@Override
public boolean isEnabled(String loggerName) {
return false;
}
@Override
public Set<ChangeType> allowedChanges(String loggerName) {
return Set.of();
}
@Override
public CallerType callerType(String loggerName) {
return CallerType.NONE;
}
}
final class DefaultLogConfig implements LogConfig {
/*
* Present only in debug modes that check for unused keys.
*/
@Nullable UnusedKeyCheck unusedKeyCheck() {
return unusedKeyCheck;
}
private final ServiceRegistry registry;
private final LogProperties properties;
private final DebugModeType debugMode;
private final LevelConfig levelResolver;
private final ChangePublisher changePublisher;
private final LogOutputRegistry outputRegistry;
private final LogEncoderRegistry encoderRegistry;
private final LogPublisherRegistry publisherRegistry;
private final LogAlerts alerts;
private final LogMetrics metrics;
private final LoggerRegistry loggerRegistry;
/*
* Registered outputs/appenders, alert listeners, and service registry closeables are
* never reset when a RainbowGum closes, so a second RainbowGum on this config would
* fail on duplicate output names, flush/reopen the first one's closed appenders, and
* lose alert listeners. Ownership and built state change together in one CAS so
* concurrent build() calls cannot both win.
*/
private final AtomicReference<@Nullable Claim> claim = new AtomicReference<>();
private record Claim(Object owner, boolean built) {
}
private final @Nullable UnusedKeyCheck unusedKeyCheck;
DefaultLogConfig(ServiceRegistry registry, LogProperties properties, LevelConfig levelResolver, LogAlerts alerts,
LogMetrics metrics, DebugModeType debugMode, @Nullable UnusedKeyCheck unusedKeyCheck) {
super();
this.unusedKeyCheck = unusedKeyCheck;
this.registry = registry;
this.properties = properties;
this.debugMode = debugMode;
this.levelResolver = levelResolver;
this.alerts = alerts;
this.metrics = metrics;
this.loggerRegistry = new DefaultLoggerRegistry(metrics);
boolean changeable = properties.forKey(LogProperties.GLOBAL_CHANGE_PROPERTY)
.ofBoolean()
.or(false)
.validateNow(DefaultLogConfig.class);
this.changePublisher = changeable ? new DefaultChangePublisher() : IgnoreChangePublisher.INSTANT;
applyGlobalAppenderReentrantLockProperty(properties);
applyGlobalThreadLocalDisabledProperty(properties);
applyGlobalOptimizeProperty(properties);
registry.put(KeyValuesContributors.Disabled.class, KeyValuesContributors.Disabled.of(properties));
this.outputRegistry = DefaultOutputRegistry.of(registry);
this.encoderRegistry = DefaultEncoderRegistry.of();
this.publisherRegistry = DefaultPublisherRegistry.of();
/*
* addInternalListener, not addListener: a passive in-process counter nobody has
* wired an exporter to does not count as "someone is watching" for
* LogAlerts.UnobservedErrorsAction's purposes - see DefaultLogAlerts's own
* comment on hasExternalListener. Config alerts is always a DefaultLogAlerts;
* SwappableLogAlerts is only passed to PropertiesProviders before config exists.
*/
((DefaultLogAlerts) this.alerts).addInternalListener(event -> {
switch (event.level()) {
case ERROR -> this.metrics.errorCounter(event.loggerName(), 1);
case WARNING -> this.metrics.warnCounter(event.loggerName(), 1);
default -> this.metrics.infoCounter(event.loggerName(), 1);
}
});
}
void bind(Object owner) {
acquire(owner, false);
}
void markBuilt(Object owner) {
acquire(owner, true);
}
@SuppressWarnings("ReferenceEquality") // owner is a specific builder instance
private void acquire(Object owner, boolean build) {
while (true) {
var current = claim.get();
if (current != null && current.owner() != owner) {
throw new IllegalStateException("LogConfig is already used by another RainbowGum. "
+ "A LogConfig can only be used by one RainbowGum, even after that RainbowGum is closed. "
+ "Build a new LogConfig instead.");
}
if (current != null && current.built() && build) {
throw new IllegalStateException("This builder already built a RainbowGum. "
+ "A LogConfig can only be used by one RainbowGum. Build a new LogConfig instead.");
}
if (current != null && (current.built() || !build)) {
return;
}
if (claim.compareAndSet(current, new Claim(owner, build))) {
return;
}
}
}
/*
* Deliberately mutates process-wide static state (AbstractLogAppender's field) rather
* than per-instance state - LogProperties#GLOBAL_APPENDER_REENTRANT_LOCK_PROPERTY is
* a global guarantee by design (see its javadoc), not a per-LogConfig setting.
* Last-writer-wins if multiple RainbowGum instances with different values coexist in
* one JVM - an accepted tradeoff for a property whose whole point is a process-wide
* guarantee independent of any single instance's configuration.
*/
private static void applyGlobalAppenderReentrantLockProperty(LogProperties properties) {
AbstractLogAppender.forceReentrantLockAppenders = properties
.forKey(LogProperties.GLOBAL_APPENDER_REENTRANT_LOCK_PROPERTY)
.ofBoolean()
.or(false)
.validateNow(DefaultLogConfig.class);
}
/*
* An enum (TRUE/FALSE) rather than .ofBoolean() deliberately, unlike its sibling just
* above - a typo'd value here (e.g. "yse") fails loudly through the existing
* Property/Result machinery instead of silently resolving to "not disabled", which is
* the wrong failure mode for a property whose whole point is a hard, otherwise
* easy-to-silently-miss guarantee.
*/
private enum ThreadLocalDisabled {
TRUE, FALSE;
static ThreadLocalDisabled parse(String value) {
return LogProperty.enumValue(ThreadLocalDisabled.class, value);
}
}
/*
* Same last-writer-wins/process-wide-guarantee tradeoff as
* applyGlobalAppenderReentrantLockProperty just above - see that method's comment.
* rainbowgum-slf4j independently reads this same property key at its own SLF4J
* provider initialize() touchpoint to decide whether to disable MDC too - see
* LogProperties#GLOBAL_THREADLOCAL_DISABLED_PROPERTY's javadoc for why that is two
* independent readers rather than one shared flag.
*/
private static void applyGlobalThreadLocalDisabledProperty(LogProperties properties) {
AbstractLogAppender.forceNoThreadLocalAppenders = properties
.forKey(LogProperties.GLOBAL_THREADLOCAL_DISABLED_PROPERTY)
.ofString()
.map(ThreadLocalDisabled::parse)
.or(ThreadLocalDisabled.FALSE)
.validateNow(DefaultLogConfig.class) == ThreadLocalDisabled.TRUE;
}
/*
* Same enum-not-boolean reasoning as ThreadLocalDisabled above. Unlike the other two
* global properties this one is not a "force/guarantee" downgrade applied after a
* type is already resolved - it changes what the *default* resolves to when nothing
* more specific (an explicit appenderType(...)/useGetBytes(...) call, or a more
* specific property) was set, so it is read into the same kind of process-wide static
* flag but consulted at a different point (LogAppender.Builder#build(),
* LogEncoder.Builder#build()) - see LogProperties#GLOBAL_OPTIMIZE_PROPERTY's javadoc.
*/
private enum GlobalOptimize {
TRUE, FALSE;
static GlobalOptimize parse(String value) {
return LogProperty.enumValue(GlobalOptimize.class, value);
}
}
private static void applyGlobalOptimizeProperty(LogProperties properties) {
AbstractLogAppender.globalOptimizeEnabled = properties.forKey(LogProperties.GLOBAL_OPTIMIZE_PROPERTY)
.ofString()
.map(GlobalOptimize::parse)
.or(GlobalOptimize.FALSE)
.validateNow(DefaultLogConfig.class) == GlobalOptimize.TRUE;
}
class DefaultChangePublisher extends AbstractChangePublisher {
@Override
protected LogConfig config() {
return DefaultLogConfig.this;
}
@Override
protected LogConfig reload() {
levelResolver.clear();
return DefaultLogConfig.this;
}
}
@Override
public LogProperties properties() {
return properties;
}
@Override
public DebugModeType debugMode() {
return debugMode;
}
@Override
public LevelConfig levelResolver() {
return this.levelResolver;
}
@Override
public ServiceRegistry serviceRegistry() {
return this.registry;
}
@Override
public ChangePublisher changePublisher() {
return this.changePublisher;
}
@Override
public LogOutputRegistry outputRegistry() {
return this.outputRegistry;
}
@Override
public LogEncoderRegistry encoderRegistry() {
return this.encoderRegistry;
}
@Override
public LogPublisherRegistry publisherRegistry() {
return this.publisherRegistry;
}
@Override
public LogAlerts alerts() {
return this.alerts;
}
@Override
public LogMetrics metrics() {
return this.metrics;
}
@Override
public LoggerRegistry loggerRegistry() {
return this.loggerRegistry;
}
}
final class DefaultLoggerRegistry implements LogConfig.LoggerRegistry {
private final Set<String> loggerNames = ConcurrentHashMap.newKeySet();
private final Set<LoggerAPI> loggerAPIs = ConcurrentHashMap.newKeySet();
private final LogMetrics metrics;
DefaultLoggerRegistry(LogMetrics metrics) {
this.metrics = metrics;
}
@Override
public void registerLoggerName(LoggerAPI api, String loggerName) {
Objects.requireNonNull(api);
loggerAPIs.add(api);
if (loggerNames.add(loggerName)) {
metrics.infoCounter(LogMetrics.LOGGER_NAMES_METRIC, 1);
}
}
/*
* Deliberately not on the LogConfig.LoggerRegistry interface itself (see its
* javadoc): DefaultLogReporter downcasts to this package-private type to reach it,
* the same pattern DefaultLogConfig's own constructor already uses to reach
* DefaultLogAlerts#addInternalListener.
*/
Set<String> loggerNames() {
return Set.copyOf(loggerNames);
}
/**
* Every {@link LoggerAPI} registered so far via {@link #registerLoggerName}, not just
* {@link LoggerAPI.Standard}: an application-provided {@link LoggerAPI} works too.
* Same downcast-only visibility as {@link #loggerNames()}.
* @return immutable snapshot.
*/
Set<LoggerAPI> loggerAPIs() {
return Set.copyOf(loggerAPIs);
}
}