// Core Info Types
org.springframework.boot.actuate.info.Info
org.springframework.boot.actuate.info.Info.Builder
org.springframework.boot.actuate.info.InfoContributor
org.springframework.boot.actuate.info.InfoPropertiesInfoContributor
org.springframework.boot.actuate.info.InfoEndpoint
// Built-in Contributors
org.springframework.boot.actuate.info.BuildInfoContributor
org.springframework.boot.actuate.info.GitInfoContributor
org.springframework.boot.actuate.info.JavaInfoContributor
org.springframework.boot.actuate.info.OsInfoContributor
org.springframework.boot.actuate.info.ProcessInfoContributor
org.springframework.boot.actuate.info.EnvironmentInfoContributor
org.springframework.boot.actuate.info.SslInfoContributor
// Helper Contributors
org.springframework.boot.actuate.info.SimpleInfoContributor
org.springframework.boot.actuate.info.MapInfoContributorWhat information needs exposure?
│
├─ Build metadata (version, artifact, timestamp)?
│ └─ BuildInfoContributor (auto-configured)
│
├─ Git commit info?
│ └─ GitInfoContributor (auto-configured)
│
├─ Runtime environment?
│ ├─ Java version → JavaInfoContributor (auto-configured)
│ ├─ OS details → OsInfoContributor (opt-in)
│ └─ Process PID/uptime → ProcessInfoContributor (opt-in)
│
├─ SSL certificates?
│ └─ SslInfoContributor
│
├─ Static properties?
│ └─ EnvironmentInfoContributor (info.* prefix)
│
└─ Custom data?
├─ Single value → SimpleInfoContributor
├─ Multiple values → MapInfoContributor
└─ Dynamic/computed → Custom InfoContributor| Contributor | Auto-Configured? | Manual Bean? | Configuration Property |
|---|---|---|---|
| JavaInfoContributor | Yes | NO | management.info.java.enabled |
| OsInfoContributor | Yes | NO | management.info.os.enabled |
| ProcessInfoContributor | Yes | NO | management.info.process.enabled |
| EnvironmentInfoContributor | Yes | NO | management.info.env.enabled |
| BuildInfoContributor | Conditional | NO | Needs build-info.properties |
| GitInfoContributor | Conditional | NO | Needs git.properties |
| Custom InfoContributor | No | YES | N/A |
// ✓ Custom contributor
@Component
public class MyInfoContributor implements InfoContributor {
@Override
public void contribute(Info.Builder builder) {
builder.withDetail("custom", data);
}
}
// ❌ DON'T manually create auto-configured contributors
@Bean
public JavaInfoContributor javaInfoContributor() { // DON'T DO THIS
return new JavaInfoContributor();
}Create custom InfoContributor for:
Use built-in contributors for:
PATTERN: Custom contributor
// ✓ Custom data requires custom contributor
@Component
public class FeatureFlagContributor implements InfoContributor {
private final FeatureFlags features;
@Override
public void contribute(Info.Builder builder) {
builder.withDetail("features", Map.of(
"newUI", features.isNewUIEnabled(),
"betaFeatures", features.isBetaEnabled()
));
}
}ANTI-PATTERN: Manually creating auto-configured contributors
// ❌ WRONG - JavaInfoContributor is auto-configured
@Configuration
public class WrongConfiguration {
@Bean
public JavaInfoContributor javaInfoContributor() {
return new JavaInfoContributor(); // Creates duplicate!
}
}
// ✓ CORRECT - Use configuration property
management.info.java.enabled=truePATTERN: Using environment properties
# ✓ Static data via properties
info:
app:
name: My Application
description: Customer Management System
company:
name: ACME Corp
website: https://acme.comANTI-PATTERN: Custom contributor for static data
// ❌ Overkill for static data
@Component
public class StaticInfoContributor implements InfoContributor {
@Override
public void contribute(Info.Builder builder) {
builder.withDetail("app", Map.of(
"name", "My Application" // Should be in application.yml
));
}
}PATTERN: Structured nested data
// ✓ Well-organized hierarchical structure
builder.withDetail("database", Map.of(
"vendor", "PostgreSQL",
"version", "15.2",
"pool", Map.of(
"max", 20,
"active", 5
)
));The info contributor framework provides an extensible system for contributing application information to the info endpoint. It includes built-in contributors for build metadata, git information, Java runtime details, OS information, SSL certificates, and custom data.
CRITICAL: Most built-in InfoContributors are automatically configured by Spring Boot Actuator and should NOT be manually registered as beans.
The following contributors are automatically registered by Spring Boot:
management.info.java.enabled=true (default: true)management.info.os.enabled=true (default: false)management.info.process.enabled=true (default: false)info.* propertiesMETA-INF/build-info.properties existsgit.properties existsTo add custom information, implement InfoContributor and register as Spring bean:
@Component
public class CustomInfoContributor implements InfoContributor {
@Override
public void contribute(Info.Builder builder) {
builder.withDetail("custom", Map.of("key", "value"));
}
}/**
* Application information container.
*
* Thread-safe: Immutable after build
* Nullability: Details values can be null
* Since: Spring Boot 1.4+
*/
public final class Info {
/**
* Get all details as map.
*
* @return Immutable map of details (never null)
*/
public Map<String, Object> getDetails();
/**
* Get specific detail.
*
* @param id Detail identifier
* @return Detail value or null if not present
*/
public @Nullable Object get(String id);
/**
* Get typed detail.
*
* @param id Detail identifier
* @param type Expected type
* @return Typed value or null
* @throws IllegalStateException if value is wrong type
*/
public <T> @Nullable T get(String id, @Nullable Class<T> type);
/**
* Builder for Info instances.
*/
public static class Builder {
/**
* Add single detail.
*
* @param key Detail key (non-null)
* @param value Detail value (nullable)
* @return This builder
*/
public Builder withDetail(String key, Object value);
/**
* Add multiple details.
*
* @param details Map of details (non-null)
* @return This builder
*/
public Builder withDetails(Map<String, Object> details);
/**
* Build immutable Info.
*
* @return Info instance
*/
public Info build();
}
}
/**
* Strategy for contributing info details.
*
* Thread-safe: Implementations should be thread-safe
* Since: Spring Boot 1.4+
*/
@FunctionalInterface
public interface InfoContributor {
/**
* Contribute details to Info.Builder.
*
* @param builder Builder to contribute to (non-null)
*/
void contribute(Info.Builder builder);
}
/**
* Base class for InfoContributors that expose InfoProperties.
*
* Provides infrastructure for exposing build metadata and git information
* with support for SIMPLE and FULL exposure modes. This is an abstract base
* class that BuildInfoContributor and GitInfoContributor extend to provide
* consistent property exposure semantics.
*
* The class handles property extraction, mode-based filtering, and content
* post-processing. Subclasses must implement toSimplePropertySource() to
* define which properties are exposed in SIMPLE mode.
*
* Thread-safe: Yes
* Since: Spring Boot 1.4.0
*
* @param <T> the type of InfoProperties to expose (BuildProperties or GitProperties)
*
* Example Usage:
* <pre>
* // BuildInfoContributor extends this class
* public class BuildInfoContributor extends InfoPropertiesInfoContributor<BuildProperties> {
* public BuildInfoContributor(BuildProperties properties) {
* super(properties, Mode.FULL);
* }
*
* @Override
* protected PropertySource<?> toSimplePropertySource() {
* Properties props = new Properties();
* copyIfSet(props, "group");
* copyIfSet(props, "artifact");
* copyIfSet(props, "version");
* return new PropertiesPropertySource("build", props);
* }
* }
* </pre>
*/
public abstract class InfoPropertiesInfoContributor<T extends InfoProperties> implements InfoContributor {
/**
* Construct InfoPropertiesInfoContributor.
*
* @param properties Properties to expose (non-null)
* @param mode Exposure mode (non-null) - SIMPLE or FULL
* @throws IllegalArgumentException if properties or mode is null
*/
protected InfoPropertiesInfoContributor(T properties, Mode mode);
/**
* Get the managed properties.
*
* @return Properties instance (never null)
*/
protected final T getProperties();
/**
* Get the exposure mode.
*
* @return Mode (never null) - either SIMPLE or FULL
*/
protected final Mode getMode();
/**
* Return PropertySource for SIMPLE mode.
* Subclasses must implement this to define simple mode behavior.
*
* In SIMPLE mode, only a predefined set of core properties should be exposed.
* For example, BuildInfoContributor exposes: group, artifact, name, version, time.
* GitInfoContributor exposes: branch, commit.id (short), commit.time.
*
* @return PropertySource for simple mode (non-null)
*/
protected abstract PropertySource<?> toSimplePropertySource();
/**
* Generate content to contribute.
* Extracts and post-processes property content based on mode.
*
* This method orchestrates the content generation by:
* 1. Getting the appropriate PropertySource (simple or full)
* 2. Extracting content from PropertySource
* 3. Post-processing the content
*
* @return Content map (never null, may be empty)
*/
protected Map<String, Object> generateContent();
/**
* Extract raw content from PropertySource.
*
* Uses Spring Boot's Binder to convert PropertySource into a nested map structure.
* The resulting map can contain nested maps for hierarchical properties.
*
* @param propertySource Source to extract from (non-null)
* @return Extracted content (never null, may be empty)
*/
protected Map<String, Object> extractContent(PropertySource<?> propertySource);
/**
* Post-process extracted content.
* Override to customize content before exposure.
*
* Common use cases:
* - Converting string timestamps to Instant objects
* - Formatting values for display
* - Removing or masking sensitive data
*
* Default implementation does nothing.
*
* @param content Content to modify (non-null) - modified in place
*/
protected void postProcessContent(Map<String, Object> content);
/**
* Get PropertySource based on mode.
*
* Returns full PropertySource if mode is FULL, otherwise returns the
* simple PropertySource from toSimplePropertySource().
*
* @return PropertySource (never null)
*/
protected PropertySource<?> toPropertySource();
/**
* Copy property to target if set.
*
* Utility method to copy a property from the managed properties to a
* target Properties object, but only if the property has a non-empty value.
*
* @param target Target properties (non-null)
* @param key Property key (non-null)
*/
protected void copyIfSet(Properties target, String key);
/**
* Replace content value if present and non-null.
*
* Utility method to replace a value in the content map only if:
* 1. The key already exists in the content
* 2. The new value is not null
*
* This is useful in postProcessContent() to replace string values with
* typed objects (e.g., replacing timestamp strings with Instant objects).
*
* @param content Content map (non-null)
* @param key Key to replace (non-null)
* @param value New value (nullable) - no replacement if null
*/
protected void replaceValue(Map<String, Object> content, String key, @Nullable Object value);
/**
* Get nested map or empty map.
*
* Utility method to safely access nested maps in the content structure.
* Returns an empty map if the key doesn't exist or the value is null.
*
* @param map Parent map (non-null)
* @param key Nested map key (non-null)
* @return Nested map or empty (never null)
* @throws ClassCastException if value exists but is not a Map
*/
protected Map<String, Object> getNestedMap(Map<String, Object> map, String key);
/**
* Defines property exposure mode.
*
* The mode controls how much information from InfoProperties is exposed
* in the info endpoint response.
*/
public enum Mode {
/**
* Expose all properties including custom ones.
*
* In FULL mode, all properties from the underlying InfoProperties are exposed,
* including any custom properties added to build-info.properties or git.properties.
* This provides complete transparency but may expose more data than desired.
*
* GitInfoContributor uses FULL mode when configured with:
* management.info.git.mode=full
*/
FULL,
/**
* Expose only core predefined properties.
*
* In SIMPLE mode, only a well-defined subset of core properties is exposed.
* This provides a consistent, predictable info structure and avoids exposing
* potentially sensitive custom properties.
*
* GitInfoContributor uses SIMPLE mode by default:
* management.info.git.mode=simple (default)
*/
SIMPLE
}
}Spring Boot Actuator provides several built-in InfoContributor implementations for exposing runtime information.
These contributors extend InfoPropertiesInfoContributor to expose build and git metadata.
/**
* Exposes BuildProperties from build-info.properties.
*
* Auto-configured when META-INF/build-info.properties exists on the classpath.
* The build-info.properties file is generated by Spring Boot Maven/Gradle plugins.
*
* Exposes information under the "build" key in the info endpoint:
* - group: Maven/Gradle group ID
* - artifact: Artifact ID
* - name: Project name
* - version: Project version
* - time: Build timestamp (as Instant)
* - Any additional custom properties
*
* Thread-safe: Yes
* Auto-configured: Yes (when build-info.properties exists)
* Configuration: N/A (always uses FULL mode)
* Since: Spring Boot 1.4.0
*
* Example Response:
* {
* "build": {
* "group": "com.example",
* "artifact": "my-app",
* "name": "My Application",
* "version": "1.0.0",
* "time": "2025-12-19T10:30:00Z"
* }
* }
*/
public class BuildInfoContributor extends InfoPropertiesInfoContributor<BuildProperties> {
/**
* Create BuildInfoContributor.
*
* @param properties Build properties loaded from build-info.properties (non-null)
*/
public BuildInfoContributor(BuildProperties properties);
/**
* Contribute build information to builder.
* Adds a "build" detail with all build properties.
*
* @param builder Builder to contribute to (non-null)
*/
@Override
public void contribute(Info.Builder builder);
/**
* Return PropertySource for SIMPLE mode.
* Includes: group, artifact, name, version, time.
*
* @return PropertySource with core build properties (non-null)
*/
@Override
protected PropertySource<?> toSimplePropertySource();
/**
* Post-process content to convert time string to Instant.
*
* @param content Content map to modify (non-null)
*/
@Override
protected void postProcessContent(Map<String, Object> content);
}
/**
* Exposes GitProperties from git.properties.
*
* Auto-configured when git.properties exists on the classpath.
* The git.properties file is generated by git-commit-id-plugin.
*
* Supports two modes:
* - SIMPLE (default): Exposes branch, commit.id (short), commit.time
* - FULL: Exposes all git properties including custom ones
*
* Exposes information under the "git" key in the info endpoint.
*
* Thread-safe: Yes
* Auto-configured: Yes (when git.properties exists)
* Configuration: management.info.git.mode (simple or full)
* Since: Spring Boot 1.4.0
*
* Example Response (SIMPLE mode):
* {
* "git": {
* "branch": "main",
* "commit": {
* "id": "abc1234",
* "time": "2025-12-19T10:15:00Z"
* }
* }
* }
*
* Example Response (FULL mode):
* {
* "git": {
* "branch": "main",
* "commit": {
* "id": "abc1234567890def",
* "id.abbrev": "abc1234",
* "time": "2025-12-19T10:15:00Z",
* "message": {
* "full": "Add new feature",
* "short": "Add new feature"
* },
* "user": {
* "name": "John Doe",
* "email": "john@example.com"
* }
* },
* "build": {
* "time": "2025-12-19T10:30:00Z",
* "version": "1.0.0"
* }
* }
* }
*/
public class GitInfoContributor extends InfoPropertiesInfoContributor<GitProperties> {
/**
* Create GitInfoContributor with default SIMPLE mode.
*
* @param properties Git properties loaded from git.properties (non-null)
*/
public GitInfoContributor(GitProperties properties);
/**
* Create GitInfoContributor with specified mode.
*
* @param properties Git properties loaded from git.properties (non-null)
* @param mode Exposure mode (non-null) - SIMPLE or FULL
*/
public GitInfoContributor(GitProperties properties, Mode mode);
/**
* Contribute git information to builder.
* Adds a "git" detail with git properties based on mode.
*
* @param builder Builder to contribute to (non-null)
*/
@Override
public void contribute(Info.Builder builder);
/**
* Return PropertySource for SIMPLE mode.
* Includes: branch, commit.id (short), commit.time.
*
* @return PropertySource with core git properties (non-null)
*/
@Override
protected PropertySource<?> toSimplePropertySource();
/**
* Post-process content to convert time strings to Instant objects.
* Converts commit.time and build.time to Instant.
*
* @param content Content map to modify (non-null)
*/
@Override
protected void postProcessContent(Map<String, Object> content);
}These contributors expose information about the runtime environment.
/**
* Contributes Java runtime information.
*
* Auto-configured by Spring Boot. Exposes Java version, vendor, and VM details
* under the "java" key in the info endpoint.
*
* Thread-safe: Yes
* Auto-configured: Yes
* Configuration: management.info.java.enabled (default: true)
* Since: Spring Boot 2.6.0
*
* Example Response:
* {
* "java": {
* "version": "17.0.5",
* "vendor": {
* "name": "Oracle Corporation",
* "version": "17.0.5+9-LTS"
* },
* "runtime": {
* "name": "Java(TM) SE Runtime Environment",
* "version": "17.0.5+9-LTS"
* },
* "jvm": {
* "name": "Java HotSpot(TM) 64-Bit Server VM",
* "vendor": "Oracle Corporation",
* "version": "17.0.5+9-LTS"
* }
* }
* }
*/
public class JavaInfoContributor implements InfoContributor {
/**
* Create JavaInfoContributor.
* Automatically discovers Java system properties.
*/
public JavaInfoContributor();
/**
* Contribute Java information to builder.
* Adds a "java" detail with runtime information.
*
* @param builder Builder to contribute to (non-null)
*/
@Override
public void contribute(Info.Builder builder);
}
/**
* Contributes operating system information.
*
* Auto-configured by Spring Boot. Exposes OS name, version, and architecture
* under the "os" key in the info endpoint.
*
* Thread-safe: Yes
* Auto-configured: Yes
* Configuration: management.info.os.enabled (default: false)
* Since: Spring Boot 2.7.0
*
* Example Response:
* {
* "os": {
* "name": "Linux",
* "version": "5.15.0-58-generic",
* "arch": "amd64"
* }
* }
*/
public class OsInfoContributor implements InfoContributor {
/**
* Create OsInfoContributor.
* Automatically discovers OS system properties.
*/
public OsInfoContributor();
/**
* Contribute OS information to builder.
* Adds an "os" detail with operating system information.
*
* @param builder Builder to contribute to (non-null)
*/
@Override
public void contribute(Info.Builder builder);
}
/**
* Contributes process information including PID and uptime.
*
* Auto-configured by Spring Boot. Exposes current process ID and uptime
* under the "process" key in the info endpoint.
*
* Thread-safe: Yes
* Auto-configured: Yes
* Configuration: management.info.process.enabled (default: false)
* Since: Spring Boot 3.3.0
*
* Example Response:
* {
* "process": {
* "pid": 12345,
* "uptime": 3600000
* }
* }
*/
public class ProcessInfoContributor implements InfoContributor {
/**
* Create ProcessInfoContributor.
* Automatically captures current process information.
*/
public ProcessInfoContributor();
/**
* Contribute process information to builder.
* Adds a "process" detail with PID and uptime.
*
* @param builder Builder to contribute to (non-null)
*/
@Override
public void contribute(Info.Builder builder);
}
/**
* Contributes SSL certificate information from configured SSL bundles.
*
* Exposes SSL certificate details under the "ssl" key in the info endpoint.
* This includes certificate validity dates, issuer, and subject information.
*
* Thread-safe: Yes
* Auto-configured: Yes (when SSL bundles are configured)
* Configuration: Requires spring.ssl.bundle.* configuration
* Since: Spring Boot 3.4.0
*
* Example Response:
* {
* "ssl": {
* "bundles": {
* "my-bundle": {
* "validFrom": "2025-01-01T00:00:00Z",
* "validUntil": "2026-01-01T00:00:00Z",
* "issuer": "CN=Example CA",
* "subject": "CN=example.com"
* }
* }
* }
* }
*/
public class SslInfoContributor implements InfoContributor {
/**
* Create SslInfoContributor with SSL information.
*
* @param sslInfo SSL information to expose (non-null)
* @throws IllegalArgumentException if sslInfo is null
*/
public SslInfoContributor(SslInfo sslInfo);
/**
* Contribute SSL information to builder.
* Adds an "ssl" detail with SSL bundle information.
*
* @param builder Builder to contribute to (non-null)
*/
@Override
public void contribute(Info.Builder builder);
}
/**
* Contributes environment properties prefixed with "info.".
*
* Auto-configured by Spring Boot. Reads all properties starting with "info."
* from the environment and exposes them in the info endpoint. This is the
* primary mechanism for adding static application information.
*
* Properties are exposed without the "info." prefix. Nested properties
* are converted to nested maps.
*
* Thread-safe: Yes
* Auto-configured: Yes (always enabled)
* Configuration: management.info.env.enabled (default: true)
* Since: Spring Boot 1.4.0
*
* Example Configuration (application.yml):
* info:
* app:
* name: My Application
* description: Customer Management System
* version: 1.0.0
* company:
* name: ACME Corp
* website: https://acme.com
*
* Example Response:
* {
* "app": {
* "name": "My Application",
* "description": "Customer Management System",
* "version": "1.0.0"
* },
* "company": {
* "name": "ACME Corp",
* "website": "https://acme.com"
* }
* }
*/
public class EnvironmentInfoContributor implements InfoContributor {
/**
* Create EnvironmentInfoContributor.
*
* @param environment Environment to read properties from (non-null)
* @throws IllegalArgumentException if environment is null
*/
public EnvironmentInfoContributor(ConfigurableEnvironment environment);
/**
* Contribute environment properties to builder.
* Reads all "info.*" properties and adds them as details.
*
* @param builder Builder to contribute to (non-null)
*/
@Override
public void contribute(Info.Builder builder);
}Utility contributors for simple use cases.
/**
* Simple InfoContributor that exposes a single detail with a key-value pair.
*
* Useful for programmatically adding static or simple dynamic information
* without creating a custom InfoContributor implementation.
*
* If the detail value is null, no information is contributed (the builder
* is not modified).
*
* Thread-safe: Yes (if detail object is immutable)
* Auto-configured: No (manual bean registration required)
* Since: Spring Boot 1.4.0
*
* Example Usage:
* <pre>
* @Bean
* public SimpleInfoContributor versionContributor() {
* return new SimpleInfoContributor("version", "1.0.0");
* }
* </pre>
*
* Example Response:
* {
* "version": "1.0.0"
* }
*/
public class SimpleInfoContributor implements InfoContributor {
/**
* Create SimpleInfoContributor.
*
* @param prefix Detail key (non-null) - the key under which the detail is exposed
* @param detail Detail value (nullable) - won't be added if null
* @throws IllegalArgumentException if prefix is null
*/
public SimpleInfoContributor(String prefix, @Nullable Object detail);
/**
* Contribute single detail to builder.
* Adds the detail only if the detail value is not null.
*
* @param builder Builder to contribute to (non-null)
*/
@Override
public void contribute(Info.Builder builder);
}
/**
* InfoContributor that exposes multiple details from a map.
*
* Useful for programmatically adding multiple related pieces of information
* at once without creating a custom InfoContributor implementation.
*
* The map is defensively copied during construction to prevent external
* modifications. All entries from the map are added as top-level details.
*
* Thread-safe: Yes (map is copied on construction)
* Auto-configured: No (manual bean registration required)
* Since: Spring Boot 1.4.0
*
* Example Usage:
* <pre>
* @Bean
* public MapInfoContributor appInfoContributor() {
* Map<String, Object> info = new HashMap<>();
* info.put("name", "My Application");
* info.put("version", "1.0.0");
* info.put("description", "Customer Management");
* return new MapInfoContributor(info);
* }
* </pre>
*
* Example Response:
* {
* "name": "My Application",
* "version": "1.0.0",
* "description": "Customer Management"
* }
*/
public class MapInfoContributor implements InfoContributor {
/**
* Create MapInfoContributor from existing map.
* The map is defensively copied to ensure immutability.
*
* @param info Map of details to expose (non-null) - copied on construction
* @throws IllegalArgumentException if info is null
*/
public MapInfoContributor(Map<String, Object> info);
/**
* Contribute all map entries to builder.
* Adds each map entry as a separate detail.
*
* @param builder Builder to contribute to (non-null)
*/
@Override
public void contribute(Info.Builder builder);
}package com.example.actuator;
import org.springframework.boot.actuate.info.Info;
import org.springframework.boot.actuate.info.InfoContributor;
import org.springframework.stereotype.Component;
import org.springframework.beans.factory.annotation.Value;
import java.time.Instant;
import java.time.Duration;
import java.util.Map;
/**
* Contributes application runtime metadata.
*
* Thread-safe: Yes
* Auto-discovered: Yes (via @Component)
* Since: Application 1.0
*/
@Component
public class ApplicationMetadataContributor implements InfoContributor {
private final String applicationName;
private final String profile;
private final Instant startupTime;
public ApplicationMetadataContributor(
@Value("${spring.application.name}") String applicationName,
@Value("${spring.profiles.active:default}") String profile) {
this.applicationName = applicationName;
this.profile = profile;
this.startupTime = Instant.now();
}
@Override
public void contribute(Info.Builder builder) {
Duration uptime = Duration.between(startupTime, Instant.now());
builder.withDetail("runtime", Map.of(
"applicationName", applicationName,
"profile", profile,
"startupTime", startupTime.toString(),
"uptime", formatDuration(uptime)
));
}
private String formatDuration(Duration duration) {
long hours = duration.toHours();
long minutes = duration.toMinutes() % 60;
long seconds = duration.getSeconds() % 60;
return String.format("%dh %dm %ds", hours, minutes, seconds);
}
}package com.example.actuator;
import org.springframework.boot.actuate.info.Info;
import org.springframework.boot.actuate.info.InfoContributor;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
/**
* Feature flags configuration properties.
*/
@ConfigurationProperties(prefix = "features")
@Component
public class FeatureFlags {
private boolean newUI = false;
private boolean betaFeatures = false;
private boolean experimentalApi = false;
private Map<String, Boolean> custom = new HashMap<>();
// Getters and setters
public boolean isNewUI() { return newUI; }
public void setNewUI(boolean newUI) { this.newUI = newUI; }
public boolean isBetaFeatures() { return betaFeatures; }
public void setBetaFeatures(boolean betaFeatures) { this.betaFeatures = betaFeatures; }
public boolean isExperimentalApi() { return experimentalApi; }
public void setExperimentalApi(boolean experimentalApi) { this.experimentalApi = experimentalApi; }
public Map<String, Boolean> getCustom() { return custom; }
public void setCustom(Map<String, Boolean> custom) { this.custom = custom; }
}
/**
* Contributes feature flag status.
*
* Thread-safe: Yes
* Auto-discovered: Yes
* Since: Application 1.0
*/
@Component
public class FeatureFlagsContributor implements InfoContributor {
private final FeatureFlags features;
public FeatureFlagsContributor(FeatureFlags features) {
this.features = features;
}
@Override
public void contribute(Info.Builder builder) {
Map<String, Object> featureMap = new HashMap<>();
featureMap.put("newUI", features.isNewUI());
featureMap.put("betaFeatures", features.isBetaFeatures());
featureMap.put("experimentalApi", features.isExperimentalApi());
featureMap.putAll(features.getCustom());
builder.withDetail("features", featureMap);
}
}
// Configuration in application.yml
/*
features:
new-ui: true
beta-features: false
experimental-api: false
custom:
darkMode: true
advancedSearch: false
*/package com.example.actuator;
import org.springframework.boot.actuate.info.Info;
import org.springframework.boot.actuate.info.InfoContributor;
import org.springframework.stereotype.Component;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.data.redis.core.RedisTemplate;
import java.util.Map;
import java.util.HashMap;
/**
* Contributes external dependencies status.
*
* Thread-safe: Yes
* Performance: Checks are fast (single query each)
* Since: Application 1.0
*/
@Component
public class DependenciesInfoContributor implements InfoContributor {
private final JdbcTemplate jdbcTemplate;
private final RedisTemplate<String, String> redisTemplate;
public DependenciesInfoContributor(
JdbcTemplate jdbcTemplate,
RedisTemplate<String, String> redisTemplate) {
this.jdbcTemplate = jdbcTemplate;
this.redisTemplate = redisTemplate;
}
@Override
public void contribute(Info.Builder builder) {
Map<String, Object> dependencies = new HashMap<>();
// Database status
dependencies.put("database", checkDatabase());
// Redis status
dependencies.put("cache", checkRedis());
builder.withDetail("dependencies", dependencies);
}
private Map<String, Object> checkDatabase() {
try {
String version = jdbcTemplate.queryForObject(
"SELECT version()",
String.class
);
return Map.of(
"status", "UP",
"version", version != null ? version : "unknown"
);
} catch (Exception e) {
return Map.of(
"status", "DOWN",
"error", e.getMessage()
);
}
}
private Map<String, Object> checkRedis() {
try {
String pong = redisTemplate.getConnectionFactory()
.getConnection()
.ping();
return Map.of(
"status", "UP",
"response", pong != null ? pong : "unknown"
);
} catch (Exception e) {
return Map.of(
"status", "DOWN",
"error", e.getMessage()
);
}
}
}package com.example.actuator;
import org.springframework.boot.actuate.info.Info;
import org.springframework.boot.actuate.info.InfoContributor;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Map;
/**
* Team contact configuration.
*/
@ConfigurationProperties(prefix = "team")
@Component
public class TeamConfiguration {
private String name;
private String email;
private String slackChannel;
private List<String> oncall;
// Getters and setters
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public String getSlackChannel() { return slackChannel; }
public void setSlackChannel(String slackChannel) { this.slackChannel = slackChannel; }
public List<String> getOncall() { return oncall; }
public void setOncall(List<String> oncall) { this.oncall = oncall; }
}
/**
* Contributes team contact information.
*
* Thread-safe: Yes
* Since: Application 1.0
*/
@Component
public class TeamInfoContributor implements InfoContributor {
private final TeamConfiguration team;
public TeamInfoContributor(TeamConfiguration team) {
this.team = team;
}
@Override
public void contribute(Info.Builder builder) {
builder.withDetail("team", Map.of(
"name", team.getName(),
"email", team.getEmail(),
"slackChannel", team.getSlackChannel(),
"oncall", team.getOncall()
));
}
}
// Configuration in application.yml
/*
team:
name: Platform Team
email: platform@example.com
slack-channel: "#platform-support"
oncall:
- alice@example.com
- bob@example.com
*/package com.example.actuator;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.BeforeEach;
import org.springframework.boot.actuate.info.Info;
import java.util.Map;
import static org.assertj.core.api.Assertions.*;
class ApplicationMetadataContributorTest {
private ApplicationMetadataContributor contributor;
@BeforeEach
void setUp() {
contributor = new ApplicationMetadataContributor("test-app", "test");
}
@Test
void contribute_AddsRuntimeInformation() {
Info.Builder builder = new Info.Builder();
contributor.contribute(builder);
Info info = builder.build();
Map<String, Object> runtime = (Map<String, Object>) info.get("runtime");
assertThat(runtime).isNotNull();
assertThat(runtime).containsEntry("applicationName", "test-app");
assertThat(runtime).containsEntry("profile", "test");
assertThat(runtime).containsKeys("startupTime", "uptime");
}
}package com.example.actuator;
import org.junit.jupiter.api.Test;
import org.springframework.boot.actuate.info.Info;
import java.util.Map;
import static org.assertj.core.api.Assertions.*;
class FeatureFlagsContributorTest {
@Test
void contribute_AddsFeatureFlags() {
FeatureFlags features = new FeatureFlags();
features.setNewUI(true);
features.setBetaFeatures(false);
FeatureFlagsContributor contributor = new FeatureFlagsContributor(features);
Info.Builder builder = new Info.Builder();
contributor.contribute(builder);
Info info = builder.build();
Map<String, Object> featureMap = (Map<String, Object>) info.get("features");
assertThat(featureMap).containsEntry("newUI", true);
assertThat(featureMap).containsEntry("betaFeatures", false);
}
}Problem: Same information appears twice in /actuator/info
Causes:
Solutions:
// ❌ WRONG - Don't manually create auto-configured beans
@Bean
public JavaInfoContributor javaInfoContributor() {
return new JavaInfoContributor(); // Creates duplicate!
}
// ✓ CORRECT - Use configuration property
management.info.java.enabled=true
// Avoid duplicate keys in custom contributors
builder.withDetail("app", ...); // First contributor
builder.withDetail("app", ...); // Second contributor - overwrites!Problem: Custom contributor data not in response
Causes:
Solutions:
// Solution 1: Add @Component
@Component // <-- Required
public class MyInfoContributor implements InfoContributor { }
// Solution 2: Handle exceptions
@Override
public void contribute(Info.Builder builder) {
try {
builder.withDetail("data", computeData());
} catch (Exception e) {
builder.withDetail("data", Map.of("error", e.getMessage()));
}
}
// Solution 3: Expose endpoint
management:
endpoints:
web:
exposure:
include: infoProblem: Build or git information not appearing
Causes:
Solutions:
<!-- Maven: Generate build-info.properties -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>build-info</goal>
</goals>
</execution>
</executions>
</plugin>
<!-- Maven: Generate git.properties -->
<plugin>
<groupId>pl.project13.maven</groupId>
<artifactId>git-commit-id-plugin</artifactId>
</plugin>
<!-- Verify files exist:
- target/classes/META-INF/build-info.properties
- target/classes/git.properties
-->// ❌ Bad - expensive operation on every info request
@Override
public void contribute(Info.Builder builder) {
// Scans entire database!
long count = repository.count();
builder.withDetail("recordCount", count);
}
// ✓ Good - cached or lightweight
@Override
public void contribute(Info.Builder builder) {
// Use cached value or approximate
builder.withDetail("recordCount", "~1M");
}// For expensive computations, cache the result
@Component
public class CachedInfoContributor implements InfoContributor {
private volatile Map<String, Object> cachedInfo;
private volatile Instant lastUpdate;
@Override
public void contribute(Info.Builder builder) {
if (shouldRefresh()) {
cachedInfo = computeExpensiveInfo();
lastUpdate = Instant.now();
}
builder.withDetail("cached", cachedInfo);
}
private boolean shouldRefresh() {
return cachedInfo == null ||
lastUpdate.isBefore(Instant.now().minusSeconds(60));
}
private Map<String, Object> computeExpensiveInfo() {
// Expensive operation
return Map.of("key", "value");
}
}