diff --git a/CLAUDE.md b/CLAUDE.md index 57c2c73c..c39469dd 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -110,6 +110,9 @@ java -jar cameleer3-server-app/target/cameleer3-server-app-1.0-SNAPSHOT.jar - `PrometheusLabelBuilder` — generates Prometheus Docker labels (`prometheus.scrape/path/port`) per runtime type for `docker_sd_configs` auto-discovery - `DisabledRuntimeOrchestrator` — no-op when runtime not enabled +**metrics/** — Prometheus observability +- `ServerMetrics` — centralized business metrics: gauges (agents by state, SSE connections, buffer depths), counters (ingestion drops, agent transitions, deployment outcomes, auth failures), timers (flush duration, deployment duration). Exposed via `/api/v1/prometheus`. + **storage/** — PostgreSQL repositories (JdbcTemplate) - `PostgresAppRepository`, `PostgresAppVersionRepository`, `PostgresEnvironmentRepository` - `PostgresDeploymentRepository` — includes JSONB replica_states, deploy_stage, findByContainerId @@ -300,7 +303,7 @@ In SaaS mode, each tenant's server and its deployed apps are isolated at the Doc # GitNexus — Code Intelligence -This project is indexed by GitNexus as **cameleer3-server** (5968 symbols, 15141 relationships, 300 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely. +This project is indexed by GitNexus as **cameleer3-server** (5987 symbols, 15177 relationships, 300 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely. > If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first. diff --git a/cameleer3-server-app/pom.xml b/cameleer3-server-app/pom.xml index 74817c04..70594769 100644 --- a/cameleer3-server-app/pom.xml +++ b/cameleer3-server-app/pom.xml @@ -31,6 +31,10 @@ org.springframework.boot spring-boot-starter-actuator + + io.micrometer + micrometer-registry-prometheus + org.springframework.boot spring-boot-starter-jdbc diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/agent/AgentLifecycleMonitor.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/agent/AgentLifecycleMonitor.java index c3f726e7..d2cec6c6 100644 --- a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/agent/AgentLifecycleMonitor.java +++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/agent/AgentLifecycleMonitor.java @@ -1,5 +1,6 @@ package com.cameleer3.server.app.agent; +import com.cameleer3.server.app.metrics.ServerMetrics; import com.cameleer3.server.core.agent.AgentEventService; import com.cameleer3.server.core.agent.AgentInfo; import com.cameleer3.server.core.agent.AgentRegistryService; @@ -26,11 +27,14 @@ public class AgentLifecycleMonitor { private final AgentRegistryService registryService; private final AgentEventService agentEventService; + private final ServerMetrics serverMetrics; public AgentLifecycleMonitor(AgentRegistryService registryService, - AgentEventService agentEventService) { + AgentEventService agentEventService, + ServerMetrics serverMetrics) { this.registryService = registryService; this.agentEventService = agentEventService; + this.serverMetrics = serverMetrics; } @Scheduled(fixedDelayString = "${agent-registry.lifecycle-check-interval-ms:10000}") @@ -53,6 +57,7 @@ public class AgentLifecycleMonitor { if (eventType != null) { agentEventService.recordEvent(agent.instanceId(), agent.applicationId(), eventType, agent.displayName() + " " + before + " -> " + agent.state()); + serverMetrics.recordAgentTransition(eventType); } } } diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/agent/SseConnectionManager.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/agent/SseConnectionManager.java index fd8d09eb..65506d26 100644 --- a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/agent/SseConnectionManager.java +++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/agent/SseConnectionManager.java @@ -131,6 +131,10 @@ public class SseConnectionManager implements AgentEventListener { /** * Check if an agent has an active SSE connection. */ + public int getConnectionCount() { + return emitters.size(); + } + public boolean isConnected(String agentId) { return emitters.containsKey(agentId); } diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/config/StorageBeanConfig.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/config/StorageBeanConfig.java index 3f9233af..e203db2b 100644 --- a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/config/StorageBeanConfig.java +++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/config/StorageBeanConfig.java @@ -1,5 +1,6 @@ package com.cameleer3.server.app.config; +import com.cameleer3.server.app.metrics.ServerMetrics; import com.cameleer3.server.app.search.ClickHouseLogStore; import com.cameleer3.server.app.storage.ClickHouseAgentEventRepository; import com.cameleer3.server.app.storage.ClickHouseUsageTracker; @@ -113,9 +114,10 @@ public class StorageBeanConfig { ClickHouseExecutionStore executionStore, ClickHouseLogStore logStore, ChunkAccumulator accumulator, - IngestionConfig config) { + IngestionConfig config, + ServerMetrics serverMetrics) { return new ExecutionFlushScheduler(executionBuffer, processorBatchBuffer, - logBuffer, executionStore, logStore, accumulator, config); + logBuffer, executionStore, logStore, accumulator, config, serverMetrics); } @Bean diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/LogIngestionController.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/LogIngestionController.java index fa2acf68..21f580cd 100644 --- a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/LogIngestionController.java +++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/LogIngestionController.java @@ -1,6 +1,7 @@ package com.cameleer3.server.app.controller; import com.cameleer3.common.model.LogEntry; +import com.cameleer3.server.app.metrics.ServerMetrics; import com.cameleer3.server.core.ingestion.BufferedLogEntry; import java.util.List; import com.cameleer3.server.core.ingestion.WriteBuffer; @@ -32,13 +33,16 @@ public class LogIngestionController { private final WriteBuffer logBuffer; private final AgentRegistryService registryService; private final TenantProperties tenantProperties; + private final ServerMetrics serverMetrics; public LogIngestionController(WriteBuffer logBuffer, AgentRegistryService registryService, - TenantProperties tenantProperties) { + TenantProperties tenantProperties, + ServerMetrics serverMetrics) { this.logBuffer = logBuffer; this.registryService = registryService; this.tenantProperties = tenantProperties; + this.serverMetrics = serverMetrics; } @PostMapping("/logs") @@ -49,6 +53,7 @@ public class LogIngestionController { String instanceId = extractAgentId(); if (instanceId == null || instanceId.isBlank()) { log.warn("Log ingestion rejected: no agent identity in request (unauthenticated or missing principal)"); + serverMetrics.recordIngestionDrop("no_identity"); return ResponseEntity.accepted().build(); } @@ -61,6 +66,7 @@ public class LogIngestionController { if (agent == null) { log.warn("Log ingestion from instance={}: agent not found in registry (not registered or expired). {} entries dropped.", instanceId, entries.size()); + serverMetrics.recordIngestionDrops("no_agent", entries.size()); return ResponseEntity.accepted().build(); } @@ -89,6 +95,7 @@ public class LogIngestionController { if (dropped > 0) { log.warn("Log buffer full: accepted={}, dropped={} from instance={}, app={}", accepted, dropped, instanceId, applicationId); + serverMetrics.recordIngestionDrops("buffer_full", dropped); } else { log.debug("Accepted {} log entries from instance={}, app={}", accepted, instanceId, applicationId); } diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/ingestion/ExecutionFlushScheduler.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/ingestion/ExecutionFlushScheduler.java index a9813508..4d1e341d 100644 --- a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/ingestion/ExecutionFlushScheduler.java +++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/ingestion/ExecutionFlushScheduler.java @@ -12,6 +12,9 @@ import org.slf4j.LoggerFactory; import org.springframework.context.SmartLifecycle; import org.springframework.scheduling.annotation.Scheduled; +import com.cameleer3.server.app.metrics.ServerMetrics; + +import java.time.Duration; import java.util.List; /** @@ -33,6 +36,7 @@ public class ExecutionFlushScheduler implements SmartLifecycle { private final ClickHouseExecutionStore executionStore; private final ClickHouseLogStore logStore; private final ChunkAccumulator accumulator; + private final ServerMetrics serverMetrics; private final int batchSize; private volatile boolean running = false; @@ -42,7 +46,8 @@ public class ExecutionFlushScheduler implements SmartLifecycle { ClickHouseExecutionStore executionStore, ClickHouseLogStore logStore, ChunkAccumulator accumulator, - IngestionConfig config) { + IngestionConfig config, + ServerMetrics serverMetrics) { this.executionBuffer = executionBuffer; this.processorBuffer = processorBuffer; this.logBuffer = logBuffer; @@ -50,14 +55,18 @@ public class ExecutionFlushScheduler implements SmartLifecycle { this.logStore = logStore; this.accumulator = accumulator; this.batchSize = config.getBatchSize(); + this.serverMetrics = serverMetrics; } - @Scheduled(fixedDelayString = "${ingestion.flush-interval-ms:1000}") + @Scheduled(fixedDelayString = "${cameleer.server.ingestion.flushintervalms:1000}") public void flush() { try { List executions = executionBuffer.drain(batchSize); if (!executions.isEmpty()) { + long start = System.nanoTime(); executionStore.insertExecutionBatch(executions); + serverMetrics.recordFlushDuration("execution", + Duration.ofNanos(System.nanoTime() - start)); log.debug("Flushed {} executions to ClickHouse", executions.size()); } } catch (Exception e) { @@ -67,7 +76,10 @@ public class ExecutionFlushScheduler implements SmartLifecycle { try { List batches = processorBuffer.drain(batchSize); if (!batches.isEmpty()) { + long start = System.nanoTime(); executionStore.insertProcessorBatches(batches); + serverMetrics.recordFlushDuration("processor", + Duration.ofNanos(System.nanoTime() - start)); log.debug("Flushed {} processor batches to ClickHouse", batches.size()); } } catch (Exception e) { @@ -77,7 +89,10 @@ public class ExecutionFlushScheduler implements SmartLifecycle { try { List logEntries = logBuffer.drain(batchSize); if (!logEntries.isEmpty()) { + long start = System.nanoTime(); logStore.insertBufferedBatch(logEntries); + serverMetrics.recordFlushDuration("log", + Duration.ofNanos(System.nanoTime() - start)); log.debug("Flushed {} log entries to ClickHouse", logEntries.size()); } } catch (Exception e) { diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/metrics/ServerMetrics.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/metrics/ServerMetrics.java new file mode 100644 index 00000000..534a4de2 --- /dev/null +++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/metrics/ServerMetrics.java @@ -0,0 +1,190 @@ +package com.cameleer3.server.app.metrics; + +import com.cameleer3.server.app.agent.SseConnectionManager; +import com.cameleer3.server.core.agent.AgentRegistryService; +import com.cameleer3.server.core.agent.AgentState; +import com.cameleer3.server.core.ingestion.BufferedLogEntry; +import com.cameleer3.server.core.ingestion.ChunkAccumulator; +import com.cameleer3.server.core.ingestion.MergedExecution; +import com.cameleer3.server.core.ingestion.WriteBuffer; +import com.cameleer3.server.core.storage.model.MetricsSnapshot; +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.Gauge; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Timer; +import org.springframework.stereotype.Component; + +import java.time.Duration; +import java.util.concurrent.TimeUnit; + +@Component +public class ServerMetrics { + + private final MeterRegistry registry; + + // Counters + private final Counter ingestionDropsBufferFull; + private final Counter ingestionDropsNoAgent; + private final Counter ingestionDropsNoIdentity; + private final Counter transitionsWentStale; + private final Counter transitionsWentDead; + private final Counter transitionsRecovered; + private final Counter deploymentsRunning; + private final Counter deploymentsFailed; + private final Counter deploymentsDegraded; + private final Counter authFailuresInvalidToken; + private final Counter authFailuresRevoked; + private final Counter authFailuresOidc; + + // Timers + private final Timer flushExecutionTimer; + private final Timer flushProcessorTimer; + private final Timer flushLogTimer; + private final Timer deploymentDurationTimer; + + public ServerMetrics(MeterRegistry registry, + AgentRegistryService registryService, + SseConnectionManager sseManager, + WriteBuffer executionBuffer, + WriteBuffer processorBuffer, + WriteBuffer logBuffer, + WriteBuffer metricsBuffer, + ChunkAccumulator accumulator) { + this.registry = registry; + + // ── Gauges (auto-polled) ──────────────────────────────────────── + + for (AgentState state : AgentState.values()) { + Gauge.builder("cameleer.agents.connected", registryService, + r -> r.findByState(state).size()) + .tag("state", state.name().toLowerCase()) + .register(registry); + } + + Gauge.builder("cameleer.agents.sse.active", sseManager, + SseConnectionManager::getConnectionCount) + .register(registry); + + Gauge.builder("cameleer.ingestion.buffer.size", executionBuffer, WriteBuffer::size) + .tag("type", "execution") + .register(registry); + Gauge.builder("cameleer.ingestion.buffer.size", processorBuffer, WriteBuffer::size) + .tag("type", "processor") + .register(registry); + Gauge.builder("cameleer.ingestion.buffer.size", logBuffer, WriteBuffer::size) + .tag("type", "log") + .register(registry); + Gauge.builder("cameleer.ingestion.buffer.size", metricsBuffer, WriteBuffer::size) + .tag("type", "metrics") + .register(registry); + + Gauge.builder("cameleer.ingestion.accumulator.pending", accumulator, + ChunkAccumulator::getPendingCount) + .register(registry); + + // ── Counters ──────────────────────────────────────────────────── + + ingestionDropsBufferFull = Counter.builder("cameleer.ingestion.drops") + .tag("reason", "buffer_full").register(registry); + ingestionDropsNoAgent = Counter.builder("cameleer.ingestion.drops") + .tag("reason", "no_agent").register(registry); + ingestionDropsNoIdentity = Counter.builder("cameleer.ingestion.drops") + .tag("reason", "no_identity").register(registry); + + transitionsWentStale = Counter.builder("cameleer.agents.transitions") + .tag("transition", "went_stale").register(registry); + transitionsWentDead = Counter.builder("cameleer.agents.transitions") + .tag("transition", "went_dead").register(registry); + transitionsRecovered = Counter.builder("cameleer.agents.transitions") + .tag("transition", "recovered").register(registry); + + deploymentsRunning = Counter.builder("cameleer.deployments.outcome") + .tag("status", "running").register(registry); + deploymentsFailed = Counter.builder("cameleer.deployments.outcome") + .tag("status", "failed").register(registry); + deploymentsDegraded = Counter.builder("cameleer.deployments.outcome") + .tag("status", "degraded").register(registry); + + authFailuresInvalidToken = Counter.builder("cameleer.auth.failures") + .tag("reason", "invalid_token").register(registry); + authFailuresRevoked = Counter.builder("cameleer.auth.failures") + .tag("reason", "revoked").register(registry); + authFailuresOidc = Counter.builder("cameleer.auth.failures") + .tag("reason", "oidc_rejected").register(registry); + + // ── Timers ────────────────────────────────────────────────────── + + flushExecutionTimer = Timer.builder("cameleer.ingestion.flush.duration") + .tag("type", "execution").register(registry); + flushProcessorTimer = Timer.builder("cameleer.ingestion.flush.duration") + .tag("type", "processor").register(registry); + flushLogTimer = Timer.builder("cameleer.ingestion.flush.duration") + .tag("type", "log").register(registry); + + deploymentDurationTimer = Timer.builder("cameleer.deployments.duration") + .register(registry); + } + + // ── Ingestion ─────────────────────────────────────────────────────── + + public void recordIngestionDrop(String reason) { + switch (reason) { + case "buffer_full" -> ingestionDropsBufferFull.increment(); + case "no_agent" -> ingestionDropsNoAgent.increment(); + case "no_identity" -> ingestionDropsNoIdentity.increment(); + } + } + + public void recordIngestionDrops(String reason, int count) { + switch (reason) { + case "buffer_full" -> ingestionDropsBufferFull.increment(count); + case "no_agent" -> ingestionDropsNoAgent.increment(count); + case "no_identity" -> ingestionDropsNoIdentity.increment(count); + } + } + + // ── Agent lifecycle ───────────────────────────────────────────────── + + public void recordAgentTransition(String transition) { + switch (transition) { + case "WENT_STALE" -> transitionsWentStale.increment(); + case "WENT_DEAD" -> transitionsWentDead.increment(); + case "RECOVERED" -> transitionsRecovered.increment(); + } + } + + // ── Deployments ───────────────────────────────────────────────────── + + public void recordDeploymentOutcome(String status) { + switch (status) { + case "RUNNING" -> deploymentsRunning.increment(); + case "FAILED" -> deploymentsFailed.increment(); + case "DEGRADED" -> deploymentsDegraded.increment(); + } + } + + public void recordDeploymentDuration(long startMillis) { + deploymentDurationTimer.record( + System.currentTimeMillis() - startMillis, TimeUnit.MILLISECONDS); + } + + // ── Flush timers ──────────────────────────────────────────────────── + + public void recordFlushDuration(String type, Duration duration) { + switch (type) { + case "execution" -> flushExecutionTimer.record(duration); + case "processor" -> flushProcessorTimer.record(duration); + case "log" -> flushLogTimer.record(duration); + } + } + + // ── Auth ──────────────────────────────────────────────────────────── + + public void recordAuthFailure(String reason) { + switch (reason) { + case "invalid_token" -> authFailuresInvalidToken.increment(); + case "revoked" -> authFailuresRevoked.increment(); + case "oidc_rejected" -> authFailuresOidc.increment(); + } + } +} diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/runtime/DeploymentExecutor.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/runtime/DeploymentExecutor.java index 21266d68..a639465a 100644 --- a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/runtime/DeploymentExecutor.java +++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/runtime/DeploymentExecutor.java @@ -1,5 +1,6 @@ package com.cameleer3.server.app.runtime; +import com.cameleer3.server.app.metrics.ServerMetrics; import com.cameleer3.server.app.storage.PostgresDeploymentRepository; import com.cameleer3.server.core.runtime.*; import org.slf4j.Logger; @@ -67,6 +68,9 @@ public class DeploymentExecutor { @Value("${cameleer.server.tenant.id:default}") private String tenantId; + @Autowired + private ServerMetrics serverMetrics; + public DeploymentExecutor(RuntimeOrchestrator orchestrator, DeploymentService deploymentService, AppService appService, @@ -82,6 +86,7 @@ public class DeploymentExecutor { @Async("deploymentTaskExecutor") public void executeAsync(Deployment deployment) { + long deployStart = System.currentTimeMillis(); try { App app = appService.getById(deployment.appId()); Environment env = envService.getById(deployment.environmentId()); @@ -215,6 +220,8 @@ public class DeploymentExecutor { } pgDeployRepo.updateDeployStage(deployment.id(), null); deploymentService.markFailed(deployment.id(), "No replicas passed health check within " + healthCheckTimeout + "s"); + serverMetrics.recordDeploymentOutcome("FAILED"); + serverMetrics.recordDeploymentDuration(deployStart); return; } @@ -245,6 +252,8 @@ public class DeploymentExecutor { } pgDeployRepo.updateDeployStage(deployment.id(), null); + serverMetrics.recordDeploymentOutcome(finalStatus.name()); + serverMetrics.recordDeploymentDuration(deployStart); log.info("Deployment {} is {} ({}/{} replicas healthy)", deployment.id(), finalStatus, healthyCount, config.replicas()); @@ -252,6 +261,8 @@ public class DeploymentExecutor { log.error("Deployment {} FAILED: {}", deployment.id(), e.getMessage(), e); pgDeployRepo.updateDeployStage(deployment.id(), null); deploymentService.markFailed(deployment.id(), e.getMessage()); + serverMetrics.recordDeploymentOutcome("FAILED"); + serverMetrics.recordDeploymentDuration(deployStart); } } diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/JwtAuthenticationFilter.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/JwtAuthenticationFilter.java index 7e1004ea..5934a5d8 100644 --- a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/JwtAuthenticationFilter.java +++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/JwtAuthenticationFilter.java @@ -1,5 +1,6 @@ package com.cameleer3.server.app.security; +import com.cameleer3.server.app.metrics.ServerMetrics; import com.cameleer3.server.core.agent.AgentRegistryService; import com.cameleer3.server.core.rbac.SystemRole; import com.cameleer3.server.core.security.JwtService; @@ -45,15 +46,18 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { private final AgentRegistryService agentRegistryService; private final JwtDecoder oidcDecoder; private final UserRepository userRepository; + private final ServerMetrics serverMetrics; public JwtAuthenticationFilter(JwtService jwtService, AgentRegistryService agentRegistryService, JwtDecoder oidcDecoder, - UserRepository userRepository) { + UserRepository userRepository, + ServerMetrics serverMetrics) { this.jwtService = jwtService; this.agentRegistryService = agentRegistryService; this.oidcDecoder = oidcDecoder; this.userRepository = userRepository; + this.serverMetrics = serverMetrics; } @Override @@ -85,6 +89,7 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { userRepository.findById(subject).ifPresent(user -> { Instant revoked = user.tokenRevokedBefore(); if (revoked != null && result.issuedAt().isBefore(revoked)) { + serverMetrics.recordAuthFailure("revoked"); throw new com.cameleer3.server.core.security.InvalidTokenException("Token revoked"); } }); @@ -117,6 +122,7 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { SecurityContextHolder.getContext().setAuthentication(auth); } catch (Exception e) { log.debug("OIDC token validation failed: {}", e.getMessage()); + serverMetrics.recordAuthFailure("oidc_rejected"); } } diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/SecurityConfig.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/SecurityConfig.java index ec98db49..63f98a39 100644 --- a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/SecurityConfig.java +++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/SecurityConfig.java @@ -1,5 +1,6 @@ package com.cameleer3.server.app.security; +import com.cameleer3.server.app.metrics.ServerMetrics; import com.cameleer3.server.core.agent.AgentRegistryService; import com.cameleer3.server.core.security.JwtService; import com.cameleer3.server.core.security.UserRepository; @@ -61,7 +62,8 @@ public class SecurityConfig { AgentRegistryService registryService, SecurityProperties securityProperties, CorsConfigurationSource corsConfigurationSource, - UserRepository userRepository) throws Exception { + UserRepository userRepository, + ServerMetrics serverMetrics) throws Exception { JwtDecoder oidcDecoder = null; String issuer = securityProperties.getOidc().getIssuerUri(); if (issuer != null && !issuer.isBlank()) { @@ -83,6 +85,7 @@ public class SecurityConfig { // Public endpoints .requestMatchers( "/api/v1/health", + "/api/v1/prometheus", "/api/v1/branding/**", "/api/v1/agents/register", "/api/v1/agents/*/refresh", @@ -142,7 +145,7 @@ public class SecurityConfig { .authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED)) ) .addFilterBefore( - new JwtAuthenticationFilter(jwtService, registryService, oidcDecoder, userRepository), + new JwtAuthenticationFilter(jwtService, registryService, oidcDecoder, userRepository, serverMetrics), UsernamePasswordAuthenticationFilter.class ); diff --git a/cameleer3-server-app/src/main/resources/application.yml b/cameleer3-server-app/src/main/resources/application.yml index d8742be5..3aa6d335 100644 --- a/cameleer3-server-app/src/main/resources/application.yml +++ b/cameleer3-server-app/src/main/resources/application.yml @@ -100,7 +100,7 @@ management: web: base-path: /api/v1 exposure: - include: health + include: health,prometheus endpoint: health: show-details: always