mirror of
https://github.com/openclaw/clawhub.git
synced 2026-08-14 00:47:57 +00:00
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
352 lines
18 KiB
Bash
Executable File
352 lines
18 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# metrics-info: Discover metrics, tags, and tag values in a dataset
|
|
#
|
|
# Usage:
|
|
# metrics-info <deployment> <dataset> metrics [--by-type] [--type T]... [--start T --end T]
|
|
# metrics-info <deployment> <dataset> metrics <metric> info [--start T --end T]
|
|
# metrics-info <deployment> <dataset> metrics <metric> describe [--no-values] [--values-limit N] [--start T --end T]
|
|
# metrics-info <deployment> <dataset> metrics <metric> tags [--start T --end T]
|
|
# metrics-info <deployment> <dataset> metrics <metric> tags <tag> values [--start T --end T]
|
|
# metrics-info <deployment> <dataset> metrics <metric> tags <tag> type [--start T --end T]
|
|
# metrics-info <deployment> <dataset> tags [--start T --end T]
|
|
# metrics-info <deployment> <dataset> tags <tag> values [--start T --end T]
|
|
# metrics-info <deployment> <dataset> find-metrics <search-value> [--start T --end T]
|
|
#
|
|
# Metric metadata. The `metrics` listing returns a v2 payload where each entry
|
|
# carries `type` (Gauge | CounterMonotonic | CounterNonMonotonic | Histogram),
|
|
# `temporality` (Cumulative | Delta | null), and `unit` (UCUM string or null).
|
|
# Use this metadata to choose the right MPL query shape (consult metrics-spec
|
|
# for the exact operator names per type).
|
|
#
|
|
# --by-type Group the listing by `type` instead of returning a flat
|
|
# name->meta object. Pure client-side reshape; no extra
|
|
# server call.
|
|
# --type T Filter the listing to entries whose `type` equals T.
|
|
# Repeatable; multiple --type flags act as OR.
|
|
# Composes with --by-type.
|
|
#
|
|
# `metrics <metric> info` extracts a single metric's metadata block
|
|
# ({type, temporality, unit}). Non-zero exit if the metric is absent.
|
|
#
|
|
# `metrics <metric> describe` bundles metadata + tags + tag values for one
|
|
# metric in a single call (replaces the typical 1+1+N round trips).
|
|
# --no-values Return tag names only, not values (1+1 calls).
|
|
# --values-limit N Cap each tag's value list at N entries (default 50;
|
|
# 0 = no limit). Applied client-side after fetch.
|
|
#
|
|
# `metrics <metric> tags <tag> type` probes whether a tag is int/float/string/
|
|
# bool-typed by running `metrics-query` with `filter <tag> is <T>` for each.
|
|
# Returns:
|
|
# {"type": "int", "present_types": ["int"]}
|
|
# {"type": "mixed", "present_types": ["int","string"]}
|
|
# {"type": "absent", "present_types": []}
|
|
# (mixed = the tag carries different types across rows; use the defensive
|
|
# `(tag is int and tag == 200) or (tag is string and tag == "200")` form.)
|
|
#
|
|
# find-metrics searches TAG VALUES, not metric names. Use it when you know a
|
|
# specific entity name (service, host, device) to find which metrics carry it.
|
|
# To list metric names, use the `metrics` subcommand instead.
|
|
#
|
|
# --start and --end accept RFC3339 (offsets allowed, e.g. 2025-06-01T00:00:00+02:00)
|
|
# or relative now / now-<N><unit> with <unit> in s/m/h/d/w, resolved to RFC3339 UTC
|
|
# client-side because the info endpoints only parse RFC3339. This is narrower than
|
|
# metrics-query, which forwards times to the server unparsed and also accepts forms
|
|
# like now-1y; here anything outside now / now-<N>[smhdw] must already be RFC3339.
|
|
# Defaults: last 24 hours.
|
|
# For sparse metrics (sensors, batch jobs), try --start with a wider range (e.g. 7 days).
|
|
#
|
|
# Examples:
|
|
# metrics-info prod my-dataset metrics # Raw listing
|
|
# metrics-info prod my-dataset metrics --by-type # Grouped by type
|
|
# metrics-info prod my-dataset metrics --type Histogram # Only histograms
|
|
# metrics-info prod my-dataset metrics --type Gauge --type Histogram --by-type
|
|
# metrics-info prod my-dataset metrics http.server.duration info # Single metric meta
|
|
# metrics-info prod my-dataset metrics http.server.duration describe # Bundle: meta + tags + values
|
|
# metrics-info prod my-dataset metrics http.server.duration describe --no-values
|
|
# metrics-info prod my-dataset metrics http.server.duration tags code type
|
|
# metrics-info prod my-dataset tags service.name values # List values for a tag
|
|
# metrics-info prod my-dataset find-metrics "frontend" # Find metrics with tag value "frontend"
|
|
# metrics-info prod my-dataset metrics http.server.duration tags --start 2025-06-01T00:00:00Z --end 2025-06-02T00:00:00Z
|
|
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
|
|
# Percent-encode one URL component (path segment or query value). Dataset,
|
|
# metric, and tag names are user/OTel-controlled and may contain characters
|
|
# that are reserved in URLs (/ % + space); times may carry a `+02:00` offset
|
|
# whose `+` would otherwise decode as a space server-side.
|
|
urlencode() {
|
|
jq -rn --arg v "$1" '$v|@uri'
|
|
}
|
|
|
|
# Normalize a time argument to RFC3339 UTC. RFC3339 input passes through
|
|
# verbatim; the relative forms `now` and `now-<N><unit>` (unit in s/m/h/d/w)
|
|
# are resolved client-side because the info endpoints only parse RFC3339.
|
|
# Note: metrics-query forwards times to the server unparsed, so it accepts a
|
|
# broader set (e.g. now-1y); those forms are NOT handled here and, if passed,
|
|
# fall through to the RFC3339-only endpoint and fail.
|
|
normalize_time() {
|
|
local t="$1"
|
|
if [[ "$t" == "now" ]]; then
|
|
date -u '+%Y-%m-%dT%H:%M:%SZ'
|
|
elif [[ "$t" =~ ^now-([0-9]+)([smhdw])$ ]]; then
|
|
local n="${BASH_REMATCH[1]}" u="${BASH_REMATCH[2]}"
|
|
if date --version &>/dev/null; then
|
|
local word
|
|
case "$u" in
|
|
s) word="seconds" ;;
|
|
m) word="minutes" ;;
|
|
h) word="hours" ;;
|
|
d) word="days" ;;
|
|
w) word="weeks" ;;
|
|
esac
|
|
date -u -d "$n $word ago" '+%Y-%m-%dT%H:%M:%SZ'
|
|
else
|
|
# BSD date: -v units are case-sensitive (M = minute, m = month).
|
|
local unit
|
|
case "$u" in
|
|
s) unit="S" ;;
|
|
m) unit="M" ;;
|
|
h) unit="H" ;;
|
|
d) unit="d" ;;
|
|
w) unit="w" ;;
|
|
esac
|
|
date -u -v "-${n}${unit}" '+%Y-%m-%dT%H:%M:%SZ'
|
|
fi
|
|
else
|
|
printf '%s\n' "$t"
|
|
fi
|
|
}
|
|
|
|
show_usage() {
|
|
echo "Usage:" >&2
|
|
echo " metrics-info <deploy> <dataset> metrics [--by-type] [--type T]..." >&2
|
|
echo " metrics-info <deploy> <dataset> metrics <metric> info" >&2
|
|
echo " metrics-info <deploy> <dataset> metrics <metric> describe [--no-values] [--values-limit N]" >&2
|
|
echo " metrics-info <deploy> <dataset> metrics <metric> tags" >&2
|
|
echo " metrics-info <deploy> <dataset> metrics <metric> tags <tag> values" >&2
|
|
echo " metrics-info <deploy> <dataset> metrics <metric> tags <tag> type" >&2
|
|
echo " metrics-info <deploy> <dataset> tags" >&2
|
|
echo " metrics-info <deploy> <dataset> tags <tag> values" >&2
|
|
echo " metrics-info <deploy> <dataset> find-metrics <search-value> (searches tag values, not metric names)" >&2
|
|
echo "" >&2
|
|
echo "Options:" >&2
|
|
echo " --start T Start time (RFC3339 or relative, e.g. now-7d). Default: 24h ago" >&2
|
|
echo " --end T End time (RFC3339 or relative, e.g. now). Default: now" >&2
|
|
echo " --by-type (metrics listing) Group entries by metric type" >&2
|
|
echo " --type T (metrics listing) Filter to type T. Repeatable." >&2
|
|
echo " --no-values (describe) Return tag names only" >&2
|
|
echo " --values-limit N (describe) Cap each tag's value list at N (default 50; 0 = unlimited)" >&2
|
|
exit 1
|
|
}
|
|
|
|
DEPLOYMENT="${1:-}"
|
|
DATASET="${2:-}"
|
|
|
|
if [[ -z "$DEPLOYMENT" || -z "$DATASET" ]]; then
|
|
show_usage
|
|
fi
|
|
|
|
shift 2
|
|
|
|
# Collect positional args and parse --start/--end, listing-view, describe flags.
|
|
POSITIONAL=()
|
|
START=""
|
|
END=""
|
|
BY_TYPE=0
|
|
TYPE_FILTERS=()
|
|
NO_VALUES=0
|
|
VALUES_LIMIT=50
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--start) START="$2"; shift 2 ;;
|
|
--end) END="$2"; shift 2 ;;
|
|
--by-type) BY_TYPE=1; shift ;;
|
|
--type) TYPE_FILTERS+=("$2"); shift 2 ;;
|
|
--no-values) NO_VALUES=1; shift ;;
|
|
--values-limit) VALUES_LIMIT="$2"; shift 2 ;;
|
|
*) POSITIONAL+=("$1"); shift ;;
|
|
esac
|
|
done
|
|
|
|
# Default time range: last 24 hours. Relative forms are resolved to RFC3339 UTC.
|
|
START=$(normalize_time "${START:-now-24h}")
|
|
END=$(normalize_time "${END:-now}")
|
|
|
|
TIME_PARAMS="start=$(urlencode "$START")&end=$(urlencode "$END")"
|
|
BASE="/v1/query/metrics/info/datasets/$(urlencode "$DATASET")"
|
|
|
|
# Resolve the regional edge URL for this dataset
|
|
RESOLVED_URL=$("$SCRIPT_DIR/resolve-url" "$DEPLOYMENT" "$DATASET" 2>/dev/null || true)
|
|
if [[ -n "$RESOLVED_URL" ]]; then
|
|
export AXIOM_URL_OVERRIDE="$RESOLVED_URL"
|
|
fi
|
|
|
|
if [[ ${#POSITIONAL[@]} -eq 0 ]]; then
|
|
show_usage
|
|
fi
|
|
|
|
# Fetch the bulk metrics listing (v2 payload with type/temporality/unit per metric).
|
|
# Used directly by `metrics` and indirectly by `metrics <metric> info`.
|
|
fetch_metrics_listing() {
|
|
AXIOM_ACCEPT="application/vnd.metrics-info.v2+json" "$SCRIPT_DIR/axiom-api" "$DEPLOYMENT" GET "${BASE}/metrics?${TIME_PARAMS}"
|
|
}
|
|
|
|
case "${POSITIONAL[0]}" in
|
|
metrics)
|
|
if [[ ${#POSITIONAL[@]} -eq 1 ]]; then
|
|
# List metrics. Default behaviour returns the raw v2 payload byte-for-byte;
|
|
# --type / --by-type apply pure client-side reshapes via jq.
|
|
if [[ $BY_TYPE -eq 0 && ${#TYPE_FILTERS[@]} -eq 0 ]]; then
|
|
fetch_metrics_listing
|
|
else
|
|
RAW=$(fetch_metrics_listing)
|
|
JQ_FILTER='.'
|
|
if [[ ${#TYPE_FILTERS[@]} -gt 0 ]]; then
|
|
# Build a JSON array of allowed types and filter entries.
|
|
TYPES_JSON=$(printf '%s\n' "${TYPE_FILTERS[@]}" | jq -R . | jq -s .)
|
|
JQ_FILTER='to_entries | map(select(.value.type as $vt | ($types | index($vt)))) | from_entries'
|
|
if [[ $BY_TYPE -eq 1 ]]; then
|
|
JQ_FILTER="$JQ_FILTER"' | to_entries | group_by(.value.type) | map({(.[0].value.type): (map({(.key): {temporality: .value.temporality, unit: .value.unit}}) | add)}) | add // {}'
|
|
fi
|
|
printf '%s' "$RAW" | jq --argjson types "$TYPES_JSON" "$JQ_FILTER"
|
|
else
|
|
# --by-type only
|
|
JQ_FILTER='to_entries | group_by(.value.type) | map({(.[0].value.type): (map({(.key): {temporality: .value.temporality, unit: .value.unit}}) | add)}) | add // {}'
|
|
printf '%s' "$RAW" | jq "$JQ_FILTER"
|
|
fi
|
|
fi
|
|
elif [[ ${#POSITIONAL[@]} -eq 3 && "${POSITIONAL[2]}" == "info" ]]; then
|
|
# Single-metric metadata: {type, temporality, unit}. Derived client-side
|
|
# from the bulk listing because there is no per-metric metadata endpoint.
|
|
METRIC="${POSITIONAL[1]}"
|
|
RAW=$(fetch_metrics_listing)
|
|
# -e exits non-zero when the value is null/false; we want non-zero on missing.
|
|
printf '%s' "$RAW" | jq -e --arg m "$METRIC" '.[$m] // error("metric not found in listing for the given time range: " + $m)'
|
|
elif [[ ${#POSITIONAL[@]} -eq 3 && "${POSITIONAL[2]}" == "describe" ]]; then
|
|
# Bundle metadata + tags + tag values into a single output. Replaces
|
|
# the typical 1+1+N round trips an agent would make to characterise
|
|
# an unfamiliar metric.
|
|
METRIC="${POSITIONAL[1]}"
|
|
METRIC_ENC=$(urlencode "$METRIC")
|
|
RAW=$(fetch_metrics_listing)
|
|
META=$(printf '%s' "$RAW" | jq -e --arg m "$METRIC" '.[$m] // error("metric not found in listing for the given time range: " + $m)')
|
|
TAGS_JSON=$("$SCRIPT_DIR/axiom-api" "$DEPLOYMENT" GET "${BASE}/metrics/${METRIC_ENC}/tags?${TIME_PARAMS}")
|
|
if [[ "$NO_VALUES" -eq 1 ]]; then
|
|
# tags as flat array of names
|
|
jq -n --argjson m "$META" --argjson tags "$TAGS_JSON" '$m + {tags: $tags}'
|
|
else
|
|
# tags as object: { tag_name: [values…] }. Per-tag value fetches
|
|
# are independent, so run them concurrently; tag counts are small
|
|
# (rarely more than a few dozen), so no concurrency cap is needed.
|
|
TAG_NAMES=()
|
|
while IFS= read -r tag; do
|
|
[[ -z "$tag" ]] && continue
|
|
TAG_NAMES+=("$tag")
|
|
done < <(printf '%s' "$TAGS_JSON" | jq -r '.[]?')
|
|
VALUES_OBJ='{}'
|
|
if [[ ${#TAG_NAMES[@]} -gt 0 ]]; then
|
|
TMP_DIR=$(mktemp -d "${TMPDIR:-/tmp}/metrics-info.XXXXXX")
|
|
trap 'rm -rf "$TMP_DIR"' EXIT
|
|
PIDS=()
|
|
for i in "${!TAG_NAMES[@]}"; do
|
|
"$SCRIPT_DIR/axiom-api" "$DEPLOYMENT" GET \
|
|
"${BASE}/metrics/${METRIC_ENC}/tags/$(urlencode "${TAG_NAMES[$i]}")/values?${TIME_PARAMS}" \
|
|
> "$TMP_DIR/$i.json" &
|
|
PIDS+=($!)
|
|
done
|
|
FETCH_FAILED=0
|
|
for i in "${!PIDS[@]}"; do
|
|
if ! wait "${PIDS[$i]}"; then
|
|
echo "Error: failed to fetch values for tag '${TAG_NAMES[$i]}'" >&2
|
|
FETCH_FAILED=1
|
|
fi
|
|
done
|
|
if [[ "$FETCH_FAILED" -eq 1 ]]; then
|
|
exit 1
|
|
fi
|
|
for i in "${!TAG_NAMES[@]}"; do
|
|
VALUES=$(cat "$TMP_DIR/$i.json")
|
|
if [[ "$VALUES_LIMIT" -gt 0 ]]; then
|
|
VALUES=$(printf '%s' "$VALUES" | jq --argjson n "$VALUES_LIMIT" '.[:$n]')
|
|
fi
|
|
VALUES_OBJ=$(jq -n --argjson o "$VALUES_OBJ" --arg t "${TAG_NAMES[$i]}" --argjson v "$VALUES" '$o + {($t): $v}')
|
|
done
|
|
fi
|
|
jq -n --argjson m "$META" --argjson tags "$VALUES_OBJ" '$m + {tags: $tags}'
|
|
fi
|
|
elif [[ ${#POSITIONAL[@]} -eq 3 && "${POSITIONAL[2]}" == "tags" ]]; then
|
|
# List tags for a metric
|
|
METRIC="${POSITIONAL[1]}"
|
|
"$SCRIPT_DIR/axiom-api" "$DEPLOYMENT" GET "${BASE}/metrics/$(urlencode "$METRIC")/tags?${TIME_PARAMS}"
|
|
elif [[ ${#POSITIONAL[@]} -eq 5 && "${POSITIONAL[2]}" == "tags" && "${POSITIONAL[4]}" == "values" ]]; then
|
|
# List tag values for a metric+tag
|
|
METRIC="${POSITIONAL[1]}"
|
|
TAG="${POSITIONAL[3]}"
|
|
"$SCRIPT_DIR/axiom-api" "$DEPLOYMENT" GET "${BASE}/metrics/$(urlencode "$METRIC")/tags/$(urlencode "$TAG")/values?${TIME_PARAMS}"
|
|
elif [[ ${#POSITIONAL[@]} -eq 5 && "${POSITIONAL[2]}" == "tags" && "${POSITIONAL[4]}" == "type" ]]; then
|
|
# Probe the typing of a metric+tag by running `metrics-query` with
|
|
# `filter <tag> is <T>` for each candidate type. The type(s) that
|
|
# return non-empty `series` are present in the dataset for that
|
|
# metric+tag in the time window.
|
|
METRIC="${POSITIONAL[1]}"
|
|
TAG="${POSITIONAL[3]}"
|
|
PRESENT_JSON='[]'
|
|
for t in int float string bool; do
|
|
# Backtick-escape dataset / metric / tag names. The probe pipeline:
|
|
# `<dataset>`:`<metric>` | filter `<tag>` is <T> | align to 5m using sum
|
|
# If <tag> is <T> matches no rows, the response has empty `series`.
|
|
PROBE_QUERY='`'"$DATASET"'`:`'"$METRIC"'` | filter `'"$TAG"'` is '"$t"' | align to 5m using sum'
|
|
# Propagate probe failures instead of swallowing them: a failed
|
|
# query (bad dataset, auth, network) must not be reported as the
|
|
# tag being "absent" — that would be a confident wrong answer.
|
|
if ! RESPONSE=$("$SCRIPT_DIR/metrics-query" "$DEPLOYMENT" "$PROBE_QUERY" "$START" "$END" 2>&1); then
|
|
echo "Error: type probe query failed (tag '$TAG' is $t):" >&2
|
|
printf '%s\n' "$RESPONSE" >&2
|
|
exit 1
|
|
fi
|
|
COUNT=$(printf '%s' "$RESPONSE" | jq -r '(.series // []) | length')
|
|
if [[ "$COUNT" -gt 0 ]]; then
|
|
PRESENT_JSON=$(printf '%s' "$PRESENT_JSON" | jq --arg t "$t" '. + [$t]')
|
|
fi
|
|
done
|
|
jq -n --argjson present "$PRESENT_JSON" '
|
|
{
|
|
type: (
|
|
if ($present | length) == 0 then "absent"
|
|
elif ($present | length) == 1 then $present[0]
|
|
else "mixed"
|
|
end
|
|
),
|
|
present_types: $present
|
|
}'
|
|
else
|
|
show_usage
|
|
fi
|
|
;;
|
|
tags)
|
|
if [[ ${#POSITIONAL[@]} -eq 1 ]]; then
|
|
# List tags
|
|
"$SCRIPT_DIR/axiom-api" "$DEPLOYMENT" GET "${BASE}/tags?${TIME_PARAMS}"
|
|
elif [[ ${#POSITIONAL[@]} -eq 3 && "${POSITIONAL[2]}" == "values" ]]; then
|
|
# List values for a tag
|
|
TAG="${POSITIONAL[1]}"
|
|
"$SCRIPT_DIR/axiom-api" "$DEPLOYMENT" GET "${BASE}/tags/$(urlencode "$TAG")/values?${TIME_PARAMS}"
|
|
else
|
|
show_usage
|
|
fi
|
|
;;
|
|
find-metrics)
|
|
if [[ ${#POSITIONAL[@]} -ne 2 ]]; then
|
|
show_usage
|
|
fi
|
|
VALUE="${POSITIONAL[1]}"
|
|
BODY=$(jq -n --arg v "$VALUE" '{value: $v}')
|
|
"$SCRIPT_DIR/axiom-api" "$DEPLOYMENT" POST "${BASE}/metrics?${TIME_PARAMS}" "$BODY"
|
|
;;
|
|
*)
|
|
show_usage
|
|
;;
|
|
esac
|