feat: Docker support, OpenAPI documentation, and full observability stack#3
Conversation
Agent-Logs-Url: https://github.com/vitorhugo-java/SpringBoot-JobApplyTracker/sessions/5f1775da-aadb-4d0a-8e9b-d95b9333e860 Co-authored-by: vitorhugo-java <65777252+vitorhugo-java@users.noreply.github.com>
Agent-Logs-Url: https://github.com/vitorhugo-java/SpringBoot-JobApplyTracker/sessions/5f1775da-aadb-4d0a-8e9b-d95b9333e860 Co-authored-by: vitorhugo-java <65777252+vitorhugo-java@users.noreply.github.com>
There was a problem hiding this comment.
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.
| String userId = resolveUserId(); | ||
| log.info("method={} path={} status={} duration={}ms userId={}", | ||
| request.getMethod(), | ||
| request.getRequestURI(), | ||
| response.getStatus(), | ||
| duration, | ||
| userId); | ||
| } | ||
| } | ||
|
|
||
| private String resolveUserId() { |
There was a problem hiding this comment.
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.
| 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() { |
| @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(), |
There was a problem hiding this comment.
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.
| String userId = (auth != null && auth.isAuthenticated()) ? auth.getName() : "unknown"; | ||
| log.info("event=LOGOUT_SUCCESS userId={}", userId); |
There was a problem hiding this comment.
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.
| 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); |
| management: | ||
| endpoints: | ||
| web: | ||
| exposure: | ||
| include: health,info,metrics,prometheus |
There was a problem hiding this comment.
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).
| "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}}" | ||
| } |
There was a problem hiding this comment.
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.
| <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> |
There was a problem hiding this comment.
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.
| <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> |
| "/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() |
There was a problem hiding this comment.
/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.
| .requestMatchers("/actuator/health", "/actuator/info", "/actuator/prometheus").permitAll() | |
| .requestMatchers("/actuator/health", "/actuator/info").permitAll() |
|
@copilot apply changes based on the comments in this thread |
Agent-Logs-Url: https://github.com/vitorhugo-java/SpringBoot-JobApplyTracker/sessions/9b792434-2e5e-4bb8-a9bd-e729d1a13085 Co-authored-by: vitorhugo-java <65777252+vitorhugo-java@users.noreply.github.com>
Agent-Logs-Url: https://github.com/vitorhugo-java/SpringBoot-JobApplyTracker/sessions/9b792434-2e5e-4bb8-a9bd-e729d1a13085 Co-authored-by: vitorhugo-java <65777252+vitorhugo-java@users.noreply.github.com>
Applied all review comments in commit
|
…-boot-api-with-docker-support
…ttps://github.com/vitorhugo-java/SpringBoot-JobApplyTracker into copilot/extend-spring-boot-api-with-docker-support
Extends the Spring Boot Job Apply Tracker API with containerization, complete Swagger documentation, structured logging, and Prometheus/Grafana metrics.
Docker
Dockerfile:eclipse-temurin:21-jdk-alpinebuild →eclipse-temurin:21-jre-alpineruntime; non-root user; container-aware JVM flags; healthcheck on management port8081docker-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; Grafanadepends_onusescondition: service_healthydocker-compose.prod.yml: Overlay for production — all secrets externalized via env vars, MariaDB port closed, resource limits,prodSpring profile activeprometheus.yml: Scrapes/actuator/prometheusatapp:8081(dedicated management port) every 15sGrafana
OpenAPI / Swagger
"Job Apply Tracker API"with version1.0.0@Tag+@Operation(summary, description, typed@ApiResponses)@Schema(field descriptions + examples)Observability
spring-boot-starter-actuator+micrometer-registry-prometheus+logstash-logback-encoder:7.4; exposeshealth,info,metrics,prometheusendpoints on a dedicated management port (8081) — completely off the main Spring Security filter chainlogback-spring.xml: Proper JSON structured logs underprodprofile usingLogstashEncoder(correct escaping of quotes, newlines, backslashes); human-readable console in dev; per-package log levelsRequestLoggingFilter: logs every inbound API request —method,path,status,duration,userEmail;/actuator/**paths excluded to avoid log spam from health probes and Prometheus scrapesAuthService: structured auth events —REGISTRATION_SUCCESS,LOGIN_SUCCESS,LOGIN_FAILURE(with reason),LOGOUT_SUCCESS— all includeuserEmailGlobalExceptionHandler:warn/errorlog for every handled exception type