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>
256 lines
10 KiB
Bash
Executable File
256 lines
10 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# chart-add: Emit a valid Axiom dashboard chart JSON object on stdout.
|
|
#
|
|
# The agent supplies *semantic* inputs (id, name, query, unit-as-string).
|
|
# This script applies all the API-shape rules:
|
|
#
|
|
# - APL vs MPL are distinct languages with distinct validation, and the
|
|
# flags reflect that:
|
|
# --apl '<APL pipeline>' → events/logs query (no dataset arg)
|
|
# --mpl '<MPL pipeline>' --dataset <name> → metrics query
|
|
# Internally both land in `query.apl` (the API field is shared); MPL
|
|
# additionally gets `query.metricsDataset`, which is what tells the
|
|
# backend to interpret the string as MPL.
|
|
#
|
|
# - MPL-specific validation:
|
|
# • Inline time ranges (`[1h..]`, `[30d..]`, etc.) are rejected at
|
|
# chart construction. They deploy fine but render with
|
|
# `AST Error (Time is provided both in the query and as a parameter)`
|
|
# because the dashboard runtime always supplies start/end via the
|
|
# API. Use `align to $__interval using …` for bucketing.
|
|
#
|
|
# - Statistic gets BOTH `unit` (enum, e.g. Percent100) AND `customUnits`
|
|
# (suffix, e.g. "%"). The mapping is delegated to scripts/metrics/unit-for,
|
|
# which already handles UCUM → Axiom-enum lookup, the "%" double-emit,
|
|
# and the "1" / "By" / "m" ambiguities.
|
|
# - TimeSeries / Table / Pie / LogStream / Heatmap accept ONLY `customUnits`
|
|
# (never the `unit` enum — the create API rejects it). They get the raw
|
|
# unit string passed verbatim as customUnits.
|
|
# - Note has neither — only `text` (top-level, never inside an `options{}`
|
|
# wrapper).
|
|
# - Always-rejected fields (`description`, `options`,
|
|
# `overrideDashboardTimeRange`, `overrideDashboardCompareAgainst`,
|
|
# `decimals`) are NEVER emitted, regardless of input.
|
|
# - Thresholds: NOT exposed yet. The API accepts `errorThreshold` /
|
|
# `warningThreshold` as bare string enums (`Above`/`AboveOrEqual`/
|
|
# `Below`/`BelowOrEqual`/`AboveOrBelow`) but the companion *value*
|
|
# field is not reachable through the create API on the deployments
|
|
# probed so far — anything we tried (`thresholdValue`, `errorValue`,
|
|
# numeric `errorThreshold`, etc.) is rejected as `Unrecognized keys`.
|
|
# Once the value field is documented or returned by `dashboard-get`
|
|
# on a UI-authored chart, add `--warn-when` / `--error-when` flags
|
|
# here. Until then, set thresholds via the UI.
|
|
#
|
|
# Usage:
|
|
# chart-add --type <type> --id <id> --name <name> [type-specific opts]
|
|
#
|
|
# Types: Note, Statistic, TimeSeries, Table, Pie, LogStream, Heatmap
|
|
#
|
|
# Per-type required flags:
|
|
# Note: --text <markdown>
|
|
# All others: exactly one of --apl <APL> or --mpl <MPL> --dataset <name>
|
|
#
|
|
# Common optional flags (data charts only):
|
|
# --unit <string> Friendly unit string ("%", "s", "ms", "B",
|
|
# "req/s", "bytes", etc.). For Statistic, mapped
|
|
# via unit-for. For other charts, becomes
|
|
# customUnits verbatim.
|
|
#
|
|
# Statistic-only flags:
|
|
# --show-chart Render a sparkline behind the value
|
|
#
|
|
# Examples:
|
|
# chart-add --type Note --id intro --name Notice --text "**Scope:** ..."
|
|
#
|
|
# # APL / events
|
|
# chart-add --type Table --id top-errs --name "Top Errors" \
|
|
# --apl "['logs'] | where status >= 500 | summarize count() by route | top 10 by count_"
|
|
#
|
|
# # MPL / metrics
|
|
# chart-add --type Statistic --id avail --name "Availability (30d)" \
|
|
# --mpl '...MPL...' --dataset k8s-metrics-staging --unit "%"
|
|
#
|
|
# chart-add --type TimeSeries --id rps --name "Read Requests (req/s)" \
|
|
# --mpl '...MPL...' --dataset k8s-metrics-staging --unit "req/s"
|
|
#
|
|
# Composing a dashboard:
|
|
# chart-add --type Note ... > /tmp/c1.json
|
|
# chart-add --type Statistic ... > /tmp/c2.json
|
|
# layout-pack c1:Note c2:Statistic > /tmp/layout.json
|
|
# dashboard-assemble --name "..." --datasets ds --layout /tmp/layout.json \
|
|
# /tmp/c1.json /tmp/c2.json > /tmp/dashboard.json
|
|
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
UNIT_FOR="$SCRIPT_DIR/metrics/unit-for"
|
|
|
|
TYPE=""
|
|
ID=""
|
|
NAME=""
|
|
APL=""
|
|
MPL=""
|
|
TEXT=""
|
|
DATASET=""
|
|
UNIT=""
|
|
SHOW_CHART=""
|
|
|
|
usage() {
|
|
sed -n '2,/^set -euo pipefail/p' "$0" | sed 's/^# \?//' | sed '$d'
|
|
exit "${1:-1}"
|
|
}
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--type) TYPE="$2"; shift 2 ;;
|
|
--id) ID="$2"; shift 2 ;;
|
|
--name) NAME="$2"; shift 2 ;;
|
|
--apl) APL="$2"; shift 2 ;;
|
|
--mpl) MPL="$2"; shift 2 ;;
|
|
--text) TEXT="$2"; shift 2 ;;
|
|
--dataset) DATASET="$2"; shift 2 ;;
|
|
--unit) UNIT="$2"; shift 2 ;;
|
|
--show-chart) SHOW_CHART=1; shift ;;
|
|
-h|--help) usage 0 ;;
|
|
*) echo "Error: unknown option '$1'" >&2; usage ;;
|
|
esac
|
|
done
|
|
|
|
# --- Required-flag validation -----------------------------------------------
|
|
|
|
[[ -z "$TYPE" ]] && { echo "Error: --type is required" >&2; usage; }
|
|
[[ -z "$ID" ]] && { echo "Error: --id is required" >&2; usage; }
|
|
[[ -z "$NAME" ]] && { echo "Error: --name is required" >&2; usage; }
|
|
|
|
case "$TYPE" in
|
|
Note|Statistic|TimeSeries|Table|Pie|LogStream|Heatmap) ;;
|
|
*) echo "Error: --type must be one of: Note, Statistic, TimeSeries, Table, Pie, LogStream, Heatmap (got '$TYPE')" >&2; exit 1 ;;
|
|
esac
|
|
|
|
# --apl and --mpl are mutually exclusive everywhere they're allowed at all.
|
|
if [[ -n "$APL" && -n "$MPL" ]]; then
|
|
echo "Error: --apl and --mpl are mutually exclusive (a chart is either an events/logs query or a metrics query, not both)" >&2
|
|
exit 1
|
|
fi
|
|
|
|
case "$TYPE" in
|
|
Note)
|
|
[[ -z "$TEXT" ]] && { echo "Error: --text is required for Note" >&2; exit 1; }
|
|
if [[ -n "$APL" || -n "$MPL" || -n "$DATASET" || -n "$UNIT" || -n "$SHOW_CHART" ]]; then
|
|
echo "Error: --apl, --mpl, --dataset, --unit, --show-chart are not valid for Note" >&2
|
|
exit 1
|
|
fi
|
|
;;
|
|
Statistic|TimeSeries|Table|Pie|LogStream|Heatmap)
|
|
[[ -n "$TEXT" ]] && { echo "Error: --text is only valid for Note" >&2; exit 1; }
|
|
if [[ -z "$APL" && -z "$MPL" ]]; then
|
|
echo "Error: $TYPE requires exactly one of --apl <APL pipeline> or --mpl <MPL pipeline>" >&2
|
|
exit 1
|
|
fi
|
|
# MPL requires --dataset (the metrics dataset name); APL forbids it.
|
|
if [[ -n "$MPL" && -z "$DATASET" ]]; then
|
|
echo "Error: --mpl requires --dataset <name> (the metrics dataset; sets query.metricsDataset, which is what flags the chart as MPL on the backend)" >&2
|
|
exit 1
|
|
fi
|
|
if [[ -n "$APL" && -n "$DATASET" ]]; then
|
|
echo "Error: --dataset is for metrics charts only — pair it with --mpl, not --apl. (APL charts read from the dataset(s) declared at dashboard level.)" >&2
|
|
exit 1
|
|
fi
|
|
# Inline-range check is MPL-only. APL doesn't use [N..] time-range
|
|
# syntax (its time semantics live in `where _time between (...)`,
|
|
# which dashboards don't need anyway).
|
|
if [[ -n "$MPL" ]] && printf '%s' "$MPL" | grep -qE '\[[^]]*\.\.[^]]*\]'; then
|
|
echo "Error: --mpl contains an inline time range (e.g. [1h..]). Dashboard chart queries cannot use inline ranges — the dashboard time picker supplies start/end. Use 'align to \$__interval using …' for bucketing instead." >&2
|
|
exit 1
|
|
fi
|
|
;;
|
|
esac
|
|
|
|
if [[ "$TYPE" != "Statistic" && -n "$SHOW_CHART" ]]; then
|
|
echo "Error: --show-chart is only valid for Statistic" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# --- Helpers ---------------------------------------------------------------
|
|
|
|
# Echo the unit-fields JSON object for a Statistic chart.
|
|
# Empty input → {} (chart will render bare numbers; caller's choice).
|
|
unit_fields_statistic() {
|
|
local u="$1"
|
|
if [[ -z "$u" ]]; then
|
|
echo '{}'
|
|
return
|
|
fi
|
|
"$UNIT_FOR" "$u"
|
|
}
|
|
|
|
# Echo the unit-fields JSON object for any non-Statistic data chart.
|
|
# Always {customUnits: ...} or {} — never the `unit` enum (API rejects it).
|
|
unit_fields_other() {
|
|
local u="$1"
|
|
if [[ -z "$u" ]]; then
|
|
echo '{}'
|
|
else
|
|
jq -n --arg u "$u" '{customUnits: $u}'
|
|
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("(?<s>\"[^\"]*\"|'\''[^'\'']*'\'')|(?<p> \\| )"; 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 "$(format_pipeline "$MPL")" --arg ds "$DATASET" '{apl: $apl, metricsDataset: $ds}'
|
|
else
|
|
jq -n --arg apl "$(format_pipeline "$APL")" '{apl: $apl}'
|
|
fi
|
|
}
|
|
|
|
# --- Emit chart JSON -------------------------------------------------------
|
|
|
|
case "$TYPE" in
|
|
Note)
|
|
jq -n --arg id "$ID" --arg name "$NAME" --arg text "$TEXT" \
|
|
'{id: $id, name: $name, type: "Note", text: $text}'
|
|
;;
|
|
Statistic)
|
|
UNIT_JSON=$(unit_fields_statistic "$UNIT")
|
|
QUERY_JSON=$(build_query)
|
|
RESULT=$(jq -n \
|
|
--arg id "$ID" \
|
|
--arg name "$NAME" \
|
|
--argjson query "$QUERY_JSON" \
|
|
--argjson unit "$UNIT_JSON" \
|
|
'{id: $id, name: $name, type: "Statistic", query: $query} + $unit')
|
|
if [[ -n "$SHOW_CHART" ]]; then
|
|
RESULT=$(jq '. + {showChart: true}' <<<"$RESULT")
|
|
fi
|
|
echo "$RESULT"
|
|
;;
|
|
TimeSeries|Table|Pie|LogStream|Heatmap)
|
|
UNIT_JSON=$(unit_fields_other "$UNIT")
|
|
QUERY_JSON=$(build_query)
|
|
jq -n \
|
|
--arg id "$ID" \
|
|
--arg name "$NAME" \
|
|
--arg type "$TYPE" \
|
|
--argjson query "$QUERY_JSON" \
|
|
--argjson unit "$UNIT_JSON" \
|
|
'{id: $id, name: $name, type: $type, query: $query} + $unit'
|
|
;;
|
|
esac
|