CLICLItypeA command you run, rather than a dependency you import.See every one →What this type is →independently maintainedindependently maintainedoriginPublished by one person or a small company outside SAP. Says nothing about quality; the readings say that.See every one →

@saptools/cf-metrics

A read-only command line that queries the container and OpenTelemetry metrics SAP Cloud Logging 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 Foundry keeps only a few minutes of that history. This command line reads the metrics-* index of SAP Cloud Logging 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

Latestv0.13.1ReleasedLast pushLicenceMIT

Where it sits

Lives in
Provides
TelemetryBTPTelemetrycapabilitySeeing what a running service did — traces across handlers and database calls, metrics, and correlation that survives an async boundary.
Runs
Cloud Foundry
Requires
SAP Cloud LoggingSAP Cloud LoggingartifactEvery query reads the metric documents stored in the OpenSearch backend of a Cloud Logging instance, through a dashboards credential found on that instance.

Who is behind it

Đông TrầnĐông TrầnPersonMaintains this
In the atlas: maintains 18, contributes to 18
@dongitran
maintainer

What it declares

Every artifact

Type
CLI
Origin
Independently maintained
Runs on
Cloud Foundry

As a CLI

Distributed through
npm

Connections2

Install

npm add @saptools/cf-metrics

Derived from the package name. No recipe recorded; the README has the configuration.

Where @saptools/cf-metrics runsYour SAP BTP subaccountBy its type: a CLI is run from a terminalcallsdocumentedrequiresYour terminal, actorYour terminal@saptools/cf-metrics, module@saptools/cf-metricsSAP Cloud Logging, service instanceSAP Cloud Logging
SAPnot SAP's, or the project's ownstated in the documentdocumented only

README

quoted from dongitran/saptools under MIT, read
dongitran/saptools/packages/cf-metrics/README.mdOn GitHub

@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:

  1. Your own cf session, when it already matches. If cf target points at the requested org/space, the read-only discovery commands run in that session as-is. It is never modified: cf api, cf auth and cf target -o -s refuse to run in it by construction, and the target is re-checked afterwards so a cf target in another terminal mid-run cannot hand back a credential from the wrong space. Nothing else is needed — no SAP_EMAIL/SAP_PASSWORD.
  2. 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 target in a temporary CF_HOME and reads SAP_EMAIL / SAP_PASSWORD from 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:

  1. Service keys, newest first (--service-key, repeatable, to pin specific ones). Keys are created deliberately, so age says nothing about whether one predates SAML.
  2. 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.
  3. 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.

ControlEffect
--verbosenames the cached credential's source and instance on a hit
--refresh-credentialignore the cached entry, rediscover, and replace it
CF_METRICS_CREDENTIAL_CACHE=0never read or write the cache (false/off/no also work)
cf-metrics credential listwhat is cached — target, instance, source, endpoint, expiry; never the username or password
cf-metrics credential clearforget 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.

ControlEffect
SAPTOOLS_AUTO_UPDATE=on|notify|offon (default) installs and re-runs; notify prints the manual command once per version; off never checks. Applies to every @saptools CLI.
CF_METRICS_AUTO_UPDATEsame values, this CLI only; wins over the global variable
SAPTOOLS_UPDATE_INTERVAL_MINUTESminutes between checks (default 60; 0 checks on every run)
SAPTOOLS_NPM_REGISTRYregistry to check and install from (default: npm's configured registry, then npmjs)
SAPTOOLS_ROOTrelocate ~/.saptools for every @saptools CLI at once (CF_METRICS_SAPTOOLS_ROOT relocates only this CLI's files)
SAPTOOLS_UPDATE_DEBUG=1explain 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

CommandPurpose
sampleDump the N most recent full metric documents, unfiltered — start here when you know nothing yet.
mappingField-type discovery (keyword vs text) on any --index pattern (default metrics-*).
fieldsList every flat attribute key on a sample metric document.
namesWhich metric names exist for a service/time-range, with kind, unit, and doc count.
historyTime-bucketed values for one or more metric names, kind-aware — the core command.
snapshotLatest 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.
topCross-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.
watchPoll 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|clearInspect results saved via --save.
credential list|clearInspect 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:

unitdescriptionmeaningmatches cf app columntypical value
1time used by an app instance per entitlementfraction of the app's CPU entitlementcpu entitlement0.278
cputime used by an app instance per single CPU corefraction of one CPU corecpu0.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

  • --unit exists on history and top only — the two commands that aggregate, and so the only two that can blend series. sample shows each document's own unit, names lists every unit a name reports, and snapshot shows the unit of the latest document; none of them accept the filter because none of them aggregate across units.
  • One --unit applies to every --name in the same history call. Combining names with different units (say container.cpu.usage --unit cpu alongside container.memory.usage, which is By) 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) — history on a HISTOGRAM metric reports count/sum/avg only. Approximating percentiles from explicitBounds/bucketCounts correctly 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.

MIT, read

History

Latest

v0.13.1

Fixes 1

Notes
Fixed
  • --service now reads "(required)" in --help for history, snapshot, watch, and names — the flag was already enforced at runtime, but its description never said so.
  1. v0.13.0

    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-otel and @saptools/cf-log-search. No CLI-facing behavior change; verified byte-identical output against the real tenant before and after.
  2. v0.12.0

    Changed 1, Fixes 9

    Notes
    Fixed
    • watch refused only a duration-shaped cursor, which left the two worse shapes through. Measured against the live backend: "24" is legal epoch_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 a parse_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 time is date_nanos, so --since …T00:00:00.000000002Z --until …T00:00:00.000000001Z was accepted and returned nothing at exit 0. It now compares at the precision the field stores, matching @saptools/cf-otel.

    • assertValidTimeRange skipped 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 nested parent is stored as a separate hidden document: measured live, a terms aggregation on buckets.count, exemplars.spanId or quantiles.value returns 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 new NESTED_IN column names which — listing them without saying so would have advertised fields no filter this package builds can reach.

    • --since/--until written 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:00 was refused as inverted although the backend sees a forward six-hour window, and --since 2026-09-05T12:00 --until 2026-09-05T08:00:00Z was 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's ZoneOffset caps there, measured exactly: +18:00 is accepted and +18:01 upward is not, along with +00:60 and +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>.keyword reported "was not found in the mapping" for a multi-field that genuinely exists and aggregates normally — metrics-* maps @timestamp.keyword and origin.keyword (measured live, both return real buckets). A multi-field hangs off a field's own fields block rather than under properties, and only the properties branch was walked.

    • mapping with no --field listed only top-level keys: 46 names for 87 leaf fields on the live pattern. resource appeared as a dead end while the nine resource.attributes.sap@cf@* fields beneath it — the ones worth filtering on — appeared nowhere, even though --field resolved each one by name. The listing now matches what the lookup already supported: 100 rows against the same index, containers included, so object versus nested is visible before a filter is written.

    • watch fed a cursor read straight from a document's time field back in as --since. A value shaped like a duration turned the cursor into a relative window that re-resolved against now every 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 real date_nanos mapping, but this package deliberately tolerates a rotated index where time is unmapped, and _source is raw.

    Changed
    • The mapping lookup moved from cli/commands/mapping.ts into src/mapping.ts, and lookUpField, listAllFieldNames, assertValidTimeBoundShape, assertValidTimeRange and isAbsoluteInstant are now exported. buildMetricBoolQuery forwards 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_above bug and a missing implicit-object check both escaped the suite.
  3. v0.11.0

    Changed 1, Fixes 11

    Notes
    Fixed
    • mapping --field on a field alias reported alias, which is true and useless. metrics-* maps nine of them (measured): short names like app_name pointing at resource.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 new ALIAS_OF column. An alias whose target is missing, malformed, or itself an alias still reports alias, 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 ambiguous rather than unknown for a field the indices disagree about.

    • A divergent ignore_above across backing indices is no longer reported as whichever index answered first. A keyword longer 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 make mapping --field call the field missing.

    • A failed --allow-mint-credential abandoned the service key it had already created. Every step after cf create-service-key can 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 list could 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. history now 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 00 were reported as a shape problem ("expected a relative duration or an ISO-8601 timestamp") rather than as the calendar errors they are, because Date.parse rejected 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. history and top now collect their warnings and print them after the callback returns.

    • watch replayed its whole --lookback window 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.ts and cf.ts rather than restated in each. 0.10.0 claimed to have brought them into line and had not: cf.ts still covered a narrower set, so apiToken was redacted on one path and printed in full on the other.

    Changed
    • mapping output carries a fourth column, ALIAS_OF, empty for a concrete field.
  4. v0.10.0

    Changed 1, Fixes 9

    Notes
    Fixed
    • history emitted 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 --name printed 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-credential re-minted on every run. --service-key/--fallback-binding-app each restrict only their own candidate type during discovery, but the cache read them as restricting both, so --fallback-binding-app x alone 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-credential went 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 clear left the password on disk. Writes go through credentials.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, and credential list reads 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/--until and 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 --name per run is gone with it.

    • --save threw 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 so ref=$(… --save) cannot silently bind a table row.

    • Calendar-invalid dates were accepted and forwarded. Date.parse rolls 2026-02-30 to 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 of T, 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 bare RangeError: Invalid time value, and a bare number now explains both readings it could have had.

    • An interrupted --save stranded a <ref>.tmp-<pid> directory that prune, list and clear all walked past, because the ref pattern they filter on never matches it. They accumulated permanently; prune now removes them.

    • --verbose printed a service instance's clientSecret and apiToken in full. The redaction list in saml-toggle.ts covered only private/password/signature while the exec layer's own covered more; both now cover the same classes.

    • CF_COLOR is forced off for every cf invocation. An inherited CF_COLOR=true makes the CLI emit ANSI escapes even when piped, and these parsers key off literal text — including the status: 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/resolveTopMetricKind take an optional lookup window; KindLookupWindow is exported alongside KindResolution.
  5. v0.9.0

    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 marks timed_out. That is worse here than in a client that reads raw documents: names, snapshot, top and history never read hits at 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. Set CF_METRICS_ALLOW_PARTIAL_SHARDS=1 to accept a partial answer when a shard is persistently down.

    • watch could 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 the since: cursor filter 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 kind silently lost all but the most common one. resolveMetricKind asked for a single terms bucket, 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. history and top now warn and name every kind found (pass --kind to pick one), and names lists them the way it already listed multiple units.

    • mapping --field could not resolve any field whose name contains a dot. The _source key 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 whole resource.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_MS above Node's timer ceiling silently did the opposite of what it says. AbortSignal.timeout reduces any delay past 2^31-1 to 1ms (emitting only a TimeoutOverflowWarning) and throws a RangeError past 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".
    • resolveMetricKind returns { kind, otherKinds } instead of a bare kind string, so a caller can see the ambiguity the CLI warns about. KindResolution is exported alongside it.
  6. v0.8.0

    Changed 2, Fixes 10, Other 3

    Notes
    Fixed
    • Pruning no longer deletes saved results it cannot read. pruneResultSessions runs 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, then rm -rf'd the directory. So a nominally read-only result list destroyed a valid unexpired session whose only difference was a newer version, 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 expiresAt is no longer immortal. Date.parse returns NaN for it and NaN <= now is false, so such a session survived every prune for ever and still read back. Expiry now falls back to createdAt plus ttlMinutes, 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 show and result list keep working on intact sessions even when the results directory cannot be listed. result show also enforces the TTL itself instead of relying on that prune having succeeded.

    • result show now 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 the RESULT_UNREADABLE error code.

    • result list reports the ref that actually resolves. The listed ref came from inside the manifest while lookup uses the directory name; when those disagreed, list advertised a ref that result show rejected. Expired sessions that prune could not remove are omitted rather than shown.

    • Pruning no longer deletes a saved result whose expiry it cannot establish. resolveExpiryMillis previously 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 Temporal ZonedDateTime, an ISO week date, epoch millis as a string, or canonical ISO with surrounding whitespace, which Date.parse does 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 ttlMinutes too large to date no longer produces an immortal session. The bound is what a Date can hold, not what a float can hold: Number.isSafeInteger admits a value whose product with 60000 reaches 5.4e20, while a Date holds at most ±8.64e15 ms — so createdAt + ttlMinutes could yield a finite expiry no clock will ever reach, which survived every prune and was not even counted. A derived expiry beyond the Date range is now treated as unresolvable, hence retained.

    • result prune names the refs it left in place instead of only counting them. A retained session is omitted from result list and no command removes one, so the ref is the only way to find the file.

    • result prune exits 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 meant if cf-metrics result prune; then … could never detect a store it had failed to clean.

    • PruneOutcome is now { removed, failed, retainedRefs }. retained was a count the user could not act on; the refs replace it.

    Changed
    • pruneResultSessions returns { removed, retained, failed } instead of a bare count. result prune still prints removed=N as its only stdout line and reports retained and undeletable counts on stderr.
    • The PruneOutcome type is exported.
    Known limitations
    • A leftover <ref>.tmp-<pid> directory from an interrupted save is invisible to result list, result prune and result clear, and no TTL reaches it. Reclaim it by hand.
    • A retained session is reported by result prune but not listed, and there is no result rm; result clear is the only in-tool removal and it removes everything it can see.
    • result list omits a session it could not read without saying so. Only result prune reports those.
  7. v0.7.1

    Other 1

    Notes
    • README: document SAPTOOLS_ROOT next to the other update controls. No code change; this release also exercises the new self-update path on installs of 0.7.0.
  8. v0.7.0

    Other 2

    Notes
    • Self-updating. Every command now checks npm for a newer @saptools/cf-metrics (at most once an hour: one 18-byte dist-tags request 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 then cf-metrics: updated to 0.7.1; re-running the command on 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-CLI CF_METRICS_AUTO_UPDATE), SAPTOOLS_UPDATE_INTERVAL_MINUTES, SAPTOOLS_NPM_REGISTRY and SAPTOOLS_UPDATE_DEBUG control it; it is off by itself in CI, in tests, from a source checkout or npm link, under npx, and inside the re-run. New cf-metrics self-update [--check] forces the check and install now.
    • --version reads package.json at runtime instead of a hand-maintained constant. Both this and the updater come from the new private @saptools/core package, bundled into dist/cli.js at build time so every @saptools CLI can share one implementation and one test suite; the published tarball gains no runtime dependency.
  9. v0.6.0

    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 names command 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 target cost about 6s together, while cf services alone 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 cf session is reused instead of logging in again. When cf target already points at the requested org/space (the normal state after cf login), the read-only discovery commands run in that session as-is — the same pattern cf-hana uses for bare app names. The session is never modified: cf api/cf auth/cf target -o -s refuse to run in it by construction, and the target is re-read after discovery so a cf target in another terminal mid-run cannot hand back a credential from the wrong space. SAP_EMAIL/SAP_PASSWORD are 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 services is gone. The Cloud Logging instance is found through one cf space --guid plus one GET /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 no cf spawn at all — a warm names is the OpenSearch query and nothing else. --verbose names the cached source; --refresh-credential rediscovers and replaces the entry; CF_METRICS_CREDENTIAL_CACHE=0 disables reading and writing; cf-metrics credential list shows what is cached (target, instance, source, endpoint, expiry — never the username or password) and cf-metrics credential clear forgets 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; watch now surfaces that rejection instead of retrying it every interval forever. --service-key/--fallback-binding-app pins 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-hana keeps HANA bindings live-only: this is the same trade cf-xsuaa already makes for XSUAA client secrets in ~/.saptools/xsuaa-data.json, with the same 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 it, it only saves re-obtaining it thirty seconds at a time. The opt-out and credential clear exist for anyone who weighs it differently.
    • Fixed on the way: the --allow-mint-credential path read the freshly minted key with cf service-key, whose CLI v8 output wraps the fields in a credentials object 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: discoverDashboardsCredential takes SapCredentials | undefined and the returned DashboardsCredential carries the resolved instance; discoverServiceInstance/ listCloudLoggingInstances take the space name and return { name, guid }; findBoundApps, cfServices and parseServicesTable are removed with the cf services path; the credential-cache functions are exported. CfMetricsError gains an optional status, with isAuthRejection() to test for 401/403. The test-only results-root override CF_METRICS_RESULTS_ROOT is renamed CF_METRICS_SAPTOOLS_ROOT, since it now also relocates the credential cache.
  10. v0.5.0

    Other 3

    Notes

    Two robustness fixes, both reproduced against the real backend before being changed.

    • Every OpenSearch request now has a deadline. fetch was 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 the cf exec layer already enforced), overridable with CF_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: only watch registered a signal listener, and the other eight commands left the directory behind. Once cf auth has run it holds .cf/config.json with 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.
  11. v0.4.0

    Other 7

    Notes
    • snapshot no longer silently caps at 50 metric names. It sent a terms aggregation 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, a terms aggregation with no explicit order selects buckets by doc_count descending, 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.
    • snapshot now takes --limit, following the same convention names and top already use (0 means all, values above OpenSearch's result-window ceiling are rejected up front).
    • names gets the same treatment, because it has the identical bug: its terms aggregation also carries no explicit order, 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. top deliberately does not warn: its cut is an explicit ordered 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 --limit names 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-otel already 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 — snapshot was the only row-returning command with no way to see all of its own data.
    • API change for library consumers: querySnapshot and queryNames now return { rows, truncated } instead of bare row arrays. SnapshotResult and NamesResult are exported.
    • Known limitation, unchanged by this release: the notice is not persisted by --save, so result show <ref> on a truncated saved result gives no hint that names were dropped. The same applies to top's multi-unit warning; storing notices alongside rows needs a result-store schema change and is deliberately left for a separate release.
  12. v0.3.1

    Other 4

    Notes
    • An inverted --since/--until window is now rejected instead of returning an empty table. OpenSearch accepts {gte: <later>, lte: <earlier>} and simply matches nothing, so --since 30m --until 2h exited 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 --since to a recent window inverted it without anything contradictory being typed. --until 3h on 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 3h still works, an equal start and end is still allowed as a legal point query, and sample, which leaves the start unbounded, still accepts --until on 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.
  13. v0.3.0

    Other 7

    Notes

    Credential discovery now asks the Cloud Controller v3 API directly instead of scraping cf output.

    • Root cause of the tool's slowness, now fixed. cf service-keys output is a three-column table, but the parser matched a header line equal to exactly name, 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 one cf env per 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=app lists every service key and app binding on the instance in one call, and …/<guid>/details returns 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 names command 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-app filter 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, parseServiceKeyNames and extractVcapServices internals along with the old VCAP parsing path. None were part of the package's public exports.
  14. v0.2.0

    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.

    • --unit on history and top, plus an automatic multi-unit warning. A metric name does not always identify one series: container.cpu.usage is published as two series under a single name — unit="1" (fraction of the app's CPU entitlement, matching cf app's cpu entitlement column) and unit="cpu" (fraction of one CPU core, matching cf app's cpu column) — 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 inverted top'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 --unit to select one series. Verified across 300 documents spanning 12 metric names that container.cpu.usage is the only affected name; HISTOGRAM, SUM and memory metrics are all single-unit.
    • names now 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.
    • history with several --name flags now emits one result instead of one per name. It previously wrote an independent document per metric, so --format json produced two concatenated arrays — not parseable JSON — and --format csv grew a second header row midway, both while still exiting 0. Combined rows carry a NAME column; a single --name keeps its original shape.
    • top now defaults --since to 2h, as history and names already did. It previously sent no time filter at all when --since was omitted, aggregating across the entire retention window for every app in the space.
    • An invalid --kind on history/top now 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 range clause carrying unmapped_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.
  15. v0.1.0

    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-otel uses for trace spans, applied to the metrics index instead.
    • Nine commands: sample, mapping, fields, names, history, snapshot, top, watch, plus a result subtree (show/list/prune/clear) for refs saved via --save.
    • history is 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.
    • top ranks every app in the targeted space by one metric, not just one service — deliberately has no --service filter.
    • watch polls for new points and prints them as they land (--json for 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-credential opt-in) a temporary SAML disable/mint/restore cycle.
    • --format table|json|json-compact|csv and --save/result show on every row-returning command.
    • Found and fixed during real-backend verification (not just the fake-backed test suite): a range filter on the time field must not carry unmapped_type — that option is sort-only; OpenSearch rejects it on range with a parsing_exception. Sort clauses elsewhere correctly keep it, since some rotated metrics-otel-v1-* indices lack a mapped date field.
0downloads per week, npm
Weekly downloads for @saptools/cf-metrics26 complete calendar weeks from 2026-04-27 to 2026-10-25; the latest total is 0.27 Apr – 3 May: 04 May – 10 May: 011 May – 17 May: 018 May – 24 May: 025 May – 31 May: 01 Jun – 7 Jun: 08 Jun – 14 Jun: 015 Jun – 21 Jun: 022 Jun – 28 Jun: 029 Jun – 5 Jul: 06 Jul – 12 Jul: 013 Jul – 19 Jul: 020 Jul – 26 Jul: 027 Jul – 2 Aug: 03 Aug – 9 Aug: 010 Aug – 16 Aug: 017 Aug – 23 Aug: 024 Aug – 30 Aug: 031 Aug – 6 Sept: 1,8547 Sept – 13 Sept: 54914 Sept – 20 Sept: 6121 Sept – 27 Sept: 3628 Sept – 4 Oct: 125 Oct – 11 Oct: 2212 Oct – 18 Oct: 2219 Oct – 25 Oct: 0
Telemetry
  1. SAP Cloud LoggingOpenSearch-based store for logs, metrics and traces
  2. Agent Activation for Dynatrace Service for SAP BTPInjects the Dynatrace agent into Java applications on SAP BTP
  3. SAP Monitoring Service for SAP BTPMetrics, availability checks and alerts for apps in Neo regions
  4. @sap/e2e-traceSAP Passport handling for Node.js components
  5. @saptools/cf-otelRead-only query tool for OpenTelemetry spans in SAP Cloud Logging

Details

Sources4
Added by handdiscovered herechecked
npm registry, @saptools/cf-metricsversion, releases, installschecked
github.com/git+https://github.com/dongitran/saptools.gittree, pushes, contributors, CIchecked
packages/cf-metrics/README.mdquoted, and what the entry declareschecked

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
deploymentCloud Foundry
latestv0.13.1
first release
releases and cadence16, ~0d median
discovered viaAdded by hand
first seen in the atlas
ecosystem and domainbtp, operations-and-runtime
History in the atlas1
  1. First seen.
For your agent
Ref
artifact:saptools-cf-metrics
JSON
/artifacts/saptools-cf-metrics.json
MCP
Connecting an agent