Skip to content

feat(request-cost): publish aggregate token telemetry to external endpoint#35749

Merged
wezell merged 13 commits into
mainfrom
feat/request-cost-publisher
May 21, 2026
Merged

feat(request-cost): publish aggregate token telemetry to external endpoint#35749
wezell merged 13 commits into
mainfrom
feat/request-cost-publisher

Conversation

@wezell
Copy link
Copy Markdown
Member

@wezell wezell commented May 19, 2026

Fixes #35750

Summary

  • Adds scheduled publishing of the per-window + lifetime request-cost (a.k.a. token) counters to an external REST collector.
  • Reuses the existing RequestCostApiImpl monitor scheduler tick (default 60s, controlled by REQUEST_COST_TIME_WINDOW_SECONDS) — no new threads, no new schedulers.
  • HTTP POST runs on DotConcurrentFactory.getInstance().getSubmitter() so a slow or dead collector never blocks the monitor scheduler.
  • Off by default. Activates implicitly when both REQUEST_COST_PUSH_URL and REQUEST_COST_PUSH_TOKEN are set.

Why

RequestCostApiImpl already aggregates request counts and token totals (window + lifetime). Today those numbers are only emitted to the local log — they aren't queryable across a cluster. This wires the same aggregates into an external time-series sink so per-cluster, per-environment token consumption can be tracked centrally.

Wire format

One snapshot per tick (10 fields, exact):

{
  "clusterId": "...",
  "serverId": "...",
  "timestamp": "2026-05-19T18:48:00Z"  // ISO-8601, truncated to seconds,
  "windowSeconds": 60,
  "windowRequests": 1234,
  "windowTokens": 5678.5,
  "windowAvgTokensPerRequest": 4.6,
  "lifetimeRequests": 999999,
  "lifetimeTokens": 12345678.25,
  "lifetimeAvgTokensPerRequest": 12.35
}
  • clusterIdClusterFactory.getClusterId()
  • serverIdConfigUtils.getServerId()
  • Both wrapped in Try with "unknown" fallback so a transient lookup failure doesn't drop the snapshot.
  • timestamp is Instant.now().toString() (ISO-8601, UTC).
  • Token values are pre-divided by REQUEST_COST_DENOMINATOR (consistent with what gets logged).

Config keys

Key Default Notes
REQUEST_COST_PUSH_URL Presence + token activates the publisher
REQUEST_COST_PUSH_TOKEN Bearer token; presence + url activates the publisher
REQUEST_COST_PUSH_TIMEOUT_MS 5000 Per-request timeout

Operability: CircuitBreakerUrl blocks private-subnet URLs unless ALLOW_ACCESS_TO_PRIVATE_SUBNETS=true is set on the same JVM. Most internal collectors live on a private network, so set that flag too if your collector hostname resolves to a private address. The flag is captured at first access via a static Lazy<Boolean>, so set it before traffic warms up.

Existing knobs (unchanged):

  • REQUEST_COST_ACCOUNTING_ENABLED — overall request-cost gating
  • REQUEST_COST_TIME_WINDOW_SECONDS — scheduler cadence (drives publish cadence too)
  • REQUEST_COST_DENOMINATOR — scaling factor

Behavior

  • Publisher emits every tick while enabled, even when the window had zero requests — keeps the time series continuous so idle vs. dead is distinguishable.
  • Log line is suppressed on consecutive idle windows (unchanged from prior behavior).

Failure handling

  • Transport errors and non-2xx responses are logged via Logger.warnEvery(..., 10 min) and the snapshot is dropped. This is observational telemetry, not durable accounting — buffering on disk would be over-engineered.
  • publish() returns immediately; the actual POST happens on the shared submitter.

Test plan

  • RequestCostSnapshotTest (3 tests) — locks JSON shape: every expected field present, all values round-trip correctly, exactly 10 fields on the wire (guards against accidental field leakage when someone adds a private helper later).
  • RequestCostPublisherTest (9 tests) — enable gate: disabled by default, URL-alone is not enough, token-alone is not enough, both-set activates, publish() is a no-op when disabled.
  • ./mvnw compile -pl :dotcms-core -DskipTests — BUILD SUCCESS.
  • ./mvnw test -pl :dotcms-core -Dtest='RequestCostSnapshotTest,RequestCostPublisherTest' — 12/12 passing.
  • Manual smoke: point REQUEST_COST_PUSH_URL at a local request bin, verify a snapshot lands within the window.

Out of scope

  • A localhost-HttpServer integration test of the actual POST. CircuitBreakerUrl blocks private subnets via a static Lazy<Boolean> that captures ALLOW_ACCESS_TO_PRIVATE_SUBNETS on first access — that makes the test fragile to JVM/test ordering. The post path is mostly orchestration; serialization is fully covered by the snapshot test.
  • Per-Price breakdown in the payload (e.g. {DB_QUERY: N, ES_QUERY: M}). Would require additional counters in incrementCost; can land as a follow-up if the collector needs it.
  • Naming. The package and existing classes still say RequestCost; the new field names use "tokens" since that's the better term, but a package rename is a separate, larger refactor (24+ callers).

Files

  • dotCMS/src/main/java/com/dotcms/cost/RequestCostSnapshot.java (new)
  • dotCMS/src/main/java/com/dotcms/cost/RequestCostPublisher.java (new)
  • dotCMS/src/main/java/com/dotcms/cost/RequestCostApiImpl.java (modified — adds snapshot construction + publisher call in logRequestCost)
  • dotCMS/src/test/java/com/dotcms/cost/RequestCostSnapshotTest.java (new)
  • dotCMS/src/test/java/com/dotcms/cost/RequestCostPublisherTest.java (new)

🤖 Generated with Claude Code

…point

Add a scheduled push of the per-window + lifetime request-cost (token) counters
to an external REST collector. The work runs on the existing monitor scheduler
tick and is dispatched to DotConcurrentFactory's submitter so HTTP latency never
blocks the monitor thread.

- RequestCostSnapshot: immutable JSON payload (clusterId, environmentId,
  timestamp, window + lifetime request/token counters + averages).
- RequestCostPublisher: @ApplicationScoped CDI bean. Activates implicitly when
  REQUEST_COST_PUSH_URL and REQUEST_COST_PUSH_TOKEN are both set. POSTs via
  CircuitBreakerUrl with bearer auth; transport failures are warnEvery-rate
  limited and the snapshot is dropped (observational telemetry).
- RequestCostApiImpl.logRequestCost: builds a snapshot from the same counters
  it already logs, calls publisher on each tick.
- Tests: lock the on-the-wire JSON shape (10 fields exact) and the enable gate.

Cluster ID via ClusterFactory.getClusterId(); environment ID via
ConfigUtils.getServerId(). Both wrapped in Try with "unknown" fallback.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added the Area : Backend PR changes Java/Maven backend code label May 19, 2026
@claude
Copy link
Copy Markdown
Contributor

claude Bot commented May 19, 2026

Claude finished @wezell's task in 2m 49s —— View job


PR Review

Read all five files plus CircuitBreakerUrl, DotConcurrentFactory.getSubmitter().submit(...), and Logger.warnEvery*. Notes below — most prior review feedback (Copilot, fabrizzio, my earlier pass) still applies and a couple of points are worth re-emphasizing.


[🟠 High] CircuitBreakerUrl#doString() emits its own un-rate-limited WARN on any non-2xxdotCMS/src/main/java/com/dotcms/cost/RequestCostPublisher.java:93
At CircuitBreakerUrl.java:208-217, doString() calls Logger.warn(this, "Invalid response detected when consuming [%s] with http status [%d] and response:%s") directly whenever isError() is true (any 4xx/5xx, or unprocessed). That fires before your Logger.warnEvery(REQUEST_COST_PUSH_FAIL_HTTP, …) check at line 102-106, so a misconfigured collector still spams the log every tick — the rate limit you added is bypassed. Copilot flagged the same thing; I'm reinforcing it because the suggested fix (use doOut(OutputStream.nullOutputStream()) or a no-warning path) is a real, mechanical change you can land here, and without it the warnEvery machinery in this file is largely cosmetic for the common failure mode (a permanently broken collector URL).


[🟡 Medium] Submitter rejection isn't handled in publish()dotCMS/src/main/java/com/dotcms/cost/RequestCostPublisher.java:65
DotConcurrentFactory.getInstance().getSubmitter().submit(...) re-throws as DotConcurrentException (a RuntimeException) when the underlying executor's queue is saturated (DotConcurrentFactory.java:1043-1044). It is caught one frame up by logRequestCost()'s outer catch (Exception), so the scheduler survives — but the message that comes out is "Error logging request tokens: ...", which doesn't tell an operator that telemetry was dropped because the system pool was over capacity. If you don't want a try/catch here, at minimum mention in a comment that publish() can throw and rely on the upstream catch — currently the contract reads "fire-and-forget" but isn't.


[🟡 Medium] httpSchemeWarned is process-wide and never resetsdotCMS/src/main/java/com/dotcms/cost/RequestCostPublisher.java:124-139
Restating from my prior pass since it didn't move: an operator who flips https://http://https://http:// only sees the first warning, then telemetry POSTs the bearer token in cleartext forever with no log trail. fabrizzio also pointed at this region. Suggest one of:

  1. Refuse to post when scheme is http AND host is non-loopback (127.0.0.0/8, ::1, localhost), and warnEvery on the drop.
  2. Or, keep the warn-once but tie the latch to the current URL so a flip back to http:// re-fires.

[🟡 Medium] sanitizeUrlForLog keeps the query string and fragmentdotCMS/src/main/java/com/dotcms/cost/RequestCostPublisher.java:164-167
Restating: operators sometimes shove auth into the URL (?token=…). Userinfo gets stripped here, but query/fragment don't, so a misconfigured push URL still leaks the secret into every transport-error log. One-line fix:

final URI safe = new URI(uri.getScheme(), null, uri.getHost(), uri.getPort(),
                          uri.getPath(), null, null);

[🟡 Medium] RequestCostPublisherTest doesn't establish a clean starting statedotCMS/src/test/java/com/dotcms/cost/RequestCostPublisherTest.java:24-29
@After clears, @Before does not. If anything in the JVM (a prior test class, a system property, an earlier setup hook) has already set REQUEST_COST_PUSH_URL or REQUEST_COST_PUSH_TOKEN, the very first test method in this class can see leaked state and test_isEnabled_returnsFalse_whenUrlAndTokenAreMissing flips false-positive. Copilot's point — make the clear method run before each test too (or rename to @Before and drop @After).


[🟢 Low / Nit] A few smaller items

  • dotCMS/src/main/java/com/dotcms/cost/RequestCostPublisher.java:35FAIL_LOG_INTERVAL_MS is hard-coded; fabrizzio suggested TimeUnit.MINUTES.toMillis(10) and/or making it configurable. Either is cheap.
  • dotCMS/src/main/java/com/dotcms/cost/RequestCostPublisher.java:81-82Map.of("Authorization", "Bearer " + token) is the idiomatic shape (fabrizzio's nit) — headers is never mutated after construction.
  • dotCMS/src/main/java/com/dotcms/cost/RequestCostPublisher.java:109-115Logger.warnEveryAndDebug(cl, message, throwable, intervalMs) exists (Logger.java:209) and bundles the warnEvery + debug-with-throwable pair you wrote into one call.
  • dotCMS/src/main/java/com/dotcms/cost/RequestCostSnapshot.java:14 — fabrizzio asked about Immutables. The class is already public final with public final fields, an all-args ctor, and locked Jackson visibility; converting to a @Value.Immutable interface mostly trades hand-written for generated. Not worth the churn unless you want the builder.
  • dotCMS/src/main/java/com/dotcms/cost/RequestCostApiImpl.java:68-69Math.max(1.0d, NaN) returns NaN. Config.getFloatProperty won't normally produce NaN, but if someone literally configures REQUEST_COST_DENOMINATOR=NaN, this clamp doesn't save you and the snapshot still ships JSON-invalid NaN literals. A Double.isFinite() guard would close that.
  • dotCMS/src/main/java/com/dotcms/cost/RequestCostApiImpl.java:131 — the isEnabled() check here and the one inside publish() are redundant; fine, just noting.

Verdict
Design is sound — reusing the existing tick, off-by-default gate, no new threads, snapshot shape locked by tests, no caller-blocking POST. The 🟠 above is the one I'd most want addressed before merge: a misconfigured collector currently makes the publisher's own rate limiter pointless because CircuitBreakerUrl warns out from under it. Everything else is incremental.
• branch: feat/request-cost-publisher

- Align javadoc with the 10-minute fail-log throttle (was stated as "once per
  minute" but the constant is 600_000ms).
- Change FAIL_LOG_INTERVAL_MS from long to int and drop the (int) casts — the
  value fits in int and Logger.warnEvery takes int, so the cast was theoretical
  overflow waiting to happen.
- Sanitize the push URL before logging: strip RFC-3986 userinfo so a misconfigured
  REQUEST_COST_PUSH_URL=https://user:pass@host/... doesn't leak credentials into
  every failure log line.
- Add tests for the sanitizer (with/without userinfo) and a regression test that
  Config.setProperty(key, null) actually unsets the property (so the enable gate
  can't silently flip on if Apache Commons ever stringifies null).

11/11 tests passing locally.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…y, deterministic timestamps, header injection guard

Addresses the second Claude AI review of PR #35749. No-blocker review, but the
behavioral and operability items are worth landing:

- Continuous time series. The skipZeroRequests gate now only suppresses the log
  line on consecutive idle windows; the publisher still emits a zero-snapshot
  every tick when enabled. This makes "idle cluster" and "agent down" graphable
  side-by-side on the collector.
- Deterministic timestamp. Truncate to seconds (Instant.now().truncatedTo(SECONDS))
  so the wire format is stable ISO-8601 seconds, matching the PR description and
  what most TSDBs index on.
- Header injection guard. New sanitizeHeaderValue() strips CR/LF + trims before
  the bearer token hits the Authorization header. Tiny defense-in-depth — only an
  operator can set the token, but CRLF in a misconfigured value would otherwise
  be HTTP header injection.
- Broaden @after cleanup to include REQUEST_COST_PUSH_TIMEOUT_MS so future tests
  don't bleed state.

Tests: 12/12 passing locally.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…il log

Third round of Claude AI review on PR #35749. The one merge-worthy item:

- Gate disagreement bug. isEnabled() checked the raw token via isSet(), but
  post() sanitized (strip CRLF + trim) before putting it in the Authorization
  header. A token of "  \r\n  " would pass the gate and result in an
  *unauthenticated* POST to the collector — strictly worse than staying off.
  Move sanitization into the gate so this can't happen, and add a regression
  test for whitespace/CRLF-only tokens.

- Clearer failure log. CircuitBreakerUrl can return without setting a response
  code (circuit open, transport error before response). Check isProcessed()
  first so we emit "did not complete" rather than "returned HTTP -1".

13/13 tests passing locally.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…questCost

Third Claude review flagged that the four LongAdder reads (window then lifetime)
aren't atomic relative to each other, so Σ(window) can briefly trail lifetime.
That's intentional — atomic would need a lock and the data is observational.
Document so the next reader doesn't "fix" it.

No behavior change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… keys

Fourth Claude review on PR #35749. Two defense-in-depth items, no behavior
contract changes:

- Warn-once if REQUEST_COST_PUSH_URL uses http:// (not https://). The bearer
  token would otherwise traverse the wire in cleartext. Warn-rather-than-refuse
  so a misconfiguration doesn't silently drop telemetry — the operator sees the
  log line and fixes it.
- Split Logger.warnEvery dedup keys into REQUEST_COST_PUSH_FAIL_TRANSPORT,
  REQUEST_COST_PUSH_FAIL_HTTP, REQUEST_COST_PUSH_ERR_EXCEPTION. Previously a
  500 storm and a circuit trip shared one key, so the second mode was silently
  suppressed for the rest of the 10-minute window.

Tests: 13/13 still passing locally.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Fifth Claude review on PR #35749 (and the user's explicit call) flagged that
the wire field environmentId was misleading: it was sourced from
ConfigUtils.getServerId(), which is a per-JVM/per-node identifier — every node
in a cluster sends a different value. Across Grafana that reads inverted.

Rename to serverId so the wire contract matches the source data exactly:
- clusterId = cluster
- serverId  = node

Done before any collector consumes the payload, so the wire format is locked
in correctly from the first emit.

Tests: 13/13 still passing locally.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…-check token on post

Sixth Claude review on PR #35749. No blocker, but three real defensive fixes:

- Clamp REQUEST_COST_DENOMINATOR to >= 1.0 at init. A misconfigured 0 would
  produce Infinity/NaN in the snapshot, which Jackson serializes as literals
  invalid under strict JSON parsers — collector-side breakage on the first
  emit.
- Drop the explicit "Content-Type: application/json" header. CircuitBreakerUrl
  already sniffs rawData starting with '{' and applies the JSON content type
  itself; setting both can emit a duplicate header on some Apache HttpClient
  versions.
- Re-check the sanitized token in post() as well as the URL. The config can be
  cleared between snapshot submission and the executor running the POST.
  Posting without an Authorization header is a worse failure mode than not
  posting — the collector sees authenticated-looking traffic that isn't.

13/13 tests passing locally.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…server ids

Seventh Claude review on PR #35749. Two real defensive fixes, then I'm done
chasing AI feedback — verdict has been "no blocker" since round 3 and the
remaining items are diminishing-returns.

- Snapshot Jackson @JsonAutoDetect: ANY -> PUBLIC_ONLY. All wire fields are
  already public final so behavior is identical, but the explicit lock makes
  it impossible to accidentally leak a future private field onto the wire.
  The "exactly 10 fields" test becomes belt-and-suspenders rather than the
  only line of defense.
- nullSafe() coalesces null returns from ClusterFactory.getClusterId() /
  ConfigUtils.getServerId(). Try.getOrElse only fires on throw — null returns
  during early startup would otherwise have serialized "clusterId": null on
  the wire instead of the documented "unknown" fallback.

13/13 tests passing locally.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@wezell wezell enabled auto-merge May 20, 2026 15:23
@wezell wezell added Team : Platform Platform Team java Pull requests that update Java code labels May 20, 2026
So the multi-agent Claude backend reviewer fires on PRs I author and can
submit a formal --approve / --request-changes (which the orchestrator's
automatic-review path can't do).

Touches .github/ which is owned by @dotCMS/dotdevelopers per CODEOWNERS —
splitting from the feature commits so it's an obvious one-line audit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@wezell wezell requested a review from a team as a code owner May 20, 2026 17:28
@claude
Copy link
Copy Markdown
Contributor

claude Bot commented May 20, 2026

🔍 dotCMS Backend Review

[🟡 Medium] dotCMS/src/main/java/com/dotcms/cost/RequestCostPublisher.java:121-136

Plain-HTTP downgrade is warned-once-then-allowed. An operator who pastes http:// (or whose config is tampered with) silently ships the bearer token in cleartext to any host — including private subnets when ALLOW_ACCESS_TO_PRIVATE_SUBNETS=true. Because httpSchemeWarned is a process-lifetime AtomicBoolean, a runtime flip https→http→https→http is silently re-enabled with no log line, and after the warning rotates out of retained logs the cleartext POSTs continue with no visible trail.

if (scheme != null && scheme.equalsIgnoreCase("http")
        && httpSchemeWarned.compareAndSet(false, true)) {
    Logger.warn(this.getClass(), "...bearer token will be sent in cleartext...");
}

💡 Refuse to post when scheme is http AND host is non-loopback. 127.0.0.0/8 / ::1 / localhost are reasonable carve-outs for dev/sidecar collectors; everything else should drop the snapshot and warnEvery so the failure is recurringly visible.


[🟡 Medium] dotCMS/src/main/java/com/dotcms/cost/RequestCostPublisher.java:140-145

sanitizeHeaderValue strips only \r and \n, but RFC 7230 §3.2.4 prohibits all CTLs in header field-values (NUL \0, vertical tab , form feed \f, the rest of , and ). Apache HttpClient typically rejects bare LF/CR, but a NUL byte can survive on some HTTP stacks and corrupt the wire format. More importantly: the gate in isEnabled() reuses this sanitizer for the empty-check — a token consisting solely of non-CRLF CTLs would currently bypass the isSet gate and activate the publisher with effectively-empty auth.

static String sanitizeHeaderValue(final String value) {
    return value == null ? null : value.replace("\r", "").replace("\n", "").trim();
}

💡 Strip the whole CTL range:

return value == null ? null : value.replaceAll("[\\x00-\\x1F\\x7F]", "").trim();

[🟡 Medium] dotCMS/src/main/java/com/dotcms/cost/RequestCostPublisher.java:148-168

sanitizeUrlForLog strips userinfo but preserves the query string and fragment. If an operator misconfigures the URL with the bearer token (or any other secret) as a query parameter — https://host/cost?token=secret — the secret lands in every transport/HTTP/exception failure log line. The query string is not needed to disambiguate the target in operator logs.

final URI safe = new URI(
        uri.getScheme(), null, uri.getHost(), uri.getPort(),
        uri.getPath(), uri.getQuery(), uri.getFragment());
return safe.toString();

💡 Drop the query and fragment too — only scheme/host/port/path are needed for log disambiguation:

final URI safe = new URI(uri.getScheme(), null, uri.getHost(), uri.getPort(),
                          uri.getPath(), null, null);

Next steps

  • 🔴 / 🟠 Fix locally and push — these need your judgment
  • 🟡 You can ask me to handle mechanical fixes inline: @claude fix <issue description> in <File.java>
  • Every new push triggers a fresh review automatically

Comment thread dotCMS/src/main/java/com/dotcms/cost/RequestCostPublisher.java
Comment thread dotCMS/src/main/java/com/dotcms/cost/RequestCostPublisher.java
Comment thread dotCMS/src/main/java/com/dotcms/cost/RequestCostPublisher.java
Comment thread dotCMS/src/main/java/com/dotcms/cost/RequestCostSnapshot.java
@fmontes fmontes requested review from a team and Copilot May 20, 2026 19:53
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds opt-in, scheduled publishing of aggregated request-cost (“token”) telemetry to an external HTTP collector, reusing the existing RequestCostApiImpl monitor tick so cluster-wide usage can be tracked outside local logs.

Changes:

  • Introduces RequestCostSnapshot as a fixed-shape JSON payload for per-window and lifetime token aggregates.
  • Adds RequestCostPublisher to POST snapshots asynchronously via DotConcurrentFactory and CircuitBreakerUrl, gated by REQUEST_COST_PUSH_URL + REQUEST_COST_PUSH_TOKEN.
  • Extends RequestCostApiImpl#logRequestCost() to build/publish snapshots each tick while keeping console log throttling for consecutive idle windows; adds unit tests locking payload shape and enablement behavior.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
dotCMS/src/main/java/com/dotcms/cost/RequestCostSnapshot.java Defines the on-the-wire snapshot payload and locks JSON field visibility.
dotCMS/src/main/java/com/dotcms/cost/RequestCostPublisher.java Implements async POST publishing with config gating and basic hardening/sanitization.
dotCMS/src/main/java/com/dotcms/cost/RequestCostApiImpl.java Builds snapshot from existing counters and triggers publisher from the existing scheduler tick.
dotCMS/src/test/java/com/dotcms/cost/RequestCostSnapshotTest.java Verifies snapshot JSON contains the expected fields/values and exactly 10 fields.
dotCMS/src/test/java/com/dotcms/cost/RequestCostPublisherTest.java Verifies enablement gate, disabled no-op, and sanitization helpers.

Comment thread dotCMS/src/main/java/com/dotcms/cost/RequestCostPublisher.java
Comment thread dotCMS/src/main/java/com/dotcms/cost/RequestCostPublisher.java
Comment thread dotCMS/src/test/java/com/dotcms/cost/RequestCostPublisherTest.java
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
@wezell wezell added this pull request to the merge queue May 20, 2026
@github-merge-queue github-merge-queue Bot removed this pull request from the merge queue due to no response for status checks May 21, 2026
@wezell wezell added this pull request to the merge queue May 21, 2026
@github-merge-queue github-merge-queue Bot removed this pull request from the merge queue due to failed status checks May 21, 2026
@wezell wezell added this pull request to the merge queue May 21, 2026
Merged via the queue into main with commit 562e2c7 May 21, 2026
66 checks passed
@wezell wezell deleted the feat/request-cost-publisher branch May 21, 2026 21:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AI: Safe To Rollback Area : Backend PR changes Java/Maven backend code java Pull requests that update Java code Team : Platform Platform Team

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

Publish aggregate request-cost (token) telemetry to external endpoint

3 participants