diff --git a/skills/cloud-monitoring/CHANGELOG.md b/skills/cloud-monitoring/CHANGELOG.md index 525a691..4f5a2b2 100644 --- a/skills/cloud-monitoring/CHANGELOG.md +++ b/skills/cloud-monitoring/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this skill will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.2.17] - 2026-04-20 +### Changed +- Replaced 'gist' terminology with 'Sparkline' and 'ASCII Art'. +- Added pipe `|` wrappers around the generated sparklines to ensure empty spaces are visibly framed. +- Delegated standalone CSV usage to the new `csv_to_sparkline.py` script hosted in `monitoring-graphs`. + ## [0.2.16] - 2026-04-16 ### Added - Rephrased gist the 'graphical' part. diff --git a/skills/cloud-monitoring/SKILL.md b/skills/cloud-monitoring/SKILL.md index f7bfef0..bed562a 100644 --- a/skills/cloud-monitoring/SKILL.md +++ b/skills/cloud-monitoring/SKILL.md @@ -1,7 +1,7 @@ --- name: cloud-monitoring description: 🐉 Skill for interacting with Google Cloud Monitoring (CM) via APIs to avoid large context bloat. Produces nice short synoptic "gists" of graphs -version: 0.2.16 +version: 0.2.17 tools: - mcp_google-monitoring_get_alert - mcp_google-monitoring_get_alert_policy @@ -36,5 +36,5 @@ This skill provides utilities for analyzing and extracting data from Google Clou ## Available Tools * `scripts/export_timeseries_to_csv.py`: Fetches time-series data for specified metric(s) and time range, outputting a CSV file with metadata headers. Supports extracting two or more variables for direct correlation and comparison. This is an amazing synoptic you can surface to the user - * It's very effective particularly to share the "**gist**" / **Shape** part showing use in TEXT the graphical shape of the curve, eg "█▇▆▇ ▂▃ ▂ ▂ ▂ ▂" or "█▆▃▂▄▅▅_▄▄▄▂▄▄█▆". This is very useful to humans for visual feedback! + * It's very effective particularly to share the "**Sparkline**" / **ASCII Art** part showing in TEXT the graphical shape of the curve (e.g., `|█▇▆▇ ▂▃ ▂ ▂|`). This is very useful for humans for rapid visual feedback! Use `csv_to_sparkline.py` from `monitoring-graphs` to recreate it for arbitrary CSVs. * `scripts/setup-frontend-slo.sh`: A bash script to automatically set up a 99.9% availability SLO using Log-based Metrics for a 'frontend' service. (See `scripts/README.md` for full manual). diff --git a/skills/cloud-monitoring/scripts/export_timeseries_to_csv.py b/skills/cloud-monitoring/scripts/export_timeseries_to_csv.py index ad14761..c9cad91 100755 --- a/skills/cloud-monitoring/scripts/export_timeseries_to_csv.py +++ b/skills/cloud-monitoring/scripts/export_timeseries_to_csv.py @@ -91,8 +91,9 @@ def generate_sparkline(vals, num_bins=16): return "▄" * len(binned) normalized = np.round((binned - vmin) / (vmax - vmin) * 7).astype(int) - chars = ['_', '▂', '▃', '▄', '▅', '▆', '▇', '█'] - return "".join([chars[i] for i in normalized]) + chars = [' ', '▂', '▃', '▄', '▅', '▆', '▇', '█'] + shape_str = "".join([chars[i] for i in normalized]) + return f"|{shape_str}|" def main(): epilog_text = """ @@ -255,7 +256,7 @@ def main(): stats_summary[metric_name] = { "count": len(vals), - "shape": generate_sparkline(vals, num_bins=args.num_bins), + "sparkline": generate_sparkline(vals, num_bins=args.num_bins), "min": vals[min_idx], "min_ts": metric_points[min_idx]["timestamp"], "min_pos": get_position_label(min_idx, len(vals)), @@ -291,7 +292,8 @@ def main(): max_time = s['max_ts'][11:19] print(f"\nMetric: {m}") - print(f" Shape: {s['shape']}") + print(f" Sparkline:{s['sparkline']}") + print(f" To gen: uv run ../monitoring-graphs/scripts/csv_to_sparkline.py --csv {args.output} --values-column-name value") print(f" Count: {s['count']}") print(f" Average: {s['avg']:.4f}") print(f" Variance: {s['var']:.4f}") @@ -299,7 +301,7 @@ def main(): print(f" Maximum: [{max_time}] {s['max']:.4f} ({s['max_pos']})") # Write to CSV header - csvfile.write(f"# stats_{m}_shape: {s['shape']}\n") + csvfile.write(f"# stats_{m}_sparkline: {s['sparkline']}\n") csvfile.write(f"# stats_{m}_avg: {s['avg']}\n") csvfile.write(f"# stats_{m}_min: [{min_time}] {s['min']} ({s['min_pos']})\n") csvfile.write(f"# stats_{m}_max: [{max_time}] {s['max']} ({s['max_pos']})\n") @@ -310,7 +312,7 @@ def main(): print("SYNOPTIC COMPARISON") print("="*40) for m, s in stats_summary.items(): - print(f"{s['shape']} {m}") + print(f"{s['sparkline']} {m}") print("="*40) csvfile.write(f"# metadata_point_count: {len(all_data_points)}\n") diff --git a/skills/monitoring-graphs/CHANGELOG.md b/skills/monitoring-graphs/CHANGELOG.md index 123c7c1..16ff4dc 100644 --- a/skills/monitoring-graphs/CHANGELOG.md +++ b/skills/monitoring-graphs/CHANGELOG.md @@ -2,6 +2,10 @@ All notable changes to the `monitoring-graphs` skill will be documented in this file. +## [0.0.6] - 2026-04-20 +### Added +- Added `scripts/csv_to_sparkline.py` script extracted from `cloud-monitoring`. This generic script allows you to auto-generate an ASCII sparkline graphic wrapped in `|` for any given CSV numeric column. You can specify `--values-column-name` explicitly. + ## [0.0.5] - 2026-04-07 ### 📊 The "Data Integrity & Aesthetics" Update diff --git a/skills/monitoring-graphs/SKILL.md b/skills/monitoring-graphs/SKILL.md index f0a8a89..cf7b8ed 100644 --- a/skills/monitoring-graphs/SKILL.md +++ b/skills/monitoring-graphs/SKILL.md @@ -4,7 +4,7 @@ description: 🐉 skill for generating high-quality, annotated incident graphs f #verbose_description: Expert SRE skill for generating high-quality, annotated incident graphs for post-mortems using Python and Monitoring MCP on Google Cloud. Use this when the user needs to visualize an outage, show error rates, or correlate metrics with incident milestones (start, detection, mitigation, end). metadata: author: Riccardo Carlesso - version: 0.0.5 + version: 0.0.6 --- # 📈 Monitoring & Incident Graphing Skill 📊 diff --git a/skills/monitoring-graphs/scripts/csv_to_sparkline.py b/skills/monitoring-graphs/scripts/csv_to_sparkline.py new file mode 100755 index 0000000..45985dd --- /dev/null +++ b/skills/monitoring-graphs/scripts/csv_to_sparkline.py @@ -0,0 +1,115 @@ +#!/usr/bin/env -S uv run +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# /// script +# requires-python = ">=3.10" +# dependencies = [ +# "pandas", +# "numpy", +# "python-dateutil" +# ] +# /// + +import argparse +import sys +import numpy as np +import pandas as pd + +def generate_sparkline(vals, num_bins=16, wrapper="|"): + """ + Generates an ASCII sparkline from a sequence of values. + Uses ' ' (space) for the lowest/empty bucket to ensure empty spots are visible when framed. + """ + if len(vals) == 0: + return f"{wrapper}{wrapper}" + if len(vals) < num_bins: + num_bins = len(vals) + + splits = np.array_split(vals, num_bins) + binned = np.array([np.mean(s) for s in splits if len(s) > 0]) + + vmin, vmax = np.min(binned), np.max(binned) + if vmin == vmax: + shape_str = "▄" * len(binned) + return f"{wrapper}{shape_str}{wrapper}" + + normalized = np.round((binned - vmin) / (vmax - vmin) * 7).astype(int) + # Replaced lowest char '_' with ' ' (space) as requested for better framing visibility + chars = [' ', '▂', '▃', '▄', '▅', '▆', '▇', '█'] + shape_str = "".join([chars[i] for i in normalized]) + return f"{wrapper}{shape_str}{wrapper}" + +def main(): + parser = argparse.ArgumentParser(description="Generates an ASCII sparkline from a CSV file.") + parser.add_argument("--csv", required=True, help="Path to the input CSV data") + parser.add_argument("--values-column-name", dest="values_column", required=True, help="Column name containing the numeric values.") + parser.add_argument("--time-column-name", dest="time_column", required=False, help="(Optional) Time column name. Will try to auto-infer if not provided.") + parser.add_argument("--bins", type=int, default=16, help="Number of bins (characters) in the sparkline.") + parser.add_argument("--wrapper", type=str, default="|", help="Wrapper character around the sparkline (default: |).") + args = parser.parse_args() + + try: + df = pd.read_csv(args.csv) + try: + df = pd.read_csv(args.csv) + except Exception as e: + print(f"Error reading CSV '{args.csv}': {e}", file=sys.stderr) + sys.exit(1) + print(f"Error reading CSV: {e}", file=sys.stderr) + sys.exit(1) + + if args.values_column not in df.columns: + print(f"Error: Column '{args.values_column}' not found in CSV. Available columns: {', '.join(df.columns)}", file=sys.stderr) + sys.exit(1) + + # Autoinfer time column if not provided + time_col = args.time_column + if not time_col: + # Check heuristics for time column + time_keywords = ['time', 'timestamp', 'date', 'datetime'] + for col in df.columns: + if any(k in col.lower() for k in time_keywords): + time_col = col + break + + # If still not found, try to see if the first column can be parsed as datetime, or just use the first column + if not time_col and len(df.columns) > 0: + potential_col = df.columns[0] + if potential_col != args.values_column: + try: + pd.to_datetime(df[potential_col][:5]) + time_col = potential_col + except: + # just pick the first column if no time column is found, as long as it's not the value! + time_col = potential_col + + if time_col and time_col in df.columns: + try: + # Sort by the inferred or explicit time column + df[time_col] = pd.to_datetime(df[time_col]) + df = df.sort_values(by=time_col) + except Exception: + pass # ignore sorting if it's not actually a time parsing compatible column + + values = df[args.values_column].dropna().values + if len(values) == 0: + print(f"Error: No valid numeric data found in column '{args.values_column}'.", file=sys.stderr) + sys.exit(1) + + sparkline = generate_sparkline(values, num_bins=args.bins, wrapper=args.wrapper) + print(sparkline) + +if __name__ == "__main__": + main()