Skip to content

feat: Docker support, OpenAPI documentation, and full observability stack#3

Merged
vitorhugo-java merged 6 commits intomainfrom
copilot/extend-spring-boot-api-with-docker-support
Apr 13, 2026
Merged

feat: Docker support, OpenAPI documentation, and full observability stack#3
vitorhugo-java merged 6 commits intomainfrom
copilot/extend-spring-boot-api-with-docker-support

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 13, 2026

Extends the Spring Boot Job Apply Tracker API with containerization, complete Swagger documentation, structured logging, and Prometheus/Grafana metrics.

Docker

  • Multi-stage Dockerfile: eclipse-temurin:21-jdk-alpine build → eclipse-temurin:21-jre-alpine runtime; non-root user; container-aware JVM flags; healthcheck on management port 8081
  • docker-compose.yml: Dev stack — app + MariaDB 11 + Prometheus (prom/prometheus:v2.54.1) + Grafana (grafana/grafana:10.4.8) with health-gated startup order; Prometheus healthcheck added; Grafana depends_on uses condition: service_healthy
  • docker-compose.prod.yml: Overlay for production — all secrets externalized via env vars, MariaDB port closed, resource limits, prod Spring profile active
  • prometheus.yml: Scrapes /actuator/prometheus at app:8081 (dedicated management port) every 15s

Grafana

  • Auto-provisioned Prometheus datasource
  • Pre-built dashboard: HTTP request rate, p99 latency, error rate (4xx/5xx), JVM heap, CPU usage, HikariCP active connections

OpenAPI / Swagger

  • Title updated to "Job Apply Tracker API" with version 1.0.0
  • Springdoc groups: Auth API, Application API, Dashboard API (path-matched)
  • All controllers annotated with @Tag + @Operation (summary, description, typed @ApiResponses)
  • All DTOs annotated with @Schema (field descriptions + examples)

Observability

  • Added spring-boot-starter-actuator + micrometer-registry-prometheus + logstash-logback-encoder:7.4; exposes health, info, metrics, prometheus endpoints on a dedicated management port (8081) — completely off the main Spring Security filter chain
  • logback-spring.xml: Proper JSON structured logs under prod profile using LogstashEncoder (correct escaping of quotes, newlines, backslashes); human-readable console in dev; per-package log levels
  • RequestLoggingFilter: logs every inbound API request — method, path, status, duration, userEmail; /actuator/** paths excluded to avoid log spam from health probes and Prometheus scrapes
  • AuthService: structured auth events — REGISTRATION_SUCCESS, LOGIN_SUCCESS, LOGIN_FAILURE (with reason), LOGOUT_SUCCESS — all include userEmail
  • GlobalExceptionHandler: warn/error log for every handled exception type
  • HTTP request histogram buckets enabled for accurate p99 latency in Grafana
# application.yml — actuator on dedicated management port
management:
  server:
    port: ${MANAGEMENT_PORT:8081}
  endpoints:
    web:
      exposure:
        include: health,info,metrics,prometheus
  metrics:
    tags:
      application: ${spring.application.name}
    distribution:
      percentiles-histogram:
        http.server.requests: true

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds containerization and an observability stack (Actuator + Prometheus + Grafana), plus expanded OpenAPI/Swagger documentation and structured logging for the Spring Boot Job Apply Tracker API.

Changes:

  • Added Docker + Compose (dev/prod overlays) and Prometheus/Grafana provisioning (datasource + dashboard).
  • Enabled Actuator + Prometheus metrics export; added request/auth/exception structured logging.
  • Expanded Springdoc OpenAPI configuration and annotated controllers/DTOs for richer Swagger docs.

Reviewed changes

Copilot reviewed 34 out of 34 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
prometheus.yml Prometheus scrape configuration targeting the app’s /actuator/prometheus.
grafana/provisioning/datasources/datasource.yml Auto-provisions Prometheus datasource in Grafana.
grafana/provisioning/dashboards/dashboard.yml Auto-provisions dashboards from the mounted dashboards path.
grafana/dashboards/job-tracker-dashboard.json Adds a Grafana dashboard with HTTP/JVM/DB panels.
docker-compose.yml Dev stack: app + MariaDB + Prometheus + Grafana, with volumes/networking.
docker-compose.prod.yml Production overrides: profile activation, secrets via env vars, reduced exposure.
Dockerfile Multi-stage Java 21 build/runtime container image for the backend.
backend/pom.xml Adds Actuator + Micrometer Prometheus registry dependencies.
backend/src/main/resources/application.yml Exposes actuator endpoints; adds global application metrics tag; springdoc group configs.
backend/src/main/resources/logback-spring.xml Adds dev vs prod logging layouts including JSON-ish prod output.
backend/src/main/java/com/jobtracker/config/SecurityConfig.java Permits actuator endpoints and installs request logging filter.
backend/src/main/java/com/jobtracker/config/RequestLoggingFilter.java Logs inbound requests with method/path/status/duration and “userId”.
backend/src/main/java/com/jobtracker/config/OpenApiConfig.java Updates OpenAPI title/description/version.
backend/src/main/java/com/jobtracker/service/AuthService.java Adds structured auth event logging (register/login/logout).
backend/src/main/java/com/jobtracker/exception/GlobalExceptionHandler.java Adds structured logs for handled exceptions/validation/general errors.
backend/src/main/java/com/jobtracker/controller/AuthController.java Adds @Tag/@Operation/@ApiResponse annotations for Swagger.
backend/src/main/java/com/jobtracker/controller/ApplicationController.java Adds Swagger annotations for CRUD/list endpoints and parameters.
backend/src/main/java/com/jobtracker/controller/DashboardController.java Adds Swagger annotations for dashboard summary endpoint.
backend/src/main/java/com/jobtracker/dto/** Adds @Schema descriptions/examples to DTOs for better OpenAPI docs.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +31 to +41
String userId = resolveUserId();
log.info("method={} path={} status={} duration={}ms userId={}",
request.getMethod(),
request.getRequestURI(),
response.getStatus(),
duration,
userId);
}
}

private String resolveUserId() {
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

userId here is derived from Authentication#getName(), which (with the current JWT auth setup) is the user’s email, not the numeric user ID. This makes logs inconsistent with other places (e.g., AuthService logs user.getId()) and can mislead dashboards/queries. Consider renaming the field to userEmail or changing the security principal to carry/log the actual user ID consistently.

Suggested change
String userId = resolveUserId();
log.info("method={} path={} status={} duration={}ms userId={}",
request.getMethod(),
request.getRequestURI(),
response.getStatus(),
duration,
userId);
}
}
private String resolveUserId() {
String userEmail = resolveUserEmail();
log.info("method={} path={} status={} duration={}ms userEmail={}",
request.getMethod(),
request.getRequestURI(),
response.getStatus(),
duration,
userEmail);
}
}
private String resolveUserEmail() {

Copilot uses AI. Check for mistakes.
Comment on lines +22 to +33
@Override
protected void doFilterInternal(@NonNull HttpServletRequest request,
@NonNull HttpServletResponse response,
@NonNull FilterChain filterChain) throws ServletException, IOException {
long start = System.currentTimeMillis();
try {
filterChain.doFilter(request, response);
} finally {
long duration = System.currentTimeMillis() - start;
String userId = resolveUserId();
log.info("method={} path={} status={} duration={}ms userId={}",
request.getMethod(),
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This filter logs every request at INFO, including high-frequency endpoints like /actuator/health and Prometheus scrapes. In production this can generate significant log volume and cost, and can drown out application logs. Consider excluding /actuator/** (override shouldNotFilter), or making the log level/path allowlist configurable.

Copilot uses AI. Check for mistakes.
Comment on lines +131 to +132
String userId = (auth != null && auth.isAuthenticated()) ? auth.getName() : "unknown";
log.info("event=LOGOUT_SUCCESS userId={}", userId);
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

auth.getName() will resolve to the authenticated principal name (currently the email), but the log field is labeled userId. This is inconsistent with other auth logs that use the numeric user.getId(), making correlation harder. Consider logging userEmail here, or consistently logging the numeric user ID everywhere.

Suggested change
String userId = (auth != null && auth.isAuthenticated()) ? auth.getName() : "unknown";
log.info("event=LOGOUT_SUCCESS userId={}", userId);
String userEmail = (auth != null && auth.isAuthenticated()) ? auth.getName() : "unknown";
log.info("event=LOGOUT_SUCCESS userEmail={}", userEmail);

Copilot uses AI. Check for mistakes.
Comment on lines +33 to +37
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

management.endpoints.web.exposure.include enables /actuator/metrics, but current SecurityConfig only permits unauthenticated access to health/info/prometheus. If /actuator/metrics is meant to be reachable (as described in the PR), you’ll need to also allow it in security (or clarify that it should remain authenticated-only).

Copilot uses AI. Check for mistakes.
Comment on lines +24 to +30
"title": "HTTP Response Time (p99 ms)",
"gridPos": { "x": 12, "y": 0, "w": 12, "h": 8 },
"targets": [
{
"expr": "histogram_quantile(0.99, sum(rate(http_server_requests_seconds_bucket{application=\"job-tracker\"}[1m])) by (le, uri)) * 1000",
"legendFormat": "p99 {{uri}}"
}
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This p99 panel queries http_server_requests_seconds_bucket and uses histogram_quantile, but Spring/Micrometer won’t emit the *_bucket series unless HTTP request histograms are enabled (e.g., management.metrics.distribution.percentiles-histogram.http.server.requests=true). Without that, this panel will be empty/error. Either add the required metrics config or switch the query to a metric that’s guaranteed to exist in the default setup.

Copilot uses AI. Check for mistakes.
Comment thread docker-compose.yml Outdated
Comment thread docker-compose.yml Outdated
Comment thread docker-compose.yml
Comment on lines +8 to +9
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>{"timestamp":"%d{yyyy-MM-dd'T'HH:mm:ss.SSSZ}","level":"%level","app":"${appName}","logger":"%logger{36}","thread":"%thread","message":"%msg"}%n</pattern>
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The JSON log line is built via a plain pattern and doesn’t JSON-escape the message (or other fields). Any log message containing quotes, newlines, or backslashes will emit invalid JSON and can break log ingestion/parsing. Consider switching to a JSON encoder (e.g., logstash-logback-encoder) or at least using a layout/encoder that properly escapes JSON values.

Suggested change
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>{"timestamp":"%d{yyyy-MM-dd'T'HH:mm:ss.SSSZ}","level":"%level","app":"${appName}","logger":"%logger{36}","thread":"%thread","message":"%msg"}%n</pattern>
<encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
<providers>
<timestamp>
<fieldName>timestamp</fieldName>
<pattern>yyyy-MM-dd'T'HH:mm:ss.SSSZ</pattern>
</timestamp>
<logLevel>
<fieldName>level</fieldName>
</logLevel>
<pattern>
<pattern>{"app":"${appName}"}</pattern>
</pattern>
<loggerName>
<fieldName>logger</fieldName>
<shortenedLoggerNameLength>36</shortenedLoggerNameLength>
</loggerName>
<threadName>
<fieldName>thread</fieldName>
</threadName>
<message>
<fieldName>message</fieldName>
</message>
</providers>

Copilot uses AI. Check for mistakes.
"/api/auth/refresh", "/api/auth/forgot-password",
"/api/auth/reset-password").permitAll()
.requestMatchers("/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html").permitAll()
.requestMatchers("/actuator/health", "/actuator/info", "/actuator/prometheus").permitAll()
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/actuator/prometheus is permitted for unauthenticated access. That endpoint can leak sensitive operational details (endpoints, JVM/process info, etc.) if the app is internet-accessible. Consider restricting it (e.g., require auth/role, run management endpoints on a separate internal port, or gate access by network) at least for the prod profile.

Suggested change
.requestMatchers("/actuator/health", "/actuator/info", "/actuator/prometheus").permitAll()
.requestMatchers("/actuator/health", "/actuator/info").permitAll()

Copilot uses AI. Check for mistakes.
@vitorhugo-java
Copy link
Copy Markdown
Owner

@copilot apply changes based on the comments in this thread

Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 13, 2026

@copilot apply changes based on the comments in this thread

Applied all review comments in commit 50b3655:

  • userIduserEmail in RequestLoggingFilter and AuthService logout log — auth.getName() returns the email, so the field name now matches the value.
  • Actuator log noiseRequestLoggingFilter now overrides shouldNotFilter() to skip all /actuator/** paths.
  • /actuator/prometheus exposure — moved all actuator endpoints to a dedicated management port (management.server.port=8081). This takes them completely off the main Spring Security filter chain. Updated prometheus.yml, Dockerfile healthchecks, and docker-compose.yml app healthcheck to use port 8081. EXPOSE 8081 added to the Dockerfile.
  • p99 Grafana panel — added management.metrics.distribution.percentiles-histogram.http.server.requests: true so Micrometer emits the *_bucket series the histogram panel needs.
  • Image pinning — Prometheus pinned to prom/prometheus:v2.54.1, Grafana to grafana/grafana:10.4.8.
  • Prometheus healthcheck — added healthcheck on /-/healthy; Grafana depends_on now uses condition: service_healthy.
  • JSON log escaping — replaced the pattern-based JSON encoder with logstash-logback-encoder:7.4 (LogstashEncoder) which correctly escapes quotes, newlines, and backslashes.

@vitorhugo-java vitorhugo-java merged commit 11201b2 into main Apr 13, 2026
1 check passed
@vitorhugo-java vitorhugo-java deleted the copilot/extend-spring-boot-api-with-docker-support branch April 13, 2026 17:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants