fix(cloudwatch): retain metric-math alarm query definitions (#4594) #6
@@ -6,6 +6,7 @@ import software.amazon.awssdk.services.cloudwatch.model.*;
|
|||||||
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.time.temporal.ChronoUnit;
|
import java.time.temporal.ChronoUnit;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.*;
|
import static org.assertj.core.api.Assertions.*;
|
||||||
|
|
||||||
@@ -185,4 +186,51 @@ class CloudWatchTest {
|
|||||||
);
|
);
|
||||||
assertThat(noMatch.datapoints()).isEmpty();
|
assertThat(noMatch.datapoints()).isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("PutMetricData stores datums nested under EntityMetricData")
|
||||||
|
void testPutMetricDataWithEntityMetricData() {
|
||||||
|
String namespace = "JavaTestEntityMetricData";
|
||||||
|
Instant now = Instant.now();
|
||||||
|
|
||||||
|
cloudWatchClient.putMetricData(request -> request
|
||||||
|
.namespace(namespace)
|
||||||
|
.metricData(datum -> datum
|
||||||
|
.metricName("TopLevel")
|
||||||
|
.value(1.0)
|
||||||
|
.timestamp(now))
|
||||||
|
.entityMetricData(entityMetricData -> entityMetricData
|
||||||
|
.entity(entity -> entity
|
||||||
|
.keyAttributes(Map.of(
|
||||||
|
"Type", "AWS::Resource",
|
||||||
|
"ResourceType", "AWS::EC2::Instance",
|
||||||
|
"Identifier", "i-0123456789abcdef0")))
|
||||||
|
.metricData(MetricDatum.builder()
|
||||||
|
.metricName("mem_used_percent")
|
||||||
|
.value(6.5)
|
||||||
|
.unit(StandardUnit.PERCENT)
|
||||||
|
.storageResolution(60)
|
||||||
|
.timestamp(now)
|
||||||
|
.dimensions(Dimension.builder().name("host").value("h1").build())
|
||||||
|
.build()))
|
||||||
|
.strictEntityValidation(false)
|
||||||
|
);
|
||||||
|
|
||||||
|
ListMetricsResponse listed = cloudWatchClient.listMetrics(request -> request.namespace(namespace));
|
||||||
|
assertThat(listed.metrics())
|
||||||
|
.extracting(Metric::metricName)
|
||||||
|
.containsExactlyInAnyOrder("TopLevel", "mem_used_percent");
|
||||||
|
|
||||||
|
GetMetricStatisticsResponse stats = cloudWatchClient.getMetricStatistics(request -> request
|
||||||
|
.namespace(namespace)
|
||||||
|
.metricName("mem_used_percent")
|
||||||
|
.dimensions(Dimension.builder().name("host").value("h1").build())
|
||||||
|
.startTime(now.minus(1, ChronoUnit.HOURS))
|
||||||
|
.endTime(now.plus(1, ChronoUnit.MINUTES))
|
||||||
|
.period(3600)
|
||||||
|
.statistics(Statistic.SUM)
|
||||||
|
);
|
||||||
|
assertThat(stats.datapoints()).hasSize(1);
|
||||||
|
assertThat(stats.datapoints().get(0).sum()).isEqualTo(6.5);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-3
@@ -82,10 +82,11 @@ public class CloudWatchMetricsJsonHandler {
|
|||||||
|
|
||||||
private Response handlePutMetricData(JsonNode request, String region) {
|
private Response handlePutMetricData(JsonNode request, String region) {
|
||||||
String namespace = request.path("Namespace").asText();
|
String namespace = request.path("Namespace").asText();
|
||||||
LOG.infov("JSON PutMetricData raw: {0}", request);
|
|
||||||
List<MetricDatum> datums = parseMetricDataJson(request.path("MetricData"));
|
List<MetricDatum> datums = parseMetricDataJson(request.path("MetricData"));
|
||||||
LOG.infov("JSON PutMetricData parsed {0} datums, sums={1}", datums.size(),
|
for (JsonNode entityMetricData : request.path("EntityMetricData")) {
|
||||||
datums.stream().map(d -> d.getMetricName() + "=" + d.getSum()).toList());
|
datums.addAll(parseMetricDataJson(entityMetricData.path("MetricData")));
|
||||||
|
}
|
||||||
|
LOG.debugv("JSON PutMetricData parsed {0} datums for namespace {1}", datums.size(), namespace);
|
||||||
metricsService.putMetricData(namespace, datums, region);
|
metricsService.putMetricData(namespace, datums, region);
|
||||||
return Response.ok(objectMapper.createObjectNode()).build();
|
return Response.ok(objectMapper.createObjectNode()).build();
|
||||||
}
|
}
|
||||||
|
|||||||
+33
-11
@@ -26,11 +26,16 @@ import java.util.LinkedHashMap;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
|
import java.util.SortedSet;
|
||||||
|
import java.util.TreeSet;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
@ApplicationScoped
|
@ApplicationScoped
|
||||||
public class CloudWatchMetricsQueryHandler {
|
public class CloudWatchMetricsQueryHandler {
|
||||||
|
|
||||||
private static final Logger LOG = Logger.getLogger(CloudWatchMetricsQueryHandler.class);
|
private static final Logger LOG = Logger.getLogger(CloudWatchMetricsQueryHandler.class);
|
||||||
|
private static final Pattern ENTITY_METRIC_DATA_MEMBER = Pattern.compile("EntityMetricData\\.member\\.(\\d{1,9})\\.");
|
||||||
|
|
||||||
private CloudWatchMetricsService metricsService;
|
private CloudWatchMetricsService metricsService;
|
||||||
private final CloudWatchDashboardsService dashboardsService;
|
private final CloudWatchDashboardsService dashboardsService;
|
||||||
@@ -591,42 +596,59 @@ public class CloudWatchMetricsQueryHandler {
|
|||||||
// ──────────────────────────── Parsing Helpers ────────────────────────────
|
// ──────────────────────────── Parsing Helpers ────────────────────────────
|
||||||
|
|
||||||
private List<MetricDatum> parseMetricData(MultivaluedMap<String, String> params) {
|
private List<MetricDatum> parseMetricData(MultivaluedMap<String, String> params) {
|
||||||
LOG.infov("Query PutMetricData params: {0}", params);
|
List<MetricDatum> datums = parseMetricData(params, "MetricData");
|
||||||
|
// An empty EntityMetricData entry serializes to no keys, so its index is absent
|
||||||
|
// while later entries keep theirs: iterate the indexes present, not 1..first gap.
|
||||||
|
SortedSet<Integer> entityIndexes = new TreeSet<>();
|
||||||
|
for (String key : params.keySet()) {
|
||||||
|
Matcher matcher = ENTITY_METRIC_DATA_MEMBER.matcher(key);
|
||||||
|
if (matcher.lookingAt()) {
|
||||||
|
entityIndexes.add(Integer.parseInt(matcher.group(1)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (int index : entityIndexes) {
|
||||||
|
datums.addAll(parseMetricData(params, "EntityMetricData.member." + index + ".MetricData"));
|
||||||
|
}
|
||||||
|
LOG.debugv("Query PutMetricData parsed {0} datums", datums.size());
|
||||||
|
return datums;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<MetricDatum> parseMetricData(MultivaluedMap<String, String> params, String listPrefix) {
|
||||||
List<MetricDatum> datums = new ArrayList<>();
|
List<MetricDatum> datums = new ArrayList<>();
|
||||||
for (int i = 1; ; i++) {
|
for (int i = 1; ; i++) {
|
||||||
String metricName = params.getFirst("MetricData.member." + i + ".MetricName");
|
String metricName = params.getFirst(listPrefix + ".member." + i + ".MetricName");
|
||||||
if (metricName == null) {
|
if (metricName == null) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
MetricDatum datum = new MetricDatum();
|
MetricDatum datum = new MetricDatum();
|
||||||
datum.setMetricName(metricName);
|
datum.setMetricName(metricName);
|
||||||
datum.setUnit(params.getFirst("MetricData.member." + i + ".Unit"));
|
datum.setUnit(params.getFirst(listPrefix + ".member." + i + ".Unit"));
|
||||||
|
|
||||||
String valueStr = params.getFirst("MetricData.member." + i + ".Value");
|
String valueStr = params.getFirst(listPrefix + ".member." + i + ".Value");
|
||||||
if (valueStr != null) {
|
if (valueStr != null) {
|
||||||
datum.setValue(parseDouble(valueStr, 0));
|
datum.setValue(parseDouble(valueStr, 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
String tsStr = params.getFirst("MetricData.member." + i + ".Timestamp");
|
String tsStr = params.getFirst(listPrefix + ".member." + i + ".Timestamp");
|
||||||
if (tsStr != null) {
|
if (tsStr != null) {
|
||||||
Instant ts = parseInstant(tsStr);
|
Instant ts = parseInstant(tsStr);
|
||||||
datum.setTimestamp(ts != null ? ts.getEpochSecond() : 0);
|
datum.setTimestamp(ts != null ? ts.getEpochSecond() : 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// StatisticValues
|
// StatisticValues
|
||||||
String sc = params.getFirst("MetricData.member." + i + ".StatisticValues.SampleCount");
|
String sc = params.getFirst(listPrefix + ".member." + i + ".StatisticValues.SampleCount");
|
||||||
if (sc != null) {
|
if (sc != null) {
|
||||||
datum.setSampleCount(parseDouble(sc, 0));
|
datum.setSampleCount(parseDouble(sc, 0));
|
||||||
datum.setSum(parseDouble(params.getFirst("MetricData.member." + i + ".StatisticValues.Sum"), 0));
|
datum.setSum(parseDouble(params.getFirst(listPrefix + ".member." + i + ".StatisticValues.Sum"), 0));
|
||||||
datum.setMinimum(parseDouble(params.getFirst("MetricData.member." + i + ".StatisticValues.Minimum"), 0));
|
datum.setMinimum(parseDouble(params.getFirst(listPrefix + ".member." + i + ".StatisticValues.Minimum"), 0));
|
||||||
datum.setMaximum(parseDouble(params.getFirst("MetricData.member." + i + ".StatisticValues.Maximum"), 0));
|
datum.setMaximum(parseDouble(params.getFirst(listPrefix + ".member." + i + ".StatisticValues.Maximum"), 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dimensions
|
// Dimensions
|
||||||
List<Dimension> dims = new ArrayList<>();
|
List<Dimension> dims = new ArrayList<>();
|
||||||
for (int j = 1; ; j++) {
|
for (int j = 1; ; j++) {
|
||||||
String dimName = params.getFirst("MetricData.member." + i + ".Dimensions.member." + j + ".Name");
|
String dimName = params.getFirst(listPrefix + ".member." + i + ".Dimensions.member." + j + ".Name");
|
||||||
String dimValue = params.getFirst("MetricData.member." + i + ".Dimensions.member." + j + ".Value");
|
String dimValue = params.getFirst(listPrefix + ".member." + i + ".Dimensions.member." + j + ".Value");
|
||||||
if (dimName == null) {
|
if (dimName == null) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
+156
@@ -0,0 +1,156 @@
|
|||||||
|
package io.github.hectorvent.floci.services.cloudwatch.metrics;
|
||||||
|
|
||||||
|
import io.quarkus.test.junit.QuarkusTest;
|
||||||
|
import io.restassured.RestAssured;
|
||||||
|
import io.restassured.config.EncoderConfig;
|
||||||
|
import io.restassured.http.ContentType;
|
||||||
|
import io.restassured.parsing.Parser;
|
||||||
|
import io.restassured.specification.RequestSpecification;
|
||||||
|
import org.junit.jupiter.api.BeforeAll;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
|
||||||
|
import static io.restassured.RestAssured.config;
|
||||||
|
import static io.restassured.RestAssured.given;
|
||||||
|
import static org.hamcrest.Matchers.containsInAnyOrder;
|
||||||
|
import static org.hamcrest.Matchers.equalTo;
|
||||||
|
import static org.hamcrest.Matchers.hasItem;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PutMetricData datums nested under EntityMetricData, as the CloudWatch Agent sends them,
|
||||||
|
* must be stored under the request namespace on every protocol path. floci-io/floci#6066.
|
||||||
|
*/
|
||||||
|
@QuarkusTest
|
||||||
|
class CloudWatchMetricsEntityMetricDataIntegrationTest {
|
||||||
|
|
||||||
|
private static final String CW_AUTH =
|
||||||
|
"AWS4-HMAC-SHA256 Credential=test/20261020/us-east-1/monitoring/aws4_request";
|
||||||
|
private static final String JSON_1_0 = "application/x-amz-json-1.0";
|
||||||
|
|
||||||
|
@BeforeAll
|
||||||
|
static void registerJsonParser() {
|
||||||
|
RestAssured.registerParser(JSON_1_0, Parser.JSON);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static RequestSpecification json(String operation) {
|
||||||
|
return given()
|
||||||
|
.header("Authorization", CW_AUTH)
|
||||||
|
.contentType(JSON_1_0)
|
||||||
|
.config(config().encoderConfig(EncoderConfig.encoderConfig()
|
||||||
|
.encodeContentTypeAs(JSON_1_0, ContentType.TEXT)))
|
||||||
|
.header("X-Amz-Target", "GraniteServiceVersion20100801." + operation);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static RequestSpecification query(String action) {
|
||||||
|
return given()
|
||||||
|
.header("Authorization", CW_AUTH)
|
||||||
|
.contentType("application/x-www-form-urlencoded")
|
||||||
|
.formParam("Action", action)
|
||||||
|
.formParam("Version", "2010-08-01");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void jsonEntityMetricDataOnlyIsListedAndAggregated() {
|
||||||
|
long now = Instant.now().getEpochSecond();
|
||||||
|
json("PutMetricData")
|
||||||
|
.body("""
|
||||||
|
{"Namespace":"CWAgent/Json","MetricData":[],
|
||||||
|
"EntityMetricData":[{
|
||||||
|
"Entity":{"KeyAttributes":{"Type":"AWS::Resource",
|
||||||
|
"ResourceType":"AWS::EC2::Instance","Identifier":"i-0123456789abcdef0"},
|
||||||
|
"Attributes":{}},
|
||||||
|
"MetricData":[
|
||||||
|
{"MetricName":"mem_used_percent","Value":6.5,"Unit":"Percent",
|
||||||
|
"StorageResolution":60,"Timestamp":%d,
|
||||||
|
"Dimensions":[{"Name":"host","Value":"h1"}]},
|
||||||
|
{"MetricName":"disk_used_percent","Value":0.75,"Unit":"Percent",
|
||||||
|
"StorageResolution":60,"Timestamp":%d,
|
||||||
|
"Dimensions":[{"Name":"host","Value":"h1"}]}]}],
|
||||||
|
"StrictEntityValidation":false}
|
||||||
|
""".formatted(now, now))
|
||||||
|
.when().post("/")
|
||||||
|
.then().statusCode(200);
|
||||||
|
|
||||||
|
json("ListMetrics")
|
||||||
|
.body("{\"Namespace\":\"CWAgent/Json\"}")
|
||||||
|
.when().post("/")
|
||||||
|
.then()
|
||||||
|
.statusCode(200)
|
||||||
|
.body("Metrics.MetricName", containsInAnyOrder("mem_used_percent", "disk_used_percent"));
|
||||||
|
|
||||||
|
json("GetMetricStatistics")
|
||||||
|
.body("""
|
||||||
|
{"Namespace":"CWAgent/Json","MetricName":"mem_used_percent",
|
||||||
|
"Dimensions":[{"Name":"host","Value":"h1"}],
|
||||||
|
"StartTime":%d,"EndTime":%d,"Period":60,"Statistics":["Sum"]}
|
||||||
|
""".formatted(now - 120, now + 120))
|
||||||
|
.when().post("/")
|
||||||
|
.then()
|
||||||
|
.statusCode(200)
|
||||||
|
.body("Datapoints.Sum", hasItem(6.5f));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void jsonTopLevelAndEntityMetricDataAreBothStored() {
|
||||||
|
json("PutMetricData")
|
||||||
|
.body("""
|
||||||
|
{"Namespace":"CWAgent/JsonMixed",
|
||||||
|
"MetricData":[{"MetricName":"TopLevel","Value":1}],
|
||||||
|
"EntityMetricData":[{"Entity":{"KeyAttributes":{"Type":"Service",
|
||||||
|
"Name":"checkout","Environment":"prod"}},
|
||||||
|
"MetricData":[{"MetricName":"FromEntity","Value":2}]}]}
|
||||||
|
""")
|
||||||
|
.when().post("/")
|
||||||
|
.then().statusCode(200);
|
||||||
|
|
||||||
|
json("ListMetrics")
|
||||||
|
.body("{\"Namespace\":\"CWAgent/JsonMixed\"}")
|
||||||
|
.when().post("/")
|
||||||
|
.then()
|
||||||
|
.statusCode(200)
|
||||||
|
.body("Metrics.MetricName", containsInAnyOrder("TopLevel", "FromEntity"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void queryTopLevelAndEntityMetricDataAreListedAndAggregated() {
|
||||||
|
Instant now = Instant.now();
|
||||||
|
query("PutMetricData")
|
||||||
|
.formParam("Namespace", "CWAgent/Query")
|
||||||
|
.formParam("MetricData.member.1.MetricName", "TopLevel")
|
||||||
|
.formParam("MetricData.member.1.Value", "1")
|
||||||
|
.formParam("EntityMetricData.member.1.Entity.KeyAttributes.entry.1.key", "Type")
|
||||||
|
.formParam("EntityMetricData.member.1.Entity.KeyAttributes.entry.1.value", "AWS::Resource")
|
||||||
|
.formParam("EntityMetricData.member.1.MetricData.member.1.MetricName", "mem_used_percent")
|
||||||
|
.formParam("EntityMetricData.member.1.MetricData.member.1.Value", "6.5")
|
||||||
|
.formParam("EntityMetricData.member.1.MetricData.member.1.Timestamp", now.toString())
|
||||||
|
.formParam("EntityMetricData.member.1.MetricData.member.1.Dimensions.member.1.Name", "host")
|
||||||
|
.formParam("EntityMetricData.member.1.MetricData.member.1.Dimensions.member.1.Value", "h1")
|
||||||
|
.formParam("StrictEntityValidation", "false")
|
||||||
|
.when().post("/")
|
||||||
|
.then().statusCode(200);
|
||||||
|
|
||||||
|
query("ListMetrics")
|
||||||
|
.formParam("Namespace", "CWAgent/Query")
|
||||||
|
.when().post("/")
|
||||||
|
.then()
|
||||||
|
.statusCode(200)
|
||||||
|
.body("ListMetricsResponse.ListMetricsResult.Metrics.member.MetricName",
|
||||||
|
containsInAnyOrder("TopLevel", "mem_used_percent"));
|
||||||
|
|
||||||
|
query("GetMetricStatistics")
|
||||||
|
.formParam("Namespace", "CWAgent/Query")
|
||||||
|
.formParam("MetricName", "mem_used_percent")
|
||||||
|
.formParam("Dimensions.member.1.Name", "host")
|
||||||
|
.formParam("Dimensions.member.1.Value", "h1")
|
||||||
|
.formParam("StartTime", now.minusSeconds(120).toString())
|
||||||
|
.formParam("EndTime", now.plusSeconds(120).toString())
|
||||||
|
.formParam("Period", "60")
|
||||||
|
.formParam("Statistics.member.1", "Sum")
|
||||||
|
.when().post("/")
|
||||||
|
.then()
|
||||||
|
.statusCode(200)
|
||||||
|
.body("GetMetricStatisticsResponse.GetMetricStatisticsResult.Datapoints.member.Sum",
|
||||||
|
equalTo("6.5"));
|
||||||
|
}
|
||||||
|
}
|
||||||
+59
@@ -17,6 +17,12 @@ import java.math.BigDecimal;
|
|||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handler-level tests for CloudWatchMetricsJsonHandler.
|
* Handler-level tests for CloudWatchMetricsJsonHandler.
|
||||||
@@ -146,6 +152,59 @@ class CloudWatchMetricsJsonHandlerTest {
|
|||||||
"metric stored with 24h-ago timestamp must not appear in a 20-second window around now");
|
"metric stored with 24h-ago timestamp must not appear in a 20-second window around now");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The CloudWatch Agent sends an empty top-level MetricData and nests every datum under
|
||||||
|
// EntityMetricData, floci-io/floci#6066.
|
||||||
|
@Test
|
||||||
|
void putMetricData_entityMetricDataOnly_storesNestedDatumsUnderTheRequestNamespace() {
|
||||||
|
ObjectNode req = MAPPER.createObjectNode();
|
||||||
|
req.put("Namespace", "CWAgent");
|
||||||
|
req.putArray("MetricData");
|
||||||
|
ObjectNode entityMetricData = req.putArray("EntityMetricData").addObject();
|
||||||
|
ObjectNode entity = entityMetricData.putObject("Entity");
|
||||||
|
entity.putObject("KeyAttributes")
|
||||||
|
.put("Type", "AWS::Resource")
|
||||||
|
.put("ResourceType", "AWS::EC2::Instance")
|
||||||
|
.put("Identifier", "i-0123456789abcdef0");
|
||||||
|
entity.putObject("Attributes");
|
||||||
|
ObjectNode datum = entityMetricData.putArray("MetricData").addObject();
|
||||||
|
datum.put("MetricName", "mem_used_percent");
|
||||||
|
datum.put("Value", 6.43);
|
||||||
|
datum.put("Unit", "Percent");
|
||||||
|
datum.put("StorageResolution", 60);
|
||||||
|
datum.put("Timestamp", sdkTimestamp(EPOCH_NOW));
|
||||||
|
datum.putArray("Dimensions").addObject().put("Name", "host").put("Value", "h1");
|
||||||
|
req.put("StrictEntityValidation", false);
|
||||||
|
|
||||||
|
assertEquals(200, handler.handle("PutMetricData", req, REGION).getStatus());
|
||||||
|
|
||||||
|
ObjectNode stats = getStats("CWAgent", "mem_used_percent", "host", "h1",
|
||||||
|
EPOCH_NOW.minusSeconds(60), EPOCH_NOW.plusSeconds(60), 60);
|
||||||
|
assertEquals(1, stats.get("Datapoints").size());
|
||||||
|
assertEquals(6.43, stats.get("Datapoints").get(0).get("Sum").asDouble());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void putMetricData_topLevelAndEntityMetricData_storesBoth() {
|
||||||
|
ObjectNode req = MAPPER.createObjectNode();
|
||||||
|
req.put("Namespace", "Mixed");
|
||||||
|
req.putArray("MetricData").addObject()
|
||||||
|
.put("MetricName", "TopLevel").put("Value", 1.0);
|
||||||
|
ArrayNode entityMetricData = req.putArray("EntityMetricData");
|
||||||
|
entityMetricData.addObject().putArray("MetricData").addObject()
|
||||||
|
.put("MetricName", "FirstEntity").put("Value", 2.0);
|
||||||
|
entityMetricData.addObject().putArray("MetricData").addObject()
|
||||||
|
.put("MetricName", "SecondEntity").put("Value", 3.0);
|
||||||
|
|
||||||
|
assertEquals(200, handler.handle("PutMetricData", req, REGION).getStatus());
|
||||||
|
|
||||||
|
ObjectNode listReq = MAPPER.createObjectNode();
|
||||||
|
listReq.put("Namespace", "Mixed");
|
||||||
|
ObjectNode listed = (ObjectNode) handler.handle("ListMetrics", listReq, REGION).getEntity();
|
||||||
|
List<String> names = new ArrayList<>();
|
||||||
|
listed.get("Metrics").forEach(m -> names.add(m.get("MetricName").asText()));
|
||||||
|
assertEquals(List.of("FirstEntity", "SecondEntity", "TopLevel"), names.stream().sorted().toList());
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void setAlarmState_updatesFieldsCorrectly() {
|
void setAlarmState_updatesFieldsCorrectly() {
|
||||||
putMetric("NS", "M", "type", "current", 100.0, EPOCH_NOW);
|
putMetric("NS", "M", "type", "current", 100.0, EPOCH_NOW);
|
||||||
|
|||||||
+58
@@ -1,6 +1,8 @@
|
|||||||
package io.github.hectorvent.floci.services.cloudwatch.metrics;
|
package io.github.hectorvent.floci.services.cloudwatch.metrics;
|
||||||
|
|
||||||
import io.github.hectorvent.floci.services.cloudwatch.dashboards.CloudWatchDashboardsService;
|
import io.github.hectorvent.floci.services.cloudwatch.dashboards.CloudWatchDashboardsService;
|
||||||
|
import io.github.hectorvent.floci.services.cloudwatch.metrics.model.MetricAlarm;
|
||||||
|
import io.github.hectorvent.floci.services.cloudwatch.metrics.model.MetricDatum;
|
||||||
import io.github.hectorvent.floci.services.cloudwatch.metricstreams.CloudWatchMetricStreamsService;
|
import io.github.hectorvent.floci.services.cloudwatch.metricstreams.CloudWatchMetricStreamsService;
|
||||||
import io.github.hectorvent.floci.services.cloudwatch.metrics.model.MetricAlarm;
|
import io.github.hectorvent.floci.services.cloudwatch.metrics.model.MetricAlarm;
|
||||||
import jakarta.ws.rs.core.MultivaluedHashMap;
|
import jakarta.ws.rs.core.MultivaluedHashMap;
|
||||||
@@ -17,6 +19,7 @@ import static org.junit.jupiter.api.Assertions.assertFalse;
|
|||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.ArgumentMatchers.anyInt;
|
import static org.mockito.ArgumentMatchers.anyInt;
|
||||||
import static org.mockito.ArgumentMatchers.anyString;
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
@@ -123,4 +126,59 @@ class CloudWatchMetricsQueryHandlerTest {
|
|||||||
anyInt(), any(), unitCaptor.capture(), any());
|
anyInt(), any(), unitCaptor.capture(), any());
|
||||||
assertEquals("Count", unitCaptor.getValue());
|
assertEquals("Count", unitCaptor.getValue());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The Query serialization of EntityMetricData nests each datum two lists deep;
|
||||||
|
// floci-io/floci#6066 is the JSON-side report of the same dropped datums.
|
||||||
|
@Test
|
||||||
|
void putMetricDataStoresTopLevelAndEntityMetricDataUnderTheRequestNamespace() {
|
||||||
|
MultivaluedMap<String, String> params = new MultivaluedHashMap<>();
|
||||||
|
params.putSingle("Namespace", "CWAgent");
|
||||||
|
params.putSingle("MetricData.member.1.MetricName", "TopLevel");
|
||||||
|
params.putSingle("MetricData.member.1.Value", "1");
|
||||||
|
params.putSingle("EntityMetricData.member.1.Entity.KeyAttributes.entry.1.key", "Type");
|
||||||
|
params.putSingle("EntityMetricData.member.1.Entity.KeyAttributes.entry.1.value", "AWS::Resource");
|
||||||
|
params.putSingle("EntityMetricData.member.1.MetricData.member.1.MetricName", "mem_used_percent");
|
||||||
|
params.putSingle("EntityMetricData.member.1.MetricData.member.1.Value", "6.5");
|
||||||
|
params.putSingle("EntityMetricData.member.1.MetricData.member.1.Unit", "Percent");
|
||||||
|
params.putSingle("EntityMetricData.member.1.MetricData.member.1.Dimensions.member.1.Name", "host");
|
||||||
|
params.putSingle("EntityMetricData.member.1.MetricData.member.1.Dimensions.member.1.Value", "h1");
|
||||||
|
params.putSingle("EntityMetricData.member.2.MetricData.member.1.MetricName", "disk_used_percent");
|
||||||
|
params.putSingle("EntityMetricData.member.2.MetricData.member.1.Value", "0.75");
|
||||||
|
params.putSingle("StrictEntityValidation", "false");
|
||||||
|
|
||||||
|
handler.handle("PutMetricData", params, REGION);
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
ArgumentCaptor<List<MetricDatum>> captor = ArgumentCaptor.forClass(List.class);
|
||||||
|
verify(metricsService).putMetricData(eq("CWAgent"), captor.capture(), eq(REGION));
|
||||||
|
List<MetricDatum> datums = captor.getValue();
|
||||||
|
assertEquals(List.of("TopLevel", "mem_used_percent", "disk_used_percent"),
|
||||||
|
datums.stream().map(MetricDatum::getMetricName).toList());
|
||||||
|
MetricDatum entityDatum = datums.get(1);
|
||||||
|
assertEquals(6.5, entityDatum.getValue());
|
||||||
|
assertEquals("Percent", entityDatum.getUnit());
|
||||||
|
assertEquals("host", entityDatum.getDimensions().get(0).name());
|
||||||
|
assertEquals("h1", entityDatum.getDimensions().get(0).value());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void putMetricDataStoresEntityMetricDataAfterAnEmptyEntityEntry() {
|
||||||
|
MultivaluedMap<String, String> params = new MultivaluedHashMap<>();
|
||||||
|
params.putSingle("Namespace", "CWAgent");
|
||||||
|
params.putSingle("EntityMetricData.member.2.MetricData.member.1.MetricName", "M");
|
||||||
|
params.putSingle("EntityMetricData.member.2.MetricData.member.1.Value", "1");
|
||||||
|
params.putSingle("EntityMetricData.member.10.MetricData.member.1.MetricName", "N");
|
||||||
|
params.putSingle("EntityMetricData.member.10.MetricData.member.1.Value", "2");
|
||||||
|
params.putSingle("StrictEntityValidation", "false");
|
||||||
|
|
||||||
|
handler.handle("PutMetricData", params, REGION);
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
ArgumentCaptor<List<MetricDatum>> captor = ArgumentCaptor.forClass(List.class);
|
||||||
|
verify(metricsService).putMetricData(eq("CWAgent"), captor.capture(), eq(REGION));
|
||||||
|
List<MetricDatum> datums = captor.getValue();
|
||||||
|
assertEquals(List.of("M", "N"), datums.stream().map(MetricDatum::getMetricName).toList());
|
||||||
|
assertEquals(1.0, datums.get(0).getValue());
|
||||||
|
assertEquals(2.0, datums.get(1).getValue());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user