spring_boot_starter_logging 0.4.1
spring_boot_starter_logging: ^0.4.1 copied to clipboard
Logging starter for spring_boot_dart: console patterns, log files with rollover, logging.* properties and an in-memory log buffer for in-app log viewers.
spring_boot_starter_logging #
No backward compatibility before 1.0. Any release may rename or change APIs, properties and behaviour; the breaking changes are listed in each package's CHANGELOG.
Logging for spring_boot_dart applications, like
spring-boot-starter-logging. Add the dependency; the generator discovers
the auto-configuration and a LoggingSystem is installed behind the
SpringLog facade. No code needed.
dependencies:
spring_boot_starter_logging: ^0.4.1
import 'package:neon/app.spring.dart'; // the generated file
@Service()
@Log(topic: 'neon.checkout') // generates the `log` getter, like Lombok's @Slf4j
class Checkout {
void pay(int order, double amount) =>
log.info('paid $amount', fields: {'order': order});
}
Spring Boot's console line (with spring.application.name=shop):
2026-10-23T23:08:01.580+02:00 INFO 4711 --- [shop] [ main] neon.checkout : paid 12.5 {order=1}
In the guides: server, 11. Logging, app, 10. Logging.
Properties #
logging.level.root=info # Boot's default with this starter (ROOT in any case)
logging.level.neon.checkout=debug # a logger and its descendants
logging.group.payments=neon.card,neon.cash
logging.level.payments=trace
logging.pattern.console=%d{HH:mm:ss.SSS} %5p [%logger{20}] %m%X%n%ex # default: Boot's
logging.pattern.dateformat=yyyy-MM-dd'T'HH:mm:ss.SSSXXX # %d of the default patterns
logging.pattern.level=%5p # the level of the default patterns
logging.pattern.correlation= # after [thread] in the default patterns
logging.exception-conversion-word=%wEx # the error of the default patterns
logging.include-application-name=true # [spring.application.name] in the default patterns
logging.include-application-group=true # [spring.application.group]
spring.output.ansi.enabled=always # console colours: always (default), detect (a terminal, Flutter debug), never
logging.threshold.console=warn # console: warnings and errors only
logging.console.enabled=true
logging.buffer.enabled=true
logging.buffer.capacity=1000
logging.threshold.buffer=debug
logging.file.name=logs/shop.log # a log file (or logging.file.path=logs → logs/spring.log)
logging.pattern.file=%d %5p [%logger] %m%X%n%ex # default: Boot's
logging.threshold.file=info
logging.charset.file=UTF-8 # also ISO-8859-1, US-ASCII
logging.logback.rollingpolicy.max-file-size=10MB # roll over into shop.log.1, .2, ...
logging.logback.rollingpolicy.max-history=7 # rolled-over files kept; 0: all
logging.logback.rollingpolicy.total-size-cap=0B # their total size; 0B: no limit
logging.logback.rollingpolicy.clean-history-on-start=false
logging.enabled=false # turn the starter off (on/off values: true / false in any case)
Patterns #
Logback's PatternLayout syntax with Spring Boot's converters
(LogPattern):
| Conversion | Output |
|---|---|
%d, %date{pattern[, UTC]} |
time; Java pattern letters (yyyy-MM-dd'T'HH:mm:ss.SSSXXX, EEE, MMM, a, ...), ISO8601 (the default: yyyy-MM-dd HH:mm:ss,SSS) |
%p, %le, %level |
TRACE DEBUG INFO WARN ERROR |
%c, %lo, %logger{n} |
logger name, shortened like Logback (%logger{15}: n.c.Payments; {0}: the last segment) |
%m, %msg, %message |
message |
%t, %thread |
isolate name (main) |
%X{key}, %X{key:-default}, %X |
one field; all fields as {k=v, ...} |
%ex, %exception, %throwable{short|full|n}, %xEx, %rEx |
error and stack trace |
%wEx |
the same between line breaks (Boot) |
%nopex |
no error output |
%clr(...){color} |
colours (Boot): faint, red, green, yellow, blue, magenta, cyan, black, white, bright_*; none: by level |
%esb(...){VAR} |
[value] of its content or a variable, nothing when empty (Boot) |
%red(...), %boldRed(...), %gray(...), ..., %highlight(...) |
Logback's colours, always ANSI: black red green yellow blue magenta cyan white gray, bold...; %highlight by level |
%replace(...){regex, replacement} |
its content with the matches replaced ($1 for groups) |
%kvp{DOUBLE|SINGLE|NONE} |
the fields as key="value" pairs |
%prefix(...) |
each conversion inside as name=value (level=INFO) |
%property{key} |
a variable or property (null when there is none) |
%(...) |
groups content for a format modifier |
%r, %relative |
milliseconds since logging started |
%seq |
record sequence number |
%n, %%, \%, \(, \) |
line break, %, (, ) |
Format modifiers as in Logback: %5p pads left, %-5p right, %.10c
keeps the last 10 characters, %.-10c the first 10, %-40.40logger{39}
all of it. ${PID}, ${LOG_LEVEL_PATTERN:-%5p} and other ${NAME} /
${NAME:-default} are replaced first: Boot's logging variables (PID,
APPLICATION_NAME, LOG_DATEFORMAT_PATTERN, ...), then properties
(${spring.application.name}). As in Logback, a pattern without an error
conversion still prints errors after the line. Anything the parser
cannot read (an unknown word, an unclosed (, an unknown colour, date
letter or variable) fails the start.
Log files #
logging.file.name (a file) or logging.file.path (a directory with
spring.log) turns on a FileAppender. Lines logged in one synchronous
burst are written together at the end of the microtask; the rest is
written when the context closes. When the file would grow past
max-file-size it becomes shop.log.1 (older ones shift to .2, .3,
...); only max-history of them are kept (all with 0, like Logback), and
the oldest are deleted while together they exceed total-size-cap.
Relative locations are resolved against LoggingSystem.instance.fileDirectory
when it is set, else against the working directory. In Flutter the
writable directory is only known at runtime, so set it before starting
(the coming Flutter starter will do it for you):
LoggingSystem.instance.fileDirectory =
(await getApplicationDocumentsDirectory()).path; // path_provider
await runApplicationAsync(args);
On the web there is no file system: logging.file.* is ignored with a
warning and FileAppender.isSupported is false.
A log screen (Flutter) #
LogBuffer keeps the last records in memory. It is a bean (inject it) and
also LoggingSystem.instance.buffer. Its API is made for a log viewer:
class LogScreen extends StatefulWidget { ... }
class _LogScreenState extends State<LogScreen> {
final buffer = LoggingSystem.instance.buffer;
var query = const LogQuery(minLevel: SpringLogLevel.info);
@override
Widget build(BuildContext context) => Column(children: [
TextField(onChanged: (text) => setState(() => query = query.copyWith(text: text))),
Expanded(
child: StreamBuilder<List<SpringLogRecord>>(
stream: buffer.watch(query), // current list, then one per burst
builder: (context, snapshot) {
final records = snapshot.data ?? const [];
return ListView.builder(
itemCount: records.length,
itemBuilder: (context, i) => ListTile(
key: ValueKey(records[i].sequence),
title: Text(records[i].message),
subtitle: Text('${records[i].loggerName} · ${records[i].fields}'),
),
);
},
),
),
]);
}
| Need | API |
|---|---|
| Live filtered list | buffer.watch(query) → Stream<List<SpringLogRecord>> |
| Snapshot | buffer.records, buffer.query(query) |
| Every new record | buffer.onRecord |
| Badges | buffer.counts[SpringLogLevel.error], buffer.dropped |
| Logger filter menu | buffer.loggerNames |
| Share / copy | buffer.export(query) (or with a LogPattern) |
| Clear | buffer.clear() |
| Filters | LogQuery(minLevel, levels, logger, text, fields, since), immutable, copyWith (null removes a criterion) |
| Change levels at runtime | loggingSystem.setLogLevel('neon', SpringLogLevel.debug), setLogLevel('ROOT', ...), rootLevel |
Records are immutable and carry time, sequence (a stable list key),
level, loggerName, message, fields, error, stackTrace.
In Flutter, send the console to debugPrint before starting:
LoggingSystem.instance.console.out = debugPrint;
Own appenders #
class CrashReportAppender extends LogAppender {
CrashReportAppender() : super(threshold: SpringLogLevel.error);
@override
void write(SpringLogRecord record) =>
crashlytics.recordError(record.error ?? record.message, record.stackTrace);
}
LoggingSystem.instance.addAppender(CrashReportAppender());
A failing appender does not stop the others.
Without Spring Boot #
LoggingSystem.instance
..configure(environment) // the logging.* properties
..install();
Differences from Spring #
- One line per record: patterns are Logback's, but the console (
print) and the file add the line break, so a pattern's final%nis dropped (LogPattern.formatLine). Boot's default patterns are used as they are (%m%n%wEx), with the record's fields (%X) after the message. %threadis the isolate name;%Xprints{k=v, ...}(with a leading space, nothing without fields) instead ofk=v, k=v.- Caller data is impossible: a Dart record does not know the class,
method, line or file that logged it (
%C,%M,%L,%F,%callerstop the start as unknown words). - Not supported:
%marker,%contextName,%dtime zones other than UTC (Dart has no time zone database),logback.xml/logback-spring.xml. - The console is coloured by default (
spring.output.ansi.enabled=always; Spring Boot's default isdetect):%clr, and in a pattern that colours nothing itself (no%clr, no Logback colour) the time faint, the level by level and the logger cyan, as in Spring Boot's pattern. So a shortlogging.pattern.console=%d %5p [%logger] %mis coloured too. Files are never coloured. Where a program reads the output (a log collector, Xcode's console,adb logcat, output redirected to a file), setnever;detectcolours only a terminal on standard output or a Flutter debug or profile build. Logback's colour words (%red(...),%boldRed(...),%highlight(...)) always write ANSI codes, as Logback does. %kvpprints the record's fields (Dart has no SLF4J key-value pairs);%prefixnames conversions by Logback's long names (level=,logger=,%X{key}askey=);%propertyreads the pattern variables, then the environment.- Rolling is by size only, into numbered files (
shop.log.1,.2, ...):logging.logback.rollingpolicy.file-name-pattern(dates,%i,.gz) is ignored with a warning;max-historycounts files, not days; archives are not compressed. logging.charset.filetakes the encodings Dart knows (UTF-8, ISO-8859-1, US-ASCII, by their IANA or Java names such asutf8);logging.charset.consoleis only checked: the console isprint, which writes text, not bytes.- An empty
logging.pattern.console=(orlogging.pattern.file=) turns that output off, as Logback's appender does not start without a pattern (Logback also reports a status error). - Lines logged while the application starts, before the logging system
is set up during the refresh, are replayed into the file, the buffer and
added appenders (Spring Boot's
DeferredLogs); the console showed them already through theSpringLog.handlerof the time, in its format. - A failed start keeps the logging system installed until the failure report is logged (so it reaches the file), then uninstalls it.
- No structured logging (
logging.structured.*), nologging.config, no predefinedweb/sqllogger groups.