vidocq-runtime-core is the Vidocq Runtime runtime engine. It discovers the extensions through ServiceLoader, builds the configuration, boots the Vauban CDI container and drives the extension lifecycle.

Coordinates

Artefact

io.vidocq.runtime:vidocq-runtime-core:0.4.0-SNAPSHOT

Java module

io.vidocq.runtime.core

Source

vidocq-runtime-core/src/main/java/io/vidocq/runtime/core/

Responsibilities

  • Discover every VidocqExtension on the module path via java.util.ServiceLoader (ExtensionLoader), sorted by ascending priority().

  • Build the configuration (VidocqConfigImpl over the built-in ConfigSource chain, wrapped by the legacy VidocqConfigurationImpl).

  • Run the lifecycle in order (VidocqBootstrap):

    1. configure — each extension reads its keys: ext.configure(configuration).

    2. beforeStart — create the VaubanContainerBuilder (bean index from META-INF/vauban-beans.list, no classpath scanning) and let each extension enrich it: ext.beforeStart(builder). The Mansart pool extension opens its JDBC pools here, so that the extensions after it and the container find them open.

    3. CDI start — builder.build() boots Vauban.

    4. onStart — each extension receives the ExtensionContext (container, bean manager, config): Cassini mounts the REST routes, and the Chappe extension opens the HTTP listeners last.

  • Warn about configured vidocq.* keys nobody consumes (ConfigKeyAudit, based on the core’s own keys and VidocqExtension.configKeys()), as VIDOCQ-CFG-003 — key read by nothing.

  • Print the startup banner with the exact Vidocq version and build (Startup banner NEW).

  • Log the startup report, one record that says what has just started, just before Vidocq - Started in, and report a boot that fails (Startup report NEW).

  • Give the extensions a read-only view of that report, ExtensionContext.startupReport() NEW, which the dev console shows while the application runs.

  • Print readable console logs when the application has not configured logging (Console logging NEW).

  • Manage the shutdown cycle — onStop() runs in reverse priority order, virtual threads drain cleanly.

  • Install the Vauban application layer when the launch vehicle requires it (VidocqAppLayer, Vidocq.run), including the vidocq:dev hot-reload loop (VidocqDevReloadLoop).

Entry point

package io.example;

public class App {
    public static void main(String[] args) {
        io.vidocq.runtime.core.Vidocq.main(args);
    }
}

Vidocq.main is a simple facade over VidocqBootstrap. User code does not need a @SpringBootApplication nor an application class — extensions and beans are discovered from the module path (ServiceLoader + the compile-time bean index). For a zero-config IDE launch, annotate a trampoline main with @VidocqMain and call Vidocq.run(App.class, args) — the application is then re-loaded inside the Vauban application layer.

Which main methods are accepted NEW

Vidocq chooses the application’s main itself whenever the application is handed over as a path rather than as a module to launch: mvn vidocq:run, vidocq:dev with -Dvidocq.dev.hotReload=false, and the packaged launcher of an application that has no @VidocqMain trampoline. All three pass -Dvidocq.app.main, and the runtime reaches that main by reflection, inside the application layer. (With a trampoline the script launches --module app/acme.App and the JVM launcher makes the choice; vidocq:dev’s hot-reload loop, the default, ignores `vidocq.app.main on purpose and reboots the layer instead.)

The choice accepts the four shapes the Java SE launcher accepts since Java 25 (JEP 512), and, like the launcher, never requires public:

static void main(String[] args) { }   // and public, protected or package-private
static void main() { }
void main(String[] args) { }          // an instance method: run on a fresh instance
void main() { }

Selection follows the launcher’s own algorithm, and its consequences are worth knowing:

  • main(String[]) is searched before main(), and each search walks the class, then its superclasses, then the interfaces it implements. A default void main() on an implemented interface therefore starts the application; a static main declared in an interface never does.

  • A main declared in the class ends the search whether or not it can be used. A private main(String[]) that hides a perfectly good inherited one sends the launch to main() — which is what java does too.

  • An instance main is run on an instance built with the class' no-argument constructor, which must not be private.

  • A method named main that is private or that returns something other than void is not a main method. The failure names it, and the class that declares it, rather than reporting that nothing was found.

This matters in an IDE on JDK 25: the inspection that flags the public of a main as an unnecessary modifier is right, and following it no longer breaks the launch. For an application module the layer defines, the controller exports and opens the package of the class named by vidocq.app.main and the package of the class that declares the selected method — two different packages, in two different modules, as soon as the main is inherited.

Lifecycle hooks

An extension subscribes to phases by overriding the VidocqExtension methods:

public class MyExtension implements VidocqExtension {

    @Override
    public String name() { return "my-extension"; }

    @Override
    public void onStart(ExtensionContext ctx) {
        // the container is up — resolve beans, mount handlers
    }

    @Override
    public void onStop() {
        // reverse-order shutdown — release resources
    }
}

All hooks have empty default implementations; priority() (default 1000, lower runs first) orders the extensions within every phase.

Threading

Boot itself is intentionally sequential on a single thread (easier diagnosis, readable stack traces). The virtual threads appear after onStart:

  • the Chappe HTTP I/O — one virtual thread per inbound connection;

  • the Mansart JDBC housekeeping;

  • request-time work of extensions such as vidocq-runtime-knock-health-extension, whose health checks execute on the incoming request’s virtual thread.

Startup banner NEW

Vidocq tells which Vidocq is starting, once per JVM, right after the configuration is loaded and before Vidocq - Configuration phase. Two 0.4.0-SNAPSHOT builds are told apart by their commit, so the version is in the output before anything later can fail.

When someone is watching (a terminal, a dev launch), the art and two lines go to standard output:

vidocq-runtime-cassini-rest-example in a terminal, launched with -Dvidocq.profile=dev
__     ___     _
\ \   / (_) __| | ___   ___ __ _
 \ \ / /| |/ _` |/ _ \ / __/ _` |
  \ V / | | (_| | (_) | (_| (_| |
   \_/  |_|\__,_|\___/ \___\__, |
                              |_|
 Vidocq 0.4.0-SNAPSHOT (9beafc47, built 2026-09-17T16:36:42Z)
 Java 25+36-LTS | dev | vidocq-runtime-cassini-rest-example 0.4.0-SNAPSHOT

Everywhere else (a container, CI, a pipe, > file, tests) there is no art, only one log line:

[INFO ][2026-09-17 18:38:35.246][main           ][i.v.r.c.b.StartupBanner#show                 ] : Vidocq 0.4.0-SNAPSHOT (9beafc47, built 2026-09-17T16:36:42Z) | Java 25+36-LTS (Eclipse Adoptium) | prod (no dev or test signal) | vidocq-runtime-cassini-rest-example 0.4.0-SNAPSHOT

What it shows

The identity line is Vidocq <version>, followed by details only for a snapshot or a build from a modified working tree:

Build Identity line

Clean release

Vidocq 0.3.0

Snapshot

Vidocq 0.4.0-SNAPSHOT (9beafc47, built 2026-09-17T14:02:11Z)

Built from a modified tree

Vidocq 0.4.0-SNAPSHOT (9beafc47+dirty, built 2026-09-17T14:02:11Z)

Without a build time

Vidocq 0.4.0-SNAPSHOT (9beafc47, committed 2026-08-30T20:07:01Z)

Jar without a build identity file (built with an older vidocq-parent)

Vidocq 0.4.0-SNAPSHOT (jar dated 2026-09-15T15:53:21Z)

Classes recompiled by an IDE

Vidocq version unknown (classes in vidocq-runtime-core/target/classes)

Classes directory with a version from the last Maven build

Vidocq 0.4.0-SNAPSHOT? (last Maven build, classes in vauban-api/target/classes)

Times are UTC, to the second. A directory of classes shows where the classes come from rather than a commit: an IDE recompiles without Maven, so its build identity file and its version may be older than the classes, hence the ?.

The context line is Java <version> (<vendor>) | <mode> (<reason>) | debug <address> | devconsole <address> | <application> <version>:

  • the Java version and vendor of the running JVM;

  • the launch mode — dev, test or prod — with, in parentheses, the signal it was read from: dev (IntelliJ agent), prod (no dev or test signal);

  • the debugger, when the JVM runs with a JDWP agent;

  • the dev console NEW, when it is on the module path and on for this launch: devconsole :8888 for a loopback host, devconsole <host>:<port> otherwise, nothing for port 0. It is the configured address, a promise made before the bind; the console’s own Vidocq dev console: http://…/ record, logged once it listens, is the one to trust;

  • the application: the module of the class that calls Vidocq.run, else the module of -Dvidocq.app.main, else the module launched with java -m. It is named by the artifactId of its pom.properties when packaged, by its module name otherwise.

Both lines stay within 80 columns. For the context line, what is given up goes in this order: the JVM vendor, then the segments of a module name are abbreviated (i.v.t.l.mcptimeserver), then the name is cut with …​ — always before the application version — then the dev console NEW, and the name comes back whole if it then fits, then the debugger, then the reason of the launch mode, and with it the whole segment when the mode is a prod that no signal proves; the whole line is cut last. The vendor goes first because the line is read to know where the process runs and how to attach to it: 19 columns of (Eclipse Adoptium) never cost the address of a debugger, nor the mode and the reason it was read from. A long directory path drops its last Maven build label before being cut. The single INFO line is never cut: it carries the mode, its reason, the debugger and the dev console whatever their length.

The Vidocq bricks have their own versions. When a brick (Vauban, Chappe, Cassini, …​) has another version than the runtime, was built from a modified tree, or is a snapshot without a commit, one more INFO record lists them:

Vidocq bricks: chappe 0.4.0-SNAPSHOT (2d8ec095+dirty, built 2026-09-16T08:48:26Z), vauban 0.3.0

Launch mode NEW

Vidocq says which of dev, test and prod it is running in, and in parentheses the signal it read it from — the claim and its evidence, never one without the other. It is an observation, not a switch: the container, the configuration and the extensions' lifecycle are the same in every mode. It decides what is printed — the art of the banner, and the default level of the startup report — so that a log or a screenshot says what the launch was. An extension reads it with ExtensionContext.launchMode() NEW to offer what only makes sense in development (Reading the launch mode).

The first signal found wins:

Mode Signal Reason shown

as set

vidocq.launch.mode is dev, test or prod

vidocq.launch.mode

as set

vidocq.profile is dev, test or prod — both vidocq dev and the Maven vidocq:dev set it

profile dev

dev

vidocq.dev.reload.file, the reload loop of vidocq dev

dev reload loop

test

a JUnit, TestNG, Surefire or Arquillian frame on the booting thread, otherwise a JUnit Platform launcher on the class path

JUnit on the stack, Surefire on the stack, JUnit on the class path

dev

an application archive that is a directory of a build tree: one ending in target/classes, build/classes/<language>/main or out/production/<name>, or one with a pom.xml, build.gradle(.kts) or settings.gradle(.kts) within three parent levels

target/classes with a pom.xml above, build/classes/java/main, a build.gradle above the classes

dev

IntelliJ’s Run console, recognised by the -javaagent:…idea_rt.jar argument it adds

IntelliJ agent

prod

nothing above matched

no dev or test signal

A profile that is no mode (staging) is not a signal: the detection goes on without it. vidocq.launch.mode=auto NEW, the default, lets the detection decide too; any other value that names no mode is reported as VIDOCQ-CFG-001 — invalid value and does the same.

prod is the only mode that rests on an absence, and it never shows without saying so: when the context line is too narrow for prod (no dev or test signal), the whole segment goes rather than leave a bare prod claiming more than is known.

Reading the archives is bounded: at most four of them, their shape is a string comparison, and only one of them has its parents probed for a build file — a handful of stat calls, once per boot: every reload of the dev loop reads its launch again. Nothing in the resolution throws: a probe that fails is a signal that was not found, and the worst case is the prod above.

Debugger NEW

When the JVM runs with a JDWP agent (-agentlib:jdwp=…, or the older -Xrunjdwp:…), the context line also carries what a debugger has to attach to, in every mode and not only in dev:

 Java 25+36-LTS | dev (IntelliJ agent) | debug *:5005 | mcp-time-server 0.1.0-SNAPSHOT

The address is repeated exactly as the agent was given it (address=:5005 shows :5005, address=127.0.0.1:5005 shows 127.0.0.1:5005), and suspend=y is added when the JVM waits for the debugger before running. An agent with no address shows debug alone: the JVM then chooses the port, and only its own Listening for transport dt_socket at address: … line tells which.

The debugger outlives the JVM vendor and the whole application name: it is given up only when even a cut name leaves no room — a long reason and a long address do not both fit behind a long module name in 80 columns. Whenever what was printed does not carry the address, one more INFO record repeats it, so a launch with a debugger never hides how to attach to it:

Vidocq debugger: attach to 127.0.0.1:18098

A custom banner that shows no context line gets that record too; an agent with no address says so instead of an address. The single INFO line of the log output always carries the segment itself, so it never needs the record.

When the art appears

vidocq.banner.mode Output

auto (default)

One INFO line for an embedded deployment (Arquillian, the TCK: configure(List)) and for a JUnit test run. The art and the two lines on standard output when standard output is a terminal or on a dev launch. One INFO line everywhere else.

console

The art and the two lines on standard output, in one write.

log

The art and the two lines in one INFO record, which starts with a line break so that the art stays in column 0. Never coloured.

off

Nothing.

A terminal means that System.console() exists and isTerminal() is true: redirecting standard input or standard output turns the art off. A dev launch is a mode of dev read from a deliberate signal — vidocq.launch.mode, a profile, the dev reload loop, or IntelliJ’s Run console, which is not a terminal and is what shows the art on an IDE Run. A dev read from the shape of a build tree does not show the art without a terminal: a piped CI job runs out of target/classes too.

The banner is printed once per JVM: a hot reload or a second deployment in the same JVM prints nothing more. It never fails the boot. Its logger, io.vidocq.runtime.core.banner.StartupBanner, can be silenced through the logging configuration.

Code that embeds Vidocq and owns its standard output (a tool whose output is data, a CLI plugin) sets the mode itself; this wins over the configuration:

VidocqBootstrap.create()
        .banner(BannerMode.OFF)
        .configure()
        .start();

Configuration

Key Default Description

vidocq.banner.mode NEW

auto

auto, console, log or off (above). An unknown value logs a warning and means auto.

vidocq.banner.location NEW

(empty)

A custom banner: classpath:<resource>, file:<path> or <path>, which take the path as written, or an encoded file:// URI (file:///opt/my%20app/banner.txt). When unset, a vidocq-banner.txt resource of the application is used if there is one. A missing file, or a value that names no file (such as a file:// URI with a raw space), logs a warning and prints the built-in art.

vidocq.launch.mode NEW

auto

dev, test or prod, forced instead of detected; auto detects it (Launch mode NEW). Another value is reported as VIDOCQ-CFG-001 — invalid value and means auto. It changes what is printed — the banner, the level of the startup report — and what ExtensionContext.launchMode() tells the extensions; the runtime itself behaves the same.

vidocq.console.color

auto

Colours, shared with the console logs (Console logging NEW).

Like every key, they can be set with -D, an environment variable (VIDOCQ_BANNER_MODE), vidocq.properties or a profile (%dev.vidocq.banner.mode). There is no vidocq.banner.enabled: the startup audit reports it with the known keys of the namespace.

Colours

On a terminal or in IntelliJ’s Run console, the art is cyan, Vidocq <version> bold, the build details yellow and the context line faint. The policy is the one of the console logs: a non-empty NO_COLOR environment variable always wins, vidocq.console.color=always or never forces the choice, and auto needs TERM other than dumb and IntelliJ’s Run console or a terminal on an operating system other than Windows. Output piped to a file or a container log never carries escape sequences.

Custom banner

A vidocq-banner.txt file at the root of the application’s resources replaces the art, with no configuration; vidocq.banner.location points to another file. The file is read as UTF-8 through the context class loader, as vidocq.properties is. Its lines are printed as they are, after placeholder substitution:

Placeholder Value

${vidocq.version}

0.4.0-SNAPSHOT

${vidocq.build}

9beafc47+dirty, built 2026-09-17T14:02:11Z, empty for a clean release

${vidocq.identity}

the whole identity line

${java.version}

Java 25+36-LTS (Eclipse Adoptium)

${app.name}, ${app.version}

the application’s name and version

${vidocq.launch}

dev (IntelliJ agent), prod (no dev or test signal) or empty

${vidocq.debug} NEW

debug *:5005, debug 127.0.0.1:5005 suspend=y, or empty without a JDWP agent

${ansi.bold}, ${ansi.faint}, ${ansi.red}, ${ansi.green}, ${ansi.yellow}, ${ansi.blue}, ${ansi.magenta}, ${ansi.cyan}, ${ansi.reset}

the escape sequence, or empty when colours are off

${<configuration key>}

the configured value

${<key>:<default>}

<default> when the key has no value

An unknown placeholder becomes empty, and a ${ without its closing brace on the same line is printed as is. Vidocq appends its identity and context lines after the file, so the version survives any custom banner, unless the file places ${vidocq.identity} itself.

src/main/resources/vidocq-banner.txt
${ansi.cyan}  T O D O   S E R V I C E${ansi.reset}
  port ${vidocq.chappe.listener.default.port:8080} - ${app.name} ${app.version}

Build identity NEW

Every jar built with io.vidocq:vidocq-parent (Vidocq, Vauban, Cassini, Chappe, and the applications created by vidocq create, through vidocq-runtime-parent) carries a build identity file, META-INF/vidocq/build-info/<artifactId>.properties, written by git-commit-id-maven-plugin at the initialize phase:

#Generated by Git-Commit-Id-Plugin
git.build.time=2026-09-17T14\:02\:11Z
git.build.version=0.4.0-SNAPSHOT
git.commit.id.abbrev=9beafc47
git.commit.time=2026-09-15T14\:08\:34Z
git.dirty=false
  • git.dirty is true when tracked files had uncommitted changes; untracked files do not count.

  • git.build.time is the time of the build that wrote the file. On its own, the plugin leaves an existing file alone when only that time would change, so a build without mvn clean keeps the time of the first build of the same commit and dirty state. vidocq-runtime-parent (the Vidocq runtime modules and the applications created by vidocq create) deletes the file at the validate phase, so every build writes its own time; for a brick built directly from vidocq-parent, built can be that first build.

  • Releases have no git.build.time and stay byte-for-byte reproducible (project.build.outputTimestamp is the commit time in the release profile).

  • The file name carries the artifactId: on the class path, a shared name would make every jar read the first one found. The banner reads each file inside the jar of its own module.

  • -Dmaven.gitcommitid.skip=true builds without it; the banner then falls back to the jar date.

To find the commit a snapshot was built from, read the file in the jar, then look the commit up in the repository of that brick:

unzip -p ~/.m2/repository/io/vidocq/runtime/vidocq-runtime-core/0.4.0-SNAPSHOT/vidocq-runtime-core-0.4.0-SNAPSHOT.jar \
    META-INF/vidocq/build-info/vidocq-runtime-core.properties
git log -1 9beafc47

Startup report NEW

At the end of every boot, just before Vidocq - Started in, one INFO record says what has just started: the launch, the application layer, the configuration sources, the extensions, and a section for every library that contributes one. It is written for the moment something does not work — a key that nothing reads, an extension that did not start, a module that is not where it was expected — and for the bug report that has to say what ran. Vidocq - Started in stays the last line of a boot.

A packaged application gets the summary, one line per section:

The vidocq-runtime-cassini-rest-example jlink image (prod)
[INFO ][2026-09-18 14:18:32.141][main           ][i.v.r.c.r.StartupRecorder#log                ] : Vidocq startup report
  launch      prod (auto: no dev or test signal) | report summary | details -Dvidocq.startup.report=detailed
  layer       2 modules (boot-layer detection from io.vidocq.runtime.examples.rest)
  extensions  chappe-engine, rest-cassini, chappe-mount-config, chappe-bootstrap
  anomalies   none
[INFO ][2026-09-18 14:18:32.142][main           ][i.v.r.c.VidocqBootstrap#start                ] : Vidocq - Started in 112.956 ms (process running for 539 ms)

The first boot of a dev launch gets the detailed report:

The same example launched from its target/classes (dev), its library jar taken from the local repository
[INFO ][2026-09-18 14:18:49.712][main           ][i.v.r.c.r.StartupRecorder#log                ] : Vidocq startup report
  launch      dev (auto: target/classes with a pom.xml above) | report detailed |
              override -Dvidocq.launch.mode=prod
  vidocq      0.4.0-SNAPSHOT on Java 25.0.3+9-LTS
  phases      configure 20 ms | weaving 2 ms | scan 4 ms | beforeStart 0 ms | build 55 ms |
              extensions 24 ms | audit 0 ms | report 2 ms
layer         2 modules (boot-layer detection from io.vidocq.runtime.examples.rest)
  io.vidocq.runtime.examples.rest    target/classes                                                 directory
  io.vidocq.runtime.examples.extlib  ~/.m2/.../vidocq-runtime-external-rest-lib-0.4.0-SNAPSHOT.jar  jar
  weaving     none
configuration
  sources     SystemProperties 400, Environment 300, ExternalFile(absent) 250, PropertiesFile 100
  audited     vidocq.banner.*, vidocq.chappe.*, vidocq.console.*, vidocq.http.*, vidocq.launch.*,
              vidocq.log.*, vidocq.startup.*
extensions
  100    chappe-engine        onStart 0 ms   io.vidocq.runtime.extensions.essentials.chappe
  500    rest-cassini         onStart 1 ms   io.vidocq.runtime.extensions.jakartaee.core.cassini
  7000   chappe-mount-config  onStart 16 ms  io.vidocq.runtime.extensions.essentials.chappe
  10000  chappe-bootstrap     onStart 7 ms   io.vidocq.runtime.extensions.essentials.chappe  vidocq.chappe.*, vidocq.http.*
anomalies     none
[INFO ][2026-09-18 14:18:49.713][main           ][i.v.r.c.VidocqBootstrap#start                ] : Vidocq - Started in 113.185 ms (process running for 210 ms)

The whole report is one log record: one timestamp, printed in one piece, and nothing else can come between its lines. It is plain ASCII, indented with spaces and never coloured, so it reads the same in a terminal, a log file and a Windows console, through Vidocq’s console output or the JDK’s own formatter.

What it shows

Section summary detailed

launch

The launch mode and the signal it was read from, the report level, and how to see more: details -Dvidocq.startup.report=detailed.

The same, with how to force the other mode instead: override -Dvidocq.launch.mode=prod.

vidocq

—

The Vidocq version and the Java version, taken from the banner when it printed them.

phases

— (Started in gives the total)

The time each phase took: configure, weaving, scan, beforeStart, build, extensions (every onStart), audit, and report (the report itself, its contributors included).

layer

How many modules the application layer has and how they were found: boot-layer detection from <module>, or vidocq.app.path= followed by file names; no application layer without one.

The same count and origin; for vidocq.app.path, how many archives it lists instead of their names: 2 modules (vidocq.app.path, 2 archives). Then one line per module: its name, its archive and its kind (directory, jar, or jrt in a jlink image), the application’s module first, the others in the order of their archives. Then weaving: none, who weaves the beans of an IDE build (Vauban class loader, 1 planned bean: …​, or the load-time weaving agent), or failed (VAUBAN-009).

configuration

—

The configuration sources in lookup order, each with its ordinal; the provider whose sources replaced the native ones, if any; the namespaces whose keys are audited (VIDOCQ-CFG-003 — key read by nothing). Never a value.

extensions

The extensions, in the order they started.

One line per extension: its priority, its name, how long its onStart took (onStart failed, or not started after a failure), its module, then the namespaces it declares in configKeys(). The module is followed by its layer, (boot layer) or (application layer), only when the extensions are not all in the boot layer.

a contributed section

Its summary line, when it has one.

Its title and how long it took to write (slow beyond 50 ms, which is not an anomaly), its summary line, then everything else it wrote. See Contributing to the startup report.

anomalies

none, or how many and their codes: 2: VIDOCQ-CFG-001, VIDOCQ-CFG-003 (logged above), a code logged twice written VIDOCQ-CFG-003 x2.

One line per anomaly: its code and the first sentence of its message; after 50, …​ and N more.

The contributed sections follow the sections of the core: first those of the extensions that contribute one, in the order they started, then those of the other libraries. The report is made of what the boot already holds — the layer’s resolved modules, the loaded extensions, the configuration sources — and its own time is the report phase of the detailed report.

Report level

vidocq.startup.report chooses how much is printed: auto (the default), off, summary or detailed. An explicit level is honoured on every boot. auto follows the launch mode:

Launch auto gives

dev, first boot of the JVM

detailed

dev, every reload of the vidocq dev or vidocq:dev loop

summary

test

summary

prod

summary

An embedded deployment (VidocqBootstrap.configure(List): the Arquillian container, the TCK)

off

A dev launch shows everything once, then one line per section on every reload. An embedded deployment boots hundreds of times in one JVM, so it prints no report unless vidocq.startup.report asks for one — the banner is reduced to one line there too.

While the dev console is on NEW, the contributors are given the detailed level whatever the report prints, so that the console has every row even on a reload that logs the summary; what the log prints still follows the level above.

off only removes the report. The contributors are still called, and every anomaly is still logged as its own warning. A value that is none of the four is reported as VIDOCQ-CFG-001 — invalid value and read as auto. Like every key, it can be set with -D, an environment variable (VIDOCQ_STARTUP_REPORT) or vidocq.properties.

Anomalies

An anomaly is something wrong that does not stop the boot. Each one is its own WARNING record on the logger io.vidocq.startup.anomaly, logged the moment it is detected, its code first:

[WARN ][2026-09-18 14:19:46.775][main           ][i.v.r.c.r.StartupAnomalies#warn              ] : [VIDOCQ-CFG-003] Configuration key 'vidocq.startup.reprot' is read by nothing and has no effect. Known keys in this namespace: vidocq.startup.report

It is logged at every report level, off included, and before the report, so a boot that fails later still shows it, and a log pipeline can alert on its code. It is always one line: a line break or another control character in what it quotes, a configured value or a key name, becomes ?, so no anomaly can start a line that looks like another record. It is never cut. The report only recalls the codes, in its anomalies section. A library can report anomalies of its own, under its own codes, from its section of the report (Contributing to the startup report).

The core raises these codes.

VIDOCQ-CFG-001 — invalid value

vidocq.startup.report or vidocq.launch.mode has a value that it does not accept. The key is read as auto, and the boot goes on.

[VIDOCQ-CFG-001] Invalid value 'detaild' for vidocq.startup.report (auto, off, summary, detailed): using auto

It is logged while the configuration is read, before the banner. Fix the value; the message lists the accepted ones.

VIDOCQ-CFG-002 — audit failed

The audit of the configured keys could not run: a configuration source could not list its keys, or an extension’s configKeys() threw. Keys that nothing reads are then not reported on this boot, and the boot goes on.

[VIDOCQ-CFG-002] Configuration key audit failed: java.lang.IllegalStateException: ...; keys that nothing reads are not reported

VIDOCQ-CFG-003 — key read by nothing

A configured vidocq.* key, under a namespace that the core or a loaded extension claims, is read by nothing: the application keeps the default without a word, and the mistake stays invisible whenever the configured value happens to be the default. The message lists the keys known in that namespace.

[VIDOCQ-CFG-003] Configuration key 'vidocq.startup.reprot' is read by nothing and has no effect. Known keys in this namespace: vidocq.startup.report

The core claims vidocq.log., vidocq.console., vidocq.banner., vidocq.launch. and vidocq.startup.; every extension claims the namespaces of its configKeys() (Configuration keys audit). The detailed report lists the audited namespaces. Build-time keys such as vidocq.mainClass and the vidocq.dev.* keys are never reported.

VAUBAN-009 — load-time weaving failed

The beans of the bean index need load-time weaving, as those of an IDE build do, and it could not be prepared: vauban-weaver is not on the module path, META-INF/vauban-beans.list cannot be read, or the JVM refused the weaving agent. The message is Vauban’s reason; the layer section of the detailed report shows weaving failed (VAUBAN-009), and the beans that needed weaving may then fail the deployment.

[VAUBAN-009] Load-time weaving could not be installed (...). Either build with Maven/Gradle (build-time weaving), run with -javaagent:vauban-weaver.jar=<plan>, or add the (ProxyLink) constructors manually.

Build with Maven or Gradle, which weave at compile time, or launch through a @VidocqMain trampoline, whose application layer weaves without an agent.

VIDOCQ-RPT-001 — contributor failed

A startup report contributor could not be loaded or created, has no id (null or blank), threw from id(), title() or order(), or threw a RuntimeException or a LinkageError from contribute. The record carries the stack trace. Its section is left out, the anomalies it already reported stay, and the boot goes on. Any other Error is not caught.

[VIDOCQ-RPT-001] Startup report contributor 'translate' (com.example.translate.report.TranslateReport) failed; its section is skipped

VIDOCQ-RPT-002 — duplicate section id

Two contributors of different classes use the same id: the second one is skipped. So is a contributor whose id names a section of the core (launch, vidocq, phases, layer, configuration, extensions or anomalies), or one of the dev console's own panels, startup, config, cdi, logs, tests and jvm NEW. The same class found twice is not an anomaly: see Layer twins.

[VIDOCQ-RPT-002] Contributors com.example.translate.report.TranslateReport and com.acme.i18n.TranslateReport both use id 'translate'; com.acme.i18n.TranslateReport is skipped

When the boot fails

A boot that fails, in configure() or in start(), logs one WARNING record on io.vidocq.startup before the exception goes on, at every report level: what was running, the time since the bootstrap was created, the exception class and the codes of the anomalies already logged. In a dev launch, the partial report follows in the same record, unless the report level is off:

A dev launch whose port is already taken (shortened)
[WARN ][2026-09-18 14:19:58.822][main           ][i.v.r.c.r.StartupRecorder#logFailure         ] : Vidocq startup failed in onStart chappe-bootstrap after 1129 ms (io.vidocq.chappe.api.ChappeException$ServerException); anomalies already logged: none
Vidocq startup report (partial)
  launch      dev (auto: target/classes with a pom.xml above) | report detailed |
              override -Dvidocq.launch.mode=prod
  vidocq      0.4.0-SNAPSHOT on Java 25.0.3+9-LTS
  phases      configure 19 ms | weaving 2 ms | scan 4 ms | beforeStart 0 ms | build 54 ms
...
extensions
  100    chappe-engine        onStart 0 ms    io.vidocq.runtime.extensions.essentials.chappe
  500    rest-cassini         onStart 0 ms    io.vidocq.runtime.extensions.jakartaee.core.cassini
  7000   chappe-mount-config  onStart 15 ms   io.vidocq.runtime.extensions.essentials.chappe
  10000  chappe-bootstrap     onStart failed  io.vidocq.runtime.extensions.essentials.chappe  vidocq.chappe.*, vidocq.http.*
anomalies     none
Exception in thread "main" io.vidocq.chappe.api.ChappeException$ServerException: Failed to bind to 127.0.0.1:18082
	at io.vidocq.chappe.core@0.4.0-SNAPSHOT/io.vidocq.chappe.core.ChappeServer.start(ChappeServer.java:102)
	...
Caused by: java.net.BindException: Address already in use

What was running is a phase — configure, weaving, scan, beforeStart, build, extensions, audit, report — or, more precisely, the extension or the contributor it was calling: configure rest-cassini, beforeStart rest-cassini, onStart chappe-bootstrap, contributor mcp. The exception is the one that was thrown, untouched: not wrapped, nothing added to it, with its own stack trace. The contributors are not called when the boot fails before the report, since the container and the routes may be incomplete: a partial report has the sections of the core only.

The report of a boot that went well never fails it: if the report cannot be produced, one WARNING, Startup report skipped: …​, says why, and the boot goes on.

What is never printed

The report goes to logs that are shared, pasted into issues and kept. It prints names and counts, never what an application keeps to itself:

  • No configuration value. The configuration section names the sources and their ordinals, never what they hold. The only configured values the core’s sections show are the launch mode, when vidocq.launch.mode or a vidocq.profile named after a mode set it, the report level, and vidocq.app.path, whose archives the summary prints as file names. A contributor shows a configured value that may carry a secret, such as a JDBC URL or a user name, in a dev launch only, without its credentials NEW; outside it, it says what the value is, such as the database kind (the rule).

  • No system property, environment variable or JVM argument as it is — a -D can carry a password. Only what is derived from them: IntelliJ agent, target/classes with a pom.xml above.

  • No secret. A contributor reports a secret as configured or not configured; the API has no way to pass its value.

  • No directory in the summary, the default of a packaged application: the core’s sections print file names only (vidocq.app.path=…​/app). The detailed report prints a path relative to the working directory, or under ~ for the home directory, or else absolute; a path of more than four names keeps its first two and its last one: ~/.m2/…​/langchain4j-cdi-mcp-server-1.4.0-SNAPSHOT.jar.

  • No forged line. Every value is cleaned before it is printed: control characters — line breaks, ESC, and the invisible formatting characters such as bidirectional overrides — become ?, so no value can start a line that looks like another record. A value longer than 200 characters is cut and ends with …​; a list, a table or the anomalies of the detailed report stop after 50 items with …​ and N more. The anomaly records are cleaned the same way, and never cut.

Loggers

Logger Records

io.vidocq.startup

The report, one INFO record; the failure of a boot, one WARNING record.

io.vidocq.startup.anomaly

One WARNING record per anomaly, [CODE] …​.

To hide the report, prefer vidocq.startup.report=off, which keeps everything else. Through the logging configuration, io.vidocq.startup.level=WARNING hides the report and keeps the warnings; OFF would silence the failure record and, since io.vidocq.startup.anomaly inherits the level of io.vidocq.startup, the anomalies as well.

Configuration

Key Default Description

vidocq.startup.report NEW

auto

auto, off, summary or detailed (Report level). Another value is reported as VIDOCQ-CFG-001 — invalid value and means auto.

vidocq.launch.mode NEW

auto

The launch mode that auto follows, detected unless forced (Launch mode NEW).

Console logging NEW

When the application has not configured logging, Vidocq prints one aligned line per record on standard output. The JDK default would print two lines per record on standard error, which an IDE shows in red. Vidocq keeps logging through System.Logger and java.util.logging: only the console handler of the root logger changes.

The vidocq-runtime-cassini-rest-example boot (messages shortened)
[INFO ][2026-09-17 18:10:10.880][main           ][i.v.r.c.VidocqAppLayer#installLayer          ] : Application layer ready: modules [...]
[INFO ][2026-09-17 18:10:10.885][main           ][i.v.r.c.VidocqBootstrap#configure            ] : Vidocq - Configuration phase
[INFO ][2026-09-17 18:10:10.890][main           ][i.v.r.c.ExtensionLoader#load                 ] : Discovered 4 extension(s):
[INFO ][2026-09-17 18:10:10.937][main           ][CassiniScopeExtension#addPathDefaultScope    ] :   @Path class ... defaulting to @RequestScoped
[INFO ][2026-09-17 18:10:10.987][main           ][i.v.r.e.e.c.ChappeServerBootstrap#onStart    ] : Chappe listener 'default' started on http://localhost:8080/
[INFO ][2026-09-17 18:10:10.989][main           ][i.v.r.c.VidocqBootstrap#start                ] : Vidocq - Started in 102.615 ms (process running for 183 ms)

Format

[LEVEL][timestamp][thread][source] : message, one line per record:

Column Content

Level

ERROR, WARN, INFO, CONF, DEBUG or TRACE, from JUL’s SEVERE, WARNING, INFO, CONFIG, FINE and FINER/FINEST. These are the System.Logger names Vidocq’s code uses, never localized (no INFOS on a French JVM). Padded to 5 columns.

Timestamp

yyyy-MM-dd HH:mm:ss.SSS, in local time.

Thread

The thread that logged, padded to 15 columns. A longer name keeps its end after a ~. An unnamed virtual thread shows virtual-<id>.

Source

class#method, padded to 45 columns. Packages are always abbreviated to their first letter (i.v.r.c.VidocqBootstrap#configure), so a class always looks the same. When that is still too wide, the packages are dropped (CassiniScopeExtension#addProviderDefaultScope), then the method is cut with a ~. A class name that does not fit alone keeps its end after a ~. The logger name stands in when the class is unknown.

Message

Formatted with its {0} parameters. The extra lines of a message and the stack trace follow unchanged.

Every level goes to standard output, flushed after each record: the logs stay in one stream, in order, with what the application prints.

When Vidocq steps back

Vidocq replaces the JDK’s default console handler first thing in Vidocq.main, in both Vidocq.run overloads and in VidocqBootstrap.create(), and only when the application has chosen nothing. The JDK default stays untouched when any of these holds:

  • java.util.logging.config.file, java.util.logging.config.class, java.util.logging.manager or java.util.logging.SimpleFormatter.format is set;

  • System.LoggerFinder is not the JDK’s java.util.logging one, for example with an SLF4J or Log4j System.Logger provider on the module path;

  • the root logger’s handlers are not exactly one ConsoleHandler with a SimpleFormatter, for example after SLF4JBridgeHandler.install() or LogManager.readConfiguration(…​);

  • vidocq.log.console=jdk.

An application that calls LogManager.readConfiguration after the boot still replaces Vidocq’s handler.

Configuration

Key Default Description

vidocq.log.console NEW

auto

auto: Vidocq’s console output, unless the application configured logging (above). jdk: the JDK’s console handler, untouched.

vidocq.console.color NEW

auto

Colours of the level: auto, always or never.

The first install happens before the configuration is loaded, so only -D system properties and environment variables (VIDOCQ_LOG_CONSOLE, VIDOCQ_CONSOLE_COLOR) are visible then. Once vidocq.properties is loaded, both keys apply from the configuration as well: vidocq.log.console=jdk there puts the JDK handler back before any extension is configured. An unknown value logs a warning and falls back to auto.

Colours

Only the level is coloured: ERROR bold red, WARN yellow, INFO green, CONF cyan, DEBUG and TRACE faint.

  • A non-empty NO_COLOR environment variable always wins: no colour, even with always.

  • always colours the level wherever the output goes; never never does.

  • auto needs TERM other than dumb, and either IntelliJ’s Run console or a terminal on an operating system other than Windows. IntelliJ’s Run console is not a terminal but decodes ANSI escapes; Vidocq recognises it by the -javaagent:…idea_rt.jar argument IntelliJ adds to the JVM. A terminal means System.console() exists and isTerminal() is true, so output piped to a file or a container log stays plain.

Keeping full control

Vidocq never overrides a logging setup the application chose. To keep the JDK output or to use your own:

  • logging.properties — pass -Djava.util.logging.config.file=conf/logging.properties, or load it with LogManager.readConfiguration(…​).

  • SLF4J, Logback, Log4j — put a System.Logger provider on the module path (slf4j-jdk-platform-logging, log4j-jpl), or route java.util.logging with SLF4JBridgeHandler.removeHandlersForRootLogger() then SLF4JBridgeHandler.install().

  • The JDK default, unchanged — set vidocq.log.console=jdk (-Dvidocq.log.console=jdk, VIDOCQ_LOG_CONSOLE=jdk or vidocq.properties).

Next steps

  • Internals — detailed sequence with diagram

  • SPI — interfaces consumed by core