or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

audit.mdbuiltin-endpoints.mdendpoint-framework.mdhttp-exchanges.mdindex.mdinfo-contributors.mdjmx-integration.mdmanagement-operations.mdoperation-invocation.mdsanitization.mdsecurity.mdweb-integration.md
tile.json

builtin-endpoints.mddocs/

Built-in Endpoints

QUICK REFERENCE

Key Built-in Endpoints

// Information Endpoints
org.springframework.boot.actuate.info.InfoEndpoint                    // /actuator/info
org.springframework.boot.actuate.env.EnvironmentEndpoint              // /actuator/env
org.springframework.boot.actuate.logging.LoggersEndpoint              // /actuator/loggers
org.springframework.boot.actuate.context.properties.ConfigurationPropertiesReportEndpoint  // /actuator/configprops

// Introspection Endpoints
org.springframework.boot.actuate.beans.BeansEndpoint                  // /actuator/beans
org.springframework.boot.actuate.scheduling.ScheduledTasksEndpoint    // /actuator/scheduledtasks
org.springframework.boot.actuate.web.mappings.MappingsEndpoint        // /actuator/mappings

// Audit and Monitoring
org.springframework.boot.actuate.audit.AuditEventsEndpoint            // /actuator/auditevents
org.springframework.boot.actuate.web.exchanges.HttpExchangesEndpoint  // /actuator/httpexchanges

// Operations
org.springframework.boot.actuate.management.ThreadDumpEndpoint        // /actuator/threaddump
org.springframework.boot.actuate.management.HeapDumpWebEndpoint       // /actuator/heapdump
org.springframework.boot.actuate.context.ShutdownEndpoint             // /actuator/shutdown

Endpoint Selection Guide

┌─ What information do you need?
│
├─ Application Metadata
│  ├─ Build info, git commit → InfoEndpoint
│  ├─ Configuration properties → ConfigurationPropertiesReportEndpoint
│  └─ Environment variables → EnvironmentEndpoint
│
├─ Runtime Information
│  ├─ All Spring beans → BeansEndpoint
│  ├─ Request mappings → MappingsEndpoint
│  └─ Scheduled tasks → ScheduledTasksEndpoint
│
├─ Logging
│  ├─ View/change log levels → LoggersEndpoint
│  └─ Download log file → LogFileWebEndpoint
│
├─ Monitoring
│  ├─ Recent HTTP requests → HttpExchangesEndpoint
│  └─ Security events → AuditEventsEndpoint
│
└─ Diagnostics
   ├─ Thread dump → ThreadDumpEndpoint
   ├─ Heap dump → HeapDumpWebEndpoint
   └─ Startup timeline → StartupEndpoint

Common Configuration Matrix

EndpointDefault EnabledDefault ExposedSensitiveUse Case
infoYesYesNoBuild/version info
envYesNoYesConfig values
loggersYesNoYesLog level control
configpropsYesNoYes@ConfigurationProperties
beansYesNoNoBean introspection
auditeventsYesNoYesSecurity auditing
httpexchangesYesNoYesHTTP monitoring
threaddumpYesNoYesThread diagnostics
heapdumpYesNoYesMemory analysis
shutdownNoNoYesGraceful shutdown

AGENT GUIDANCE

When to Use Each Endpoint

InfoEndpoint - Use when:

  • Need to display application version to users
  • Want to expose build information
  • Need to show git commit details
  • Building status dashboard

EnvironmentEndpoint - Use when:

  • Debugging configuration issues
  • Verifying property sources
  • Checking which properties are active
  • Troubleshooting property precedence

LoggersEndpoint - Use when:

  • Need to change log levels at runtime
  • Debugging production issues
  • Temporarily enabling trace logging
  • Investigating specific package behavior

ConfigurationPropertiesReportEndpoint - Use when:

  • Verifying @ConfigurationProperties binding
  • Debugging property validation issues
  • Checking actual vs expected values
  • Documenting configuration

BeansEndpoint - Use when:

  • Investigating autowiring issues
  • Checking bean definitions
  • Verifying bean scopes
  • Understanding Spring context

AuditEventsEndpoint - Use when:

  • Tracking security events
  • Investigating authentication failures
  • Monitoring authorization decisions
  • Compliance reporting

HttpExchangesEndpoint - Use when:

  • Analyzing API usage patterns
  • Debugging HTTP issues
  • Monitoring response times
  • Investigating errors

Pattern vs Anti-Pattern

PATTERN: Enable selective exposure

management:
  endpoints:
    web:
      exposure:
        include: info,health  # ✓ Only expose what's needed

ANTI-PATTERN: Expose all endpoints

management:
  endpoints:
    web:
      exposure:
        include: "*"  # ❌ Security risk

PATTERN: Sanitize sensitive data

management:
  endpoint:
    env:
      show-values: when-authorized  # ✓ Protect credentials
      roles: ADMIN

ANTI-PATTERN: Always show values

management:
  endpoint:
    env:
      show-values: always  # ❌ Exposes secrets

PATTERN: Secure shutdown endpoint

management:
  endpoint:
    shutdown:
      enabled: true  # Only if explicitly needed

spring:
  security:
    user:
      roles: ADMIN  # ✓ Require authentication

ANTI-PATTERN: Unsecured shutdown

management:
  endpoint:
    shutdown:
      enabled: true  # ❌ Anyone can shut down app
  endpoints:
    web:
      exposure:
        include: shutdown
# No security configured!

Production-ready endpoints providing monitoring and management capabilities including application information, environment properties, logger configuration, log file access, audit events, beans, configuration properties, scheduled tasks, thread dumps, heap dumps, HTTP exchanges, request mappings, SBOM, and shutdown. (Note: Health endpoints are in the separate spring-boot-health module.)

API REFERENCE

InfoEndpoint

@Endpoint(id = "info")
class InfoEndpoint {
    InfoEndpoint(List<InfoContributor> infoContributors);

    @ReadOperation
    Map<String, Object> info();
}

EnvironmentEndpoint

@Endpoint(id = "env")
class EnvironmentEndpoint {
    EnvironmentEndpoint(Environment environment,
                       Iterable<SanitizingFunction> sanitizingFunctions,
                       Show showValues);

    @ReadOperation
    EnvironmentDescriptor environment(@Nullable String pattern);

    @ReadOperation
    EnvironmentEntryDescriptor environmentEntry(@Selector String toMatch);

    // Descriptor classes
    static final class EnvironmentDescriptor implements OperationResponseBody {
        List<String> getActiveProfiles();
        List<String> getDefaultProfiles();
        List<PropertySourceDescriptor> getPropertySources();
    }

    static final class EnvironmentEntryDescriptor {
        @Nullable PropertySummaryDescriptor getProperty();
        List<String> getActiveProfiles();
        List<String> getDefaultProfiles();
        List<PropertySourceEntryDescriptor> getPropertySources();
    }

    static final class PropertySummaryDescriptor {
        PropertySummaryDescriptor(String source, @Nullable Object value);
        String getSource();
        @Nullable Object getValue();
    }

    static final class PropertySourceDescriptor {
        String getName();
        Map<String, PropertyValueDescriptor> getProperties();
    }

    static final class PropertySourceEntryDescriptor {
        String getName();
        @Nullable PropertyValueDescriptor getProperty();
    }

    static final class PropertyValueDescriptor {
        @Nullable Object getValue();
        @Nullable String getOrigin();
        String @Nullable [] getOriginParents();
    }
}

@EndpointWebExtension(endpoint = EnvironmentEndpoint.class)
class EnvironmentEndpointWebExtension {
    EnvironmentEndpointWebExtension(EnvironmentEndpoint delegate,
                                   Show showValues,
                                   Set<String> roles);

    @ReadOperation
    EnvironmentDescriptor environment(SecurityContext securityContext,
                                     @Nullable String pattern);

    @ReadOperation
    WebEndpointResponse<EnvironmentEntryDescriptor> environmentEntry(SecurityContext securityContext,
                                                                     @Selector String toMatch);
}

LoggersEndpoint

Note: LogLevel enum referenced below is from org.springframework.boot.logging package (spring-boot-core module), not the actuator module. It defines standard log levels: TRACE, DEBUG, INFO, WARN, ERROR, FATAL, OFF.

@Endpoint(id = "loggers")
class LoggersEndpoint {
    LoggersEndpoint(LoggingSystem loggingSystem, LoggerGroups loggerGroups);

    @ReadOperation
    LoggersDescriptor loggers();

    @ReadOperation
    @Nullable LoggerLevelsDescriptor loggerLevels(@Selector String name);

    @WriteOperation
    void configureLogLevel(@Selector String name, @Nullable LogLevel configuredLevel);

    // Descriptor classes
    static class LoggersDescriptor implements OperationResponseBody {
        LoggersDescriptor(@Nullable NavigableSet<LogLevel> levels,
                         @Nullable Map<String, LoggerLevelsDescriptor> loggers,
                         @Nullable Map<String, GroupLoggerLevelsDescriptor> groups);
        @Nullable NavigableSet<LogLevel> getLevels();
        @Nullable Map<String, LoggerLevelsDescriptor> getLoggers();
        @Nullable Map<String, GroupLoggerLevelsDescriptor> getGroups();
    }

    static class LoggerLevelsDescriptor implements OperationResponseBody {
        LoggerLevelsDescriptor(@Nullable LogLevel configuredLevel);
        /**
         * Returns the configured log level as a String.
         * Note: While the constructor accepts LogLevel enum, the getter returns the level name as String.
         * This allows for custom log level implementations beyond the standard LogLevel enum values.
         *
         * @return the configured level name, or null if not configured
         */
        @Nullable String getConfiguredLevel();
    }

    static class GroupLoggerLevelsDescriptor extends LoggerLevelsDescriptor {
        GroupLoggerLevelsDescriptor(@Nullable LogLevel configuredLevel, List<String> members);
        List<String> getMembers();
    }

    static class SingleLoggerLevelsDescriptor extends LoggerLevelsDescriptor {
        SingleLoggerLevelsDescriptor(LoggerConfiguration configuration);
        String getEffectiveLevel();
    }
}

// Note: LogLevel enum is from org.springframework.boot.logging package (spring-boot-core module)
// The configureLogLevel() method accepts LogLevel values: TRACE, DEBUG, INFO, WARN, ERROR, FATAL, OFF
// However, getConfiguredLevel() returns the level as a String to support custom logging implementations

LogFileWebEndpoint

@WebEndpoint(id = "logfile")
class LogFileWebEndpoint {
    LogFileWebEndpoint(@Nullable LogFile logFile, @Nullable File externalFile);

    @ReadOperation(produces = "text/plain; charset=UTF-8")
    @Nullable Resource logFile();
}

BeansEndpoint

@Endpoint(id = "beans")
class BeansEndpoint {
    BeansEndpoint(ConfigurableApplicationContext context);

    @ReadOperation
    BeansDescriptor beans();

    // Descriptor classes
    static final class BeansDescriptor implements OperationResponseBody {
        Map<@Nullable String, ContextBeansDescriptor> getContexts();
    }

    static final class ContextBeansDescriptor {
        @Nullable String getParentId();
        Map<String, BeanDescriptor> getBeans();
    }

    static final class BeanDescriptor {
        String[] getAliases();
        @Nullable String getScope();
        @Nullable Class<?> getType();
        @Nullable String getResource();
        String[] getDependencies();
    }
}

ConfigurationPropertiesReportEndpoint

@Endpoint(id = "configprops")
class ConfigurationPropertiesReportEndpoint implements ApplicationContextAware {
    ConfigurationPropertiesReportEndpoint(Iterable<SanitizingFunction> sanitizingFunctions,
                                         Show showValues);

    void setApplicationContext(ApplicationContext context) throws BeansException;

    @ReadOperation
    ConfigurationPropertiesDescriptor configurationProperties();

    @ReadOperation
    ConfigurationPropertiesDescriptor configurationPropertiesWithPrefix(@Selector String prefix);

    // Descriptor classes
    static final class ConfigurationPropertiesDescriptor implements OperationResponseBody {
        Map<@Nullable String, ContextConfigurationPropertiesDescriptor> getContexts();
    }

    static final class ContextConfigurationPropertiesDescriptor {
        Map<String, ConfigurationPropertiesBeanDescriptor> getBeans();
        @Nullable String getParentId();
    }

    static final class ConfigurationPropertiesBeanDescriptor {
        String getPrefix();
        Map<String, @Nullable Object> getProperties();
        Map<String, Object> getInputs();
    }
}

@EndpointWebExtension(endpoint = ConfigurationPropertiesReportEndpoint.class)
class ConfigurationPropertiesReportEndpointWebExtension {
    ConfigurationPropertiesReportEndpointWebExtension(ConfigurationPropertiesReportEndpoint delegate,
                                                     Show showValues,
                                                     Set<String> roles);

    @ReadOperation
    ConfigurationPropertiesDescriptor configurationProperties(SecurityContext securityContext);

    @ReadOperation
    WebEndpointResponse<ConfigurationPropertiesDescriptor> configurationPropertiesWithPrefix(
            SecurityContext securityContext,
            @Selector String prefix);
}

// Bean serialization strategy (package: org.springframework.boot.actuate.context.properties)
@FunctionalInterface
interface BeanSerializer {
    /**
     * Serialize a bean into a Map for JSON representation.
     * Used by ConfigurationPropertiesReportEndpoint to serialize @ConfigurationProperties beans.
     *
     * @param bean the bean to serialize (may be null)
     * @return Map representation of the bean (never null)
     */
    Map<String, @Nullable Object> serialize(@Nullable Object bean);
}

class JacksonBeanSerializer implements BeanSerializer {
    /**
     * Default constructor using Jackson for serialization.
     * Automatically configured to:
     * - Hide properties starting with '$$' (CGLIB artifacts)
     * - Skip self-referential properties
     * - Filter to only bindable properties
     * - Handle DataSize and other Spring Boot types
     */
    JacksonBeanSerializer();

    Map<String, @Nullable Object> serialize(@Nullable Object bean);
}

AuditEventsEndpoint

@Endpoint(id = "auditevents")
class AuditEventsEndpoint {
    AuditEventsEndpoint(AuditEventRepository auditEventRepository);

    @ReadOperation
    AuditEventsDescriptor events(@Nullable String principal,
                                 @Nullable OffsetDateTime after,
                                 @Nullable String type);
}

HttpExchangesEndpoint

@Endpoint(id = "httpexchanges")
class HttpExchangesEndpoint {
    HttpExchangesEndpoint(HttpExchangeRepository repository);

    @ReadOperation
    HttpExchangesDescriptor httpExchanges();

    static final class HttpExchangesDescriptor implements OperationResponseBody {
        List<HttpExchange> getExchanges();
    }
}

ScheduledTasksEndpoint

@Endpoint(id = "scheduledtasks")
class ScheduledTasksEndpoint {
    ScheduledTasksEndpoint(Collection<ScheduledTaskHolder> scheduledTaskHolders);

    @ReadOperation
    ScheduledTasksDescriptor scheduledTasks();
}

ThreadDumpEndpoint

@Endpoint(id = "threaddump")
class ThreadDumpEndpoint {
    @ReadOperation
    ThreadDumpDescriptor threadDump();
}

HeapDumpWebEndpoint

@WebEndpoint(id = "heapdump", defaultAccess = Access.NONE)
class HeapDumpWebEndpoint {
    HeapDumpWebEndpoint();
    protected HeapDumpWebEndpoint(long timeout);

    @ReadOperation
    WebEndpointResponse<Resource> heapDump(@Nullable Boolean live);
}

StartupEndpoint

@Endpoint(id = "startup")
class StartupEndpoint {
    StartupEndpoint(BufferingApplicationStartup applicationStartup);

    @ReadOperation
    StartupDescriptor startupSnapshot();

    @WriteOperation
    StartupDescriptor startup();
}

SbomEndpoint

@Endpoint(id = "sbom")
class SbomEndpoint {
    SbomEndpoint(SbomProperties properties, ResourceLoader resourceLoader);

    @ReadOperation
    Sboms sboms();

    @ReadOperation
    @Nullable Resource sbom(@Selector String id);
}

ShutdownEndpoint

@Endpoint(id = "shutdown", defaultAccess = Access.NONE)
class ShutdownEndpoint implements ApplicationContextAware {
    @WriteOperation
    ShutdownDescriptor shutdown();
}

MappingsEndpoint

@Endpoint(id = "mappings")
class MappingsEndpoint {
    MappingsEndpoint(Collection<MappingDescriptionProvider> descriptionProviders,
                    ApplicationContext context);

    @ReadOperation
    ApplicationMappingsDescriptor mappings();
}

COMPLETE WORKING EXAMPLES

Example 1: Using InfoEndpoint with Custom Contributors

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.util.Map;

/**
 * Custom info contributor for application metadata.
 *
 * Thread-safe: Yes
 * Auto-discovered: Yes (via @Component)
 * Since: Application 1.0
 */
@Component
public class ApplicationInfoContributor implements InfoContributor {

    private final String applicationName;
    private final String environment;
    private final Instant startupTime;

    public ApplicationInfoContributor(
            @Value("${spring.application.name}") String applicationName,
            @Value("${spring.profiles.active:default}") String environment) {
        this.applicationName = applicationName;
        this.environment = environment;
        this.startupTime = Instant.now();
    }

    @Override
    public void contribute(Info.Builder builder) {
        builder.withDetail("application", Map.of(
            "name", applicationName,
            "environment", environment,
            "startupTime", startupTime.toString(),
            "uptime", java.time.Duration.between(startupTime, Instant.now()).toString()
        ));
    }
}

// Configuration
@Configuration
public class InfoEndpointConfiguration {

    /**
     * Configure which built-in info contributors to enable.
     */
    @Bean
    public InfoEndpointAutoConfiguration infoConfig() {
        return new InfoEndpointAutoConfiguration();
    }
}

// application.yml
/*
management:
  endpoints:
    web:
      exposure:
        include: info
  info:
    java:
      enabled: true
    os:
      enabled: true

info:
  app:
    name: My Application
    description: Customer Management System
*/

// Response: GET /actuator/info
/*
{
  "application": {
    "name": "customer-service",
    "environment": "production",
    "startupTime": "2024-01-15T10:00:00Z",
    "uptime": "PT2H30M"
  },
  "java": {
    "version": "21.0.1",
    "vendor": {
      "name": "Oracle Corporation"
    }
  },
  "os": {
    "name": "Linux",
    "version": "5.15.0-1028-aws",
    "arch": "amd64"
  },
  "app": {
    "name": "My Application",
    "description": "Customer Management System"
  }
}
*/

Example 2: Programmatically Changing Log Levels

package com.example.actuator;

import org.springframework.boot.actuate.logging.LoggersEndpoint;
import org.springframework.boot.logging.LogLevel;
import org.springframework.stereotype.Service;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Service for dynamic log level management.
 *
 * Thread-safe: Yes (delegates to LoggersEndpoint)
 * Nullability: Methods handle null log levels appropriately
 * Since: Application 1.0
 */
@Service
public class LogManagementService {

    private static final Logger logger = LoggerFactory.getLogger(LogManagementService.class);
    private final LoggersEndpoint loggersEndpoint;

    public LogManagementService(LoggersEndpoint loggersEndpoint) {
        this.loggersEndpoint = loggersEndpoint;
    }

    /**
     * Enable debug logging for specific package temporarily.
     *
     * @param packageName Package name (e.g., "com.example.service")
     * @param durationSeconds Duration in seconds
     */
    public void enableDebugLogging(String packageName, long durationSeconds) {
        logger.info("Enabling DEBUG logging for {} for {} seconds", packageName, durationSeconds);

        // Get current level (returns String)
        LoggersEndpoint.LoggerLevelsDescriptor current = loggersEndpoint.loggerLevels(packageName);
        String originalLevel = current.getConfiguredLevel();

        // Set to DEBUG
        loggersEndpoint.configureLogLevel(packageName, LogLevel.DEBUG);

        // Schedule restoration (convert String back to LogLevel)
        scheduleLogLevelRestoration(packageName,
            originalLevel != null ? LogLevel.valueOf(originalLevel) : null,
            durationSeconds);
    }

    /**
     * Get current log level for logger.
     *
     * @param loggerName Logger name
     * @return Current log level name as String, or null if not configured
     */
    public String getLogLevel(String loggerName) {
        LoggersEndpoint.LoggerLevelsDescriptor descriptor = loggersEndpoint.loggerLevels(loggerName);
        return descriptor.getConfiguredLevel();
    }

    /**
     * Set log level for logger.
     *
     * @param loggerName Logger name
     * @param level Log level to set
     */
    public void setLogLevel(String loggerName, LogLevel level) {
        logger.info("Setting log level for {} to {}", loggerName, level);
        loggersEndpoint.configureLogLevel(loggerName, level);
    }

    /**
     * Reset logger to default level.
     *
     * @param loggerName Logger name
     */
    public void resetLogLevel(String loggerName) {
        logger.info("Resetting log level for {}", loggerName);
        loggersEndpoint.configureLogLevel(loggerName, null);
    }

    private void scheduleLogLevelRestoration(String packageName, LogLevel originalLevel, long durationSeconds) {
        // Use ScheduledExecutorService in real implementation
        new Thread(() -> {
            try {
                Thread.sleep(durationSeconds * 1000);
                loggersEndpoint.configureLogLevel(packageName, originalLevel);
                logger.info("Restored log level for {} to {}", packageName, originalLevel);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }).start();
    }
}

Example 3: Analyzing HTTP Exchanges

package com.example.actuator;

import org.springframework.boot.actuate.web.exchanges.HttpExchange;
import org.springframework.boot.actuate.web.exchanges.HttpExchangeRepository;
import org.springframework.stereotype.Service;

import java.time.Duration;
import java.util.*;
import java.util.stream.Collectors;

/**
 * Service for analyzing HTTP traffic patterns.
 *
 * Thread-safe: Yes
 * Since: Application 1.0
 */
@Service
public class HttpAnalyticsService {

    private final HttpExchangeRepository exchangeRepository;

    public HttpAnalyticsService(HttpExchangeRepository exchangeRepository) {
        this.exchangeRepository = exchangeRepository;
    }

    /**
     * Get average response time.
     *
     * @return Average duration in milliseconds
     */
    public double getAverageResponseTime() {
        List<HttpExchange> exchanges = exchangeRepository.findAll();

        return exchanges.stream()
            .map(HttpExchange::getTimeTaken)
            .filter(Objects::nonNull)
            .mapToLong(Duration::toMillis)
            .average()
            .orElse(0.0);
    }

    /**
     * Get endpoint call counts.
     *
     * @return Map of endpoints to call counts
     */
    public Map<String, Long> getEndpointCallCounts() {
        List<HttpExchange> exchanges = exchangeRepository.findAll();

        return exchanges.stream()
            .collect(Collectors.groupingBy(
                ex -> ex.getRequest().getUri().getPath(),
                Collectors.counting()
            ));
    }

    /**
     * Find slow requests (> 1 second).
     *
     * @return List of slow requests
     */
    public List<SlowRequest> findSlowRequests() {
        List<HttpExchange> exchanges = exchangeRepository.findAll();

        return exchanges.stream()
            .filter(ex -> ex.getTimeTaken() != null)
            .filter(ex -> ex.getTimeTaken().toMillis() > 1000)
            .map(ex -> new SlowRequest(
                ex.getRequest().getUri().toString(),
                ex.getTimeTaken().toMillis(),
                ex.getResponse().getStatus()
            ))
            .collect(Collectors.toList());
    }

    /**
     * Get error rate by status code.
     *
     * @return Map of status codes to counts
     */
    public Map<Integer, Long> getStatusCodeDistribution() {
        List<HttpExchange> exchanges = exchangeRepository.findAll();

        return exchanges.stream()
            .collect(Collectors.groupingBy(
                ex -> ex.getResponse().getStatus(),
                Collectors.counting()
            ));
    }

    /**
     * Get requests by remote address.
     *
     * @return Map of IP addresses to request counts
     */
    public Map<String, Long> getRequestsByIpAddress() {
        List<HttpExchange> exchanges = exchangeRepository.findAll();

        return exchanges.stream()
            .filter(ex -> ex.getRequest().getRemoteAddress() != null)
            .collect(Collectors.groupingBy(
                ex -> ex.getRequest().getRemoteAddress(),
                Collectors.counting()
            ));
    }

    public record SlowRequest(String uri, long durationMs, int status) {}
}

TESTING EXAMPLES

Test 1: InfoEndpoint with Custom Contributors

package com.example.actuator;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.BeforeEach;
import org.springframework.boot.actuate.info.Info;
import org.springframework.boot.actuate.info.InfoEndpoint;
import java.util.Map;
import java.util.List;
import static org.assertj.core.api.Assertions.*;

class InfoEndpointTest {

    private InfoEndpoint infoEndpoint;
    private ApplicationInfoContributor contributor;

    @BeforeEach
    void setUp() {
        contributor = new ApplicationInfoContributor("test-app", "test");
        infoEndpoint = new InfoEndpoint(List.of(contributor));
    }

    @Test
    void info_ContainsCustomContribution() {
        Map<String, Object> info = infoEndpoint.info();

        assertThat(info).containsKey("application");

        @SuppressWarnings("unchecked")
        Map<String, Object> appInfo = (Map<String, Object>) info.get("application");

        assertThat(appInfo).containsEntry("name", "test-app");
        assertThat(appInfo).containsEntry("environment", "test");
        assertThat(appInfo).containsKey("startupTime");
        assertThat(appInfo).containsKey("uptime");
    }
}

Test 2: LogManagementService

package com.example.actuator;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.BeforeEach;
import org.springframework.boot.actuate.logging.LoggersEndpoint;
import org.springframework.boot.logging.LogLevel;
import org.springframework.boot.logging.LoggingSystem;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;

class LogManagementServiceTest {

    private LoggersEndpoint loggersEndpoint;
    private LogManagementService logManagementService;

    @BeforeEach
    void setUp() {
        LoggingSystem loggingSystem = mock(LoggingSystem.class);
        loggersEndpoint = new LoggersEndpoint(loggingSystem, null);
        logManagementService = new LogManagementService(loggersEndpoint);
    }

    @Test
    void setLogLevel_ChangesLevel() {
        logManagementService.setLogLevel("com.example", LogLevel.DEBUG);

        // Verify through endpoint
        LoggersEndpoint.LoggerLevelsDescriptor descriptor =
            loggersEndpoint.loggerLevels("com.example");

        assertThat(descriptor.getConfiguredLevel()).isEqualTo("DEBUG");
    }

    @Test
    void resetLogLevel_ClearsConfiguredLevel() {
        // Set level first
        logManagementService.setLogLevel("com.example", LogLevel.DEBUG);

        // Reset
        logManagementService.resetLogLevel("com.example");

        // Verify cleared
        LoggersEndpoint.LoggerLevelsDescriptor descriptor =
            loggersEndpoint.loggerLevels("com.example");

        assertThat(descriptor.getConfiguredLevel()).isNull();
    }
}

Test 3: HttpAnalyticsService

package com.example.actuator;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.BeforeEach;
import org.springframework.boot.actuate.web.exchanges.HttpExchange;
import org.springframework.boot.actuate.web.exchanges.InMemoryHttpExchangeRepository;
import java.net.URI;
import java.time.Duration;
import java.time.Instant;
import java.util.List;
import java.util.Map;
import static org.assertj.core.api.Assertions.*;

class HttpAnalyticsServiceTest {

    private InMemoryHttpExchangeRepository repository;
    private HttpAnalyticsService analyticsService;

    @BeforeEach
    void setUp() {
        repository = new InMemoryHttpExchangeRepository();
        analyticsService = new HttpAnalyticsService(repository);

        // Add test data
        addExchange("/api/users", 200, Duration.ofMillis(100));
        addExchange("/api/users", 200, Duration.ofMillis(150));
        addExchange("/api/orders", 200, Duration.ofMillis(2000)); // Slow
        addExchange("/api/orders", 500, Duration.ofMillis(50));
    }

    @Test
    void getAverageResponseTime_CalculatesCorrectly() {
        double average = analyticsService.getAverageResponseTime();

        assertThat(average).isEqualTo(575.0); // (100+150+2000+50)/4
    }

    @Test
    void getEndpointCallCounts_CountsCorrectly() {
        Map<String, Long> counts = analyticsService.getEndpointCallCounts();

        assertThat(counts).containsEntry("/api/users", 2L);
        assertThat(counts).containsEntry("/api/orders", 2L);
    }

    @Test
    void findSlowRequests_FindsRequestsOver1Second() {
        List<HttpAnalyticsService.SlowRequest> slowRequests =
            analyticsService.findSlowRequests();

        assertThat(slowRequests).hasSize(1);
        assertThat(slowRequests.get(0).uri()).contains("/api/orders");
        assertThat(slowRequests.get(0).durationMs()).isEqualTo(2000);
    }

    @Test
    void getStatusCodeDistribution_CountsByStatus() {
        Map<Integer, Long> distribution =
            analyticsService.getStatusCodeDistribution();

        assertThat(distribution).containsEntry(200, 3L);
        assertThat(distribution).containsEntry(500, 1L);
    }

    private void addExchange(String path, int status, Duration timeTaken) {
        HttpExchange.Request request = new HttpExchange.Request(
            URI.create("http://localhost:8080" + path),
            "127.0.0.1",
            "GET",
            Map.of()
        );

        HttpExchange.Response response = new HttpExchange.Response(
            status,
            Map.of()
        );

        HttpExchange exchange = new HttpExchange(
            Instant.now(),
            request,
            response,
            null,
            null,
            timeTaken
        );

        repository.add(exchange);
    }
}

TROUBLESHOOTING

Common Error: Endpoint Returns Empty Data

Problem: Endpoint returns empty or null data

Causes:

  1. No contributors registered
  2. Contributors not in component scan
  3. Required beans missing

Solutions:

// Solution 1: Ensure contributors are Spring beans
@Component  // <-- Add this
public class MyInfoContributor implements InfoContributor { }

// Solution 2: Verify component scanning
@SpringBootApplication
@ComponentScan(basePackages = {"com.example", "com.example.actuator"})
public class Application { }

// Solution 3: Check auto-configuration
@Configuration
@EnableConfigurationProperties
public class ActuatorConfiguration {
    // Ensure actuator auto-configuration is enabled
}

Common Error: ConfigProps Shows Unexpected Values

Problem: Configuration properties show wrong or missing values

Causes:

  1. Properties not bound
  2. @ConfigurationProperties missing
  3. Wrong prefix

Solutions:

// Ensure class is annotated correctly
@ConfigurationProperties(prefix = "app")  // <-- Correct prefix
@Component  // <-- Or enable via @EnableConfigurationProperties
public class AppProperties {
    private String name;
    // getters/setters required for binding
}

// Verify in application.yml
app:
  name: My Application  # Must match prefix

Common Error: Loggers Endpoint Doesn't Change Levels

Problem: Log level changes don't take effect

Causes:

  1. Wrong logger name
  2. Logging system not supported
  3. Logger configured in logging config

Solutions:

// Use correct logger name
loggersEndpoint.configureLogLevel("com.example.service", LogLevel.DEBUG);
// Not "ServiceClass" - use fully qualified package/class name

// Check logging system is supported
@Bean
public LoggingSystem loggingSystem() {
    // Should auto-configure for Logback, Log4j2, Java Util Logging
}

// For custom logging configs, ensure runtime changes are allowed
// In logback-spring.xml:
// <configuration scan="true" scanPeriod="30 seconds">

PERFORMANCE NOTES

Repository Capacity

// In-memory repositories have limits
@Bean
public InMemoryHttpExchangeRepository httpExchangeRepository() {
    InMemoryHttpExchangeRepository repository =
        new InMemoryHttpExchangeRepository();

    // Default is 100, consider impact:
    // - Low value: Less memory, lose older data
    // - High value: More memory, better history
    repository.setCapacity(1000);  // Adjust based on traffic

    return repository;
}

// For high-traffic apps, implement persistent repository
@Bean
public HttpExchangeRepository persistentRepository(DataSource dataSource) {
    return new JdbcHttpExchangeRepository(dataSource);
}

Thread Dump Performance

// Thread dumps can be expensive
@Configuration
public class ThreadDumpConfiguration {

    // Consider rate limiting for thread dump endpoint
    @Bean
    public ThreadDumpEndpoint threadDumpEndpoint() {
        // Thread dumps lock all threads briefly
        // Can impact performance if called frequently
        return new ThreadDumpEndpoint();
    }
}

// In production, restrict access
management:
  endpoint:
    threaddump:
      enabled: true
      # Only allow authorized users

Heap Dump Considerations

// Heap dumps are VERY expensive
@Bean
public HeapDumpWebEndpoint heapDumpWebEndpoint() {
    // Default timeout is 10 seconds
    // For large heaps, increase timeout
    return new HeapDumpWebEndpoint(30_000); // 30 seconds
}

// WARNING: Heap dumps can:
// - Pause application for seconds/minutes
// - Use significant disk space
// - Expose sensitive data in memory
// - Impact other applications on shared hosts

Cross-References

  • For endpoint framework: Endpoint Framework
  • For custom info contributors: Info Contributors
  • For HTTP monitoring: HTTP Exchange Tracking
  • For audit events: Audit Framework
  • For security: Security Integration
  • For data protection: Data Sanitization