From 29ee5126de7cc2308b4b0eca9bf2247f864a1d4b Mon Sep 17 00:00:00 2001 From: "openclaw-barnacle[bot]" <257215752+openclaw-barnacle[bot]@users.noreply.github.com> Date: Tue, 11 Aug 2026 10:51:38 -0700 Subject: [PATCH] chore: update skills (#3406) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .agents/skills/axiomhq-skills.provenance.json | 2 +- .../building-dashboards/scripts/chart-add | 19 ++- .../scripts/metrics/axiom-api | 4 + .../scripts/metrics/metrics-info | 137 ++++++++++++++---- .../scripts/metrics/metrics-query | 75 +++++++++- .../scripts/metrics/metrics-spec | 35 ++--- .../tests/test-script-output.sh | 33 +++++ .../openclaw-carapace/references/tokens.md | 11 ++ .../references/tokens.md | 11 ++ .agents/skills/query-metrics/README.md | 2 +- .agents/skills/query-metrics/SKILL.md | 65 +++++++-- .../skills/query-metrics/scripts/axiom-api | 4 + .../skills/query-metrics/scripts/metrics-info | 137 ++++++++++++++---- .../query-metrics/scripts/metrics-query | 75 +++++++++- .../skills/query-metrics/scripts/metrics-spec | 35 ++--- .agents/skills/query-metrics/scripts/setup | 2 +- skills-lock.json | 8 +- 17 files changed, 523 insertions(+), 132 deletions(-) diff --git a/.agents/skills/axiomhq-skills.provenance.json b/.agents/skills/axiomhq-skills.provenance.json index 89f3fcf6..b19811e2 100644 --- a/.agents/skills/axiomhq-skills.provenance.json +++ b/.agents/skills/axiomhq-skills.provenance.json @@ -1,6 +1,6 @@ { "repository": "https://github.com/axiomhq/skills", - "resolvedCommit": "0e98ebaeec76a70c8fda9a7737605800c2f1245d", + "resolvedCommit": "7f29f9a97ffd71bf2ad375e035ba6f3ba30dcc8b", "license": "MIT", "skills": { "axiom-alerting": { diff --git a/.agents/skills/building-dashboards/scripts/chart-add b/.agents/skills/building-dashboards/scripts/chart-add index 79ad908d..5c7cb0c2 100755 --- a/.agents/skills/building-dashboards/scripts/chart-add +++ b/.agents/skills/building-dashboards/scripts/chart-add @@ -195,13 +195,28 @@ unit_fields_other() { fi } +# Newline before each pipeline `|` so stored queries read one stage per line. +# The split only tracks plain '...'/"..." literals, so it bails out and stores +# the query untouched when it holds a construct whose string boundaries it +# cannot follow: a backslash escape, an @-verbatim literal (where `\` is not an +# escape), or a // comment. Formatting is cosmetic, silently rewriting a query +# is not, so anything ambiguous stays on one line. +format_pipeline() { + if [[ "$1" == *\\* || "$1" == *"@'"* || "$1" == *'@"'* || "$1" == *"//"* ]]; then + printf '%s' "$1" + return + fi + jq -rn --arg apl "$1" \ + '$apl | gsub("(?\"[^\"]*\"|'\''[^'\'']*'\'')|(?

\\| )"; if .s then .s else "\n| " end)' +} + # Build the query object. Both APL and MPL land in `query.apl` (shared API # field); MPL also gets `query.metricsDataset`. build_query() { if [[ -n "$MPL" ]]; then - jq -n --arg apl "$MPL" --arg ds "$DATASET" '{apl: $apl, metricsDataset: $ds}' + jq -n --arg apl "$(format_pipeline "$MPL")" --arg ds "$DATASET" '{apl: $apl, metricsDataset: $ds}' else - jq -n --arg apl "$APL" '{apl: $apl}' + jq -n --arg apl "$(format_pipeline "$APL")" '{apl: $apl}' fi } diff --git a/.agents/skills/building-dashboards/scripts/metrics/axiom-api b/.agents/skills/building-dashboards/scripts/metrics/axiom-api index 2c7bccef..dc5d83af 100755 --- a/.agents/skills/building-dashboards/scripts/metrics/axiom-api +++ b/.agents/skills/building-dashboards/scripts/metrics/axiom-api @@ -8,6 +8,8 @@ # # Reads credentials from ~/.axiom.toml (shared with axiom-sre) # Set AXIOM_URL_OVERRIDE to route requests to a specific edge deployment endpoint. +# Set AXIOM_CONNECT_TIMEOUT / AXIOM_MAX_TIME (seconds) to override the default +# connection (10s) and total request (120s) timeouts. # # Examples: # axiom-api prod GET /v1/datasets @@ -54,6 +56,8 @@ fi CURL_ARGS=( -s + --connect-timeout "${AXIOM_CONNECT_TIMEOUT:-10}" + --max-time "${AXIOM_MAX_TIME:-120}" -w '\n%{http_code}' -X "$METHOD" -H "Authorization: Bearer $TOKEN" diff --git a/.agents/skills/building-dashboards/scripts/metrics/metrics-info b/.agents/skills/building-dashboards/scripts/metrics/metrics-info index b99e7ff8..6d3f156b 100755 --- a/.agents/skills/building-dashboards/scripts/metrics/metrics-info +++ b/.agents/skills/building-dashboards/scripts/metrics/metrics-info @@ -47,7 +47,12 @@ # specific entity name (service, host, device) to find which metrics carry it. # To list metric names, use the `metrics` subcommand instead. # -# --start and --end default to the last 24 hours if omitted. +# --start and --end accept RFC3339 (offsets allowed, e.g. 2025-06-01T00:00:00+02:00) +# or relative now / now- with 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-[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: @@ -67,6 +72,53 @@ 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-` (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 metrics [--by-type] [--type T]..." >&2 @@ -80,8 +132,8 @@ show_usage() { echo " metrics-info find-metrics (searches tag values, not metric names)" >&2 echo "" >&2 echo "Options:" >&2 - echo " --start T Start time (RFC3339). Default: 24h ago" >&2 - echo " --end T End time (RFC3339). Default: now" >&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 @@ -118,20 +170,12 @@ while [[ $# -gt 0 ]]; do esac done -# Default time range: last 24 hours -if [[ -z "$START" ]]; then - if date --version &>/dev/null 2>&1; then - START=$(date -u -d '24 hours ago' '+%Y-%m-%dT%H:%M:%SZ') - else - START=$(date -u -v-24H '+%Y-%m-%dT%H:%M:%SZ') - fi -fi -if [[ -z "$END" ]]; then - END=$(date -u '+%Y-%m-%dT%H:%M:%SZ') -fi +# 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=${START}&end=${END}" -BASE="/v1/query/metrics/info/datasets/${DATASET}" +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) @@ -185,34 +229,62 @@ case "${POSITIONAL[0]}" in # 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}/tags?${TIME_PARAMS}") + 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…] } - VALUES_OBJ='{}' + # 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 - VALUES=$("$SCRIPT_DIR/axiom-api" "$DEPLOYMENT" GET "${BASE}/metrics/${METRIC}/tags/${tag}/values?${TIME_PARAMS}") - 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" --argjson v "$VALUES" '$o + {($t): $v}') + 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/${METRIC}/tags?${TIME_PARAMS}" + "$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/${METRIC}/tags/${TAG}/values?${TIME_PARAMS}" + "$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 is ` for each candidate type. The type(s) that @@ -226,8 +298,15 @@ case "${POSITIONAL[0]}" in # ``:`` | filter `` is | align to 5m using sum # If is matches no rows, the response has empty `series`. PROBE_QUERY='`'"$DATASET"'`:`'"$METRIC"'` | filter `'"$TAG"'` is '"$t"' | align to 5m using sum' - RESPONSE=$("$SCRIPT_DIR/metrics-query" "$DEPLOYMENT" "$PROBE_QUERY" "$START" "$END" 2>/dev/null || echo '{}') - COUNT=$(printf '%s' "$RESPONSE" | jq -r '(.series // []) | length' 2>/dev/null || echo 0) + # 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 @@ -253,7 +332,7 @@ case "${POSITIONAL[0]}" in elif [[ ${#POSITIONAL[@]} -eq 3 && "${POSITIONAL[2]}" == "values" ]]; then # List values for a tag TAG="${POSITIONAL[1]}" - "$SCRIPT_DIR/axiom-api" "$DEPLOYMENT" GET "${BASE}/tags/${TAG}/values?${TIME_PARAMS}" + "$SCRIPT_DIR/axiom-api" "$DEPLOYMENT" GET "${BASE}/tags/$(urlencode "$TAG")/values?${TIME_PARAMS}" else show_usage fi diff --git a/.agents/skills/building-dashboards/scripts/metrics/metrics-query b/.agents/skills/building-dashboards/scripts/metrics/metrics-query index 9dbbbb76..b094bef7 100755 --- a/.agents/skills/building-dashboards/scripts/metrics/metrics-query +++ b/.agents/skills/building-dashboards/scripts/metrics/metrics-query @@ -1,10 +1,23 @@ #!/usr/bin/env bash # metrics-query: Execute a metrics query against Axiom MetricsDB # -# Usage: metrics-query [-p name=value]... +# Usage: metrics-query [-p name=value]... [-w pixels] [--pixel-per-point n] \ +# # # Times: RFC3339 (e.g. 2025-01-01T00:00:00Z) or relative (e.g. now-1h, now-1d). # +# Adaptive resolution ($__interval): +# Reference $__interval anywhere a Duration is expected (e.g. +# `align to $__interval using avg`, `bucket to $__interval ...`) and the +# server resolves it to a "nice" step computed from the query time range and +# the target chart width. No `param $__interval` declaration is needed -- the +# metrics service registers it automatically. Tune the density with: +# -w / --chart-width target chart width; the server aims for +# ~chart-width/pixel-per-point buckets +# (default ~500 buckets when -w is omitted). +# --pixel-per-point pixels per data point (server default 10). +# Both are forwarded under the request body's queryOptions object. +# # Parameter values (-p / --param name=value, repeatable): # For each MPL parameter declared in the query (e.g. `param $svc: string;`), # pass the variable name without the leading `$` and an MPL literal as the @@ -32,6 +45,8 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PARAMS=() POSITIONAL=() +CHART_WIDTH="" +PIXEL_PER_POINT="" while [[ $# -gt 0 ]]; do case "$1" in -p|--param) @@ -46,6 +61,30 @@ while [[ $# -gt 0 ]]; do PARAMS+=("${1#--param=}") shift ;; + -w|--chart-width) + if [[ $# -lt 2 ]]; then + echo "Error: $1 requires a pixel-width argument" >&2 + exit 1 + fi + CHART_WIDTH="$2" + shift 2 + ;; + --chart-width=*) + CHART_WIDTH="${1#--chart-width=}" + shift + ;; + --pixel-per-point) + if [[ $# -lt 2 ]]; then + echo "Error: $1 requires an integer argument" >&2 + exit 1 + fi + PIXEL_PER_POINT="$2" + shift 2 + ;; + --pixel-per-point=*) + PIXEL_PER_POINT="${1#--pixel-per-point=}" + shift + ;; --) shift while [[ $# -gt 0 ]]; do POSITIONAL+=("$1"); shift; done @@ -63,13 +102,17 @@ START_TIME="${POSITIONAL[2]:-}" END_TIME="${POSITIONAL[3]:-}" if [[ -z "$DEPLOYMENT" || -z "$MPL" || -z "$START_TIME" || -z "$END_TIME" ]]; then - echo "Usage: metrics-query [-p name=value]... " >&2 + echo "Usage: metrics-query [-p name=value]... [-w pixels] [--pixel-per-point n] " >&2 echo "" >&2 echo "Times: RFC3339 (e.g. 2025-01-01T00:00:00Z) or relative (e.g. now-1h, now-1d)." >&2 echo "" >&2 echo "-p / --param name=value (repeatable): supply an MPL parameter value." >&2 echo " name - variable name without the leading \$ (e.g. 'svc' for \$svc)." >&2 echo " value - MPL literal, forwarded verbatim under params.param__." >&2 + echo "" >&2 + echo "-w / --chart-width target chart width; lets the server resolve" >&2 + echo " \$__interval to a nice step (queryOptions)." >&2 + echo "--pixel-per-point pixels per data point (server default 10)." >&2 exit 1 fi @@ -97,6 +140,17 @@ if [[ ${#PARAMS[@]} -gt 0 ]]; then done fi +# Validate the optional chart-sizing options. They must be positive integers; +# they are forwarded under queryOptions so the server can resolve $__interval. +if [[ -n "$CHART_WIDTH" && ! "$CHART_WIDTH" =~ ^[1-9][0-9]*$ ]]; then + echo "Error: --chart-width must be a positive integer (got: $CHART_WIDTH)" >&2 + exit 1 +fi +if [[ -n "$PIXEL_PER_POINT" && ! "$PIXEL_PER_POINT" =~ ^[1-9][0-9]*$ ]]; then + echo "Error: --pixel-per-point must be a positive integer (got: $PIXEL_PER_POINT)" >&2 + exit 1 +fi + # Extract dataset name from MPL: `dataset`:`metric` ... or dataset:`metric` ... # Strip leading `param : ;` declarations first so their `:` doesn't # get mistaken for the dataset:metric separator. @@ -141,6 +195,23 @@ if [[ ${#PARAM_NAMES[@]} -gt 0 ]]; then JQ_EXPR="$JQ_EXPR + {params: ($PARAMS_EXPR)}" fi +# Forward chart-sizing hints under queryOptions. The edge translates these into +# the x-axiom-chart-width / x-axiom-pixel-per-point headers, which the metrics +# service uses to resolve $__interval. Values are JSON numbers (--argjson). +if [[ -n "$CHART_WIDTH" || -n "$PIXEL_PER_POINT" ]]; then + QO_EXPR="" + if [[ -n "$CHART_WIDTH" ]]; then + JQ_ARGS+=(--argjson chartWidth "$CHART_WIDTH") + QO_EXPR="{\"chart-width\": \$chartWidth}" + fi + if [[ -n "$PIXEL_PER_POINT" ]]; then + JQ_ARGS+=(--argjson pixelPerPoint "$PIXEL_PER_POINT") + if [[ -n "$QO_EXPR" ]]; then QO_EXPR+=" + "; fi + QO_EXPR+="{\"pixel-per-point\": \$pixelPerPoint}" + fi + JQ_EXPR="$JQ_EXPR + {queryOptions: ($QO_EXPR)}" +fi + BODY=$(jq -n "${JQ_ARGS[@]}" "$JQ_EXPR") AXIOM_ACCEPT="application/json+metrics.v2" "$SCRIPT_DIR/axiom-api" "$DEPLOYMENT" POST "/v1/query/_mpl" "$BODY" diff --git a/.agents/skills/building-dashboards/scripts/metrics/metrics-spec b/.agents/skills/building-dashboards/scripts/metrics/metrics-spec index 5f022253..6a1cd014 100755 --- a/.agents/skills/building-dashboards/scripts/metrics/metrics-spec +++ b/.agents/skills/building-dashboards/scripts/metrics/metrics-spec @@ -1,31 +1,18 @@ #!/usr/bin/env bash -# metrics-spec: Fetch the metrics query specification from Axiom +# metrics-spec: Fetch the MPL metrics query specification from Axiom # -# Usage: metrics-spec +# Usage: metrics-spec # -# Calls OPTIONS /v1/query/_mpl to retrieve the complete metrics query -# spec with syntax, operators, and examples. Read this before composing queries. -# -# The dataset is needed to resolve the correct edge deployment URL. -# -# Example: -# metrics-spec prod my-metrics-dataset +# Retrieves the complete MPL query spec with syntax, operators, and examples. +# Read this before composing queries. set -euo pipefail -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SPEC_URL="https://us-east-1.aws.edge.axiom.co/v1/query/_mpl" -DEPLOYMENT="${1:-}" -DATASET="${2:-}" - -if [[ -z "$DEPLOYMENT" || -z "$DATASET" ]]; then - echo "Usage: metrics-spec " >&2 - exit 1 -fi - -RESOLVED_URL=$("$SCRIPT_DIR/resolve-url" "$DEPLOYMENT" "$DATASET" 2>/dev/null || true) -if [[ -n "$RESOLVED_URL" ]]; then - export AXIOM_URL_OVERRIDE="$RESOLVED_URL" -fi - -AXIOM_ACCEPT="text/markdown" "$SCRIPT_DIR/axiom-api" "$DEPLOYMENT" OPTIONS "/v1/query/_mpl" +# Match the timeout convention used by axiom-api so a stalled edge can't hang +# the caller indefinitely. Override via AXIOM_CONNECT_TIMEOUT / AXIOM_MAX_TIME. +curl -sS -X OPTIONS -H "Accept: text/markdown" \ + --connect-timeout "${AXIOM_CONNECT_TIMEOUT:-10}" \ + --max-time "${AXIOM_MAX_TIME:-120}" \ + "$SPEC_URL" diff --git a/.agents/skills/building-dashboards/tests/test-script-output.sh b/.agents/skills/building-dashboards/tests/test-script-output.sh index 42375435..c18848df 100755 --- a/.agents/skills/building-dashboards/tests/test-script-output.sh +++ b/.agents/skills/building-dashboards/tests/test-script-output.sh @@ -125,6 +125,39 @@ else fail "dashboard-chart-patch outputs valid JSON only" "got: $patch_out" fi +apl_fmt=$("$SCRIPTS_DIR/chart-add" --type Statistic --id t --name T \ + --apl "['logs'] | where a=='x' | summarize c=count()" | jq -r '.query.apl') +if [[ "$(printf '%s' "$apl_fmt" | grep -c '^| ')" == "2" && "$apl_fmt" != *" | "* ]]; then + ok "chart-add breaks each pipeline stage onto its own line" +else + fail "chart-add breaks each pipeline stage onto its own line" "got: $apl_fmt" +fi + +apl_str=$("$SCRIPTS_DIR/chart-add" --type Statistic --id t --name T \ + --apl "['logs'] | where msg=='a | b'" | jq -r '.query.apl') +if [[ "$apl_str" == *"msg=='a | b'"* ]]; then + ok "chart-add leaves a pipe inside a string literal untouched" +else + fail "chart-add leaves a pipe inside a string literal untouched" "got: $apl_str" +fi + +# Constructs whose string boundaries the split cannot follow must round-trip +# byte-for-byte rather than risk a newline landing inside a literal. +check_verbatim() { + local label="$1" input="$2" got + got=$("$SCRIPTS_DIR/chart-add" --type Statistic --id t --name T --apl "$input" | jq -r '.query.apl') + if [[ "$got" == "$input" ]]; then + ok "chart-add stores $label untouched" + else + fail "chart-add stores $label untouched" "got: $got" + fi +} + +check_verbatim "a backslash-escaped quote" '["logs"] | where msg == "a \" b | c" | project msg' +check_verbatim "an @-verbatim literal" '["logs"] | where p == @"c:\x | y" | project p' +check_verbatim "a // comment" '["logs"] // note | here +| count' + echo "" echo "======================" echo "Passed: $passed | Failed: $failed" diff --git a/.agents/skills/openclaw-carapace/references/tokens.md b/.agents/skills/openclaw-carapace/references/tokens.md index 64cf8463..29e35d35 100644 --- a/.agents/skills/openclaw-carapace/references/tokens.md +++ b/.agents/skills/openclaw-carapace/references/tokens.md @@ -11,9 +11,16 @@ exports when the consumer must control reset and adapter order. | Semantic | `--oc-bg-*`, `--oc-text-*`, `--oc-accent-*` | Theme-aware UI intent | | Scale | `--oc-space-*`, `--oc-font-size-*`, `--oc-radius-*` | Shared dimensions | | Motion | `--oc-duration-*`, `--oc-ease-*` | Shared interaction timing | +| Layer | `--oc-layer-*` | Popover and non-modal notification stacking roles | | Product | `--oc-status-*`, `--oc-input-*`, `--oc-diff-*` | Opt-in operational UI | | Consumer alias | Unprefixed legacy names | Migration compatibility only | +Component styles consume semantic and product roles in their owning +stylesheet; Carapace does not define a global component-token namespace. When +a component genuinely needs a local custom property, scope it to the component +root and document the override point beside that component rather than turning +it into a second palette. + ## Semantic Choices - Page background: `--oc-bg-page` @@ -27,6 +34,10 @@ exports when the consumer must control reset and adapter order. `--oc-accent-primary-hover` - Secondary accent: `--oc-accent-secondary` - Neutral control backgrounds: `--oc-control-bg`, `--oc-control-bg-hover` +- Modal isolation: `--oc-surface-modal-backdrop`; ordinary translucent + surfaces continue to use `--oc-surface-overlay` +- Product fields: `--oc-input-*`; status feedback: paired `--oc-status-*-bg` + and `--oc-status-*-fg` roles - Subtle, strong, and accent borders: `--oc-border-subtle`, `--oc-border-strong`, `--oc-border-accent` - Focus: `--oc-focus-ring` diff --git a/.agents/skills/openclaw-design-system/references/tokens.md b/.agents/skills/openclaw-design-system/references/tokens.md index 20263ce9..1d121942 100644 --- a/.agents/skills/openclaw-design-system/references/tokens.md +++ b/.agents/skills/openclaw-design-system/references/tokens.md @@ -13,9 +13,16 @@ to `@openclaw/carapace`. | Semantic | `--oc-bg-*`, `--oc-text-*`, `--oc-accent-*` | Theme-aware UI intent | | Scale | `--oc-space-*`, `--oc-font-size-*`, `--oc-radius-*` | Shared dimensions | | Motion | `--oc-duration-*`, `--oc-ease-*` | Shared interaction timing | +| Layer | `--oc-layer-*` | Popover and non-modal notification stacking roles | | Product | `--oc-status-*`, `--oc-input-*`, `--oc-diff-*` | Opt-in operational UI | | Consumer alias | Unprefixed legacy names | Migration compatibility only | +Component styles consume semantic and product roles in their owning +stylesheet; Carapace does not define a global component-token namespace. When +a component genuinely needs a local custom property, scope it to the component +root and document the override point beside that component rather than turning +it into a second palette. + ## Semantic Choices - Page background: `--oc-bg-page` @@ -29,6 +36,10 @@ to `@openclaw/carapace`. `--oc-accent-primary-hover` - Secondary accent: `--oc-accent-secondary` - Neutral control backgrounds: `--oc-control-bg`, `--oc-control-bg-hover` +- Modal isolation: `--oc-surface-modal-backdrop`; ordinary translucent + surfaces continue to use `--oc-surface-overlay` +- Product fields: `--oc-input-*`; status feedback: paired `--oc-status-*-bg` + and `--oc-status-*-fg` roles - Subtle, strong, and accent borders: `--oc-border-subtle`, `--oc-border-strong`, `--oc-border-accent` - Focus: `--oc-focus-ring` diff --git a/.agents/skills/query-metrics/README.md b/.agents/skills/query-metrics/README.md index 82a4f0b4..b9e966cf 100644 --- a/.agents/skills/query-metrics/README.md +++ b/.agents/skills/query-metrics/README.md @@ -53,7 +53,7 @@ scripts/datasets prod scripts/datasets prod --kind otel:metrics:v1 # Fetch the metrics query spec -scripts/metrics-spec prod +scripts/metrics-spec # List available metrics in a dataset scripts/metrics-info prod my-dataset metrics diff --git a/.agents/skills/query-metrics/SKILL.md b/.agents/skills/query-metrics/SKILL.md index f00ef14e..9bab11b5 100644 --- a/.agents/skills/query-metrics/SKILL.md +++ b/.agents/skills/query-metrics/SKILL.md @@ -12,7 +12,7 @@ Setup, prerequisites, and `~/.axiom.toml` configuration: see `README.md`. Edge-d ## Workflow 1. `scripts/datasets --kind otel:metrics:v1` — list metrics datasets. -2. `scripts/metrics-spec ` — **required** before composing any query. MPL evolves; the spec is the source of truth. +2. `scripts/metrics-spec` — **required** before composing any query. MPL evolves; the spec is the source of truth. Also use it to answer general MPL/metrics questions. 3. `scripts/metrics-info metrics` — list metrics with `{type, temporality, unit}` metadata. Read this before writing the query (see [Choosing a Query Shape](#choosing-a-query-shape)). 4. `scripts/metrics-info tags [ values]` — explore filter dimensions. 5. `scripts/metrics-query '' ` — execute. Iterate. @@ -35,7 +35,7 @@ Rules per type (consult `metrics-spec` for exact operator names — they evolve) - **CounterMonotonic + Cumulative** — running total (resets aside). The raw values are rarely what you want. Convert to a per-second rate first, **then** align/aggregate. - **CounterMonotonic + Delta** — already per-interval. Sum/align without a rate step. - **CounterNonMonotonic** — can go up or down (queue depth, balance). Intent is ambiguous: rate, delta, or current value all make sense for different questions. **Ask the user** before picking one. -- **Histogram** — not a scalar. `align using avg` produces nonsense. Use the bucket/quantile operators from `metrics-spec`. +- **Histogram** — not a scalar. `align using avg` produces nonsense. Use `bucket … using` with the histogram functions from `metrics-spec`; quantiles are float specs to those functions, and `temporality` selects the variant (`Cumulative` vs `Delta` interpolation). Consult `metrics-spec` for the exact signatures. - **`temporality: null`** — "not applicable for this instrument type" (the norm for Gauges), not "missing data". When surfacing numbers, attach the `unit` (treat `null` as unitless). If you combine metrics with mismatched units in arithmetic, warn rather than silently producing a meaningless number. @@ -43,7 +43,7 @@ When surfacing numbers, attach the `unit` (treat `null` as unitless). If you com ## Query Metrics ```bash -scripts/metrics-query '' +scripts/metrics-query [-w pixels] [--pixel-per-point n] '' ``` | Parameter | Notes | @@ -51,22 +51,57 @@ scripts/metrics-query '' | `deploy` | Name from `~/.axiom.toml` (e.g. `prod`). | | `MPL` | Pipeline string. Dataset is parsed from the MPL itself. | | `start` / `end` | RFC3339 (`2025-01-01T00:00:00Z`) or relative (`now-1h`, `now`). | +| `-w` / `--chart-width ` | Optional. Target chart width in pixels; lets the server resolve `$__interval`. | +| `--pixel-per-point ` | Optional. Pixels per point (server default 10); with `-w` sets the bucket count. | + +**Always single-quote the MPL string in the shell.** MPL is full of backticks; inside double quotes the shell executes them as command substitution, silently mangling the query (or running whatever the identifier names). + +**Bound the output before grouping.** `group by ` returns one series per tag value with no cap — on a high-cardinality tag this floods the output. Check cardinality first (`describe`, or `tags values`) and prefer plain `group using ` while exploring. Examples: ```bash -scripts/metrics-query prod \ - '`my-dataset`:`http.server.duration` | align to 5m using avg' \ +scripts/metrics-query prod -w 1200 \ + '`my-dataset`:`http.server.duration` | align to $__interval using avg' \ now-1h now -scripts/metrics-query prod \ +scripts/metrics-query prod -w 1200 \ '`my-dataset`:`http.server.duration` | where `service.name` == "frontend" and method == "GET" - | align to 5m using avg + | align to $__interval using avg | group by status_code using sum' \ now-1d now ``` +### Adaptive resolution (`$__interval`) + +Hardcoding a step (`align to 5m`) makes charts look wrong at other zoom +levels — too sparse zoomed in, too dense zoomed out. Prefer the system +parameter `$__interval` wherever a `Duration` is expected, and pass the chart +width so the server picks the step: + +```bash +scripts/metrics-query prod -w 1200 \ + '`my-dataset`:`http.server.duration` | align to $__interval using avg' \ + now-7d now +``` + +The metrics service computes `$__interval` from the query's time range and the +target chart width, then snaps it **up** to a nice resolution from the ladder +`1s, 5s, 10s, 15s, 30s, 1m, 5m, 10m, 15m, 30m, 1h, 12h, 1d, 1w, 1M, 1Y`. It +never drops below a metric's stored resolution. + +- **No declaration needed** — the server auto-registers `$__interval`; do *not* + add `param $__interval: Duration;` (the edge forwards the query verbatim and + the metrics service injects the parameter). +- **Bucket count** ≈ `chart-width / pixel-per-point` (`pixel-per-point` default + 10). Omit `-w` and the server targets ~500 buckets. +- Works anywhere a `Duration` is valid, e.g. `bucket to $__interval using + histogram(0.5, 0.95)`. +- Set `-w` to your render width (e.g. the `metrics-chart` skill's plot width) + so one bucket ≈ one pixel column. The value is forwarded under the request + body's `queryOptions` (`chart-width`, `pixel-per-point`). + ### Parameters MPL can declare parameters (`param $svc: string;`). Pass values with repeated `-p name=value`. The script applies the API's `param__` prefix; values are forwarded verbatim as MPL literals (string literals include their quotes). @@ -96,7 +131,7 @@ Literal syntax per type lives in `metrics-spec`. ## Discovery (`metrics-info`) -Time range defaults to the last 24h; override with `--start` / `--end`. +Time range defaults to the last 24h; override with `--start` / `--end`. Both accept RFC3339 (offsets allowed) or relative `now` / `now-` with `` in `s m h d w`, resolved to RFC3339 UTC client-side. This is **narrower** than `metrics-query`, which forwards times to the server unparsed and so also accepts forms like `now-1y`; in `metrics-info` anything outside `now` / `now-[smhdw]` must already be RFC3339 or the request 400s. | Command | Returns | |---|---| @@ -114,21 +149,25 @@ Time range defaults to the last 24h; override with `--start` / `--end`. ## Error Handling -HTTP errors return JSON with `message`, `code`, and optional `detail`: +HTTP errors return JSON with `code` and `message`; some include a `detail` object: ```json -{"message": "...", "code": 400, "detail": {"errorType": 1, "message": "raw error"}} +{"code": 400, "message": "MPL syntax error: …"} ``` +Syntax errors (400) include an annotated source pointer listing the valid operators at the failure position — read it, it usually names the fix. + | Code | Cause | |---|---| | 400 | Invalid query syntax or bad dataset name | | 401 | Missing/invalid auth | | 403 | No permission | | 404 | Dataset not found | -| 429 | Rate limited | +| 429 | Rate limited — back off and retry; don't tight-loop | | 500 | Internal error | +Requests time out client-side after 120s (`AXIOM_MAX_TIME` to override; `AXIOM_CONNECT_TIMEOUT` for the 10s connect timeout). + On 500, re-run with `curl -v` to capture the `traceparent` / `x-axiom-trace-id` header and report it — the trace ID is what the backend team needs to debug. ## Scripts @@ -137,8 +176,8 @@ On 500, re-run with `curl -v` to capture the `traceparent` / `x-axiom-trace-id` |---|---| | `scripts/setup` | Check requirements and config. | | `scripts/datasets [--kind ]` | List datasets with edge deployment. | -| `scripts/metrics-spec ` | Fetch the MPL query spec. | -| `scripts/metrics-query ` | Execute a query. | +| `scripts/metrics-spec` | Fetch the MPL query spec. | +| `scripts/metrics-query [-w px] [--pixel-per-point n] ` | Execute a query; use `$__interval` + `-w` for adaptive resolution. | | `scripts/metrics-info ...` | Discover metrics, tags, values. | | `scripts/axiom-api [body]` | Low-level API calls. | | `scripts/resolve-url ` | Resolve to the edge deployment URL. | diff --git a/.agents/skills/query-metrics/scripts/axiom-api b/.agents/skills/query-metrics/scripts/axiom-api index 8f63e353..0e607039 100755 --- a/.agents/skills/query-metrics/scripts/axiom-api +++ b/.agents/skills/query-metrics/scripts/axiom-api @@ -5,6 +5,8 @@ # # Reads credentials from ~/.axiom.toml (shared with axiom-sre) # Set AXIOM_URL_OVERRIDE to route requests to a specific edge deployment endpoint. +# Set AXIOM_CONNECT_TIMEOUT / AXIOM_MAX_TIME (seconds) to override the default +# connection (10s) and total request (120s) timeouts. # # Examples: # axiom-api prod GET /v1/datasets @@ -51,6 +53,8 @@ fi CURL_ARGS=( -s + --connect-timeout "${AXIOM_CONNECT_TIMEOUT:-10}" + --max-time "${AXIOM_MAX_TIME:-120}" -w '\n%{http_code}' -X "$METHOD" -H "Authorization: Bearer $TOKEN" diff --git a/.agents/skills/query-metrics/scripts/metrics-info b/.agents/skills/query-metrics/scripts/metrics-info index b99e7ff8..6d3f156b 100755 --- a/.agents/skills/query-metrics/scripts/metrics-info +++ b/.agents/skills/query-metrics/scripts/metrics-info @@ -47,7 +47,12 @@ # specific entity name (service, host, device) to find which metrics carry it. # To list metric names, use the `metrics` subcommand instead. # -# --start and --end default to the last 24 hours if omitted. +# --start and --end accept RFC3339 (offsets allowed, e.g. 2025-06-01T00:00:00+02:00) +# or relative now / now- with 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-[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: @@ -67,6 +72,53 @@ 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-` (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 metrics [--by-type] [--type T]..." >&2 @@ -80,8 +132,8 @@ show_usage() { echo " metrics-info find-metrics (searches tag values, not metric names)" >&2 echo "" >&2 echo "Options:" >&2 - echo " --start T Start time (RFC3339). Default: 24h ago" >&2 - echo " --end T End time (RFC3339). Default: now" >&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 @@ -118,20 +170,12 @@ while [[ $# -gt 0 ]]; do esac done -# Default time range: last 24 hours -if [[ -z "$START" ]]; then - if date --version &>/dev/null 2>&1; then - START=$(date -u -d '24 hours ago' '+%Y-%m-%dT%H:%M:%SZ') - else - START=$(date -u -v-24H '+%Y-%m-%dT%H:%M:%SZ') - fi -fi -if [[ -z "$END" ]]; then - END=$(date -u '+%Y-%m-%dT%H:%M:%SZ') -fi +# 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=${START}&end=${END}" -BASE="/v1/query/metrics/info/datasets/${DATASET}" +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) @@ -185,34 +229,62 @@ case "${POSITIONAL[0]}" in # 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}/tags?${TIME_PARAMS}") + 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…] } - VALUES_OBJ='{}' + # 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 - VALUES=$("$SCRIPT_DIR/axiom-api" "$DEPLOYMENT" GET "${BASE}/metrics/${METRIC}/tags/${tag}/values?${TIME_PARAMS}") - 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" --argjson v "$VALUES" '$o + {($t): $v}') + 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/${METRIC}/tags?${TIME_PARAMS}" + "$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/${METRIC}/tags/${TAG}/values?${TIME_PARAMS}" + "$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 is ` for each candidate type. The type(s) that @@ -226,8 +298,15 @@ case "${POSITIONAL[0]}" in # ``:`` | filter `` is | align to 5m using sum # If is matches no rows, the response has empty `series`. PROBE_QUERY='`'"$DATASET"'`:`'"$METRIC"'` | filter `'"$TAG"'` is '"$t"' | align to 5m using sum' - RESPONSE=$("$SCRIPT_DIR/metrics-query" "$DEPLOYMENT" "$PROBE_QUERY" "$START" "$END" 2>/dev/null || echo '{}') - COUNT=$(printf '%s' "$RESPONSE" | jq -r '(.series // []) | length' 2>/dev/null || echo 0) + # 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 @@ -253,7 +332,7 @@ case "${POSITIONAL[0]}" in elif [[ ${#POSITIONAL[@]} -eq 3 && "${POSITIONAL[2]}" == "values" ]]; then # List values for a tag TAG="${POSITIONAL[1]}" - "$SCRIPT_DIR/axiom-api" "$DEPLOYMENT" GET "${BASE}/tags/${TAG}/values?${TIME_PARAMS}" + "$SCRIPT_DIR/axiom-api" "$DEPLOYMENT" GET "${BASE}/tags/$(urlencode "$TAG")/values?${TIME_PARAMS}" else show_usage fi diff --git a/.agents/skills/query-metrics/scripts/metrics-query b/.agents/skills/query-metrics/scripts/metrics-query index 9dbbbb76..b094bef7 100755 --- a/.agents/skills/query-metrics/scripts/metrics-query +++ b/.agents/skills/query-metrics/scripts/metrics-query @@ -1,10 +1,23 @@ #!/usr/bin/env bash # metrics-query: Execute a metrics query against Axiom MetricsDB # -# Usage: metrics-query [-p name=value]... +# Usage: metrics-query [-p name=value]... [-w pixels] [--pixel-per-point n] \ +# # # Times: RFC3339 (e.g. 2025-01-01T00:00:00Z) or relative (e.g. now-1h, now-1d). # +# Adaptive resolution ($__interval): +# Reference $__interval anywhere a Duration is expected (e.g. +# `align to $__interval using avg`, `bucket to $__interval ...`) and the +# server resolves it to a "nice" step computed from the query time range and +# the target chart width. No `param $__interval` declaration is needed -- the +# metrics service registers it automatically. Tune the density with: +# -w / --chart-width target chart width; the server aims for +# ~chart-width/pixel-per-point buckets +# (default ~500 buckets when -w is omitted). +# --pixel-per-point pixels per data point (server default 10). +# Both are forwarded under the request body's queryOptions object. +# # Parameter values (-p / --param name=value, repeatable): # For each MPL parameter declared in the query (e.g. `param $svc: string;`), # pass the variable name without the leading `$` and an MPL literal as the @@ -32,6 +45,8 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PARAMS=() POSITIONAL=() +CHART_WIDTH="" +PIXEL_PER_POINT="" while [[ $# -gt 0 ]]; do case "$1" in -p|--param) @@ -46,6 +61,30 @@ while [[ $# -gt 0 ]]; do PARAMS+=("${1#--param=}") shift ;; + -w|--chart-width) + if [[ $# -lt 2 ]]; then + echo "Error: $1 requires a pixel-width argument" >&2 + exit 1 + fi + CHART_WIDTH="$2" + shift 2 + ;; + --chart-width=*) + CHART_WIDTH="${1#--chart-width=}" + shift + ;; + --pixel-per-point) + if [[ $# -lt 2 ]]; then + echo "Error: $1 requires an integer argument" >&2 + exit 1 + fi + PIXEL_PER_POINT="$2" + shift 2 + ;; + --pixel-per-point=*) + PIXEL_PER_POINT="${1#--pixel-per-point=}" + shift + ;; --) shift while [[ $# -gt 0 ]]; do POSITIONAL+=("$1"); shift; done @@ -63,13 +102,17 @@ START_TIME="${POSITIONAL[2]:-}" END_TIME="${POSITIONAL[3]:-}" if [[ -z "$DEPLOYMENT" || -z "$MPL" || -z "$START_TIME" || -z "$END_TIME" ]]; then - echo "Usage: metrics-query [-p name=value]... " >&2 + echo "Usage: metrics-query [-p name=value]... [-w pixels] [--pixel-per-point n] " >&2 echo "" >&2 echo "Times: RFC3339 (e.g. 2025-01-01T00:00:00Z) or relative (e.g. now-1h, now-1d)." >&2 echo "" >&2 echo "-p / --param name=value (repeatable): supply an MPL parameter value." >&2 echo " name - variable name without the leading \$ (e.g. 'svc' for \$svc)." >&2 echo " value - MPL literal, forwarded verbatim under params.param__." >&2 + echo "" >&2 + echo "-w / --chart-width target chart width; lets the server resolve" >&2 + echo " \$__interval to a nice step (queryOptions)." >&2 + echo "--pixel-per-point pixels per data point (server default 10)." >&2 exit 1 fi @@ -97,6 +140,17 @@ if [[ ${#PARAMS[@]} -gt 0 ]]; then done fi +# Validate the optional chart-sizing options. They must be positive integers; +# they are forwarded under queryOptions so the server can resolve $__interval. +if [[ -n "$CHART_WIDTH" && ! "$CHART_WIDTH" =~ ^[1-9][0-9]*$ ]]; then + echo "Error: --chart-width must be a positive integer (got: $CHART_WIDTH)" >&2 + exit 1 +fi +if [[ -n "$PIXEL_PER_POINT" && ! "$PIXEL_PER_POINT" =~ ^[1-9][0-9]*$ ]]; then + echo "Error: --pixel-per-point must be a positive integer (got: $PIXEL_PER_POINT)" >&2 + exit 1 +fi + # Extract dataset name from MPL: `dataset`:`metric` ... or dataset:`metric` ... # Strip leading `param : ;` declarations first so their `:` doesn't # get mistaken for the dataset:metric separator. @@ -141,6 +195,23 @@ if [[ ${#PARAM_NAMES[@]} -gt 0 ]]; then JQ_EXPR="$JQ_EXPR + {params: ($PARAMS_EXPR)}" fi +# Forward chart-sizing hints under queryOptions. The edge translates these into +# the x-axiom-chart-width / x-axiom-pixel-per-point headers, which the metrics +# service uses to resolve $__interval. Values are JSON numbers (--argjson). +if [[ -n "$CHART_WIDTH" || -n "$PIXEL_PER_POINT" ]]; then + QO_EXPR="" + if [[ -n "$CHART_WIDTH" ]]; then + JQ_ARGS+=(--argjson chartWidth "$CHART_WIDTH") + QO_EXPR="{\"chart-width\": \$chartWidth}" + fi + if [[ -n "$PIXEL_PER_POINT" ]]; then + JQ_ARGS+=(--argjson pixelPerPoint "$PIXEL_PER_POINT") + if [[ -n "$QO_EXPR" ]]; then QO_EXPR+=" + "; fi + QO_EXPR+="{\"pixel-per-point\": \$pixelPerPoint}" + fi + JQ_EXPR="$JQ_EXPR + {queryOptions: ($QO_EXPR)}" +fi + BODY=$(jq -n "${JQ_ARGS[@]}" "$JQ_EXPR") AXIOM_ACCEPT="application/json+metrics.v2" "$SCRIPT_DIR/axiom-api" "$DEPLOYMENT" POST "/v1/query/_mpl" "$BODY" diff --git a/.agents/skills/query-metrics/scripts/metrics-spec b/.agents/skills/query-metrics/scripts/metrics-spec index 5f022253..6a1cd014 100755 --- a/.agents/skills/query-metrics/scripts/metrics-spec +++ b/.agents/skills/query-metrics/scripts/metrics-spec @@ -1,31 +1,18 @@ #!/usr/bin/env bash -# metrics-spec: Fetch the metrics query specification from Axiom +# metrics-spec: Fetch the MPL metrics query specification from Axiom # -# Usage: metrics-spec +# Usage: metrics-spec # -# Calls OPTIONS /v1/query/_mpl to retrieve the complete metrics query -# spec with syntax, operators, and examples. Read this before composing queries. -# -# The dataset is needed to resolve the correct edge deployment URL. -# -# Example: -# metrics-spec prod my-metrics-dataset +# Retrieves the complete MPL query spec with syntax, operators, and examples. +# Read this before composing queries. set -euo pipefail -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SPEC_URL="https://us-east-1.aws.edge.axiom.co/v1/query/_mpl" -DEPLOYMENT="${1:-}" -DATASET="${2:-}" - -if [[ -z "$DEPLOYMENT" || -z "$DATASET" ]]; then - echo "Usage: metrics-spec " >&2 - exit 1 -fi - -RESOLVED_URL=$("$SCRIPT_DIR/resolve-url" "$DEPLOYMENT" "$DATASET" 2>/dev/null || true) -if [[ -n "$RESOLVED_URL" ]]; then - export AXIOM_URL_OVERRIDE="$RESOLVED_URL" -fi - -AXIOM_ACCEPT="text/markdown" "$SCRIPT_DIR/axiom-api" "$DEPLOYMENT" OPTIONS "/v1/query/_mpl" +# Match the timeout convention used by axiom-api so a stalled edge can't hang +# the caller indefinitely. Override via AXIOM_CONNECT_TIMEOUT / AXIOM_MAX_TIME. +curl -sS -X OPTIONS -H "Accept: text/markdown" \ + --connect-timeout "${AXIOM_CONNECT_TIMEOUT:-10}" \ + --max-time "${AXIOM_MAX_TIME:-120}" \ + "$SPEC_URL" diff --git a/.agents/skills/query-metrics/scripts/setup b/.agents/skills/query-metrics/scripts/setup index 7467df38..0e872a22 100755 --- a/.agents/skills/query-metrics/scripts/setup +++ b/.agents/skills/query-metrics/scripts/setup @@ -79,7 +79,7 @@ echo "" echo "Usage:" echo " scripts/datasets prod # List datasets" echo " scripts/datasets prod --kind otel:metrics:v1 # List metrics datasets" -echo " scripts/metrics-spec prod # Fetch query spec" +echo " scripts/metrics-spec # Fetch query spec" echo " scripts/metrics-info prod metrics # List metrics" echo " scripts/metrics-info prod tags # List tags" echo " scripts/metrics-query prod '' '' '' # Run query" diff --git a/skills-lock.json b/skills-lock.json index 2d86d4f3..d3dae2a2 100644 --- a/skills-lock.json +++ b/skills-lock.json @@ -17,7 +17,7 @@ "source": "axiomhq/skills", "sourceType": "github", "skillPath": "skills/building-dashboards/SKILL.md", - "computedHash": "331e80b2aa6d7a8a9d5ec69c50d5a843f125534ed01d93b9cc916a5a92ca9552" + "computedHash": "84a130aab324297fe3fe1203b56743359732d4386dff6f0d065726d175b6e413" }, "controlling-costs": { "source": "axiomhq/skills", @@ -35,7 +35,7 @@ "source": "openclaw/carapace", "sourceType": "github", "skillPath": "openclaw-carapace/SKILL.md", - "computedHash": "6d7a33003875107ab35d4ac978753cc95b38b38aaaa306ae3db54f935d5451f7" + "computedHash": "84d8602ecdf8e1ad39cd4c5ae8f1165da571619746ab705a471d6fdfe7f4ef3e" }, "openclaw-design": { "source": "openclaw/carapace", @@ -53,7 +53,7 @@ "source": "openclaw/carapace", "sourceType": "github", "skillPath": "openclaw-design-system/SKILL.md", - "computedHash": "6d94e825c58fc5650a532e4ed8bc96208ede8a166089598d1924393cc46f3275" + "computedHash": "e7ff42933430aaf28bd2ca7a9e861c62e4aadddc7604d777118b7fec3304b042" }, "openclaw-marketing-pages": { "source": "openclaw/carapace", @@ -65,7 +65,7 @@ "source": "axiomhq/skills", "sourceType": "github", "skillPath": "skills/query-metrics/SKILL.md", - "computedHash": "3da9845936d9cb99f8fa6fdcdced06cc25eb9f16aa401e4838991d7f039fc177" + "computedHash": "955a35d22ca2244c14abafe10c57b8314ddbbc5e9ce1040ddc91edf6cfb05f8e" }, "sentry-fix-issues": { "source": "getsentry/sentry-for-ai",