SpringLog
SpringLog is a named logger, and its static members are the switches of logging as a whole: levels, the handler and listeners.
Why use it
Code logs through a SpringLog; a backend decides where the records go. This keeps libraries and applications on one API, and lets you set levels per logger name without touching code.
- Get a logger with
@Log()on a class, or declare one as a constant:const log = SpringLog('neon.checkout');. Aconstlogger costs nothing until it logs. - Levels follow the dots in names:
neon.checkout.paymentuses its own level if set, else that ofneon.checkout, elseneon, else the root level. - The root level is
infounless changed. - Without a backend,
printLogHandlerwrites each record as one line:[logger] LEVEL: message, followed by the error if there is one (the stack trace is not printed). It writes to standard output on the Dart VM, to the device log in Flutter and to the browser console on the web. In a terminal the line is coloured.
Example
import 'package:spring_core_dart/spring_core_dart.dart';
import 'package:neon/spring_gen/app.spring.dart'; // the generated file
@Service()
@Log(topic: 'neon.checkout') // logger name chosen explicitly
class Checkout {
Checkout(this.gateway);
final PaymentGateway gateway;
void pay(Order order) {
log.info('order placed', fields: {'order': order.id});
log.debug(() => 'basket: ${order.dump()}'); // built only if enabled
try {
gateway.charge(order);
} on PaymentException catch (e, s) {
log.error('payment failed', error: e, stackTrace: s);
}
}
}
// `dart:math` also declares `log`, so pick another getter name there.
@Component()
@Log(fieldName: 'logger') // logger 'neon.Payments'
class Payments {
void refund() => logger.warning('refund');
}
Levels, fields and listeners in code:
const log = SpringLog('neon.jobs');
void configureLogging() {
SpringLog.level = SpringLogLevel.warning; // root level
SpringLog.setLevel('neon.checkout', SpringLogLevel.debug); // and below
}
Future<void> runJob(String jobId) =>
// Every record logged inside, also after an `await`, carries `job`.
SpringLog.withFields({'job': jobId}, () async {
log.info('started');
await Future<void>.delayed(const Duration(seconds: 1));
log.info('finished');
});
void captureInTest() {
final records = <SpringLogRecord>[];
final stop = SpringLog.listen(records.add); // after the handler
log.warning('disk almost full');
stop();
}
@Log writes the log getter into the .spring.dart file of the @ComponentScan that scans the class. Import that file in the class's library. The default logger name is <package>.<Class>.
Members
@Log:
| Name | Type | Default | Meaning |
|---|---|---|---|
topic |
String? |
null |
The logger name; <package>.<Class> when null. |
fieldName |
String |
'log' |
The name of the generated getter. |
SpringLog instance members:
| Name | Meaning |
|---|---|
SpringLog(name) |
Creates a logger; usually const. |
name |
The logger name, used to look up its level. |
trace(message, {fields}), debug(...), info(...) |
Log at that level. |
warning(message, {error, stackTrace, fields}), error(...) |
Log at that level, with an optional error and stack trace. |
log(level, message, {error, stackTrace, fields}) |
Log at a level chosen at run time. |
isEnabled(level) |
Whether this logger emits records of level now. |
message may be a function returning the text; it is called only when the record is emitted. Any other object is turned into text with toString.
SpringLog static members:
| Name | Meaning |
|---|---|
level |
The root level; SpringLogLevel.info unless set. |
setLevel(name, level) |
Sets the level of a logger and the loggers below it; null makes it inherit again. |
levelOf(name) |
The level that applies to a logger name. |
configuredLevels, resetLevels() |
The levels set with setLevel; remove them all. |
handler |
Where records go: a SpringLogHandler. printLogHandler unless a backend replaces it. |
listen(listener) |
Also sends every emitted record to listener; returns a function that stops it. |
withFields(fields, body) |
Runs body with fields added to every record logged inside, including asynchronous work. |
currentFields |
The fields set by withFields for the code running now. |
hold() |
Holds back records until release() on the returned SpringLogHold; used by startup code before levels are known. |
SpringLogLevel: trace, debug, info, warning, error, and off, which silences a logger.
SpringLogRecord (what handlers and listeners receive): level, loggerName, message, error, stackTrace, fields, time, sequence.
Other helpers: printLogHandlerColours forces colours on or off; parseSpringLogLevel(text) reads a level name as written in configuration (warn and warning both work); LogFormatUtils.formatValue(value) quotes a value from outside and replaces line breaks and control characters, so it cannot forge log lines.
In Spring (Java)
// Spring's commons-logging facade
private static final Log log = LogFactory.getLog(Checkout.class);
log.info("order placed");
// Lombok
@Slf4j(topic = "neon.payments")
class Payments { void refund() { log.warn("refund"); } }
Differences that matter:
@Logplays the role of Lombok's@Slf4j, but the getter is generated by the build, not by a compiler plugin.- The level is named
warning, notwarn(configuration accepts both). withFieldsis SLF4J's MDC, scoped by Dart zones instead of threads, so it followsasynccode.- Records are dispatched by
SpringLog.handler; there is no Logback or Log4j configuration file. With Spring Boot, levels come fromlogging.level.<name>properties.