@saptools/cf-metrics
A read-only command line that queries the container and OpenTelemetry metrics SAP Cloud LoggingSAP Cloud LoggingAn instance-based observability service built on OpenSearch that stores and analyses logs, metrics and traces from Cloud Foundry, Kyma, Kubernetes and other runtime environments.sap official, btp has ingested for Cloud Foundry apps, as history over a time range, a ranking across apps or a live watch.
cf app shows an app's CPU and memory now, and the Log Cache of Cloud FoundrySAP BTP, Cloud Foundry RuntimeThe Cloud Foundry runtime of SAP BTP: memory for applications in a Cloud Foundry org, run from SAP's Java, Node.js and Python buildpacks or community buildpacks and managed with the standard Cloud Foundry tools.sap official, btp keeps only a few minutes of that history. This command line reads the metrics-* index of SAP Cloud LoggingSAP Cloud LoggingAn instance-based observability service built on OpenSearch that stores and analyses logs, metrics and traces from Cloud Foundry, Kyma, Kubernetes and other runtime environments.sap official, btp instead: history buckets container CPU, memory and filesystem usage or a custom gauge, sum or histogram over a time range, top ranks apps on one metric, and watch polls for new points during a deploy or an incident. It finds the dashboards credential on the Cloud Logging instance from service keys or an app binding made before SAML was enabled, caches it for seven days under ~/.saptools, and warns when container.cpu.usage mixes its two series, fraction of entitlement and fraction of one core, in one window. Histograms report count, sum and average with no percentiles, and since it never instruments an app, it reads only what is already exported to Cloud Logging.
Facts
Where it sits
Who is behind it
In the atlas: maintains 18, contributes to 18@dongitranmaintainer
What it declares
Every artifact
- Type
- CLI
- Origin
- Independently maintained
- Runs on
- Cloud Foundry
As a CLI
- Distributed through
- npm
Connections
Traces, metrics and correlation for a running service
OpenSearch-based store for logs, metrics and traces
Download
Install
npm add @saptools/cf-metricsDerived from the package name. No recipe recorded; the README has the configuration.
Architecture
Download
README
quoted from dongitran/saptools under MIT, read@saptools/cf-metrics
Query container CPU/RAM/filesystem usage and custom OTel gauge/sum/histogram metrics already
ingested into SAP Cloud Logging's OpenSearch backend (index pattern metrics-*). This answers
"how did resource usage look over the last N hours" — something the base cf app command cannot
do, since Cloud Foundry's Log Cache only retains a few minutes of container-metric history. This is
a read-only, near-real-time tool: it never instruments a running process and never mutates
application data. For live process instrumentation, see @saptools/cf-inspector and
@saptools/cf-live-trace instead.
Install
npm install -g @saptools/cf-metrics
Auth
Each command is a complete, one-shot operation (except watch, which stays running until Ctrl-C);
there is no separate login step. To reach the Cloud Logging credential it needs a Cloud Foundry
session, and takes the first of these that works:
- Your own
cfsession, when it already matches. Ifcf targetpoints at the requested org/space, the read-only discovery commands run in that session as-is. It is never modified:cf api,cf authandcf target -o -srefuse to run in it by construction, and the target is re-checked afterwards so acf targetin another terminal mid-run cannot hand back a credential from the wrong space. Nothing else is needed — noSAP_EMAIL/SAP_PASSWORD. - An isolated login, when no session matches or the session turns out to be dead (not logged
in, token expired). This runs
cf api/cf auth/cf targetin a temporaryCF_HOMEand readsSAP_EMAIL/SAP_PASSWORDfrom the environment — never pass them as flags. Without them, the error says which of the two situations it hit and what to do about it.
export SAP_EMAIL=you@example.com
export SAP_PASSWORD=your-password
Targeting
Pass --region, --org, and --space explicitly, or omit any of them to fall back to the
currently targeted cf target session. Whichever way it resolves, the CLI prints a one-line
notice to stderr naming the resolved target:
cf-metrics: target br10/example-org/space-demo (resolved from ambient 'cf target'; pass --region/--org/--space to pin)
--service <name> is a plain query filter on resource.attributes.sap@cf@app_name — it never
targets or connects to a running app the way cf-inspector/cf-hana do. If more than one
Cloud Logging service instance exists in the targeted space, pass --service-instance <name> to
pick one explicitly.
Credential discovery
Reaching OpenSearch requires a Cloud Logging dashboards basic-auth credential, which is harder to get than it sounds once SAML is enabled on the instance's dashboards: only credentials created before SAML was switched on keep a username and password — newer ones expose the endpoint and mTLS ingest material but nothing you can log in with.
cf-metrics finds the instance with one cf space --guid plus one GET /v3/service_instances
(not cf services, which the CF CLI implements as one request per instance in the space and which
measured 15–38 seconds on a real space), then lists every credential binding on the instance in one
Cloud Controller v3 request and reads their details, preferring:
- Service keys, newest first (
--service-key, repeatable, to pin specific ones). Keys are created deliberately, so age says nothing about whether one predates SAML. - App bindings, oldest first (
--fallback-binding-app, repeatable, to pin specific apps) — an app bound before SAML keeps its original basic-auth credential forever, so the oldest is the best bet. - Only behind
--allow-mint-credential: temporarily disable SAML, mint a new key, restore SAML immediately after. This is disruptive (breaks SSO dashboards login for everyone during the window) and is never attempted by default.
Candidates are probed in small parallel batches, so a binding whose credential is not ultimately used may still be read; the one that wins is always the highest-priority match, never whichever request happened to return first.
Pass --verbose to see how many bindings were found and which one succeeded. If every candidate
fails, the error names each one that was tried.
Credential reuse
Discovery is the expensive part of every command: measured on a real tenant with 61 bindings, a
plain names took 33 seconds before this cache existed, almost all of it Cloud Foundry round trips
that produced the same credential every time. So the resolved dashboards credential is kept under
~/.saptools/cf-metrics/credentials.json (directory 0700, file 0600, written atomically), keyed
by API endpoint, org, space and instance, for 7 days. A hit is silent and spawns no cf at all — a
warm command is just the OpenSearch query.
| Control | Effect |
|---|---|
--verbose | names the cached credential's source and instance on a hit |
--refresh-credential | ignore the cached entry, rediscover, and replace it |
CF_METRICS_CREDENTIAL_CACHE=0 | never read or write the cache (false/off/no also work) |
cf-metrics credential list | what is cached — target, instance, source, endpoint, expiry; never the username or password |
cf-metrics credential clear | forget every cached credential |
A cached credential that OpenSearch rejects (HTTP 401/403 — typically its service key or binding
was deleted) is dropped and rediscovered within the same command, which then retries once and says
so on stderr. --service-key/--fallback-binding-app pins apply to the cache too: a cached
credential from a binding you did not name is treated as a miss.
On keeping a secret on disk: this is the same trade cf-xsuaa makes for XSUAA client secrets in
~/.saptools/xsuaa-data.json, with the same file protections, and ~/.cf/config.json already
holds a refresh token that can fetch this very credential from the Cloud Controller. The cache
widens nothing about who can obtain the credential; it only saves re-obtaining it. If you weigh
that differently, set CF_METRICS_CREDENTIAL_CACHE=0 and run cf-metrics credential clear once.
Updates
Every command first checks npm for a newer @saptools/cf-metrics (at most once an hour: one 18-byte
request with a 2-second timeout) and, when one exists, installs that exact version with the package
manager that owns the running binary and re-runs the command you typed on the new version. Both steps
are announced on stderr; nothing is printed when the install is already current:
cf-metrics: updating 0.7.0 -> 0.7.1 ...
cf-metrics: updated to 0.7.1; re-running the command
If the install cannot complete (offline, read-only prefix), one stderr line gives the manual command
and the command runs on the installed version; that version is not retried for a day.
cf-metrics self-update forces the check and install now; cf-metrics self-update --check only reports.
| Control | Effect |
|---|---|
SAPTOOLS_AUTO_UPDATE=on|notify|off | on (default) installs and re-runs; notify prints the manual command once per version; off never checks. Applies to every @saptools CLI. |
CF_METRICS_AUTO_UPDATE | same values, this CLI only; wins over the global variable |
SAPTOOLS_UPDATE_INTERVAL_MINUTES | minutes between checks (default 60; 0 checks on every run) |
SAPTOOLS_NPM_REGISTRY | registry to check and install from (default: npm's configured registry, then npmjs) |
SAPTOOLS_ROOT | relocate ~/.saptools for every @saptools CLI at once (CF_METRICS_SAPTOOLS_ROOT relocates only this CLI's files) |
SAPTOOLS_UPDATE_DEBUG=1 | explain on stderr why nothing happened |
The updater switches itself off in CI (CI set), under NODE_ENV=test or NO_UPDATE_NOTIFIER, when
the binary runs from a source checkout, an npm link or an npx cache, and inside the re-run itself.
It never writes to stdout, never asks for input, never uses sudo, and never moves onto a prerelease.
Its state lives in ~/.saptools/updates/.
Commands
| Command | Purpose |
|---|---|
sample | Dump the N most recent full metric documents, unfiltered — start here when you know nothing yet. |
mapping | Field-type discovery (keyword vs text) on any --index pattern (default metrics-*). |
fields | List every flat attribute key on a sample metric document. |
names | Which metric names exist for a service/time-range, with kind, unit, and doc count. |
history | Time-bucketed values for one or more metric names, kind-aware — the core command. |
snapshot | Latest single value per metric name for a service, point-in-time, no bucketing. --limit bounds how many names come back (0 for all); a truncated list is flagged on stderr, as it is for names. |
top | Cross-app ranking for one metric name over a range. No --service filter; that's the point. Kind-aware like history. Pass --unit for container.cpu.usage — see the unit caveat below. |
watch | Poll for new metric points as they land, --json for NDJSON — live monitoring during a deploy or incident. --lookback sets the initial look-back window (default 2m). |
result show|list|prune|clear | Inspect results saved via --save. |
credential list|clear | Inspect or forget the cached dashboards credentials (see Credential reuse). |
self-update [--check] | Check npm for a newer release and install it now; every other command does this on its own (see Updates). |
Every row-returning command supports --format table|json|json-compact|csv (default table) and
--save, which prints ref=<id> instead of the result and stores it under
~/.saptools/cf-metrics/results/<ref>/ for later inspection with cf-metrics result show <ref>.
Saved results
A saved result is kept for 7 days, then removed by the next cf-metrics command that touches the
store (or immediately by cf-metrics result prune). Nothing else expires it and nothing caps how many
accumulate. Files are written 0600 inside 0700 directories.
Pruning only ever removes a result that has expired, or a ref directory it has verified is empty. One this version cannot read — a permission
error, a partial write, or a format a newer cf-metrics wrote — is deliberately left on disk and
reported by cf-metrics result prune on stderr, so a downgrade or a stale global install cannot destroy
saved results it merely fails to understand. cf-metrics result show says which of those happened
rather than reporting a readable file as missing.
The trade-off is that a result this version cannot read is then kept indefinitely: no TTL reaches it,
and the only way to reclaim it today is cf-metrics result clear, which removes every saved result it can see — it does not reclaim a leftover <ref>.tmp-<pid> directory from an interrupted save.
See .skills/cf-metrics/SKILL.md (or the installed ~/.claude/skills/cf-metrics/SKILL.md) for the
full command reference with worked examples.
history's kind-aware behavior
A metric name normally has one fixed kind, resolved automatically (or pass --kind to skip the
lookup when already known). A name reporting more than one — an instrumentation change mid-rollout,
or two emitters sharing a name — is a data anomaly rather than an expected shape: history and
top use the most common kind and warn, naming every kind found, and names lists them all in its
KIND column the way it already lists multiple units.
- GAUGE (container CPU/memory/filesystem, DB pool stats, most queue metrics) — reports avg/min/max per time bucket.
- SUM (queue message counters) — reports a per-bucket total. This assumes delta temporality, the only kind observed in real data; a document reporting cumulative temporality triggers a warning instead of a guessed correction.
- HISTOGRAM (HTTP request duration) — reports count/sum/derived-avg per bucket. Percentile approximation from bucket boundaries is not implemented in this release (see Limitations).
top resolves kind the same way (or accepts its own --kind override) because HISTOGRAM
documents carry no value field to rank on: GAUGE/SUM metrics rank by avg/max value as usual, but
a HISTOGRAM metric ranks apps by derived avg latency (sum(sum)/sum(count), the same math
history uses per bucket) and its rows have no MAX column, since no true per-request max is
available without the percentile approximation this release doesn't implement.
⚠️ container.cpu.usage carries two different series under one name
Cloud Foundry emits two distinct measurements under the metric name container.cpu.usage,
interleaved in the same time window and told apart only by the unit field:
unit | description | meaning | matches cf app column | typical value |
|---|---|---|---|---|
1 | time used by an app instance per entitlement | fraction of the app's CPU entitlement | cpu entitlement | 0.278 |
cpu | time used by an app instance per single CPU core | fraction of one CPU core | cpu | 0.016 |
The two differ by more than an order of magnitude for a small app. Note also that OpenTelemetry's
semantic convention reserves the name container.cpu.usage for CPU in cores, so the unit="1"
series does not mean what the metric name suggests, and the unit="1" value can legitimately
exceed 1.0 when an app bursts past its entitled share (values above 5.0 occur in real data).
Aggregating across both series at once produces a number with no physical meaning, so history
and top warn on stderr whenever the queried window contains more than one unit, and
--unit <unit> narrows to a single series. Real output for the same app and window:
$ cf-metrics history --service my-app --name container.cpu.usage --since 30m --interval 15m
cf-metrics: WARNING: "container.cpu.usage" reports 2 different units in this window (1, cpu) —
the values below average incommensurable series and are NOT meaningful. Re-run with --unit <1>.
TIME | AVG | MIN | MAX | DOC_COUNT
2026-08-31T15:30:00.000Z | 0.157980 | 0.015969 | 0.309866 | 110 <- MIN and MAX are different series
$ cf-metrics history ... --unit cpu # fraction of one core, matches `cf app` cpu 1.7%
2026-08-31T15:30:00.000Z | 0.016908 | 0.015969 | 0.018127 | 53
$ cf-metrics history ... --unit 1 # fraction of entitlement, matches `cf app` 28%
2026-08-31T15:30:00.000Z | 0.289152 | 0.272990 | 0.309866 | 57
Note how the blended MIN is simply the cpu series and the blended MAX the 1 series, and how
the two filtered runs' document counts add back to the blended total (53 + 57 = 110).
When ranking with top, remember that unit="1" measures how close each app is to its own
limit — the right question for "which app is about to be throttled", the wrong one for "which
app is burning the most CPU", because entitlement scales with each app's memory quota (Cloud
Foundry grants roughly 25% of a core per 1 GB). Use --unit cpu for absolute CPU comparisons.
Every other metric is single-unit and unaffected — verified across 300 sampled documents covering
12 distinct metric names. In particular container.memory.usage and container.memory.capacity
are plain byte counts and agree with cf app exactly.
Limitations
--unitexists onhistoryandtoponly — the two commands that aggregate, and so the only two that can blend series.sampleshows each document's own unit,nameslists every unit a name reports, andsnapshotshows the unit of the latest document; none of them accept the filter because none of them aggregate across units.- One
--unitapplies to every--namein the samehistorycall. Combining names with different units (saycontainer.cpu.usage --unit cpualongsidecontainer.memory.usage, which isBy) simply returns no buckets for the ones that do not match — the per-metric bucket count printed to stderr makes that visible rather than silent. - No cumulative-temporality SUM stitching (last-minus-first per bucket) — not implemented because no real cumulative-temporality metric has been observed; a warning fires instead of a silent guess if one appears.
- No histogram percentile approximation (p50/p95/p99) —
historyon a HISTOGRAM metric reports count/sum/avg only. Approximating percentiles fromexplicitBounds/bucketCountscorrectly needs either server-side scripting (unverified whether the Dashboards console-proxy allows it) or careful cross-document bucket merging; neither was validated against real data for this release.
Example
cf-metrics history --service example-app --name container.memory.usage \
--since 1h --interval 20m
TIME | AVG | MIN | MAX | DOC_COUNT
-------------------------+--------------------+-----------+-----------+----------
2026-08-31T14:40:00.000Z | 123978028.42857143 | 123968637 | 123985072 | 49
2026-08-31T15:00:00.000Z | 124032009.38235295 | 123985072 | 124054101 | 68
2026-08-31T15:20:00.000Z | 124089882.71428572 | 124054101 | 124165861 | 70
2026-08-31T15:40:00.000Z | 124177294.04347827 | 124169148 | 124179009 | 23
Memory is used here rather than CPU deliberately: container.memory.usage is a plain byte count
that agrees exactly with cf app, whereas container.cpu.usage currently blends two units — see
the caveat above.
Timeouts and interruption
Every OpenSearch request carries a 60-second deadline; override it with
CF_METRICS_HTTP_TIMEOUT_MS when a query is genuinely slow. A timeout is reported as a timeout,
not as a generic request failure, whether it fires while connecting or while the response body is
still streaming. A value beyond what Node's timers hold is clamped rather than silently collapsing
to a 1ms deadline, and an unusable one falls back to the default.
An OpenSearch query that fails on only some shards comes back as HTTP 200 carrying whatever the
surviving shards found. Because names, snapshot, top and history are pure aggregations, that
would change the numbers and the ranking rather than visibly shorten a list, so every command fails
instead of reporting it. Set CF_METRICS_ALLOW_PARTIAL_SHARDS=1 to accept a partial answer when a
shard is persistently down.
Ctrl-C is safe at any point. When a command has to log in on its own (see Auth), it does so inside
a temporary CF_HOME that holds a real access token and a long-lived refresh token once cf auth
has run; that directory is removed on interruption as well as on normal exit, and the process still
exits with the conventional 128+signal status. A command that reuses your own cf session or a
cached credential creates no such directory in the first place.
Development
pnpm --filter @saptools/cf-metrics lint
pnpm --filter @saptools/cf-metrics typecheck
pnpm --filter @saptools/cf-metrics test:unit
pnpm --filter @saptools/cf-metrics test:e2e
pnpm --filter @saptools/cf-metrics build
Unit tests mock the OpenSearch console-proxy and cf CLI layers directly; e2e tests spawn the
built CLI against a fake cf binary and an in-memory fake OpenSearch Dashboards console-proxy
server — no real network or SAP credentials are used in either suite.
History
Fixes 1
Notes
Fixed
--servicenow reads "(required)" in--helpforhistory,snapshot,watch, andnames— the flag was already enforced at runtime, but its description never said so.
Changed 1
Notes
Changed
- Target resolution, dashboards-credential discovery, the OpenSearch console-proxy client, the credential cache, and the saved-result store now live in
packages/core's shared cloud-logging module group, shared with@saptools/cf-oteland@saptools/cf-log-search. No CLI-facing behavior change; verified byte-identical output against the real tenant before and after.
- Target resolution, dashboards-credential discovery, the OpenSearch console-proxy client, the credential cache, and the saved-result store now live in
Changed 1, Fixes 9
Notes
Fixed
-
watchrefused only a duration-shaped cursor, which left the two worse shapes through. Measured against the live backend:"24"is legalepoch_millis, so the cursor silently rewinds to 1970 and the watch crawls forward through the whole index printing ancient points as if they were live;"n/a"is aparse_exception, so every later poll fails and retries forever, emitting nothing at exit 0. The guard now states the requirement positively — a cursor is an instant this package would accept as a bound, or it is not a cursor. -
The window check compared bounds in milliseconds although
timeisdate_nanos, so--since …T00:00:00.000000002Z --until …T00:00:00.000000001Zwas accepted and returned nothing at exit 0. It now compares at the precision the field stores, matching@saptools/cf-otel. -
assertValidTimeRangeskipped shape validation for a command's default--since, which was unreachable in-repo but is now public API alongside the builder it guards. -
The deeper listing surfaced a second silent dead end, so it now names it. A field inside a
nestedparent is stored as a separate hidden document: measured live, atermsaggregation onbuckets.count,exemplars.spanIdorquantiles.valuereturns zero buckets and zero shard failures, while an ordinary sibling returns real ones. Fifteen of the hundred listed rows sit under one of the five nested parents, and a newNESTED_INcolumn names which — listing them without saying so would have advertised fields no filter this package builds can reach. -
--since/--untilwritten without a timezone were compared in the operator's local zone while OpenSearch reads them as UTC, so the inverted-window check failed in both directions — measured live in UTC+7 against a real instance.--since 2026-09-05 --until 2026-09-05T06:00was refused as inverted although the backend sees a forward six-hour window, and--since 2026-09-05T12:00 --until 2026-09-05T08:00:00Zwas accepted although the backend sees an inverted one and returns nothing at exit 0 — the exact silent-empty result the check was added to remove. Which direction a user hit depended on the sign of their offset, so it was invisible to anyone testing in UTC. Bounds are now compared as the backend reads them. -
Timezone offsets past ±18:00 were forwarded and rejected by the backend after a full credential discovery.
java.time'sZoneOffsetcaps there, measured exactly:+18:00is accepted and+18:01upward is not, along with+00:60and+2500. Enumerating all 40,000 offset strings the old pattern could match confirms the bounded form accepts exactly the backend's set — no regression, no over-accept — where the unbounded one admitted 359 well-formed offsets per sign per separator spelling beyond the cap, plus 8,560 malformed ones. -
mapping --field <name>.keywordreported "was not found in the mapping" for a multi-field that genuinely exists and aggregates normally —metrics-*maps@timestamp.keywordandorigin.keyword(measured live, both return real buckets). A multi-field hangs off a field's ownfieldsblock rather than underproperties, and only thepropertiesbranch was walked. -
mappingwith no--fieldlisted only top-level keys: 46 names for 87 leaf fields on the live pattern.resourceappeared as a dead end while the nineresource.attributes.sap@cf@*fields beneath it — the ones worth filtering on — appeared nowhere, even though--fieldresolved each one by name. The listing now matches what the lookup already supported: 100 rows against the same index, containers included, soobjectversusnestedis visible before a filter is written. -
watchfed a cursor read straight from a document'stimefield back in as--since. A value shaped like a duration turned the cursor into a relative window that re-resolved againstnowevery poll, replaying the same range forever, and an absurd magnitude threw from a call site outside the poll loop's own error handling, ending the session. Impossible under the realdate_nanosmapping, but this package deliberately tolerates a rotated index wheretimeis unmapped, and_sourceis raw.
Changed
- The mapping lookup moved from
cli/commands/mapping.tsintosrc/mapping.ts, andlookUpField,listAllFieldNames,assertValidTimeBoundShape,assertValidTimeRangeandisAbsoluteInstantare now exported.buildMetricBoolQueryforwards an absolute bound verbatim, so without the validators a library consumer had no way to reject one the backend will refuse. While the mapping logic was module-private its only observable surface was four rendered CLI columns, which is how a divergent-ignore_abovebug and a missing implicit-object check both escaped the suite.
-
Changed 1, Fixes 11
Notes
Fixed
-
mapping --fieldon a field alias reportedalias, which is true and useless.metrics-*maps nine of them (measured): short names likeapp_namepointing atresource.attributes.sap@cf@app_name. OpenSearch resolves an alias in queries and aggregations alike — measured, an aggregation on the alias and on its target return identical buckets — so the type that governs whether the field is safe to aggregate on is the target's, and that is what is now reported, alongside the target path in a newALIAS_OFcolumn. An alias whose target is missing, malformed, or itself an alias still reportsalias, now naming what it points at; an alias chain is never followed, which OpenSearch forbids anyway and a self-reference would spin on. -
A field present in every backing index but mapped inconsistently was reported as "was not found in the mapping" — introduced in 0.9.0 along with the agreement check itself, and misleading in the way that costs the most time: it sends the reader hunting for a typo that is not there. The two cases are now distinct, and the listing shows
ambiguousrather thanunknownfor a field the indices disagree about. -
A divergent
ignore_aboveacross backing indices is no longer reported as whichever index answered first. Akeywordlonger than its cap is stored but never indexed, so the same term matches on one shard and not another. It reports(varies)rather than blank: an empty cap reads as "no cap at all", which is the safe interpretation, exactly inverting the hazard. The type still answers — withholding it would only makemapping --fieldcall the field missing. -
A failed
--allow-mint-credentialabandoned the service key it had already created. Every step aftercf create-service-keycan fail — reading the key back, a payload without the dashboards fields, the broker timing out — and the generated name was lost with the error, so an unusable key stayed on the shared Cloud Logging instance, invisible to the user and accumulating one per retry. The key is now deleted, strictly after the SAML restore so cleanup cannot extend the window in which SSO is disabled for everyone, and if the deletion itself fails the key is named in the error with the command to remove it. -
The temp-directory sweep added in 0.10.0 deleted directories belonging to other processes. It runs from the housekeeping prune, which fires at the start of every save, read and list, so one process's routine
result listcould delete the directory another was mid-way through writing — measured: two concurrent saves lost one in twenty-four, and a save racing a prune loop failed every time. It now skips any directory whose owning process is still alive, and no longer counts a removal it did not perform. -
The windowed kind lookup added in 0.10.0 turned "this metric was quiet in the requested window" into a hard failure whose message blamed the metric name.
historynow falls back to an unwindowed lookup before failing, so a metric that exists but produced nothing in the window charts as an empty table at exit 0 the way it did before, and the error is reserved for a name that matches nothing anywhere. -
A month outside 1-12 and a day of
00were reported as a shape problem ("expected a relative duration or an ISO-8601 timestamp") rather than as the calendar errors they are, becauseDate.parserejected them before the calendar check ran. A bare eight-digit value is now pointed at its punctuated form instead of being offered a duration suffix. -
Warnings printed from inside the work callback appeared once per attempt when a rejected cached credential made the bootstrap re-run it — the same defect as the duplicated rows, one layer over.
historyandtopnow collect their warnings and print them after the callback returns. -
watchreplayed its whole--lookbackwindow as duplicate output when a mid-session credential rejection restarted it, and announced the session a second time. -
A cached minted credential is no longer reused by a run that did not pass
--allow-mint-credential. Reusing it disrupts nothing by itself, but the flag is the only record that anyone consented to that credential existing. -
The redaction key list is now shared between
saml-toggle.tsandcf.tsrather than restated in each. 0.10.0 claimed to have brought them into line and had not:cf.tsstill covered a narrower set, soapiTokenwas redacted on one path and printed in full on the other.
Changed
mappingoutput carries a fourth column,ALIAS_OF, empty for a concrete field.
-
Changed 1, Fixes 9
Notes
Fixed
-
historyemitted every already-fetched row twice when a cached credential turned out to be rejected partway through. The client bootstrap deliberately re-runs the whole work callback against a freshly discovered credential, and the row accumulator lived outside it, so a run that hit an HTTP 401 on its second--nameprinted the first name's rows again — at exit 0, and persisted that way under--save. Nothing caught it because no test drove a command through the real retry; one does now. -
A pin flag defeated the credential cache entirely, and with
--allow-mint-credentialre-minted on every run.--service-key/--fallback-binding-appeach restrict only their own candidate type during discovery, but the cache read them as restricting both, so--fallback-binding-app xalone rejected every cached service-key credential (and vice versa) and paid the full ~30s rediscovery every time. A cached minted credential matched no pin at all, so--allow-mint-credentialwent back through minting on every invocation — and minting disables SAML on the shared Cloud Logging instance to do its work, breaking SSO for every user of that instance each time. -
credential clearleft the password on disk. Writes go throughcredentials.json.tmp-<pid>before the rename, so an interruption in that window stranded a file holding the basic-auth password in cleartext; nothing reclaimed it, andcredential listreads only the real file, so the CLI reported the credential gone while the secret stayed. The clear now sweeps those too. -
The kind lookup ignored
--since/--untiland scanned all of retention. It answered with the kind that dominated all time rather than the window being charted, so a name whose instrumentation changed shaped a recent query with the wrong sub-aggregations and returned real document counts beside all-null values. It also made 0.9.0's ambiguity warning say "in this window" about data from every window there has ever been. One unbounded 80-shard scan per--nameper run is gone with it. -
--savethrew away the whole result when the store could not be written. A read-only or full home directory discarded the ~30s credential round trip and the query with it; the rows are now printed instead, with a notice and a non-zero exit soref=$(… --save)cannot silently bind a table row. -
Calendar-invalid dates were accepted and forwarded.
Date.parserolls2026-02-30to March 2 rather than rejecting it, so the bound reached OpenSearch — which rejects it (measured) — as a parse-exception dump after a full login. Worse, the ordering check compares resolved instants, so a typo came back as a confident "--since is later than --until", pointing at the flag that was never wrong. Also rejected now, each verified against a real instance rather than assumed: a space instead ofT, hour 24, minute or second 60, and more than nine fractional digits. A relative duration too large to land on a real date is named with its flag instead of surfacing as a bareRangeError: Invalid time value, and a bare number now explains both readings it could have had. -
An interrupted
--savestranded a<ref>.tmp-<pid>directory thatprune,listandclearall walked past, because the ref pattern they filter on never matches it. They accumulated permanently;prunenow removes them. -
--verboseprinted a service instance'sclientSecretandapiTokenin full. The redaction list insaml-toggle.tscovered onlyprivate/password/signaturewhile the exec layer's own covered more; both now cover the same classes. -
CF_COLORis forced off for everycfinvocation. An inheritedCF_COLOR=truemakes the CLI emit ANSI escapes even when piped, and these parsers key off literal text — including thestatus:reader behind the SAML restore check, where a styled label would report a restore that actually succeeded as "SSO broken for ALL users".
Changed
resolveMetricKind/resolveTopMetricKindtake an optional lookup window;KindLookupWindowis exported alongsideKindResolution.
-
Changed 2, Fixes 7
Notes
Fixed
-
A query that failed on only some shards was reported as a complete result.
metrics-*spans 40 backing indices over 80 shards (measured), and OpenSearch answers a partial failure with HTTP 200, the count in_shards.failed, and whatever the surviving shards found — which this client read as the whole answer. The same applies to a search OpenSearch markstimed_out. That is worse here than in a client that reads raw documents:names,snapshot,topandhistorynever readhitsat all, so a partial failure does not shorten a list, it silently changes the numbers and the ranking, and the result still looks entirely plausible. Every command now fails loudly instead of returning a wrong answer at exit 0. SetCF_METRICS_ALLOW_PARTIAL_SHARDS=1to accept a partial answer when a shard is persistently down. -
watchcould drop points permanently rather than merely late. Its cursor only advances past documents a poll actually returned, so a failed poll is retried — but a partial-shard answer was not a failure, it was a short page, so the cursor moved past whatever the missing shards held and thesince: cursorfilter then excluded those points for good once the shard recovered. With the check above they are delayed by one interval instead of lost. -
A metric name reporting more than one
kindsilently lost all but the most common one.resolveMetricKindasked for a singletermsbucket, so a second kind — an instrumentation change mid-rollout, or two emitters sharing a name — was not merely ignored, it was invisible even to detect.historyandtopnow warn and name every kind found (pass--kindto pick one), andnameslists them the way it already listed multiple units. -
mapping --fieldcould not resolve any field whose name contains a dot. The_sourcekey is one flat string on every document, but the mapping tree still nests on the.segments, so the single-level lookup found nothing for the wholeresource.attributes.*family — the fields most worth checking with a command whose purpose is "check keyword vs. text before aggregating on any field". Each segment is now walked in turn. -
A field's mapping type was taken from whichever backing index reported it first, even though the query runs against all of them. Dynamic mapping can give one path different types in different indices after an ingest change, so a type sampled from one index can be wrong for another's shards. The type is now reported only when every index holding the field agrees.
-
A timeout that fired while the response body was still streaming escaped unwrapped. Headers can arrive well before a wide aggregation finishes streaming, so the deadline often lands on the body read rather than the request — where it surfaced as a bare "The operation was aborted due to timeout" with no path, no ceiling and no hint, instead of this package's own message.
-
CF_METRICS_HTTP_TIMEOUT_MSabove Node's timer ceiling silently did the opposite of what it says.AbortSignal.timeoutreduces any delay past 2^31-1 to 1ms (emitting only aTimeoutOverflowWarning) and throws aRangeErrorpast 2^32-1, so raising the ceiling too far aborted requests almost immediately while the resulting error still claimed to have waited the full configured time. The value is now clamped, and a fractional or negative one falls back to the default rather than reaching the timer at all.
Changed
- Commands can now fail where they previously returned a result at exit 0. That is the point of the shard and timeout checks above, but it is a behaviour change for any script that treated exit 0 as "the query ran".
resolveMetricKindreturns{ kind, otherKinds }instead of a bare kind string, so a caller can see the ambiguity the CLI warns about.KindResolutionis exported alongside it.
-
Changed 2, Fixes 10, Other 3
Notes
Fixed
-
Pruning no longer deletes saved results it cannot read.
pruneResultSessionsruns at the head of every save, read and list, and it treated four distinct outcomes — file absent, I/O error, invalid JSON, unrecognized shape — as one "missing" answer, thenrm -rf'd the directory. So a nominally read-onlyresult listdestroyed a valid unexpired session whose only difference was a newerversion, and one that was merely unreadable at that moment (a permission error, with the data fully intact). Because the self-updater makes mixed versions on one machine normal, that also meant an older cf-metrics silently wiped a store a newer one had written.Reads are now classified, and only an expired session or an empty ref directory is removed. A manifest that cannot be read or recognized is left exactly where it is and counted as retained; a ref directory holding files under names this version does not know is retained too.
-
A saved result with a damaged
expiresAtis no longer immortal.Date.parsereturnsNaNfor it andNaN <= nowisfalse, so such a session survived every prune for ever and still read back. Expiry now falls back tocreatedAtplusttlMinutes, and a session whose age cannot be established by either route is treated as expired. -
One undeletable directory no longer takes the whole store offline. Deletion is attempted per session, and the housekeeping prune is best-effort, so
result showandresult listkeep working on intact sessions even when the results directory cannot be listed.result showalso enforces the TTL itself instead of relying on that prune having succeeded. -
result shownow says which of the three things went wrong. An unreadable manifest and one written in a newer format used to report "Saved result not found or expired", which was untrue; both now name the file, and a newer format says it was left in place. Added theRESULT_UNREADABLEerror code. -
result listreports the ref that actually resolves. The listed ref came from inside the manifest while lookup uses the directory name; when those disagreed,listadvertised a ref thatresult showrejected. Expired sessions that prune could not remove are omitted rather than shown. -
Pruning no longer deletes a saved result whose expiry it cannot establish.
resolveExpiryMillispreviously fell back to "treat as expired", so a manifest with damaged timestamps had its rows deleted even though they were perfectly readable — and a version that changed only the timestamp encoding (a TemporalZonedDateTime, an ISO week date, epoch millis as a string, or canonical ISO with surrounding whitespace, whichDate.parsedoes not trim) would have had its data destroyed by an older binary. Such a session is now retained and reported, like any other manifest this version cannot fully interpret. One rule now covers every case: only an expired session with a resolvable date, or a ref directory verified to be empty, is ever deleted. -
A
ttlMinutestoo large to date no longer produces an immortal session. The bound is what aDatecan hold, not what a float can hold:Number.isSafeIntegeradmits a value whose product with 60000 reaches 5.4e20, while aDateholds at most ±8.64e15 ms — socreatedAt + ttlMinutescould yield a finite expiry no clock will ever reach, which survived every prune and was not even counted. A derived expiry beyond theDaterange is now treated as unresolvable, hence retained. -
result prunenames the refs it left in place instead of only counting them. A retained session is omitted fromresult listand no command removes one, so the ref is the only way to find the file. -
result pruneexits non-zero when it could not delete an expired session. It is the only machine-readable health signal the store has, and reporting a partial sweep as success meantif cf-metrics result prune; then …could never detect a store it had failed to clean. -
PruneOutcomeis now{ removed, failed, retainedRefs }.retainedwas a count the user could not act on; the refs replace it.
Changed
pruneResultSessionsreturns{ removed, retained, failed }instead of a bare count.result prunestill printsremoved=Nas its only stdout line and reports retained and undeletable counts on stderr.- The
PruneOutcometype is exported.
Known limitations
- A leftover
<ref>.tmp-<pid>directory from an interrupted save is invisible toresult list,result pruneandresult clear, and no TTL reaches it. Reclaim it by hand. - A retained session is reported by
result prunebut not listed, and there is noresult rm;result clearis the only in-tool removal and it removes everything it can see. result listomits a session it could not read without saying so. Onlyresult prunereports those.
-
Other 1
Notes
- README: document
SAPTOOLS_ROOTnext to the other update controls. No code change; this release also exercises the new self-update path on installs of 0.7.0.
- README: document
Other 2
Notes
- Self-updating. Every command now checks npm for a newer
@saptools/cf-metrics(at most once an hour: one 18-bytedist-tagsrequest with a 2 s timeout, remembered under~/.saptools/updates/) and, when one exists, installs that exact version with the package manager that owns the running binary and re-runs the command on it —cf-metrics: updating 0.7.0 -> 0.7.1 ...and thencf-metrics: updated to 0.7.1; re-running the commandon stderr, nothing when already current. A failed install prints the manual command once and the command runs on the installed version; that version is not retried for a day.SAPTOOLS_AUTO_UPDATE=on|notify|off(per-CLICF_METRICS_AUTO_UPDATE),SAPTOOLS_UPDATE_INTERVAL_MINUTES,SAPTOOLS_NPM_REGISTRYandSAPTOOLS_UPDATE_DEBUGcontrol it; it is off by itself in CI, in tests, from a source checkout ornpm link, undernpx, and inside the re-run. Newcf-metrics self-update [--check]forces the check and install now. --versionreadspackage.jsonat runtime instead of a hand-maintained constant. Both this and the updater come from the new private@saptools/corepackage, bundled intodist/cli.jsat build time so every@saptoolsCLI can share one implementation and one test suite; the published tarball gains no runtime dependency.
- Self-updating. Every command now checks npm for a newer
Other 6
Notes
Every command used to pay the full Cloud Foundry round trip on every run. Measured on a real tenant (61 credential bindings on the Cloud Logging instance, 39 service instances in the space) before anything was changed, a plain
namescommand took 33.8s and 32.5s on two consecutive runs. The breakdown was not what the previous release notes assumed:cf api+cf auth+cf targetcost about 6s together, whilecf servicesalone took 15–38s — the CF CLI implements it as one request per instance in the space — and the bindings listing plus the per-binding probes another 6–20s. Three changes, each independently useful:- A matching
cfsession is reused instead of logging in again. Whencf targetalready points at the requested org/space (the normal state aftercf login), the read-only discovery commands run in that session as-is — the same patterncf-hanauses for bare app names. The session is never modified:cf api/cf auth/cf target -o -srefuse to run in it by construction, and the target is re-read after discovery so acf targetin another terminal mid-run cannot hand back a credential from the wrong space.SAP_EMAIL/SAP_PASSWORDare now optional whenever such a session exists; they are still used, exactly as before, when no session matches or the session turns out to be dead (not logged in, token expired), and the error says which of those it was. cf servicesis gone. The Cloud Logging instance is found through onecf space --guidplus oneGET /v3/service_instances?space_guids=…&fields[service_plan.service_offering]=name(2.4s + 2.0s measured), which also returns the instance GUID the bindings listing needs. A dead session is recognized on the first failing command and abandoned, rather than being blamed on each of the bindings one at a time.- The discovered dashboards credential is cached under
~/.saptools/cf-metrics/credentials.json(directory 0700, file 0600, written atomically), keyed by API endpoint, org, space and instance, with a 7-day time-to-live. A hit is silent and costs nocfspawn at all — a warmnamesis the OpenSearch query and nothing else.--verbosenames the cached source;--refresh-credentialrediscovers and replaces the entry;CF_METRICS_CREDENTIAL_CACHE=0disables reading and writing;cf-metrics credential listshows what is cached (target, instance, source, endpoint, expiry — never the username or password) andcf-metrics credential clearforgets it. A cached credential that OpenSearch rejects (HTTP 401/403 — its key or binding was deleted) is dropped and rediscovered within the same command, which then retries once;watchnow surfaces that rejection instead of retrying it every interval forever.--service-key/--fallback-binding-apppins are honoured: a cached credential from a binding the caller did not name is a miss. - Why cache a secret on disk at all, when
cf-hanakeeps HANA bindings live-only: this is the same tradecf-xsuaaalready makes for XSUAA client secrets in~/.saptools/xsuaa-data.json, with the same protections, and~/.cf/config.jsonalready holds a refresh token that can fetch this very credential from the Cloud Controller — the cache widens nothing about who can obtain it, it only saves re-obtaining it thirty seconds at a time. The opt-out andcredential clearexist for anyone who weighs it differently. - Fixed on the way: the
--allow-mint-credentialpath read the freshly minted key withcf service-key, whose CLI v8 output wraps the fields in acredentialsobject where v7 printed them flat, so a minted key looked empty (after SAML had already been toggled). Both shapes are read. - API changes for library consumers:
discoverDashboardsCredentialtakesSapCredentials | undefinedand the returnedDashboardsCredentialcarries the resolvedinstance;discoverServiceInstance/listCloudLoggingInstancestake the space name and return{ name, guid };findBoundApps,cfServicesandparseServicesTableare removed with thecf servicespath; the credential-cache functions are exported.CfMetricsErrorgains an optionalstatus, withisAuthRejection()to test for 401/403. The test-only results-root overrideCF_METRICS_RESULTS_ROOTis renamedCF_METRICS_SAPTOOLS_ROOT, since it now also relocates the credential cache.
- A matching
Other 3
Notes
Two robustness fixes, both reproduced against the real backend before being changed.
- Every OpenSearch request now has a deadline.
fetchwas called with no timeout at all, so a Dashboards endpoint that accepted the connection and then went silent hung the command forever — no output, no error, nothing to distinguish it from a slow query, and no way out but Ctrl-C. Requests now carry a 60s ceiling (matching what thecfexec layer already enforced), overridable withCF_METRICS_HTTP_TIMEOUT_MS, and a timeout is reported as a timeout rather than a generic failure.watch's own Ctrl-C signal is combined with the deadline rather than replacing it — previously, supplying a caller signal silently opted that request out of any timeout, which is precisely the long-running command that needs one. - Ctrl-C no longer strands a temporary CF_HOME containing CF credentials. Cleanup lived in a
try/finally, but Node terminates immediately on an unhandled SIGINT/SIGTERM, so the block never ran: onlywatchregistered a signal listener, and the other eight commands left the directory behind. Oncecf authhas run it holds.cf/config.jsonwith the CF access token and a long-lived opaque refresh token. Measured on a developer machine before the fix: 13 stranded directories, 6 of them holding credentials. The session now registers its own SIGINT/SIGTERM handler, removes the directory synchronously (an async unlink is not guaranteed to finish while the process is exiting), and re-raises the signal with the default disposition restored so the shell still sees the conventional 128+signal status.saml-toggle's own temporary directory, which holds the instance's full params blob, is protected the same way. - Directories stranded by earlier versions are not cleaned up retroactively — remove any leftover
/tmp/saptools-cf-metrics-*by hand once, and treat any CF refresh token they contain as exposed.
- Every OpenSearch request now has a deadline.
Other 7
Notes
snapshotno longer silently caps at 50 metric names. It sent atermsaggregation with a hardcoded size and exposed no--limit, so an app reporting more names lost the rest with no warning and no way to see them. Worse, atermsaggregation with no explicitorderselects buckets bydoc_countdescending, so the names dropped were the sparsest ones — the rarely-written custom metric someone is most likely hunting for, while the high-volume container metrics that any command would surface anyway were always kept.snapshotnow takes--limit, following the same conventionnamesandtopalready use (0means all, values above OpenSearch's result-window ceiling are rejected up front).namesgets the same treatment, because it has the identical bug: itstermsaggregation also carries no explicitorder, so its own default cap of 50 silently discarded the sparsest names. Both commands now print a stderr notice when the aggregation actually dropped something.topdeliberately does not warn: its cut is an explicitordered top-N ranking, so what it drops is the lowest-ranked apps — the ones the command exists to let you ignore.- Truncation is detected from OpenSearch's own
sum_other_doc_count, not by comparing the returned row count against the requested limit. The row-count heuristic was wrong twice over: it fired when an app happened to have exactly--limitnames and nothing was lost (reproduced live on an app with exactly 14 names), and it could never fire under--limit 0— the very flag the notice tells people to reach for.@saptools/cf-otelalready reads the same field for the same reason. - Scope, measured rather than assumed: real apps on the tenant tested report 6–13 metric names, so
the old cap of 50 was not actually being hit. This closes a latent failure and an inconsistency —
snapshotwas the only row-returning command with no way to see all of its own data. - API change for library consumers:
querySnapshotandqueryNamesnow return{ rows, truncated }instead of bare row arrays.SnapshotResultandNamesResultare exported. - Known limitation, unchanged by this release: the notice is not persisted by
--save, soresult show <ref>on a truncated saved result gives no hint that names were dropped. The same applies totop's multi-unit warning; storing notices alongside rows needs a result-store schema change and is deliberately left for a separate release.
Other 4
Notes
- An inverted
--since/--untilwindow is now rejected instead of returning an empty table. OpenSearch accepts{gte: <later>, lte: <earlier>}and simply matches nothing, so--since 30m --until 2hexited 0 with(no rows)— indistinguishable from a genuinely quiet period, and only after a full ~20s credential round trip. It now fails in ~50ms, before any network call. - The same guard covers a case the user could not have spotted: commands that default
--sinceto a recent window inverted it without anything contradictory being typed.--until 3hon its own resolved to{gte: now-2h, lte: now-3h}and silently returned nothing. That shape gets its own message naming the default as the cause, rather than blaming a value the user never wrote. - Windows that run forwards are unaffected:
--since 4h --until 3hstill works, an equal start and end is still allowed as a legal point query, andsample, which leaves the start unbounded, still accepts--untilon its own. - Folded four near-identical per-command time-flag checks into one shared
assertValidTimeRange, so no command can validate the shape of a bound but forget the ordering.
- An inverted
Other 7
Notes
Credential discovery now asks the Cloud Controller v3 API directly instead of scraping
cfoutput.- Root cause of the tool's slowness, now fixed.
cf service-keysoutput is a three-column table, but the parser matched a header line equal to exactlyname, so it always returned an empty list and every run reported "no service keys exist" even when usable keys were there. With keys apparently absent, each command fell through to a fallback scan that ran onecf envper bound app, serially, dumping each app's entire environment just to find one binding. - Both steps are replaced by two requests:
GET /v3/service_credential_bindings?service_instance_guids=…&include=applists every service key and app binding on the instance in one call, and…/<guid>/detailsreturns one binding's credentials. Candidates are probed in bounded-parallel batches, and the winner is chosen by priority rather than by whichever request returns first, so the credential used never depends on network timing. - Measured on a real instance with 66 bindings: credential discovery went from 14.8s to 1.8s,
and a full
namescommand from 24.9s to 15.6s. It is also predictable — the old scan's cost depended on where a usable binding happened to sit in the app list, up to ~90s in the worst case. - Ordering reflects what actually predicts a usable credential. Only bindings created before SAML was enabled keep a basic-auth username/password, so app bindings are tried oldest-first. Keys keep the previous newest-first convention: they are created deliberately, and one minted during an intentional SAML-off window can be newer than a key without credentials.
- A failed
--service-key/--fallback-binding-appfilter now says the filters excluded every binding, rather than claiming the instance has none. - Behaviour change worth noting: because candidates are probed concurrently, a binding whose credential is not ultimately used may still be read. The previous implementation stopped at the first success.
- Removed the now-unused
cfEnv,cfServiceKeys,parseServiceKeyNamesandextractVcapServicesinternals along with the old VCAP parsing path. None were part of the package's public exports.
- Root cause of the tool's slowness, now fixed.
Other 6
Notes
Correctness and ergonomics pass driven by verification against a real Cloud Logging backend and a cross-check of every reported number against
cf app, an independent data path. The headline item is a metric whose aggregates were silently meaningless.--unitonhistoryandtop, plus an automatic multi-unit warning. A metric name does not always identify one series:container.cpu.usageis published as two series under a single name —unit="1"(fraction of the app's CPU entitlement, matchingcf app'scpu entitlementcolumn) andunit="cpu"(fraction of one CPU core, matchingcf app'scpucolumn) — whose values differ by roughly 17x. Aggregating both at once produced a plausible-looking but physically meaningless average, with a bucket's MIN drawn from one series and its MAX from the other, and it invertedtop's ranking: an app consuming more absolute CPU could fall out of the top 10 entirely. Both commands now detect the ambiguity via a sibling aggregation on the query they already send (no extra round trip), warn loudly on stderr, and accept--unitto select one series. Verified across 300 documents spanning 12 metric names thatcontainer.cpu.usageis the only affected name; HISTOGRAM, SUM and memory metrics are all single-unit.namesnow lists every unit a metric name reports (e.g.cpu, 1) rather than only the most common one. As the discovery command it is where a user first meets a metric, and showing a single unit while its DOC_COUNT counted both series actively concealed the ambiguity above.historywith several--nameflags now emits one result instead of one per name. It previously wrote an independent document per metric, so--format jsonproduced two concatenated arrays — not parseable JSON — and--format csvgrew a second header row midway, both while still exiting 0. Combined rows carry aNAMEcolumn; a single--namekeeps its original shape.topnow defaults--sinceto 2h, ashistoryandnamesalready did. It previously sent no time filter at all when--sincewas omitted, aggregating across the entire retention window for every app in the space.- An invalid
--kindonhistory/topnow fails in milliseconds instead of after a full credential-discovery round trip, matching how every other flag is validated. - The e2e fake backend now rejects a
rangeclause carryingunmapped_type, exactly as real OpenSearch does. That bug reached production in 0.1.0 precisely because the fake accepted it, so the whole suite stayed green; the guard is now pinned from both sides.
Other 8
Notes
Initial release.
- Read-only query CLI over container CPU/RAM/filesystem and custom OTel gauge/sum/histogram
metrics already ingested into SAP Cloud Logging's OpenSearch backend (
metrics-*), reached through the OpenSearch Dashboards console-proxy — the same access path@saptools/cf-oteluses for trace spans, applied to the metrics index instead. - Nine commands:
sample,mapping,fields,names,history,snapshot,top,watch, plus aresultsubtree (show/list/prune/clear) for refs saved via--save. historyis kind-aware: GAUGE metrics (container CPU/memory/filesystem, DB pool stats) report avg/min/max per time bucket; SUM metrics (queue counters) report a per-bucket total, assuming delta temporality — the only kind observed in real data; HISTOGRAM metrics (HTTP request duration) report count/sum/derived-avg per bucket. Cumulative-temporality SUM stitching and histogram percentile approximation are explicitly out of scope for this release — see the package README's Limitations section.topranks every app in the targeted space by one metric, not just one service — deliberately has no--servicefilter.watchpolls for new points and prints them as they land (--jsonfor NDJSON), ported from@saptools/cf-events' live-watch loop shape.- Same credential-discovery decision tree as
cf-otel: existing service keys, then pre-SAML app bindings, then (only behind the explicit--allow-mint-credentialopt-in) a temporary SAML disable/mint/restore cycle. --format table|json|json-compact|csvand--save/result showon every row-returning command.- Found and fixed during real-backend verification (not just the fake-backed test suite): a
rangefilter on thetimefield must not carryunmapped_type— that option is sort-only; OpenSearch rejects it onrangewith aparsing_exception. Sort clauses elsewhere correctly keep it, since some rotatedmetrics-otel-v1-*indices lack a mappeddatefield.
- Read-only query CLI over container CPU/RAM/filesystem and custom OTel gauge/sum/histogram
metrics already ingested into SAP Cloud Logging's OpenSearch backend (
Others providing
Compare all 6 on the Telemetry page →- SAP Cloud LoggingSAP Cloud LoggingAn instance-based observability service built on OpenSearch that stores and analyses logs, metrics and traces from Cloud Foundry, Kyma, Kubernetes and other runtime environments.sap official, btpOpenSearch-based store for logs, metrics and traces
- Agent Activation for Dynatrace Service for SAP BTPAgent Activation for Dynatrace Service for SAP BTPInjects the Dynatrace agent into Java applications on SAP BTP, so their metrics, events and end-to-end traces go to a Dynatrace SaaS monitoring environment the customer already runs.sap official, btpInjects the Dynatrace agent into Java applications on SAP BTP
- SAP Monitoring Service for SAP BTPSAP Monitoring Service for SAP BTPMetrics, availability and JMX checks, and email or webhook alerts for Java, HTML5 and SAP HANA XS applications and database systems, in Neo regions of SAP BTP.sap official, btpMetrics, availability checks and alerts for apps in Neo regions
- @sap/e2e-trace@sap/e2e-traceSAP Passport handling for a Node.js component: it reads the identifiers a request arrives with, updates them for the next system, and writes a statistics record per request to standard output.sap official, btpSAP Passport handling for Node.js components
- @saptools/cf-otel@saptools/cf-otelA read-only command line that queries the OpenTelemetry trace spans SAP Cloud Logging has already ingested and ranks the spans of a trace by self-time.independent, btpRead-only query tool for OpenTelemetry spans in SAP Cloud Logging
Details
Sources
Sources are graph entities: the same page may back several entries, and an unreachable source flags every row that rests on it.
Compatibility and provenance
History in the atlas
- First seen.
For your agent
- Ref
artifact:saptools-cf-metrics- JSON
- /artifacts/saptools-cf-metrics.json
- MCP
- Connecting an agent