refactor: extract MetricsQueryStore interface from AgentMetricsController
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
package com.cameleer3.server.app.config;
|
package com.cameleer3.server.app.config;
|
||||||
|
|
||||||
|
import com.cameleer3.server.app.storage.PostgresMetricsQueryStore;
|
||||||
import com.cameleer3.server.core.admin.AuditRepository;
|
import com.cameleer3.server.core.admin.AuditRepository;
|
||||||
import com.cameleer3.server.core.admin.AuditService;
|
import com.cameleer3.server.core.admin.AuditService;
|
||||||
import com.cameleer3.server.core.detail.DetailService;
|
import com.cameleer3.server.core.detail.DetailService;
|
||||||
@@ -11,6 +12,7 @@ import com.cameleer3.server.core.storage.model.MetricsSnapshot;
|
|||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
public class StorageBeanConfig {
|
public class StorageBeanConfig {
|
||||||
@@ -41,4 +43,9 @@ public class StorageBeanConfig {
|
|||||||
return new IngestionService(executionStore, diagramStore, metricsBuffer,
|
return new IngestionService(executionStore, diagramStore, metricsBuffer,
|
||||||
searchIndexer::onExecutionUpdated, bodySizeLimit);
|
searchIndexer::onExecutionUpdated, bodySizeLimit);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public MetricsQueryStore metricsQueryStore(JdbcTemplate jdbc) {
|
||||||
|
return new PostgresMetricsQueryStore(jdbc);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,22 +2,23 @@ package com.cameleer3.server.app.controller;
|
|||||||
|
|
||||||
import com.cameleer3.server.app.dto.AgentMetricsResponse;
|
import com.cameleer3.server.app.dto.AgentMetricsResponse;
|
||||||
import com.cameleer3.server.app.dto.MetricBucket;
|
import com.cameleer3.server.app.dto.MetricBucket;
|
||||||
import org.springframework.jdbc.core.JdbcTemplate;
|
import com.cameleer3.server.core.storage.MetricsQueryStore;
|
||||||
|
import com.cameleer3.server.core.storage.model.MetricTimeSeries;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
import java.sql.Timestamp;
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.time.temporal.ChronoUnit;
|
import java.time.temporal.ChronoUnit;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/v1/agents/{agentId}/metrics")
|
@RequestMapping("/api/v1/agents/{agentId}/metrics")
|
||||||
public class AgentMetricsController {
|
public class AgentMetricsController {
|
||||||
|
|
||||||
private final JdbcTemplate jdbc;
|
private final MetricsQueryStore metricsQueryStore;
|
||||||
|
|
||||||
public AgentMetricsController(JdbcTemplate jdbc) {
|
public AgentMetricsController(MetricsQueryStore metricsQueryStore) {
|
||||||
this.jdbc = jdbc;
|
this.metricsQueryStore = metricsQueryStore;
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping
|
@GetMapping
|
||||||
@@ -32,34 +33,18 @@ public class AgentMetricsController {
|
|||||||
if (to == null) to = Instant.now();
|
if (to == null) to = Instant.now();
|
||||||
|
|
||||||
List<String> metricNames = Arrays.asList(names.split(","));
|
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<>();
|
Map<String, List<MetricTimeSeries.Bucket>> raw =
|
||||||
for (String name : metricNames) {
|
metricsQueryStore.queryTimeSeries(agentId, metricNames, from, to, buckets);
|
||||||
result.put(name.trim(), new ArrayList<>());
|
|
||||||
}
|
|
||||||
|
|
||||||
String sql = """
|
Map<String, List<MetricBucket>> result = raw.entrySet().stream()
|
||||||
SELECT time_bucket(CAST(? AS interval), collected_at) AS bucket,
|
.collect(Collectors.toMap(
|
||||||
metric_name,
|
Map.Entry::getKey,
|
||||||
AVG(metric_value) AS avg_value
|
e -> e.getValue().stream()
|
||||||
FROM agent_metrics
|
.map(b -> new MetricBucket(b.time(), b.value()))
|
||||||
WHERE agent_id = ?
|
.toList(),
|
||||||
AND collected_at >= ? AND collected_at < ?
|
(a, b) -> a,
|
||||||
AND metric_name = ANY(?)
|
LinkedHashMap::new));
|
||||||
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, Timestamp.from(from), Timestamp.from(to), namesArray);
|
|
||||||
|
|
||||||
return new AgentMetricsResponse(result);
|
return new AgentMetricsResponse(result);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
package com.cameleer3.server.app.storage;
|
||||||
|
|
||||||
|
import com.cameleer3.server.core.storage.MetricsQueryStore;
|
||||||
|
import com.cameleer3.server.core.storage.model.MetricTimeSeries;
|
||||||
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
|
|
||||||
|
import java.sql.Timestamp;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
public class PostgresMetricsQueryStore implements MetricsQueryStore {
|
||||||
|
|
||||||
|
private final JdbcTemplate jdbc;
|
||||||
|
|
||||||
|
public PostgresMetricsQueryStore(JdbcTemplate jdbc) {
|
||||||
|
this.jdbc = jdbc;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, List<MetricTimeSeries.Bucket>> queryTimeSeries(
|
||||||
|
String agentId, List<String> metricNames,
|
||||||
|
Instant from, Instant to, int buckets) {
|
||||||
|
|
||||||
|
long intervalMs = (to.toEpochMilli() - from.toEpochMilli()) / Math.max(buckets, 1);
|
||||||
|
String intervalStr = intervalMs + " milliseconds";
|
||||||
|
|
||||||
|
Map<String, List<MetricTimeSeries.Bucket>> 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 MetricTimeSeries.Bucket(bucket, value));
|
||||||
|
}, intervalStr, agentId, Timestamp.from(from), Timestamp.from(to), namesArray);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package com.cameleer3.server.core.storage;
|
||||||
|
|
||||||
|
import com.cameleer3.server.core.storage.model.MetricTimeSeries;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public interface MetricsQueryStore {
|
||||||
|
|
||||||
|
Map<String, List<MetricTimeSeries.Bucket>> queryTimeSeries(
|
||||||
|
String agentId, List<String> metricNames,
|
||||||
|
Instant from, Instant to, int buckets);
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package com.cameleer3.server.core.storage.model;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public record MetricTimeSeries(String metricName, List<Bucket> buckets) {
|
||||||
|
|
||||||
|
public record Bucket(Instant time, double value) {}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user