From 9ebbe7590bf41298f563565162aac322f8df992c Mon Sep 17 00:00:00 2001 From: 98001yash Date: Sat, 10 Oct 2026 00:08:17 +0530 Subject: [PATCH 1/3] obeserve Registry implemented --- pom.xml | 6 + .../config/TrafficControlConfiguration.java | 15 ++- .../traffic/TrafficDecisionEngine.java | 107 +++++++++++++++--- 3 files changed, 109 insertions(+), 19 deletions(-) diff --git a/pom.xml b/pom.xml index 95f7ef3..9c0e1e6 100644 --- a/pom.xml +++ b/pom.xml @@ -34,6 +34,12 @@ org.springframework.boot spring-boot-starter-actuator + + + org.springframework.boot + spring-boot-starter-opentelemetry + + org.springframework.boot spring-boot-starter-webmvc diff --git a/src/main/java/com/yashdotdev/distributed_traffic_control/config/TrafficControlConfiguration.java b/src/main/java/com/yashdotdev/distributed_traffic_control/config/TrafficControlConfiguration.java index af90417..50fd33c 100644 --- a/src/main/java/com/yashdotdev/distributed_traffic_control/config/TrafficControlConfiguration.java +++ b/src/main/java/com/yashdotdev/distributed_traffic_control/config/TrafficControlConfiguration.java @@ -14,6 +14,7 @@ import com.yashdotdev.distributed_traffic_control.traffic.TrafficControlService; import com.yashdotdev.distributed_traffic_control.traffic.TrafficDecisionEngine; import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.observation.ObservationRegistry; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.core.StringRedisTemplate; @@ -73,8 +74,18 @@ public CapacityAllocator capacityAllocator(LeaseCoordinator leaseCoordinator, Al } @Bean - public TrafficDecisionEngine trafficDecisionEngine(PolicyProvider policyProvider, QuotaCoordinator quotaCoordinator, CapacityAllocator capacityAllocator, TrafficControlMetrics metrics) { - return new TrafficDecisionEngine(policyProvider, quotaCoordinator, capacityAllocator, metrics); + public TrafficDecisionEngine trafficDecisionEngine( + PolicyProvider policyProvider, + QuotaCoordinator quotaCoordinator, + CapacityAllocator capacityAllocator, + TrafficControlMetrics metrics, + ObservationRegistry observationRegistry) { + return new TrafficDecisionEngine( + policyProvider, + quotaCoordinator, + capacityAllocator, + metrics, + observationRegistry); } @Bean diff --git a/src/main/java/com/yashdotdev/distributed_traffic_control/traffic/TrafficDecisionEngine.java b/src/main/java/com/yashdotdev/distributed_traffic_control/traffic/TrafficDecisionEngine.java index 9506186..3073b86 100644 --- a/src/main/java/com/yashdotdev/distributed_traffic_control/traffic/TrafficDecisionEngine.java +++ b/src/main/java/com/yashdotdev/distributed_traffic_control/traffic/TrafficDecisionEngine.java @@ -10,6 +10,8 @@ import com.yashdotdev.distributed_traffic_control.quota.QuotaCoordinator; import com.yashdotdev.distributed_traffic_control.quota.QuotaKey; import io.micrometer.core.instrument.Timer; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationRegistry; import java.util.Optional; @@ -19,64 +21,122 @@ public class TrafficDecisionEngine { private final QuotaCoordinator quotaCoordinator; private final CapacityAllocator capacityAllocator; private final TrafficControlMetrics metrics; + private final ObservationRegistry observationRegistry; - public TrafficDecisionEngine(PolicyProvider policyProvider, QuotaCoordinator quotaCoordinator, CapacityAllocator capacityAllocator) { - this(policyProvider, quotaCoordinator, capacityAllocator, null); + public TrafficDecisionEngine( + PolicyProvider policyProvider, + QuotaCoordinator quotaCoordinator, + CapacityAllocator capacityAllocator) { + this(policyProvider, quotaCoordinator, capacityAllocator, null, null); } - public TrafficDecisionEngine(PolicyProvider policyProvider, QuotaCoordinator quotaCoordinator, CapacityAllocator capacityAllocator, TrafficControlMetrics metrics) { + public TrafficDecisionEngine( + PolicyProvider policyProvider, + QuotaCoordinator quotaCoordinator, + CapacityAllocator capacityAllocator, + TrafficControlMetrics metrics) { + this(policyProvider, quotaCoordinator, capacityAllocator, metrics, null); + } + + public TrafficDecisionEngine( + PolicyProvider policyProvider, + QuotaCoordinator quotaCoordinator, + CapacityAllocator capacityAllocator, + TrafficControlMetrics metrics, + ObservationRegistry observationRegistry) { this.policyProvider = policyProvider; this.quotaCoordinator = quotaCoordinator; this.capacityAllocator = capacityAllocator; this.metrics = metrics; + this.observationRegistry = observationRegistry; } public TrafficDecision evaluate(TrafficRequest request) { + return observe("traffic.decision", () -> evaluateDecision(request)); + } + private TrafficDecision evaluateDecision(TrafficRequest request) { Timer.Sample timer = null; if (metrics != null) { timer = metrics.startDecisionTimer(); } try { - Optional policy = policyProvider.findPolicy(request); + Optional policy = observe( + "traffic.policy.lookup", + () -> policyProvider.findPolicy(request)); + if (policy.isEmpty()) { recordRejected(); - return new TrafficDecision(TrafficDecisionStatus.REJECTED, "No traffic policy found", 0); + return new TrafficDecision( + TrafficDecisionStatus.REJECTED, + "No traffic policy found", + 0); } TrafficPolicy trafficPolicy = policy.get(); if (!trafficPolicy.isActive()) { recordRejected(); - return new TrafficDecision(TrafficDecisionStatus.REJECTED, "Traffic policy is inactive", 0); + return new TrafficDecision( + TrafficDecisionStatus.REJECTED, + "Traffic policy is inactive", + 0); } - QuotaKey quotaKey = new QuotaKey(trafficPolicy.getPolicyId(), request.getSubject(), request.getResource()); - QuotaConsumptionResult consumptionResult = quotaCoordinator.tryConsume(quotaKey, trafficPolicy); + QuotaKey quotaKey = new QuotaKey( + trafficPolicy.getPolicyId(), + request.getSubject(), + request.getResource()); + + QuotaConsumptionResult consumptionResult = observe( + "traffic.quota.evaluate", + () -> quotaCoordinator.tryConsume(quotaKey, trafficPolicy)); + if (consumptionResult.isConsumed()) { recordAllowed(); - - return new TrafficDecision(TrafficDecisionStatus.ALLOWED, "Request allowed", consumptionResult.getRemainingCapacity()); + return new TrafficDecision( + TrafficDecisionStatus.ALLOWED, + "Request allowed", + consumptionResult.getRemainingCapacity()); } + recordQuotaExhausted(); recordLeaseAllocationAttempt(); - Optional lease = capacityAllocator.allocate(trafficPolicy, quotaKey); + + Optional lease = observe( + "traffic.lease.allocate", + () -> capacityAllocator.allocate(trafficPolicy, quotaKey)); if (lease.isEmpty()) { recordLeaseAllocationFailure(); recordRejected(); - - return new TrafficDecision(TrafficDecisionStatus.REJECTED, "Traffic quota exhausted", consumptionResult.getRemainingCapacity()); + return new TrafficDecision( + TrafficDecisionStatus.REJECTED, + "Traffic quota exhausted", + consumptionResult.getRemainingCapacity()); } + recordLeaseAllocationSuccess(); - LeaseConsumptionResult leaseConsumptionResult = capacityAllocator.tryConsume(lease.get(), request.getRequestedAt()); + + LeaseConsumptionResult leaseConsumptionResult = observe( + "traffic.lease.consume", + () -> capacityAllocator.tryConsume( + lease.get(), + request.getRequestedAt())); + if (!leaseConsumptionResult.isConsumed()) { recordRejected(); - - return new TrafficDecision(TrafficDecisionStatus.REJECTED, "Traffic quota exhausted", leaseConsumptionResult.getRemainingCapacity()); + return new TrafficDecision( + TrafficDecisionStatus.REJECTED, + "Traffic quota exhausted", + leaseConsumptionResult.getRemainingCapacity()); } + recordAllowed(); - return new TrafficDecision(TrafficDecisionStatus.ALLOWED, "Request allowed", leaseConsumptionResult.getRemainingCapacity()); + return new TrafficDecision( + TrafficDecisionStatus.ALLOWED, + "Request allowed", + leaseConsumptionResult.getRemainingCapacity()); } finally { if (timer != null) { @@ -84,6 +144,19 @@ public TrafficDecision evaluate(TrafficRequest request) { } } } + + private T observe( + String name, + java.util.function.Supplier operation) { + + if (observationRegistry == null) { + return operation.get(); + } + + return Observation.createNotStarted(name, observationRegistry) + .observe(operation); + } + private void recordAllowed() { if (metrics != null) { metrics.recordAllowed(); From cc24a29054f8d90c474c66137b38a8a951e54475 Mon Sep 17 00:00:00 2001 From: 98001yash Date: Sat, 10 Oct 2026 00:22:53 +0530 Subject: [PATCH 2/3] implemented the jaegar --- docker-compose.yml | 22 ++++++- .../config/TrafficControlConfiguration.java | 10 ++- .../lease/RedisLeaseCoordinator.java | 61 +++++++++++++++++-- src/main/resources/application.properties | 4 ++ 4 files changed, 88 insertions(+), 9 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index ef13747..9d752cb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,12 +12,22 @@ services: ports: - "6379:6379" + jaeger: + image: jaegertracing/jaeger:latest + container_name: traffic-control-jaeger + ports: + - "16686:16686" + - "4317:4317" + - "4318:4318" + gateway-1: image: distributed-traffic-control:latest container_name: traffic-control-gateway-1 depends_on: redis: condition: service_healthy + jaeger: + condition: service_started environment: SERVER_PORT: 8081 REDIS_HOST: redis @@ -25,6 +35,8 @@ services: NODE_ID: gateway-1 LEASE_DURATION: PT30S LEASE_CAPACITY: 10 + OTEL_EXPORTER_OTLP_ENDPOINT: http://jaeger:4318/v1/traces + TRACING_SAMPLING_PROBABILITY: "1.0" healthcheck: test: ["CMD", "curl", "--fail", "http://localhost:8081/actuator/health"] interval: 10s @@ -40,6 +52,8 @@ services: depends_on: redis: condition: service_healthy + jaeger: + condition: service_started environment: SERVER_PORT: 8081 REDIS_HOST: redis @@ -47,6 +61,8 @@ services: NODE_ID: gateway-2 LEASE_DURATION: PT30S LEASE_CAPACITY: 10 + OTEL_EXPORTER_OTLP_ENDPOINT: http://jaeger:4318/v1/traces + TRACING_SAMPLING_PROBABILITY: "1.0" healthcheck: test: ["CMD", "curl", "--fail", "http://localhost:8081/actuator/health"] interval: 10s @@ -62,6 +78,8 @@ services: depends_on: redis: condition: service_healthy + jaeger: + condition: service_started environment: SERVER_PORT: 8081 REDIS_HOST: redis @@ -69,6 +87,8 @@ services: NODE_ID: gateway-3 LEASE_DURATION: PT30S LEASE_CAPACITY: 10 + OTEL_EXPORTER_OTLP_ENDPOINT: http://jaeger:4318/v1/traces + TRACING_SAMPLING_PROBABILITY: "1.0" healthcheck: test: ["CMD", "curl", "--fail", "http://localhost:8081/actuator/health"] interval: 10s @@ -100,4 +120,4 @@ services: - ./monitoring/grafana/provisioning/dashboards:/etc/grafana/provisioning/dashboards:ro - ./monitoring/grafana/dashboards:/var/lib/grafana/dashboards:ro ports: - - "3000:3000" + - "3000:3000" \ No newline at end of file diff --git a/src/main/java/com/yashdotdev/distributed_traffic_control/config/TrafficControlConfiguration.java b/src/main/java/com/yashdotdev/distributed_traffic_control/config/TrafficControlConfiguration.java index 50fd33c..d0328b5 100644 --- a/src/main/java/com/yashdotdev/distributed_traffic_control/config/TrafficControlConfiguration.java +++ b/src/main/java/com/yashdotdev/distributed_traffic_control/config/TrafficControlConfiguration.java @@ -54,8 +54,14 @@ public QuotaCoordinator quotaCoordinator(Clock clock) { } @Bean - public LeaseCoordinator leaseCoordinator(StringRedisTemplate redisTemplate, Clock clock) { - return new RedisLeaseCoordinator(redisTemplate, clock); + public LeaseCoordinator leaseCoordinator( + StringRedisTemplate redisTemplate, + Clock clock, + ObservationRegistry observationRegistry) { + return new RedisLeaseCoordinator( + redisTemplate, + clock, + observationRegistry); } @Bean diff --git a/src/main/java/com/yashdotdev/distributed_traffic_control/lease/RedisLeaseCoordinator.java b/src/main/java/com/yashdotdev/distributed_traffic_control/lease/RedisLeaseCoordinator.java index de8226a..0c99fec 100644 --- a/src/main/java/com/yashdotdev/distributed_traffic_control/lease/RedisLeaseCoordinator.java +++ b/src/main/java/com/yashdotdev/distributed_traffic_control/lease/RedisLeaseCoordinator.java @@ -1,6 +1,8 @@ package com.yashdotdev.distributed_traffic_control.lease; import com.yashdotdev.distributed_traffic_control.quota.QuotaKey; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationRegistry; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.core.script.DefaultRedisScript; @@ -346,22 +348,31 @@ public class RedisLeaseCoordinator implements LeaseCoordinator { private final StringRedisTemplate redisTemplate; private final Clock clock; + private final ObservationRegistry observationRegistry; public RedisLeaseCoordinator(StringRedisTemplate redisTemplate) { - this(redisTemplate, Clock.systemUTC()); + this(redisTemplate, Clock.systemUTC(), null); } public RedisLeaseCoordinator(StringRedisTemplate redisTemplate, Clock clock) { + this(redisTemplate, clock, null); + } + + public RedisLeaseCoordinator( + StringRedisTemplate redisTemplate, + Clock clock, + ObservationRegistry observationRegistry) { if (redisTemplate == null) { throw new IllegalArgumentException("redisTemplate must not be null"); } if (clock == null) { throw new IllegalArgumentException("clock must not be null"); } + this.redisTemplate = redisTemplate; this.clock = clock; + this.observationRegistry = observationRegistry; } - @Override public void registerCapacity(GlobalCapacityKey capacityKey, long capacity) { if (capacityKey == null) { @@ -404,7 +415,16 @@ public Optional acquireLease(QuotaKey quotaKey, String nodeId, long String quotaRedisKey = buildGlobalCapacityRedisKey(capacityKey); String leaseRedisKey = buildLeaseRedisKey(leaseId); String leaseRegistryRedisKey = buildLeaseRegistryRedisKey(capacityKey); - Long result = redisTemplate.execute(ACQUIRE_LEASE_SCRIPT, List.of(quotaRedisKey, leaseRedisKey, leaseRegistryRedisKey), String.valueOf(requestedCapacity), nodeId, issuedAt.toString(), String.valueOf(expiresAt.toEpochMilli()), String.valueOf(issuedAt.toEpochMilli())); + Long result = observe( + "redis.lease.acquire", + () -> redisTemplate.execute( + ACQUIRE_LEASE_SCRIPT, + List.of(quotaRedisKey, leaseRedisKey, leaseRegistryRedisKey), + String.valueOf(requestedCapacity), + nodeId, + issuedAt.toString(), + String.valueOf(expiresAt.toEpochMilli()), + String.valueOf(issuedAt.toEpochMilli()))); if (!Long.valueOf(1L).equals(result)) { return Optional.empty(); @@ -426,7 +446,13 @@ public LeaseConsumptionResult tryConsume(QuotaLease lease, String nodeId, Instan } String leaseRedisKey = buildLeaseRedisKey(lease.getLeaseId()); - Long result = redisTemplate.execute(CONSUME_LEASE_SCRIPT, List.of(leaseRedisKey), nodeId, String.valueOf(currentTime.toEpochMilli())); + Long result = observe( + "redis.lease.consume", + () -> redisTemplate.execute( + CONSUME_LEASE_SCRIPT, + List.of(leaseRedisKey), + nodeId, + String.valueOf(currentTime.toEpochMilli()))); if (result == null) { return new LeaseConsumptionResult(false, 0); @@ -467,7 +493,14 @@ public boolean renewLease(QuotaLease lease, String nodeId, Duration extension) { GlobalCapacityKey capacityKey = new GlobalCapacityKey(lease.getQuotaKey().getPolicyId(), lease.getQuotaKey().getResources()); String leaseRegistryRedisKey = buildLeaseRegistryRedisKey(capacityKey); Instant currentTime = clock.instant(); - Long result = redisTemplate.execute(RENEW_LEASE_SCRIPT, List.of(leaseRedisKey, leaseRegistryRedisKey), nodeId, String.valueOf(currentTime.toEpochMilli()), String.valueOf(extension.toMillis())); + Long result = observe( + "redis.lease.renew", + () -> redisTemplate.execute( + RENEW_LEASE_SCRIPT, + List.of(leaseRedisKey, leaseRegistryRedisKey), + nodeId, + String.valueOf(currentTime.toEpochMilli()), + String.valueOf(extension.toMillis()))); if (!Long.valueOf(1L).equals(result)) { return false; @@ -489,7 +522,11 @@ public boolean releaseLease(QuotaLease lease) { GlobalCapacityKey capacityKey = new GlobalCapacityKey(quotaKey.getPolicyId(), quotaKey.getResources()); String quotaRedisKey = buildGlobalCapacityRedisKey(capacityKey); String leaseRegistryRedisKey = buildLeaseRegistryRedisKey(capacityKey); - Long result = redisTemplate.execute(RELEASE_LEASE_SCRIPT, List.of(leaseRedisKey, quotaRedisKey, leaseRegistryRedisKey)); + Long result = observe( + "redis.lease.release", + () -> redisTemplate.execute( + RELEASE_LEASE_SCRIPT, + List.of(leaseRedisKey, quotaRedisKey, leaseRegistryRedisKey))); return Long.valueOf(1L).equals(result); } @@ -508,4 +545,16 @@ private void validateQuotaKey(QuotaKey quotaKey) { throw new IllegalArgumentException("quotaKey must not be null"); } } + + private T observe( + String name, + java.util.function.Supplier operation) { + + if (observationRegistry == null) { + return operation.get(); + } + return Observation.createNotStarted(name, observationRegistry) + .observe(operation); + } + } \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 86e86cb..228b7a8 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -12,3 +12,7 @@ allocation.lease-capacity=${LEASE_CAPACITY:10} management.endpoints.web.exposure.include=health,info,prometheus +# OpenTelemetry tracing +management.opentelemetry.tracing.export.otlp.endpoint=${OTEL_EXPORTER_OTLP_ENDPOINT:http://localhost:4318/v1/traces} +management.tracing.sampling.probability=${TRACING_SAMPLING_PROBABILITY:1.0} + From 3c4164c918829baac2da46483e95af81618a0964 Mon Sep 17 00:00:00 2001 From: 98001yash Date: Sat, 10 Oct 2026 00:41:07 +0530 Subject: [PATCH 3/3] feat(observability): add OpenTelemetry tracing for traffic decisions --- README.md | Bin 50638 -> 53136 bytes docker-compose.yml | 6 +- docs/open-telemetry.md | 165 ++++++++++++++++++++++ src/main/resources/application.properties | 5 +- 4 files changed, 172 insertions(+), 4 deletions(-) create mode 100644 docs/open-telemetry.md diff --git a/README.md b/README.md index 14bbfca9b854fe88615d27275de4dcfe32977dd7..01be8d7b9847457bc07dd67c6287d6b6ed3a1181 100644 GIT binary patch delta 1693 zcmah}O-~bH5PmCuAPFH-O3@ZrP=a49sUcBgMB$=AiAGTmLekQ1p<(Hk?v{!sG{&O` zjdL)ZjCV;yu0;F=8WZnE{{c5W;WM*++Y$;M-tP9@nP;Avd1k)8Y(NWXN zRhG)DMU}vNM%g%?T2j4gSl!v{D~*I>>(TI+htyS!()EtRG`Z2(W2mAy&ng41IlyMs z1b%Itc^r>^z6ep&>ZKon_VIoMHq!ZSBiGF?PY;oXe zsV9i+;LfA>_an@$l*BE^a*(9PpHR2eU3DI}RnB85fu&~3HaTInh}bEdEba{yW)@!! zWaNN#0#Cln?$9BZA!u_o)NBAolXPio zDng&;TEj)$<<*=@OV}d9BtR!cfdlZetRPFjcc^l5hK58P!&B32j;k1UXY0T4|->Tw`fB4!4w*>GR<7JE%wj`D#SJ4Lb6G zh(LSz4bvW+MSZplKy;MXg|tl&=aJAQvMPb3fJZA|DR3${w3{$%K7q6?AW4z{X2q}o ze9H9MK0C6%BdZ}Rg5vSfnbzLsXtOVJjP<~(KrQ_+^n5u;@v(y=ex7-F`+6{8QA;eT zIOh!dUd6vxv1YYwkM>>NAElO09TZx11}?`XLOl)~Ig3-Qo4giwj(r*xOIbF=2Hd7Jl`4 z#j(z{(hbqRzXVef2fb{Bi|i1Jt%PXfW>bud$3zSP;%^3)t|j4lPUK73?+`c=C`su$h) A span is emitted only when the corresponding code path executes. For example, lease-acquisition spans require a request that reaches the lease-allocation path. + +## Implementation locations + +The main implementation points are: + +- `src/main/java/com/yashdotdev/distributed_traffic_control/traffic/TrafficDecisionEngine.java` — wraps the decision flow and its key operations in observations. +- `src/main/java/com/yashdotdev/distributed_traffic_control/lease/RedisLeaseCoordinator.java` — wraps Redis lease operations in observations. +- `src/main/java/com/yashdotdev/distributed_traffic_control/config/TrafficControlConfiguration.java` — supplies the shared `ObservationRegistry` to the components. +- `src/main/resources/application.properties` — configures the OTLP trace endpoint and sampling probability. +- `docker-compose.yml` — configures Jaeger and the OTLP endpoint for the gateway containers. + +## Configuration + +The application configuration uses environment-variable overrides so the same application can run locally or in Docker. + +Relevant settings in `src/main/resources/application.properties`: + +```properties +# OpenTelemetry tracing +management.opentelemetry.tracing.export.otlp.endpoint=${OTEL_EXPORTER_OTLP_TRACES_ENDPOINT:http://localhost:4318/v1/traces} +management.tracing.sampling.probability=${TRACING_SAMPLING_PROBABILITY:1.0} + +# Do not export application metrics to Jaeger +management.otlp.metrics.export.enabled=false +``` + +For Docker Compose, each gateway is configured with: + +```yaml +OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: http://jaeger:4318/v1/traces +TRACING_SAMPLING_PROBABILITY: "1.0" +``` + +The hostname `jaeger` is the Compose service name and is reachable by the gateways on the Compose network. The trace endpoint uses OTLP over HTTP on port `4318`. + +A sampling probability of `1.0` samples every trace. This is useful for local development and debugging; production deployments should choose a sampling strategy appropriate for their traffic volume and observability requirements. + +## Local Jaeger setup + +The Compose configuration includes a Jaeger service exposing: + +- `16686` — Jaeger web UI +- `4317` — OTLP over gRPC +- `4318` — OTLP over HTTP + +From the project root, build the application image and start the services: + +```powershell +docker build -t distributed-traffic-control:latest . +docker compose up -d +docker compose ps +``` + +Wait until the three gateway containers and Redis are healthy. Open the Jaeger UI at: + + + +In Jaeger, select the `distributed-traffic-control` service and click **Find Traces**. + +## Generate a test trace + +The traffic evaluation endpoint is: + +`POST http://localhost:8081/api/v1/traffic/evaluate` + +Example PowerShell request: + +```powershell +$body = @{ + requestId = "trace-test-001" + subject = @{ + type = "USER" + subjectId = "demo-user" + } + resource = "demo-resource" + requestedAt = (Get-Date).ToUniversalTime().ToString("o") +} | ConvertTo-Json -Depth 5 + +Invoke-RestMethod ` + -Uri "http://localhost:8081/api/v1/traffic/evaluate" ` + -Method Post ` + -ContentType "application/json" ` + -Body $body +``` + +This example uses a demo identifier only to exercise the endpoint. Do not attach request IDs, subject IDs, API keys, or other sensitive/high-cardinality identifiers as span attributes. + +To inspect logs: + +```powershell +docker compose logs --since=5m gateway-1 +docker compose logs --since=5m jaeger +``` + +## Troubleshooting + +### Jaeger shows no services or traces + +1. Check that Jaeger and all gateways are running: + + ```powershell + docker compose ps + ``` + +2. Confirm the gateway environment points to `http://jaeger:4318/v1/traces`, not `localhost` from inside a container. +3. Check gateway logs for `Failed to export spans` and HTTP status errors. +4. Send a request to the traffic evaluation endpoint and refresh Jaeger. + +### OTLP exporter returns HTTP 404 + +Check that the traces endpoint is configured specifically as: + +`http://jaeger:4318/v1/traces` + +Use `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` for the trace-specific endpoint. Do not use a traces URL as a shared metrics endpoint. The application metrics are intended to remain available through the existing Prometheus/Actuator setup. + +### Lease spans do not appear + +Lease spans are conditional on the request reaching lease acquisition, consumption, renewal, or release. A request that is allowed immediately by the quota coordinator may not acquire a lease. Use a test scenario that reaches the lease path to verify those spans. + +## Privacy and cardinality + +Do not add request IDs, user IDs, API keys, raw subject IDs, or similarly unique/sensitive values as span attributes. Prefer bounded, low-cardinality attributes such as operation names and outcome categories when additional attributes are needed. + +## Validation status + +During local verification, the gateway containers and Redis reached healthy status, a request to `/api/v1/traffic/evaluate` returned `ALLOWED`, and Jaeger displayed the `distributed-traffic-control` service and traces. + +That confirms the local trace-export path is working. To fully validate the instrumentation, inspect individual trace details and exercise both the ordinary decision path and a request that reaches Redis lease operations. diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 228b7a8..93748b5 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -13,6 +13,9 @@ management.endpoints.web.exposure.include=health,info,prometheus # OpenTelemetry tracing -management.opentelemetry.tracing.export.otlp.endpoint=${OTEL_EXPORTER_OTLP_ENDPOINT:http://localhost:4318/v1/traces} +management.opentelemetry.tracing.export.otlp.endpoint=${OTEL_EXPORTER_OTLP_TRACES_ENDPOINT:http://localhost:4318/v1/traces} management.tracing.sampling.probability=${TRACING_SAMPLING_PROBABILITY:1.0} +# Do not export application metrics to Jaeger +management.otlp.metrics.export.enabled=false +