Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ public void testRawMeasurementsHandler() {

Document document = measurementAccessor.find(Filters.empty()).findFirst().orElseThrow();
assertNotNull(document);
assertEquals(14, document.size());
assertEquals(15, document.size());
assertEquals("val1", document.get("attr1"));
assertEquals("val2", document.get("attr2"));
assertFalse(document.containsKey("plan"));
Expand All @@ -74,6 +74,8 @@ public void testRawMeasurementsHandler() {
assertEquals("PASSED", document.get("rnStatus"));
assertEquals(plan.getId().toHexString(), document.get("planId"));
assertEquals("", document.get("canonicalPlanName")); // there is no importResult on the execution
// The hash of the artefact the measurement was produced by, i.e. its position in the resolved plan
assertTrue(((String) document.get("artefactHash")).matches("[0-9A-F]{32}"));
assertEquals("", document.get("taskId"));
}

Expand Down Expand Up @@ -113,7 +115,7 @@ public void testWithSchedule() {

Document document = measurementAccessor.find(Filters.empty()).findFirst().orElseThrow();
assertNotNull(document);
assertEquals(14, document.size());
assertEquals(15, document.size());
assertEquals("val1", document.get("attr1"));
assertEquals("val2", document.get("attr2"));
assertFalse(document.containsKey("plan"));
Expand All @@ -125,6 +127,8 @@ public void testWithSchedule() {
assertEquals("PASSED", document.get("rnStatus"));
assertEquals(plan.getId().toHexString(), document.get("planId"));
assertEquals("", document.get("canonicalPlanName")); // there is no importResult on the execution
// The hash of the artefact the measurement was produced by, i.e. its position in the resolved plan
assertTrue(((String) document.get("artefactHash")).matches("[0-9A-F]{32}"));
assertEquals(taskParameters.getId().toHexString(), document.get("taskId"));


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ public void testMergeHistogramDistributionsMerged() {
// -------------------------------------------------------------------------

private void save(String rnId, MetricSample sample) {
ExecutionMetricSample sms = new ExecutionMetricSample(sample, EXEC_ID, rnId, PLAN_ID,
ExecutionMetricSample sms = new ExecutionMetricSample(sample, EXEC_ID, rnId, "HASH-" + rnId, PLAN_ID,
"myPlan", "canonical", "", "", "my test", null, null, null, null);
accessor.save(Collections.singletonList(sms));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
import java.util.stream.Collectors;

import static step.core.metrics.MetricsExecutionPlugin.AGENT_URL;
import static step.core.metrics.MetricsExecutionPlugin.ARTEFACT_HASH;
import static step.core.metrics.MetricsExecutionPlugin.BEGIN;
import static step.core.metrics.MetricsExecutionPlugin.RN_ID;
import static step.core.metrics.MetricsExecutionPlugin.VALUE;
Expand All @@ -66,7 +67,7 @@ public class TimeSeriesControllerPlugin extends AbstractControllerPlugin {
// Before Step 30, the list of supported attributed by the time-series were defined with below default values and could be customized via step.properties
// This was used to determine if we had to fall back to RAW measurement when a filter or group by used unknown fields
public static final String TIME_SERIES_ATTRIBUTES_PROPERTY = "timeseries.attributes";
public static final String TIME_SERIES_ATTRIBUTES_DEFAULT = MetricsConstants.getAllAttributeNames() + ",metricType,origin,project";
public static final String TIME_SERIES_ATTRIBUTES_DEFAULT = MetricsConstants.getAllAttributeNames() + ",metricType,origin,project," + ARTEFACT_HASH;

// Following properties are used by the UI. In the future we could remove the prefix 'plugins.' to align with other properties
public static final String PARAM_KEY_EXECUTION_DASHBOARD_ID = "plugins.timeseries.execution.dashboard.id";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
import java.util.stream.Stream;

import static step.core.metrics.ExecutionMetricSample.SAMPLE_SAMPLE_TIME;
import static step.core.metrics.MetricsExecutionPlugin.ARTEFACT_HASH;
import static step.core.timeseries.TimeSeriesConstants.ATTRIBUTES_PREFIX;
import static step.core.timeseries.TimeSeriesConstants.TIMESTAMP_ATTRIBUTE;
import static step.core.metrics.StepMetricSample.METRIC_TYPE;
Expand All @@ -55,7 +56,7 @@ public class TimeSeriesHandler {
}
};
//Ugly implementation
private static final List<String> executionMetricSampleBaseFields = List.of("eId", "rnId", "planId", "plan", "taskId", "schedule", "execution", "agentUrl", "origin", "metricType");
private static final List<String> executionMetricSampleBaseFields = List.of("eId", "rnId", ARTEFACT_HASH, "planId", "plan", "taskId", "schedule", "execution", "agentUrl", "origin", "metricType");
private static final List<String> executionMetricSampleAttributesFields = List.of("project", "projectName");
private static final Function<String, String> attributesPrefixRemovalSamples = (attribute) -> {
if (attribute.startsWith(ATTRIBUTES_PREFIX)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
import org.junit.Test;
import step.core.collections.Filters;
import step.core.collections.inmemory.InMemoryCollection;
import step.core.metrics.Measurement;
import step.core.metrics.MetricSample;
import step.core.metrics.MetricsExecutionPlugin;
import step.core.metrics.InstrumentType;
import step.core.timeseries.TimeSeries;
import step.core.timeseries.TimeSeriesBuilder;
Expand Down Expand Up @@ -135,13 +137,51 @@ public void onlyHandledAttributesAreIncludedInBucket() {
Assert.assertEquals("qa", buckets.get(0).getAttributes().get("env"));
}

// ── Artefact hash ────────────────────────────────────────────────────────

/**
* In exclude-mode, the production default, the artefact hash of the report node the sample was
* produced by is ingested as a bucket dimension, for both metrics and measurements.
*/
@Test
public void artefactHashIsIngestedAsBucketAttribute() {
TimeSeriesMetricSamplesHandler excludeModeHandler = newExcludeModeHandler();

MetricSample snapshot = new MetricSample(0L,
"requests", Map.of(), InstrumentType.COUNTER, 1, 1, 1, 1, 1, null);
excludeModeHandler.processMetrics(null, List.of(buildMetricMeasurement(snapshot)));

Measurement measurement = new Measurement();
measurement.setExecId("exec-1");
measurement.setName("myKeyword");
measurement.setBegin(0L);
measurement.setValue(1L);
measurement.addCustomField(MetricsExecutionPlugin.ARTEFACT_HASH, "HASH-2");
excludeModeHandler.processMeasurements(null, List.of(measurement));

excludeModeHandler.flush();

Set<Object> hashes = allBuckets().stream()
.map(b -> b.getAttributes().get(MetricsExecutionPlugin.ARTEFACT_HASH))
.collect(Collectors.toSet());
Assert.assertEquals(Set.of("HASH-1", "HASH-2"), hashes);
}

// ── Helpers ───────────────────────────────────────────────────────────────

private TimeSeriesMetricSamplesHandler newExcludeModeHandler() {
bucketsCollection = new InMemoryCollection<>();
TimeSeriesCollection tsCollection = new TimeSeriesCollection(bucketsCollection, BUCKET_RESOLUTION);
TimeSeries timeSeries = new TimeSeriesBuilder().registerCollection(tsCollection).build();
return new TimeSeriesMetricSamplesHandler(timeSeries, Set.of(), Set.of("begin", "value"));
}

private ExecutionMetricSample buildMetricMeasurement(MetricSample snapshot) {
return new ExecutionMetricSample(
snapshot,
"exec-1", // execId
"rn-1", // rnId
"HASH-1", // artefactHash
"plan-1", // planId
"MyPlan", // plan name
"canonical", // canonicalPlanName
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -308,14 +308,30 @@ public void raisesLabelCountNoticeOncePerMetric() {
labelCountNotices.forEach(n -> Assert.assertEquals("1", n.parameters().get("quota")));
}

// ── Context labels set by Step itself are exempt from the quota ─────────────────────────────

/**
* The artefact hash is a context label set by Step, not a keyword-author-defined one, so it must
* survive even the strictest quota, which drops every custom label.
*/
@Test
public void artefactHashIsNotSubjectToTheLabelQuota() {
TimeSeriesMetricSamplesHandler handler = newLabelCountHandler(0, null);
handler.processMetrics(null, metricsWithDistinctLabel("user_id", 3, "m1", "e1"));
handler.flush();

Assert.assertTrue(bucketAttributeValues("user_id").isEmpty());
Assert.assertEquals(Set.of("HASH-0", "HASH-1", "HASH-2"), bucketAttributeValues("artefactHash"));
}

// ── Helpers ─────────────────────────────────────────────────────────────────────────────────

private List<ExecutionMetricSample> metricsWithDistinctLabel(String labelName, int count, String metricName, String execId) {
List<ExecutionMetricSample> samples = new ArrayList<>();
for (int i = 0; i < count; i++) {
MetricSample snapshot = new MetricSample(0L, metricName,
Map.of(labelName, labelName + "-" + i), InstrumentType.GAUGE, 1, 1, 1, 1, 1, null);
samples.add(new ExecutionMetricSample(snapshot, execId, "rn-" + i, "plan-1", "MyPlan",
samples.add(new ExecutionMetricSample(snapshot, execId, "rn-" + i, "HASH-" + i, "plan-1", "MyPlan",
"canonical", "", "", "", null, null, null, null));
}
return samples;
Expand All @@ -324,7 +340,7 @@ private List<ExecutionMetricSample> metricsWithDistinctLabel(String labelName, i
private ExecutionMetricSample metricWithLabel(String metricName, String execId, String labelName, String labelValue) {
MetricSample snapshot = new MetricSample(0L, metricName,
Map.of(labelName, labelValue), InstrumentType.GAUGE, 1, 1, 1, 1, 1, null);
return new ExecutionMetricSample(snapshot, execId, "rn", "plan-1", "MyPlan",
return new ExecutionMetricSample(snapshot, execId, "rn", "HASH-1", "plan-1", "MyPlan",
"canonical", "", "", "", null, null, null, null);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ public class ExecutionMetricSample extends StepMetricSample {

public final String eId;
public final String rnId;
/**
* Hash of the artefact path of the report node this sample was produced by, i.e. the static
* position of the artefact within the resolved plan. Null for samples not bound to a report node
* (e.g. execution level metrics).
*/
public final String artefactHash;
public final String planId;
public final String plan;
public final String canonicalPlanName;
Expand All @@ -34,6 +40,7 @@ public class ExecutionMetricSample extends StepMetricSample {
public ExecutionMetricSample(@JsonProperty("sample") MetricSample sample,
@JsonProperty("eId") String eId,
@JsonProperty("rnId") String rnId,
@JsonProperty("artefactHash") String artefactHash,
@JsonProperty("planId") String planId,
@JsonProperty("plan") String plan,
@JsonProperty("canonicalPlanName") String canonicalPlanName,
Expand All @@ -47,6 +54,7 @@ public ExecutionMetricSample(@JsonProperty("sample") MetricSample sample,
super(sample, metricType);
this.eId = eId;
this.rnId = rnId;
this.artefactHash = artefactHash;
this.planId = planId;
this.plan = plan;
this.canonicalPlanName = canonicalPlanName;
Expand Down Expand Up @@ -78,6 +86,7 @@ public TreeMap<String, String> getEffectiveLabels() {
}
// Context labels are authoritative — set last so they cannot be overridden
putIfNotEmpty(labels, MetricsExecutionPlugin.ATTRIBUTE_EXECUTION_ID, eId);
putIfNotEmpty(labels, MetricsExecutionPlugin.ARTEFACT_HASH, artefactHash);
putIfNotEmpty(labels, MetricsExecutionPlugin.PLAN_ID, planId);
putIfNotEmpty(labels, MetricsExecutionPlugin.PLAN, plan);
putIfNotEmpty(labels, MetricsExecutionPlugin.CANONICAL_PLAN_NAME, canonicalPlanName);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ private static ExecutionMetricSample buildHeartbeat(ExecutionMetricSample origin
heartbeatSample = new MetricSample(now, orig.getName(), orig.getLabels(), InstrumentType.GAUGE,
1, last, last, last, last, null);
}
return new ExecutionMetricSample(heartbeatSample, original.eId, original.rnId, original.planId,
return new ExecutionMetricSample(heartbeatSample, original.eId, original.rnId, original.artefactHash, original.planId,
original.plan, original.canonicalPlanName, original.taskId, original.schedule, original.execution,
original.agentUrl, original.origin, original.getAttributes(), original.metricType);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ public class MetricsExecutionPlugin extends AbstractExecutionEnginePlugin {
public static final String BEGIN = "begin";
public static final String VALUE = "value";
public static final String RN_ID = "rnId";
public static final String ARTEFACT_HASH = "artefactHash";
public static final String STATUS = "status";
public static final String RN_STATUS = "rnStatus";
public static final String AGENT_URL = "agentUrl";
Expand All @@ -80,7 +81,7 @@ public class MetricsExecutionPlugin extends AbstractExecutionEnginePlugin {
// These are used by the MeasurementControllerPlugin to "reconstruct" measures from measurements, and indicate the
// "internal" fields which should NOT be added to the measure data field. Keep this in sync with the fields defined above.
static final Set<String> MEASURE_NOT_DATA_KEYS = java.util.Set.of("_id", PROJECT, "projectName", ATTRIBUTE_EXECUTION_ID, RN_ID,
ORIGIN, RN_STATUS, PLAN_ID, PLAN, AGENT_URL, TASK_ID, SCHEDULE, TEST_CASE, EXECUTION_DESCRIPTION, CANONICAL_PLAN_NAME);
ARTEFACT_HASH, ORIGIN, RN_STATUS, PLAN_ID, PLAN, AGENT_URL, TASK_ID, SCHEDULE, TEST_CASE, EXECUTION_DESCRIPTION, CANONICAL_PLAN_NAME);
// Same use, but for defining which fields SHOULD be directly copied to the top-level fields of a measure.
static final Set<String> MEASURE_FIELDS = java.util.Set.of(NAME, BEGIN, VALUE, STATUS);

Expand All @@ -96,6 +97,10 @@ public static synchronized void registerSamplesHandlers(MetricSamplesHandler han
SAMPLES_HANDLERS.add(handler);
}

public static synchronized void unregisterSamplesHandler(MetricSamplesHandler handler) {
SAMPLES_HANDLERS.remove(handler);
}

@Override
public void initializeExecutionEngineContext(AbstractExecutionEngineContext parentContext, ExecutionEngineContext executionEngineContext) {
controllerUrl = Optional.ofNullable(parentContext).map(AbstractExecutionEngineContext::getControllerUrl).orElse("");
Expand Down Expand Up @@ -203,7 +208,7 @@ private void processThreadReportNode(ExecutionContext context, ThreadReportNode
1, count, count, count, count, null);

ExecutionMetricSample stepSample = new ExecutionMetricSample(
sample, execId, node.getId().toString(),
sample, execId, node.getId().toString(), node.getArtefactHash(),
planId, plan, canonicalPlanName, scheduleId, schedule, execution,
null, null, additionalAttributes, THREAD_GROUP);

Expand All @@ -216,7 +221,7 @@ private void processThreadReportNode(ExecutionContext context, ThreadReportNode
InstrumentType.GAUGE,
1, currentCount, currentCount, currentCount, currentCount, null);
ExecutionMetricSample heartbeatStepSample = new ExecutionMetricSample(
heartbeatSample, execId, node.getId().toString(),
heartbeatSample, execId, node.getId().toString(), node.getArtefactHash(),
planId, plan, canonicalPlanName, scheduleId, schedule, execution,
null, null, additionalAttributes, THREAD_GROUP);
MetricHeartbeatRegistry.getInstance().update(heartbeatStepSample);
Expand Down Expand Up @@ -344,11 +349,12 @@ private ExecutionMetricSample createExecutionMetricSample(ExecutionContext execu
String execution = Objects.requireNonNullElse((String) executionContext.get(CTX_EXECUTION_DESCRIPTION), "");
String execId = executionContext.getExecutionId();
String rnId = (functionReport != null) ? functionReport.getId().toString() : null;
String artefactHash = (functionReport != null) ? functionReport.getArtefactHash() : null;
String agentUrl = (functionReport != null) ? functionReport.getAgentUrl() : null;
Map<String, String> functionAttributes = (functionReport != null) ? functionReport.getFunctionAttributes() : null;
String origin = (functionAttributes != null) ? functionAttributes.get(AbstractOrganizableObject.NAME) : null;
TreeMap<String, String> additionalAttributes = (TreeMap<String, String>) executionContext.get(CTX_ADDITIONAL_ATTRIBUTES);
return new ExecutionMetricSample(metricSample, execId, rnId, planId, planName, canonicalPlanName, taskId, schedule, execution,
return new ExecutionMetricSample(metricSample, execId, rnId, artefactHash, planId, planName, canonicalPlanName, taskId, schedule, execution,
agentUrl, origin, additionalAttributes, metricType);
}

Expand Down Expand Up @@ -407,6 +413,11 @@ private boolean isArtefactInstrumented(AbstractArtefact artefactInstance) {
private void enrichWithNodeAttributes(Measurement measurement, ReportNode node) {
measurement.setExecId(node.getExecutionID());
measurement.addCustomField(RN_ID, node.getId().toString());
// Report nodes created outside the artefact handler chain have no artefact hash. Such measurements
// are ingested without the attribute rather than with a null value.
if (node.getArtefactHash() != null) {
measurement.addCustomField(ARTEFACT_HASH, node.getArtefactHash());
}
// If a measurement already has its own status (mandatory for live measures, optional for output measures),
// keep it unconditionally, otherwise set the status from the report node.
if (measurement.getStatus() == null) {
Expand Down
Loading