feat: add GET /agents/{id}/metrics endpoint for JVM metrics
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -34,7 +34,7 @@ public class OpenApiConfig {
|
|||||||
"ProcessorNode",
|
"ProcessorNode",
|
||||||
"AppCatalogEntry", "RouteSummary", "AgentSummary",
|
"AppCatalogEntry", "RouteSummary", "AgentSummary",
|
||||||
"RouteMetrics", "AgentEventResponse", "AgentInstanceResponse",
|
"RouteMetrics", "AgentEventResponse", "AgentInstanceResponse",
|
||||||
"ProcessorMetrics"
|
"ProcessorMetrics", "AgentMetricsResponse", "MetricBucket"
|
||||||
);
|
);
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
package com.cameleer3.server.app.controller;
|
||||||
|
|
||||||
|
import com.cameleer3.server.app.dto.AgentMetricsResponse;
|
||||||
|
import com.cameleer3.server.app.dto.MetricBucket;
|
||||||
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.temporal.ChronoUnit;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/agents/{agentId}/metrics")
|
||||||
|
public class AgentMetricsController {
|
||||||
|
|
||||||
|
private final JdbcTemplate jdbc;
|
||||||
|
|
||||||
|
public AgentMetricsController(JdbcTemplate jdbc) {
|
||||||
|
this.jdbc = jdbc;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public AgentMetricsResponse getMetrics(
|
||||||
|
@PathVariable String agentId,
|
||||||
|
@RequestParam String names,
|
||||||
|
@RequestParam(required = false) Instant from,
|
||||||
|
@RequestParam(required = false) Instant to,
|
||||||
|
@RequestParam(defaultValue = "60") int buckets) {
|
||||||
|
|
||||||
|
if (from == null) from = Instant.now().minus(1, ChronoUnit.HOURS);
|
||||||
|
if (to == null) to = Instant.now();
|
||||||
|
|
||||||
|
List<String> metricNames = Arrays.asList(names.split(","));
|
||||||
|
long intervalMs = (to.toEpochMilli() - from.toEpochMilli()) / Math.max(buckets, 1);
|
||||||
|
String intervalStr = intervalMs + " milliseconds";
|
||||||
|
|
||||||
|
Map<String, List<MetricBucket>> result = new LinkedHashMap<>();
|
||||||
|
for (String name : metricNames) {
|
||||||
|
result.put(name.trim(), new ArrayList<>());
|
||||||
|
}
|
||||||
|
|
||||||
|
String sql = """
|
||||||
|
SELECT time_bucket(CAST(? AS interval), collected_at) AS bucket,
|
||||||
|
metric_name,
|
||||||
|
AVG(metric_value) AS avg_value
|
||||||
|
FROM agent_metrics
|
||||||
|
WHERE agent_id = ?
|
||||||
|
AND collected_at >= ? AND collected_at < ?
|
||||||
|
AND metric_name = ANY(?)
|
||||||
|
GROUP BY bucket, metric_name
|
||||||
|
ORDER BY bucket
|
||||||
|
""";
|
||||||
|
|
||||||
|
String[] namesArray = metricNames.stream().map(String::trim).toArray(String[]::new);
|
||||||
|
jdbc.query(sql, rs -> {
|
||||||
|
String metricName = rs.getString("metric_name");
|
||||||
|
Instant bucket = rs.getTimestamp("bucket").toInstant();
|
||||||
|
double value = rs.getDouble("avg_value");
|
||||||
|
result.computeIfAbsent(metricName, k -> new ArrayList<>())
|
||||||
|
.add(new MetricBucket(bucket, value));
|
||||||
|
}, intervalStr, agentId, from, to, namesArray);
|
||||||
|
|
||||||
|
return new AgentMetricsResponse(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package com.cameleer3.server.app.dto;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
|
||||||
|
public record AgentMetricsResponse(
|
||||||
|
@NotNull Map<String, List<MetricBucket>> metrics
|
||||||
|
) {}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package com.cameleer3.server.app.dto;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
|
||||||
|
public record MetricBucket(
|
||||||
|
@NotNull Instant time,
|
||||||
|
double value
|
||||||
|
) {}
|
||||||
@@ -80,6 +80,7 @@ public class SecurityConfig {
|
|||||||
// Read-only data endpoints — viewer+
|
// Read-only data endpoints — viewer+
|
||||||
.requestMatchers(HttpMethod.GET, "/api/v1/executions/**").hasAnyRole("VIEWER", "OPERATOR", "ADMIN")
|
.requestMatchers(HttpMethod.GET, "/api/v1/executions/**").hasAnyRole("VIEWER", "OPERATOR", "ADMIN")
|
||||||
.requestMatchers(HttpMethod.GET, "/api/v1/diagrams/**").hasAnyRole("VIEWER", "OPERATOR", "ADMIN")
|
.requestMatchers(HttpMethod.GET, "/api/v1/diagrams/**").hasAnyRole("VIEWER", "OPERATOR", "ADMIN")
|
||||||
|
.requestMatchers(HttpMethod.GET, "/api/v1/agents/*/metrics").hasAnyRole("VIEWER", "OPERATOR", "ADMIN")
|
||||||
.requestMatchers(HttpMethod.GET, "/api/v1/agents").hasAnyRole("VIEWER", "OPERATOR", "ADMIN")
|
.requestMatchers(HttpMethod.GET, "/api/v1/agents").hasAnyRole("VIEWER", "OPERATOR", "ADMIN")
|
||||||
.requestMatchers(HttpMethod.GET, "/api/v1/agents/events-log").hasAnyRole("VIEWER", "OPERATOR", "ADMIN")
|
.requestMatchers(HttpMethod.GET, "/api/v1/agents/events-log").hasAnyRole("VIEWER", "OPERATOR", "ADMIN")
|
||||||
.requestMatchers(HttpMethod.GET, "/api/v1/routes/**").hasAnyRole("VIEWER", "OPERATOR", "ADMIN")
|
.requestMatchers(HttpMethod.GET, "/api/v1/routes/**").hasAnyRole("VIEWER", "OPERATOR", "ADMIN")
|
||||||
|
|||||||
Reference in New Issue
Block a user