Skip to content

Commit 2a004f2

Browse files
committed
Add configurable metric_timestamp_source, metric_timestamp_granularity, stable host ID, and NodeOperationDetail dedup for APM service map processor (#6672)
Signed-off-by: vamsimanohar <vamsimanohar@users.noreply.github.com> Signed-off-by: Vamsi Manohar <reddyvam@amazon.com>
1 parent a4f0c9b commit 2a004f2

File tree

13 files changed

+948
-313
lines changed

13 files changed

+948
-313
lines changed
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*
2+
* Copyright OpenSearch Contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*
5+
* The OpenSearch Contributors require contributions made to
6+
* this file be licensed under the Apache-2.0 license or a
7+
* compatible open source license.
8+
*/
9+
10+
package org.opensearch.dataprepper.model.host;
11+
12+
import org.slf4j.Logger;
13+
import org.slf4j.LoggerFactory;
14+
15+
import java.net.InetAddress;
16+
17+
/**
18+
* Provides the hostname of the current Data Prepper instance.
19+
* This is intended as a shared utility so that hostname resolution
20+
* is consistent across all components (processors, source coordinators, etc.).
21+
*/
22+
public class HostContext {
23+
24+
private static final Logger LOG = LoggerFactory.getLogger(HostContext.class);
25+
private static final String UNKNOWN_HOST = "unknown";
26+
private static final String HOSTNAME = resolveHostname();
27+
28+
static String resolveHostname() {
29+
try {
30+
return InetAddress.getLocalHost().getHostName();
31+
} catch (final Exception e) {
32+
LOG.warn("Failed to resolve hostname, using '{}': {}", UNKNOWN_HOST, e.getMessage());
33+
return UNKNOWN_HOST;
34+
}
35+
}
36+
37+
/**
38+
* Returns the hostname of the current Data Prepper host.
39+
*
40+
* @return the hostname
41+
*/
42+
public static String getHostname() {
43+
return HOSTNAME;
44+
}
45+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/*
2+
* Copyright OpenSearch Contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*
5+
* The OpenSearch Contributors require contributions made to
6+
* this file be licensed under the Apache-2.0 license or a
7+
* compatible open source license.
8+
*/
9+
10+
package org.opensearch.dataprepper.model.host;
11+
12+
import org.junit.jupiter.api.Test;
13+
import org.mockito.MockedStatic;
14+
15+
import java.net.InetAddress;
16+
import java.net.UnknownHostException;
17+
18+
import static org.hamcrest.CoreMatchers.equalTo;
19+
import static org.hamcrest.CoreMatchers.notNullValue;
20+
import static org.hamcrest.MatcherAssert.assertThat;
21+
import static org.hamcrest.Matchers.not;
22+
import static org.hamcrest.Matchers.emptyString;
23+
import static org.mockito.Mockito.mockStatic;
24+
25+
class HostContextTest {
26+
27+
@Test
28+
void getHostname_returns_non_null_non_empty_value() {
29+
final String hostname = HostContext.getHostname();
30+
assertThat(hostname, notNullValue());
31+
assertThat(hostname, not(emptyString()));
32+
}
33+
34+
@Test
35+
void getHostname_returns_consistent_value() {
36+
final String first = HostContext.getHostname();
37+
final String second = HostContext.getHostname();
38+
assertThat(first, equalTo(second));
39+
}
40+
41+
@Test
42+
void getHostname_matches_InetAddress_hostname() throws UnknownHostException {
43+
final String expected = InetAddress.getLocalHost().getHostName();
44+
assertThat(HostContext.getHostname(), equalTo(expected));
45+
}
46+
47+
@Test
48+
void resolveHostname_returns_valid_hostname() throws UnknownHostException {
49+
final String hostname = HostContext.resolveHostname();
50+
assertThat(hostname, equalTo(InetAddress.getLocalHost().getHostName()));
51+
}
52+
53+
@Test
54+
void resolveHostname_returns_unknown_when_hostname_cannot_be_resolved() {
55+
try (final MockedStatic<InetAddress> inetAddressMock = mockStatic(InetAddress.class)) {
56+
inetAddressMock.when(InetAddress::getLocalHost)
57+
.thenThrow(new UnknownHostException("test exception"));
58+
59+
assertThat(HostContext.resolveHostname(), equalTo("unknown"));
60+
}
61+
}
62+
63+
@Test
64+
void constructor_can_be_created() {
65+
final HostContext hostContext = new HostContext();
66+
assertThat(hostContext, notNullValue());
67+
}
68+
}

data-prepper-plugins/otel-apm-service-map-processor/README.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ processor:
3434
| `window_duration` | Duration | `60s` | Fixed time window in seconds for evaluating APM service map relationships |
3535
| `db_path` | String | `"data/otel-apm-service-map/"` | Directory path for database files storing transient processing data |
3636
| `group_by_attributes` | List\<String\> | `[]` | OpenTelemetry resource attributes to include in service grouping |
37+
| `metric_timestamp_source` | String | `"arrival_time"` | Timestamp source for emitted metrics. `"arrival_time"` uses processing time at window evaluation (avoids late-span data loss in Prometheus/AMP). `"span_end_time"` uses the span's `endTime` field. |
38+
| `metric_timestamp_granularity` | String | `"seconds"` | Truncation granularity for metric and service map timestamps. `"seconds"` truncates to second boundaries (1s collision window). `"minutes"` truncates to minute boundaries (60s collision window). |
3739

3840
### Advanced Configuration
3941

@@ -42,13 +44,37 @@ processor:
4244
- otel_apm_service_map:
4345
window_duration: 120s # 2-minute windows for high-latency services
4446
db_path: "/tmp/apm-service-map/"
47+
metric_timestamp_source: arrival_time
48+
metric_timestamp_granularity: seconds
4549
group_by_attributes:
4650
- "service.version"
4751
- "deployment.environment"
4852
- "service.namespace"
4953
- "k8s.cluster.name"
5054
```
5155

56+
### Metric Timestamp Source
57+
58+
The `metric_timestamp_source` option controls what timestamp is used for emitted metrics.
59+
60+
| Value | Timestamp used | Late-span safe | Description |
61+
|---|---|---|---|
62+
| `arrival_time` (default) | `clock.instant()` at window evaluation | Yes | All spans in a window share the same processing timestamp. Matches the [OTel Collector spanmetrics connector](https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/connector/spanmetricsconnector) approach. |
63+
| `span_end_time` | Span's `endTime` field | No | Each span's end time is used. Late-arriving spans may produce metrics with timestamps that collide with previously written data points, causing silent data loss in Prometheus/AMP. |
64+
65+
**Recommendation:** Use the default `arrival_time` unless you have a specific requirement for span-aligned timestamps and accept the risk of late-span data loss.
66+
67+
### Metric Timestamp Granularity
68+
69+
The `metric_timestamp_granularity` option controls the truncation granularity for all emitted timestamps (metrics and service map events).
70+
71+
| Value | Collision window (`span_end_time` mode) | Data points per window | Description |
72+
|---|---|---|---|
73+
| `seconds` (default) | 1 second | More (one per unique second) | Truncates to second boundaries. Minimizes collision risk in `span_end_time` mode. |
74+
| `minutes` | 60 seconds | Fewer (one per unique minute) | Truncates to minute boundaries. Higher collision risk but fewer data points. |
75+
76+
In `arrival_time` mode, granularity has minimal impact since all spans in a window share the same `clock.instant()` — each window always produces one data point per label combination regardless of truncation.
77+
5278
## Pipeline Examples
5379

5480
### Basic Pipeline
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
* Copyright OpenSearch Contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*
5+
* The OpenSearch Contributors require contributions made to
6+
* this file be licensed under the Apache-2.0 license or a
7+
* compatible open source license.
8+
*
9+
*/
10+
11+
package org.opensearch.dataprepper.plugins.processor.otel_apm_service_map;
12+
13+
import com.fasterxml.jackson.annotation.JsonCreator;
14+
15+
import java.time.temporal.ChronoUnit;
16+
import java.util.Arrays;
17+
import java.util.Map;
18+
import java.util.stream.Collectors;
19+
20+
public enum MetricTimestampGranularity {
21+
SECONDS("seconds", ChronoUnit.SECONDS),
22+
MINUTES("minutes", ChronoUnit.MINUTES);
23+
24+
private static final Map<String, MetricTimestampGranularity> OPTIONS_MAP = Arrays.stream(MetricTimestampGranularity.values())
25+
.collect(Collectors.toMap(
26+
value -> value.option,
27+
value -> value
28+
));
29+
30+
private final String option;
31+
private final ChronoUnit chronoUnit;
32+
33+
MetricTimestampGranularity(final String option, final ChronoUnit chronoUnit) {
34+
this.option = option;
35+
this.chronoUnit = chronoUnit;
36+
}
37+
38+
public String getOption() {
39+
return option;
40+
}
41+
42+
public ChronoUnit getChronoUnit() {
43+
return chronoUnit;
44+
}
45+
46+
@JsonCreator
47+
public static MetricTimestampGranularity fromOptionValue(final String option) {
48+
return OPTIONS_MAP.get(option);
49+
}
50+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* Copyright OpenSearch Contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*
5+
* The OpenSearch Contributors require contributions made to
6+
* this file be licensed under the Apache-2.0 license or a
7+
* compatible open source license.
8+
*
9+
*/
10+
11+
package org.opensearch.dataprepper.plugins.processor.otel_apm_service_map;
12+
13+
import com.fasterxml.jackson.annotation.JsonCreator;
14+
15+
import java.util.Arrays;
16+
import java.util.Map;
17+
import java.util.stream.Collectors;
18+
19+
public enum MetricTimestampSource {
20+
ARRIVAL_TIME("arrival_time"),
21+
SPAN_END_TIME("span_end_time");
22+
23+
private static final Map<String, MetricTimestampSource> OPTIONS_MAP = Arrays.stream(MetricTimestampSource.values())
24+
.collect(Collectors.toMap(
25+
value -> value.option,
26+
value -> value
27+
));
28+
29+
private final String option;
30+
31+
MetricTimestampSource(final String option) {
32+
this.option = option;
33+
}
34+
35+
public String getOption() {
36+
return option;
37+
}
38+
39+
@JsonCreator
40+
public static MetricTimestampSource fromOptionValue(final String option) {
41+
return OPTIONS_MAP.get(option);
42+
}
43+
}

0 commit comments

Comments
 (0)