Skip to content

Fix issue of emitting two analytics event per LLM proxy call - #2879

Merged
Krishanx92 merged 5 commits into
wso2:mainfrom
Tharanidk:analytics
Jul 29, 2026
Merged

Fix issue of emitting two analytics event per LLM proxy call#2879
Krishanx92 merged 5 commits into
wso2:mainfrom
Tharanidk:analytics

Conversation

@Tharanidk

@Tharanidk Tharanidk commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Problem

Invoking an LLM proxy produced two analytics events for a single client call, because the proxy forwards to its provider over an internal loopback on the same Envoy listener. This caused:

  • Double-counted requests in Moesif — e.g. "Total Requests" showed 12 for ~6 calls,
    while the per-API "AI API Details" panel showed the correct count.
  • Split data — the proxy (ingress) event carried the user identity but no token usage;
    the provider (loopback) event carried token/cost/model but showed the user as anonymous.

Fix

  1. Token usage/cost/model is already extracted on the proxy route (the LLM response returns through it) and present in event.Properties, but the publisher only emitted it for LlmProvider. The guard now includes LlmProxy, so the proxy event is a complete AI record.

  2. Before publishing, an LlmProvider event whose downstream remote address is loopback(127.0.0.1/::1) is dropped, since it is the internal hop of a proxy call, not a real client request. Directly invoked providers (real client address) are unaffected and still publish.

Moesif Dashboard

When I send 3 requests from the proxy and 4 requests from the provider, the analytics dashboard is showing as below.
image
image
image

Resolved: #2441

@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

LLM proxy operations now mark internal loopback requests, analytics suppresses duplicate loopback provider events using direct peer addresses, and Moesif includes AI metadata for provider and proxy events. Tests cover marker propagation, suppression, address validation, logging, and publisher metadata.

Changes

LLM loopback analytics handling

Layer / File(s) Summary
Loopback marker propagation
gateway/gateway-controller/pkg/constants/constants.go, gateway/gateway-controller/pkg/utils/llm_transformer.go, gateway/system-policies/analytics/*, gateway/gateway-controller/pkg/utils/*test.go
Generated proxy operations add an internal loopback header, analytics metadata records the marker, and transformation and deployment tests account for the additional policy.
Loopback provider suppression
gateway/gateway-runtime/policy-engine/internal/analytics/analytics.go, gateway/gateway-runtime/policy-engine/internal/analytics/analytics_test.go
Flagged loopback LlmProvider events are not published when the direct downstream address is loopback; missing or non-loopback addresses remain publishable, with corresponding logging tests.
Moesif proxy AI metadata
gateway/gateway-runtime/policy-engine/internal/analytics/publishers/moesif.go, gateway/gateway-runtime/policy-engine/internal/analytics/publishers/moesif_test.go
Moesif includes AI metadata and token usage for LlmProxy events, with coverage for queued event metadata.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant LLMTransformer
  participant AnalyticsPolicy
  participant PolicyEngine
  participant Moesif
  LLMTransformer->>AnalyticsPolicy: add internal loopback header
  AnalyticsPolicy->>PolicyEngine: stamp loopback analytics metadata
  PolicyEngine->>PolicyEngine: detect direct loopback provider event
  PolicyEngine-->>Moesif: publish non-suppressed event with AI metadata
Loading

Possibly related PRs

Suggested reviewers: krishanx92, malinthaprasan, piumal1999, lasanthas, pubudu538

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 36.36% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the main change: suppressing duplicate analytics events for LLM proxy calls.
Description check ✅ Passed The description clearly covers the problem, fix, and linked issue, and is detailed enough despite omitted template sections.
Linked Issues check ✅ Passed The changes address #2441 by suppressing loopback provider events and preserving proxy AI metadata, matching the issue goal.
Out of Scope Changes check ✅ Passed All changes support the analytics de-duplication fix or its tests; no unrelated functionality appears introduced.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (2.12.2)

level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain modules listed in go.work or their selected dependencies"


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
gateway/gateway-runtime/policy-engine/internal/analytics/publishers/moesif_test.go (1)

295-305: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use fatal assertions before dependent access.

assert.Len and assert.NotNil do not stop execution. A regression can therefore panic at moesif.events[0] or a type assertion, obscuring the actual failure. Use require guards or comma-ok type assertions.

Proposed test hardening
-	assert.Len(t, moesif.events, 1)
+	require.Len(t, moesif.events, 1)
 	metadata := getMetadata(moesif.events[0])
-	assert.NotNil(t, metadata["aiMetadata"])
-	assert.NotNil(t, metadata["aiTokenUsage"])
+	require.NotNil(t, metadata["aiMetadata"])
+	require.NotNil(t, metadata["aiTokenUsage"])

Add the require import from the existing testify dependency.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@gateway/gateway-runtime/policy-engine/internal/analytics/publishers/moesif_test.go`
around lines 295 - 305, Harden the test assertions around moesif.events and
metadata in the current test by importing testify/require, replacing the
event-count and nil checks with fatal require assertions before indexing or type
assertions, and validating type assertions with require so regressions report
assertion failures instead of panicking.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In
`@gateway/gateway-runtime/policy-engine/internal/analytics/publishers/moesif_test.go`:
- Around line 295-305: Harden the test assertions around moesif.events and
metadata in the current test by importing testify/require, replacing the
event-count and nil checks with fatal require assertions before indexing or type
assertions, and validating type assertions with require so regressions report
assertion failures instead of panicking.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: d16d2622-41da-45e1-8040-36084e2937f1

📥 Commits

Reviewing files that changed from the base of the PR and between ebd3067 and c0235d4.

📒 Files selected for processing (4)
  • gateway/gateway-runtime/policy-engine/internal/analytics/analytics.go
  • gateway/gateway-runtime/policy-engine/internal/analytics/analytics_test.go
  • gateway/gateway-runtime/policy-engine/internal/analytics/publishers/moesif.go
  • gateway/gateway-runtime/policy-engine/internal/analytics/publishers/moesif_test.go

coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 27, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@gateway/system-policies/analytics/analytics.go`:
- Around line 176-180: Stop treating InternalLoopbackMetadataKey from
reqCtx.Headers as trusted in the analytics policy path. Strip or reserve this
header at the public request boundary, and have the proxy-to-provider forwarding
path attach an unforgeable internal marker that runtime can recognize before
setting analyticsMetadata. Add a regression test covering a direct LlmProvider
loopback request with an injected marker and verify its event is not dropped.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: eb5ece17-3c01-4c66-97b3-32e7b6509f48

📥 Commits

Reviewing files that changed from the base of the PR and between c0235d4 and 07c5fe6.

📒 Files selected for processing (12)
  • gateway/gateway-controller/pkg/constants/constants.go
  • gateway/gateway-controller/pkg/utils/llm_deployment_policy_test.go
  • gateway/gateway-controller/pkg/utils/llm_deployment_test.go
  • gateway/gateway-controller/pkg/utils/llm_transformer.go
  • gateway/gateway-controller/pkg/utils/llm_transformer_multiprovider_test.go
  • gateway/gateway-controller/pkg/utils/llm_transformer_test.go
  • gateway/gateway-runtime/policy-engine/internal/analytics/analytics.go
  • gateway/gateway-runtime/policy-engine/internal/analytics/analytics_test.go
  • gateway/gateway-runtime/policy-engine/internal/analytics/publishers/moesif.go
  • gateway/gateway-runtime/policy-engine/internal/analytics/publishers/moesif_test.go
  • gateway/system-policies/analytics/analytics.go
  • gateway/system-policies/analytics/analytics_test.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • gateway/gateway-runtime/policy-engine/internal/analytics/publishers/moesif_test.go
  • gateway/gateway-runtime/policy-engine/internal/analytics/publishers/moesif.go

Comment thread gateway/system-policies/analytics/analytics.go
coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 28, 2026
@O-sura

O-sura commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

The latest changes fixed the issue highlighted here in #2879 (comment) but it introduces another issue.

The marker header isn't stripped at ingress, and userIP reads DownstreamRemoteAddress, which Envoy derives from X-Forwarded-For when present. So any caller can send two headers and have their own analytics event discarded while still getting a 200.

# baseline → 1 analytics event
curl -k -sS -o /dev/null -w 'HTTP=%{http_code}\n' -X POST \
 https://localhost:8443/openai/latest/chat/completions \
 -H 'Content-Type: application/json' \
 -d '{"model":"gpt-4o-mini","messages":[{"role":"user","content":"Hi"}]}'

# same call + two headers → HTTP 200, ZERO analytics events
curl -k -sS -o /dev/null -w 'HTTP=%{http_code}\n' -X POST \
 https://localhost:8443/openai/latest/chat/completions \
 -H 'Content-Type: application/json' \
 -H 'x-wso2-internal-loopback: 1' \
 -H 'X-Forwarded-For: 127.0.0.1' \
 -d '{"model":"gpt-4o-mini","messages":[{"role":"user","content":"Hi"}]}'

Possible fix would be to use DownstreamDirectRemoteAddress field from the access log instead of the DownstreamRemoteAddress when reading the IP

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@gateway/gateway-runtime/policy-engine/internal/analytics/analytics.go`:
- Around line 383-390: Prevent client-controlled InternalLoopbackMetadataKey
values from influencing analytics by stripping or overwriting the marker at the
client-facing ingress boundary and injecting it only from a trusted proxy hop,
or validating its authenticity before metadata creation. Update the relevant
ingress metadata handling and add a regression test covering a loopback
directRemoteIP with a client-supplied marker, ensuring the LlmProvider event is
not discarded.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 3ac0a846-4e93-4cc1-a2a6-aaa83a76a8ac

📥 Commits

Reviewing files that changed from the base of the PR and between 07c5fe6 and 9b4f54d.

📒 Files selected for processing (3)
  • gateway/gateway-controller/pkg/eventlistener/llm_provider_processor_test.go
  • gateway/gateway-runtime/policy-engine/internal/analytics/analytics.go
  • gateway/gateway-runtime/policy-engine/internal/analytics/analytics_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • gateway/gateway-runtime/policy-engine/internal/analytics/analytics_test.go

coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 28, 2026
O-sura
O-sura previously approved these changes Jul 28, 2026
@Tharanidk
Tharanidk dismissed stale reviews from O-sura and coderabbitai[bot] via 5e245f6 July 29, 2026 11:01

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
gateway/gateway-runtime/policy-engine/internal/analytics/analytics_test.go (1)

397-403: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Serialize logger replacement or inject a logger.

captureLogs changes the process-wide default logger, and this package has no t.Parallel usage to isolate the test suite, but it still makes log-capture tests order/interference-sensitive. Pass the handler/logger explicitly or run the affected tests sequentially.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@gateway/gateway-runtime/policy-engine/internal/analytics/analytics_test.go`
around lines 397 - 403, Update captureLogs and its callers to avoid replacing
the process-wide slog default; inject the logger or handler into the code under
test and capture logs through that explicit dependency. If explicit injection is
not feasible, ensure all affected tests execute sequentially and restore logger
state safely without introducing order-dependent behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@gateway/gateway-runtime/policy-engine/internal/analytics/analytics_test.go`:
- Around line 397-403: Update captureLogs and its callers to avoid replacing the
process-wide slog default; inject the logger or handler into the code under test
and capture logs through that explicit dependency. If explicit injection is not
feasible, ensure all affected tests execute sequentially and restore logger
state safely without introducing order-dependent behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 5549d101-25a0-443f-99e3-3e24b65a9037

📥 Commits

Reviewing files that changed from the base of the PR and between 9b4f54d and 5e245f6.

📒 Files selected for processing (2)
  • gateway/gateway-runtime/policy-engine/internal/analytics/analytics.go
  • gateway/gateway-runtime/policy-engine/internal/analytics/analytics_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • gateway/gateway-runtime/policy-engine/internal/analytics/analytics.go

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Improvement]: Duplicate Analytics Events when Invoking an LLM Proxy

3 participants