Skip to content
Merged
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
11 changes: 11 additions & 0 deletions argus-apm/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
plugins {
`java-library`
}

dependencies {
testImplementation("org.junit.jupiter:junit-jupiter:${property("junitVersion")}")
}

tasks.withType<JavaCompile> {
options.release.set(17)
}
34 changes: 34 additions & 0 deletions argus-apm/src/main/java/io/argus/apm/ApmFacade.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package io.argus.apm;

import io.argus.apm.dto.ApmBackendLinksRequest;
import io.argus.apm.dto.ApmBackendLinksResponse;
import io.argus.apm.dto.ApmEndpointListResponse;
import io.argus.apm.dto.ApmIncidentListResponse;
import io.argus.apm.dto.ApmServiceDetailResponse;
import io.argus.apm.dto.ApmServiceInventoryResponse;
import io.argus.apm.dto.ApmTraceResponse;
import io.argus.apm.model.ApmPrincipal;
import io.argus.apm.model.ApmScope;
import io.argus.apm.model.ApmServiceId;

/**
* Public APM product facade.
*
* <p>Implementations authorize through {@link ApmPrincipal} and {@link ApmScope}
* before loading data from metrics, trace, log, profile, or internal Argus
* caches. This interface intentionally exposes APM product DTOs only; raw
* aggregator v1alpha1 route and model types stay internal.
*/
public interface ApmFacade {
ApmServiceInventoryResponse listServices(ApmPrincipal principal, ApmScope scope);

ApmServiceDetailResponse getService(ApmPrincipal principal, ApmScope scope, ApmServiceId service);

ApmEndpointListResponse listEndpoints(ApmPrincipal principal, ApmScope scope, ApmServiceId service);

ApmTraceResponse getTrace(ApmPrincipal principal, ApmScope scope, String traceId);

ApmIncidentListResponse listIncidents(ApmPrincipal principal, ApmScope scope);

ApmBackendLinksResponse getBackendLinks(ApmPrincipal principal, ApmBackendLinksRequest request);
}
53 changes: 53 additions & 0 deletions argus-apm/src/main/java/io/argus/apm/ApmFacadeRoutes.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package io.argus.apm;

import java.util.Set;

/**
* Contract route names for the public APM facade.
*/
public final class ApmFacadeRoutes {
public static final String SERVICES = "/apm/services";
public static final String SERVICE = "/apm/services/{service}";
public static final String SERVICE_ENDPOINTS = "/apm/services/{service}/endpoints";
public static final String TRACE = "/apm/traces/{traceId}";
public static final String INCIDENTS = "/apm/incidents";
public static final String BACKEND_LINKS = "/apm/backend-links";

private static final Set<String> PUBLIC_ROUTES = Set.of(
SERVICES,
SERVICE,
SERVICE_ENDPOINTS,
TRACE,
INCIDENTS,
BACKEND_LINKS
);

private static final Set<String> FORBIDDEN_AGGREGATOR_PREFIXES = Set.of(
"/fleet",
"/api/pods",
"/profile"
);

private ApmFacadeRoutes() {
}

public static Set<String> publicRoutes() {
return PUBLIC_ROUTES;
}

public static boolean isPublicApmRoute(String path) {
return path != null && path.startsWith("/apm/");
}

public static boolean isForbiddenAggregatorRoute(String path) {
if (path == null) {
return false;
}
for (String prefix : FORBIDDEN_AGGREGATOR_PREFIXES) {
if (path.equals(prefix) || path.startsWith(prefix + "/")) {
return true;
}
}
return false;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package io.argus.apm.correlation;

public enum ApmCorrelationReason {
TRACE_AND_SPAN_ID,
TRACE_ID,
SERVICE_AND_TIME_OVERLAP
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package io.argus.apm.correlation;

import io.argus.apm.model.ApmSpanSummary;
import io.argus.apm.model.JvmFinding;

import java.util.Objects;

public record ApmFindingCorrelation(
JvmFinding finding,
ApmSpanSummary span,
ApmCorrelationReason reason
) {
public ApmFindingCorrelation {
Objects.requireNonNull(finding, "finding");
Objects.requireNonNull(reason, "reason");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package io.argus.apm.correlation;

import io.argus.apm.model.ApmTraceContext;
import io.argus.apm.model.JvmFinding;

import java.util.List;
import java.util.Objects;

public record ApmTraceCorrelationResult(
ApmTraceContext trace,
List<ApmFindingCorrelation> matches,
List<JvmFinding> unmatchedFindings
) {
public ApmTraceCorrelationResult {
Objects.requireNonNull(trace, "trace");
matches = List.copyOf(Objects.requireNonNull(matches, "matches"));
unmatchedFindings = List.copyOf(Objects.requireNonNull(unmatchedFindings, "unmatchedFindings"));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package io.argus.apm.correlation;

import io.argus.apm.model.ApmSpanSummary;
import io.argus.apm.model.ApmTraceContext;
import io.argus.apm.model.JvmFinding;

import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

/**
* Correlates Argus JVM findings with OTel trace/span summaries.
*/
public final class ApmTraceFindingCorrelator {
private final Duration timingWindow;

public ApmTraceFindingCorrelator() {
this(Duration.ofSeconds(1));
}

public ApmTraceFindingCorrelator(Duration timingWindow) {
this.timingWindow = Objects.requireNonNull(timingWindow, "timingWindow");
if (timingWindow.isNegative()) {
throw new IllegalArgumentException("timingWindow must not be negative");
}
}

public ApmTraceCorrelationResult correlate(ApmTraceContext trace, List<JvmFinding> candidateFindings) {
Objects.requireNonNull(trace, "trace");
Objects.requireNonNull(candidateFindings, "candidateFindings");
List<ApmFindingCorrelation> matches = new ArrayList<>();
List<JvmFinding> unmatched = new ArrayList<>();

for (JvmFinding finding : candidateFindings) {
Objects.requireNonNull(finding, "candidate finding");
ApmFindingCorrelation match = match(trace, finding);
if (match == null) {
unmatched.add(finding);
} else {
matches.add(match);
}
}
return new ApmTraceCorrelationResult(trace, matches, unmatched);
}

private ApmFindingCorrelation match(ApmTraceContext trace, JvmFinding finding) {
if (!isBlank(finding.traceId()) && !finding.traceId().equals(trace.traceId())) {
return null;
}
if (!isBlank(finding.spanId())) {
for (ApmSpanSummary span : trace.spans()) {
if (finding.spanId().equals(span.spanId())) {
return new ApmFindingCorrelation(finding, span, ApmCorrelationReason.TRACE_AND_SPAN_ID);
}
}
}
if (!isBlank(finding.traceId()) && finding.traceId().equals(trace.traceId())) {
return new ApmFindingCorrelation(finding, null, ApmCorrelationReason.TRACE_ID);
}
for (ApmSpanSummary span : trace.spans()) {
if (finding.service().equals(span.service()) && overlaps(finding.observedAt(), span)) {
return new ApmFindingCorrelation(finding, span, ApmCorrelationReason.SERVICE_AND_TIME_OVERLAP);
}
}
return null;
}

private boolean overlaps(Instant observedAt, ApmSpanSummary span) {
Instant from = span.startTime().minus(timingWindow);
Instant to = span.endTime().plus(timingWindow);
return !observedAt.isBefore(from) && !observedAt.isAfter(to);
}

private static boolean isBlank(String value) {
return value == null || value.isBlank();
}
}
Loading
Loading