← Back to the section

When an application runs in production, you need to understand what happens inside: how many requests are being processed, where the latency is, what gets written to the logs. All of this is called observability. To make it work correctly, you need to set up four things once: a separate port for the management endpoints, an explicit list of what is exposed, latency histograms, and the log format. Let's go through each one.

A separate port for Actuator

By default Spring Boot runs everything on a single port: both your API (/api/orders) and the Actuator management endpoints (/actuator/health, /actuator/prometheus). This is convenient locally, but it creates problems in production.

If Actuator is on the same port as the API, a network policy can't block external access to it: a Kubernetes Ingress opens the whole port, not individual paths. The Prometheus scraper (which collects metrics every 15 seconds) and health-check probes (every 5 seconds) put load on the same thread that serves business requests.

The solution is simple — different ports:

server:
  port: 8080

management:
  server:
    port: 8081

Now the Ingress publishes only 8080, and the network policy allows Prometheus to reach only 8081. Management traffic doesn't interfere with business logic.

An explicit list of exposed endpoints

The Spring Boot default exposes only health and info. When people add '*' to quickly see everything at once, far more gets exposed than needed.

Some endpoints are unsafe for public access:

  • /actuator/env — shows all configuration properties, including those that ended up there via environment variables. If a secret landed there by accident, it will be visible.
  • /actuator/heapdump — a full dump of the JVM memory. It may contain JWT tokens, passwords, and personal data of users currently being processed.
  • /actuator/threaddump — the stack of all threads. It reveals the internal structure of the application.

The rule is simple: only list what is actually needed:

management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics,prometheus

metrics is needed for manual debugging via JSON. prometheus — for the scraper. Everything else stays closed.

If you really need heapdump in production for debugging — use Spring Security with an administrator role and audit every access. By default, don't expose it.

Latency histograms and SLO buckets

The standard http.server.requests metric in Micrometer counts only the number of requests and the total time by default. To know how many requests fit within 500 ms or what the p95 is, you need a histogram.

management:
  metrics:
    distribution:
      percentiles-histogram:
        http.server.requests: true
      slo:
        http.server.requests: 100ms,500ms,1s,5s
    tags:
      service: ${spring.application.name}
      env: ${ENV:dev}
      version: ${BUILD_VERSION:unknown}

percentiles-histogram: true makes Micrometer publish the full histogram (~64 buckets). In Prometheus this lets you compute the exact quantile via histogram_quantile() — without interpolation.

slo: 100ms,500ms,1s,5s adds explicit threshold buckets. In Prometheus a http_server_requests_seconds_bucket{le="0.5"} metric will appear — "how many requests completed faster than 500 ms". This is handy for SLO alerts: no more need to interpolate.

tags.service/env/version — global labels that are added to all metrics automatically. Without them you can't tell which service and environment the data came from. BUILD_VERSION is set from CI as an environment variable.

Two logging profiles

In development single-line readable logs are convenient. In production you need structured JSON that Logstash or Vector can parse and index.

Logback supports Spring profiles right inside logback-spring.xml:

<configuration>
    <springProfile name="dev,test">
        <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
            <encoder>
                <pattern>%d{HH:mm:ss.SSS} %-5level [%thread] %X{traceId:-} %logger{30} - %msg%n</pattern>
            </encoder>
        </appender>
    </springProfile>

    <springProfile name="prod,staging">
        <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
            <encoder class="net.logstash.logback.encoder.LogstashEncoder">
                <includeMdcKeyName>traceId</includeMdcKeyName>
                <includeMdcKeyName>spanId</includeMdcKeyName>
                <includeMdcKeyName>requestId</includeMdcKeyName>
                <includeMdcKeyName>userId</includeMdcKeyName>
            </encoder>
        </appender>
    </springProfile>

    <root level="INFO">
        <appender-ref ref="STDOUT"/>
    </root>
</configuration>

springProfile is a Logback extension that Spring Boot activates on startup. The right appender is enabled depending on the profile.

In the dev pattern %X{traceId:-} takes traceId from the MDC and substitutes it into the log line. If tracing isn't configured — an empty string, nothing breaks. Later you use this identifier to find the request in Tempo or Jaeger.

In production LogstashEncoder writes each line as a JSON object. The MDC fields listed in <includeMdcKeyName> end up in the JSON explicitly. The remaining MDC keys go into the mdc block automatically.

Common mistakes

A single port for the API and Actuator in production. Network isolation becomes impossible. management.server.port: 8081 solves the problem.

exposure.include: '*'. It exposes env, beans, mappings, loggers, configprops, heapdump. Each of them is a potential data leak. An explicit list is safer:

# don't do this
management.endpoints.web.exposure.include: '*'

# do this instead
management.endpoints.web.exposure.include: health,info,metrics,prometheus

A single Logback pattern for dev and prod. In development you lose the readability of JSON, in production you lose the structure. springProfile is the standard way to split them.

No histogram for latency metrics. Then quantiles are either not computed at all or only approximate. percentiles-histogram: true for http.server.requests is the minimal set.

In short

  • Run Actuator on a separate port (management.server.port: 8081) — this gives you network isolation and doesn't interfere with business traffic.
  • Explicitly list the exposed endpoints: health,info,metrics,prometheus. The wildcard '*' is a source of leaks.
  • Don't expose /actuator/env, /actuator/heapdump, /actuator/threaddump publicly — they contain secrets and memory dumps.
  • percentiles-histogram: true + slo: 100ms,500ms,1s,5s for http.server.requests is the foundation for latency SLO alerts.
  • Global tags service/env/version in management.metrics.tags immediately add context to all metrics.
  • Two profiles in logback-spring.xml: text for dev,test, JSON via LogstashEncoder for prod,staging.
  • Logging in Java — structured JSON, MDC, levels
  • Metrics — Micrometer, Prometheus, RED/USE
  • Tracing — OpenTelemetry, traceparent, sampling
  • Health checks — liveness, readiness, custom HealthIndicator
  • SLO and alerts — error budget and burn rate