Skip to content

Add ExternalApiResponseBody tracker - #13

Merged
nwdles merged 2 commits into
umbrellio:masterfrom
elvina-ero:TS-5797
Aug 19, 2026
Merged

Add ExternalApiResponseBody tracker#13
nwdles merged 2 commits into
umbrellio:masterfrom
elvina-ero:TS-5797

Conversation

@elvina-ero

Copy link
Copy Markdown
Contributor

Why

TS-5743 added a metric for tracking Atlant response codes, but the existing external_api_response tracker only tags metrics by host/status. Atlant is a JSON-RPC style API — every call goes through a single endpoint, and the actual method name and business error code live in the request/response JSON body, not in the HTTP status. With the current tracker it's impossible to split requests by method.

What

Adds a new tracker, ExternalApiResponseBody, alongside the existing ExternalApiResponse:

  • Hooks into Guzzle's on_stats, same integration pattern as ExternalApiResponse.
  • Parses the request and response JSON bodies and extracts configured fields via dot-notation paths (request_fields / response_fields config, powered by data_get()), tagging a Counter metric with them alongside host/status.
  • Field paths and tag names are fully configurable — the library doesn't hardcode any particular API's body shape.
  • Reads bodies safely: skips non-seekable (streamed) bodies and bodies over max_body_bytes, and rewinds the stream after reading so the rest of the application can still consume the response body normally.
  • Registered in EventTrackerServiceProvider, with a default config entry and README section documenting integration (including how to combine it with ExternalApiResponse on the same Guzzle client).

Testing

  • 12 new tests: MessageBodyFieldsExtractorTest (unit) and GuzzleClientOnStatsCallbackCreatorTest (feature).
  • Verified end-to-end against a real downstream integration (pointg's Atlant client) on staging — real and synthetic Atlant calls (successes and various business error codes) correctly produced metrics tagged by method and code.

elvina-ero and others added 2 commits August 6, 2026 14:20
external_api_response only tags metrics by host/status, which is not
enough for APIs that route every call through a single endpoint (e.g.
JSON-RPC style) and encode the actual method and error code in the
request/response JSON body instead of the HTTP status. This tracker
counts requests and extracts extra tags from those bodies via
dot-notation paths, so calls can be split by things like the RPC
method or an application-level error code.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Run the repo's ecs ruleset over the new tracker: expand inline
associative arrays to multiline, break method chains onto their own
lines, and rewrap an overlong doc comment.

Also extract the repeated request construction in the feature test into
a request() helper, matching the helper style of the existing
ExternalApiResponse feature test.

No behaviour change: `ecs check` is clean on these files and all 12 new
tests still pass on PHP 7.4.

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

@nwdles nwdles left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approving. Reviewed the tracker against the existing ExternalApiResponse implementation and ran the suite locally on PHP 7.4 (PHPUnit 9.6.3), since CI could not do it — see the note at the bottom.

What I checked

  • Follows the established tracker shape: Installer + GuzzleClientOnStatsCallbackCreator + Closure::fromCallable, with resolveAdapter($connection, Counter::TYPE), so it works both on Prometheus and on the influx/telegraf EventAdapter path.
  • The label set is stable, which is the part that matters most for a Prometheus counter: extract() returns a value for every configured field even when the message is null or the body is not valid JSON. Without that, the first network-level failure would register a different label set and blow up. Good call.
  • Body reading is safe: non-seekable streams are skipped, max_body_bytes caps the read, and rewind() in finally leaves the stream where the application expects it.
  • Backwards compatible: request_fields/response_fields default to empty and the callback has to be wired into a client by hand, so existing consumers see no new metrics until they opt in.
  • New namespace means the GuzzleClientOnStatsCallbackCreator singleton does not collide with the existing one.
  • 29 tests pass (17 before, 12 new). The one remaining failure is pre-existing on master, unrelated to this PR — details below.

What I pushed

One commit, 1eac46c, applying the repo's own ecs ruleset to the four new files — the inline associative arrays (['code' => 'error.code'] and friends) violated umbrellio/code-style-php, so vendor/bin/ecs check was failing on all four. I wrote the formatting by hand rather than committing ecs --fix output, because the autofixer mangles nested arrays inside function calls (it leaves whitespace-only lines and collapses closers into ], ]));). I also pulled the thrice-repeated request construction in the feature test into a request() helper, mirroring the helper style of the existing ExternalApiResponse feature test. No behaviour change; ecs check is now clean on these files.

Non-blocking nits, for whenever you next touch this

  • max_body_bytes is only enforced when getSize() !== null. A chunked response without Content-Length can report an unknown size, and then the body is read in full regardless of the cap. Best-effort is fine, but the README currently reads like a hard guarantee.
  • The README section documents the Prometheus metric format but not the Influx one; external_api_response documents both, and this tracker supports both adapters.
  • "exactly like external_api_response does for its main_metric" is slightly off — that tracker's stream guard works off missing handler stats, not off stream seekability.
  • DEFAULT_VALUE / DEFAULT_MAX_BODY_BYTES are duplicated between the class constants and config/event_tracker.php.

Note on CI

The red checks here are not this PR's fault. Lint targets ubuntu-20.04, which no longer exists as a runner, so the job never starts and is cancelled after ~24h; Test and Code coverage depend on it via needs, so they are skipped. build-and-push-image fails on pushes to master too. The practical consequence is that the test suite has not actually run in CI for about a year, which is how the dontWriteEveryRequestIfRedirect failure went unnoticed since 2.0.2 — filed separately, not something this PR needs to address.

@nwdles
nwdles merged commit a773642 into umbrellio:master Aug 19, 2026
0 of 2 checks passed
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.

2 participants