
# 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');`. A `const` logger costs nothing until it logs.
- Levels follow the dots in names: `neon.checkout.payment` uses its own level if set, else that of `neon.checkout`, else `neon`, else the root level.
- The root level is `info` unless changed.
- Without a backend, `printLogHandler` writes 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

```dart
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:

```dart
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)

```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:

- `@Log` plays the role of Lombok's `@Slf4j`, but the getter is generated by the build, not by a compiler plugin.
- The level is named `warning`, not `warn` (configuration accepts both).
- `withFields` is SLF4J's MDC, scoped by Dart zones instead of threads, so it follows `async` code.
- Records are dispatched by `SpringLog.handler`; there is no Logback or Log4j configuration file. With Spring Boot, levels come from `logging.level.<name>` properties.

## See also

- [Logging](index.md)
- [@Component](../beans/component.md)
- [Environment](../config/environment.md)
