Skip to main content

Management Surface (Actuator-style endpoints)

See also: Health / Readiness / Liveness Probes — the health half of the management surface, and REST Server — Mixins and Multi-Mount Paths — the @Rest(mixins=...) composition primitives this page builds on.

Starting with 10.0.0, Juneau ships a coherent, discoverable Actuator-style management surface while preserving Juneau's a-la-carte composability. Every endpoint exists in two independently-mountable flavors — a composable *Mixin and a routed *Resource — sharing one worker, and a convenience BasicActuatorGroup assembles them under a single configurable path prefix.

Endpoint catalog​

EndpointWhat it doesModuleDefault exposure
GET /infoApplication manifest / build / version / git metadatajuneau-microserviceon
GET /loggers, GET /loggers/{name}Read logger levels (JUL default; Logback/Log4j2 opt-in)juneau-rest-serveron
PUT/POST /loggers/{name}Set a logger level at runtimejuneau-rest-serverdeny-by-default
GET /metricsPrometheus scrape from a Micrometer registryjuneau-rest-server-metrics-micrometeron (501 if no registry)
GET /healthz, /readyz, /livezHealth / readiness / liveness probesjuneau-rest-serveron
GET /threaddumpThreadMXBean thread dumpjuneau-rest-serverdeny-by-default
GET /heapdumpHotSpot .hprof heap dumpjuneau-rest-serverdeny-by-default

Endpoints are split across modules by dependency: /loggers (JUL backend) and the JDK-only /threaddump//heapdump need only the REST server; non-JUL /loggers backends (Logback/Log4j2) need juneau-rest-server-management-logging; /metrics needs Micrometer; /info needs the microservice runtime that resolves the application ManifestFile.

BasicActuatorGroup — one-liner assembly​

import org.apache.juneau.microservice.management.*;
import org.apache.juneau.rest.server.*;

// Mount the whole surface under /actuator.
@Rest(children=BasicActuatorGroup.class)
public class RootResources extends BasicRestServletGroup { /* ... */ }

BasicActuatorGroup composes HealthMixin and DumpsMixin under a single prefix. The prefix defaults to /actuator and is configurable via the juneau.actuator.path system property (resolved through the standard $S{...} SVL var):

java -Djuneau.actuator.path=/manage -jar myapp.jar

/info and /loggers are off by default — unlike health, they are not mounted on the bare group, since InfoMixin discloses the full manifest and LoggersMixin's read side discloses logger topology, neither behind any auth. Re-enable either (or both) a-la-carte on a subclass:

@Rest(mixins={InfoMixin.class, LoggersMixin.class})
public class MyActuatorGroup extends BasicActuatorGroup { /* ... */ }

Mixins declared on a subclass are additive to the ones inherited from BasicActuatorGroup, so HealthMixin/DumpsMixin stay mounted too. With no such subclass, /info and /loggers return 404 under the group's prefix.

/metrics is not assembled into the group — it lives in the juneau-rest-server-metrics-micrometer module (which juneau-microservice does not depend on). Add MetricsMixin a-la-carte to your host resource when that module is on the classpath.

Composition is convenience, not the only way in — every endpoint remains independently mountable a-la-carte via its standalone *Mixin/*Resource flavor.

Exposure / security policy​

The surface is on-by-default-with-gating: non-sensitive reads are exposed; mutating/sensitive operations are deny-by-default and require an explicit opt-in bean. No auth provider is auto-wired (explicit-over-magic) — wire your own guard (e.g. a BearerTokenGuard) on top.

There are two complementary layers:

  • Functional gate (deny-by-default): a settings bean (LoggersSettings, DumpsSettings) controls whether the sensitive capability is enabled at all. Off until you opt in.
  • Authz gate (who may call it): apply a guard to the sensitive mixin at composition time with @Rest(mixinDefs=@Mixin(type=..., guards=...)) — the host-side override from Mixin Sub-Contexts. For example, to require AdminGuard on the diagnostic dumps:
import org.apache.juneau.rest.server.*;
import org.apache.juneau.rest.server.management.*;

@Rest(mixinDefs=@Mixin(type=DumpsMixin.class, guards=AdminGuard.class))
public class MyManagedResource extends BasicRestServlet { ... }

This guards the dump endpoints without subclassing DumpsMixin or editing it. Combine the two layers: DumpsSettings enables the dumps, @Mixin(guards=...) restricts who can call them.

/loggers set-level (deny-by-default)​

The read endpoints are always on; the PUT/POST set-level endpoints respond 403 unless a LoggersSettings bean enables writes:

import org.apache.juneau.commons.inject.*;
import org.apache.juneau.rest.server.management.*;

@Bean
LoggersSettings loggersSettings() {
return LoggersSettings.create().enableWrite().build();
}

/threaddump + /heapdump (deny-by-default)​

Both diagnostics respond 403 unless a DumpsSettings bean opts them in (independently). /heapdump additionally responds 501 on a JVM without HotSpot heap-dump support, and streams a self-deleting .hprof temp file when enabled:

@Bean
DumpsSettings dumpsSettings() {
return DumpsSettings.create()
.enableThreadDump()
.enableHeapDump()
.build();
}

Why deny-by-default? A thread dump can leak sensitive runtime state; a heap dump can be large and contain secrets. Both stay off until an operator explicitly opts in.

/info and manifest stamping​

/info renders the running application's META-INF/MANIFEST.MF main attributes — sourced from Microservice.getManifest() — as a JSON map. With no manifest registered it degrades cleanly to {}.

To surface build/version/git metadata, stamp them into your application jar's manifest at build time. /info reads your app jar's manifest, so the stamping goes in your build, not Juneau's. A typical Maven recipe:

<!-- 1. Capture git metadata into Maven properties. -->
<plugin>
<groupId>io.github.git-commit-id</groupId>
<artifactId>git-commit-id-maven-plugin</artifactId>
<executions>
<execution><goals><goal>revision</goal></goals></execution>
</executions>
<configuration>
<generateGitPropertiesFile>false</generateGitPropertiesFile>
</configuration>
</plugin>

<!-- 2. Stamp them (plus version/build-time) into MANIFEST.MF. -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifest>
<addDefaultImplementationEntries>true</addDefaultImplementationEntries>
</manifest>
<manifestEntries>
<Build-Time>${maven.build.timestamp}</Build-Time>
<Git-Commit-Id>${git.commit.id.abbrev}</Git-Commit-Id>
<Git-Branch>${git.branch}</Git-Branch>
</manifestEntries>
</archive>
</configuration>
</plugin>

addDefaultImplementationEntries contributes Implementation-Title/Implementation-Version; the manifestEntries add the git/build fields. All of them then appear in /info:

{
"Implementation-Version": "1.4.2",
"Build-Time": "2026-06-17T00:00:00Z",
"Git-Commit-Id": "abc1234",
"Git-Branch": "main"
}

/metrics — Micrometer scrape​

/metrics strictly consumes a consumer-provided MeterRegistry resolved from the bean store — it never auto-registers a default (consistent with micrometer-core being a provided dependency). It renders a Prometheus text scrape when a PrometheusMeterRegistry is present and degrades to 501 Not Implemented otherwise.

@Bean
MeterRegistry meterRegistry() {
return new PrometheusMeterRegistry(PrometheusConfig.DEFAULT);
}

Mount the endpoint by mixing MetricsMixin into your host resource (the juneau-rest-server-metrics-micrometer module must be on the classpath).

/loggers backends (JUL, Logback, Log4j2)​

The /loggers endpoint drives a LogBackend. By default that is java.util.logging (JulLogBackend) — the backend Juneau itself configures, and the only one bundled with juneau-rest-server (the core stays dependency-free). Applications that route logging through SLF4J→Logback or Log4j2 select the matching backend explicitly.

Explicit backend selection (no auto-detection)​

Consistent with Juneau's explicit-over-magic stance, you declare the backend on a LoggersSettings bean — the endpoint never classpath-scans for, and then silently drives, a backend you didn't choose. The Logback / Log4j2 adapters ship in the opt-in juneau-rest-server-management-logging module (the backends are declared provided, so they never leak transitively onto your classpath):

<dependency>
<groupId>org.apache.juneau</groupId>
<artifactId>juneau-rest-server-management-logging</artifactId>
<version>${juneau.version}</version>
</dependency>
<!-- plus your chosen backend at the version you want, e.g. ch.qos.logback:logback-classic -->
@Bean
public LoggersSettings loggersSettings() {
return LoggersSettings.create()
.enableWrite() // opt in to the set-level endpoints (deny-by-default)
.backend(new LogbackLogBackend()) // or new Log4j2LogBackend() — explicit, no auto-detect
.build();
}

When no LoggersSettings bean is registered (or none declares a backend), the endpoint drives JUL.

Same response shape across backends​

The endpoint's contract is identical regardless of backend:

  • Read reports each logger's configured level — the level set on the logger itself, or the empty string when it inherits from an ancestor (Log4j2 LoggerConfigs always carry a concrete level, so the empty-string "inherited" marker only appears for JUL/Logback). The root logger is keyed ROOT.
  • Set is process-lifetime-only (JUL Logger.setLevel, Logback Logger.setLevel, Log4j2 Configurator.setLevel) — it does not rewrite the backend's configuration file. A blank/empty level clears the logger's own level so it inherits again.

Level names are backend-native (JUL FINE/INFO/…; Logback & Log4j2 DEBUG/INFO/WARN/…). An invalid level name fails with 400.

SLF4J is a facade — pick the real backend​

SLF4J itself emits nothing; the levels live in the bound backend. Select the backend that actually emits: LogbackLogBackend for SLF4J→Logback, Log4j2LogBackend for Log4j2 (including SLF4J→Log4j2). When the SLF4J binding is slf4j-jdk14 (SLF4J→JUL), the backend is JUL, so the default JulLogBackend is correct. Custom backends can implement LogBackend directly.