Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
apiVersion: runwhen.com/v1
kind: GenerationRules
spec:
platform: gcp
generationRules:
- resourceTypes:
- gcp_projects
matchRules:
- type: pattern
pattern: ".+"
properties: ["name"]
mode: substring
slxs:
- baseName: gcp-artifact-registry-spend
qualifiers: ["project"]
baseTemplateName: gcp-artifact-registry-spend-analysis
levelOfDetail: basic
outputItems:
- type: slx
- type: sli
templateName: gcp-artifact-registry-spend-analysis-sli.yaml
- type: runbook
templateName: gcp-artifact-registry-spend-analysis-taskset.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
apiVersion: runwhen.com/v1
kind: ServiceLevelIndicator
metadata:
name: {{slx_name}}
labels:
{% include "common-labels.yaml" %}
annotations:
{% include "common-annotations.yaml" %}
spec:
displayUnitsLong: Health Score
displayUnitsShort: score
locations:
- {{default_location}}
description: Measures Artifact Registry spend health for {{ match_resource.project_name }} from billing export access, MoM growth, and cost anomaly signals.
codeBundle:
{% if repo_url %}
repoUrl: {{repo_url}}
{% else %}
repoUrl: https://github.com/runwhen-contrib/rw-cli-codecollection.git
{% endif %}
{% if ref %}
ref: {{ref}}
{% else %}
ref: main
{% endif %}
pathToRobot: codebundles/gcp-artifact-registry-spend-analysis/sli.robot
intervalStrategy: intermezzo
intervalSeconds: 300
configProvided:
- name: GCP_PROJECT_IDS
value: "{{ match_resource.project_name }}"
- name: GCP_BILLING_EXPORT_TABLE
value: "{{ custom.gcp_billing_export_table | default('') }}"
- name: COST_ANALYSIS_LOOKBACK_DAYS
value: "{{ custom.cost_analysis_lookback_days | default('30') }}"
- name: ARTIFACT_COST_SPIKE_MULTIPLIER
value: "{{ custom.artifact_cost_spike_multiplier | default('2') }}"
- name: ARTIFACT_MOM_GROWTH_THRESHOLD_PERCENT
value: "{{ custom.artifact_mom_growth_threshold_percent | default('25') }}"
secretsProvided:
{% if wb_version %}
{% include "gcp-auth.yaml" ignore missing %}
{% else %}
- name: gcp_credentials
workspaceKey: AUTH DETAILS NOT FOUND
{% endif %}
alertConfig:
tasks:
persona: eager-edgar
sessionTTL: 10m
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
apiVersion: runwhen.com/v1
kind: ServiceLevelX
metadata:
name: {{ slx_name }}
labels:
{% include "common-labels.yaml" %}
annotations:
{% include "common-annotations.yaml" %}
spec:
imageURL: https://storage.googleapis.com/runwhen-nonprod-shared-images/icons/gcp/billing/billing.svg
alias: {{ match_resource.project_name }} Artifact Registry Spend
asMeasuredBy: Artifact spend health based on billing export trends, MoM growth, and cost anomalies.
configProvided:
- name: SLX_PLACEHOLDER
value: SLX_PLACEHOLDER
owners:
- {{ workspace.owner_email }}
statement: GCP Artifact Registry and legacy GCR spend should remain predictable with no disproportionate project concentration or unexplained storage growth.
additionalContext:
{% include "gcp-hierarchy.yaml" ignore missing %}
qualified_name: "{{ match_resource.qualified_name }}"
tags:
{% include "gcp-tags.yaml" ignore missing %}
- name: cloud
value: gcp
- name: service
value: artifact-registry
- name: scope
value: project
- name: access
value: read-only
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
apiVersion: runwhen.com/v1
kind: Runbook
metadata:
name: {{slx_name}}
labels:
{% include "common-labels.yaml" %}
annotations:
{% include "common-annotations.yaml" %}
spec:
location: {{default_location}}
description: Analyze Artifact Registry and legacy GCR spend for project {{ match_resource.project_name }} using BigQuery billing export.
codeBundle:
{% if repo_url %}
repoUrl: {{repo_url}}
{% else %}
repoUrl: https://github.com/runwhen-contrib/rw-cli-codecollection.git
{% endif %}
{% if ref %}
ref: {{ref}}
{% else %}
ref: main
{% endif %}
pathToRobot: codebundles/gcp-artifact-registry-spend-analysis/runbook.robot
configProvided:
- name: GCP_PROJECT_IDS
value: "{{ match_resource.project_name }}"
- name: GCP_BILLING_EXPORT_TABLE
value: "{{ custom.gcp_billing_export_table | default('') }}"
- name: COST_ANALYSIS_LOOKBACK_DAYS
value: "{{ custom.cost_analysis_lookback_days | default('30') }}"
- name: ARTIFACT_COST_SPIKE_MULTIPLIER
value: "{{ custom.artifact_cost_spike_multiplier | default('2') }}"
- name: ARTIFACT_MOM_GROWTH_THRESHOLD_PERCENT
value: "{{ custom.artifact_mom_growth_threshold_percent | default('25') }}"
- name: ARTIFACT_PROJECT_COST_THRESHOLD_PERCENT
value: "{{ custom.artifact_project_cost_threshold_percent | default('20') }}"
- name: OUTPUT_FORMAT
value: "{{ custom.output_format | default('table') }}"
secretsProvided:
{% if wb_version %}
{% include "gcp-auth.yaml" ignore missing %}
{% else %}
- name: gcp_credentials
workspaceKey: AUTH DETAILS NOT FOUND
{% endif %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Test Infrastructure

Static structure validation only. Live BigQuery billing export integration requires GCP credentials and a configured billing export table.

Run from this directory:

```bash
task
```

Or:

```bash
./validate-all-tests.sh
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
version: "3"

tasks:
default:
desc: "Validate CodeBundle structure"
cmds:
- task: validate-structure

validate-structure:
desc: "Run static checks for required files"
cmds:
- ./validate-all-tests.sh

clean:
desc: "Remove local test outputs"
cmds:
- rm -f ../*.json ../*_report.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#!/usr/bin/env bash
# Static validation for gcp-artifact-registry-spend-analysis (no live GCP required).
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
test -f "$ROOT/runbook.robot"
test -f "$ROOT/sli.robot"
test -f "$ROOT/README.md"
test -f "$ROOT/.runwhen/generation-rules/gcp-artifact-registry-spend-analysis.yaml"
test -f "$ROOT/.runwhen/templates/gcp-artifact-registry-spend-analysis-slx.yaml"
test -f "$ROOT/.runwhen/templates/gcp-artifact-registry-spend-analysis-taskset.yaml"
test -f "$ROOT/.runwhen/templates/gcp-artifact-registry-spend-analysis-sli.yaml"
for f in \
artifact-billing-helpers.sh \
analyze-artifact-registry-spend.sh \
report-top-artifact-cost-contributors.sh \
compare-artifact-spend-mom.sh \
detect-artifact-cost-anomalies.sh \
generate-artifact-spend-recommendations.sh \
sli-artifact-spend-health-score.sh
do
test -f "$ROOT/$f"
done
grep -q 'severity' "$ROOT/analyze-artifact-registry-spend.sh"
grep -q 'next_steps' "$ROOT/detect-artifact-cost-anomalies.sh"
grep -q 'RW.Core.Add Issue' "$ROOT/runbook.robot"
grep -q 'RW.Core.Push Metric' "$ROOT/sli.robot"
grep -q 'type: sli' "$ROOT/.runwhen/generation-rules/gcp-artifact-registry-spend-analysis.yaml"
echo "gcp-artifact-registry-spend-analysis bundle structure OK"
68 changes: 68 additions & 0 deletions codebundles/gcp-artifact-registry-spend-analysis/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# GCP Artifact Registry Spend Analysis

Analyze Google Cloud Artifact Registry and legacy Container Registry (GCR) spend from BigQuery billing export. Surfaces storage and egress cost trends, top contributors, anomalies, and optimization recommendations so operators can align artifact spend with current or required artifacts rather than stale storage.

## Overview

- **Spend analysis**: Per-project, per-SKU artifact costs with daily, weekly, and monthly rollups from billing export
- **Top contributors**: Rank projects and SKUs; flag disproportionate project share and legacy GCR dominance
- **Month-over-month trends**: Compare the last three complete calendar months; detect storage growth without pull activity
- **Anomaly detection**: Daily cost spikes (2x 7-day average) and sustained weekly deviations from 30-day trend
- **Optimization summary**: Actionable recommendations for cleanup policies, GCR migration, and scanning right-sizing

## Configuration

### Required Variables

None. When `GCP_PROJECT_IDS` is blank, projects with artifact-related spend are inferred from the billing export.

### Optional Variables

- `GCP_PROJECT_IDS`: Comma-separated GCP project IDs to analyze; blank auto-discovers from billing export (default: `""`)
- `GCP_BILLING_EXPORT_TABLE`: BigQuery billing export table (auto-discovered if unset) (default: `""`)
- `COST_ANALYSIS_LOOKBACK_DAYS`: Days of billing history to analyze (default: `30`)
- `ARTIFACT_COST_SPIKE_MULTIPLIER`: Daily cost spike threshold as multiple of 7-day average (default: `2`)
- `ARTIFACT_MOM_GROWTH_THRESHOLD_PERCENT`: Month-over-month growth percentage that triggers an issue (default: `25`)
- `ARTIFACT_PROJECT_COST_THRESHOLD_PERCENT`: Project share of total artifact spend that triggers an issue; 0 disables (default: `20`)
- `OUTPUT_FORMAT`: Report format: `table`, `csv`, `json`, or `all` (default: `table`)

### Secrets

- `gcp_credentials`: GCP service account JSON with BigQuery billing export read access (`roles/bigquery.dataViewer`, `roles/bigquery.jobUser` on the billing export project)

## Prerequisites

1. Enable [BigQuery billing export](https://cloud.google.com/billing/docs/how-to/export-data-bigquery) for your organization
2. Install `gcloud`, `bq`, and `jq`
3. Grant the service account read access to the billing export dataset

## Tasks Overview

### Analyze Artifact Registry Spend by Project and SKU

Queries billing export for Artifact Registry and legacy GCR SKUs. Produces per-project, per-SKU totals with time rollups. May raise an issue when no artifact spend is found in the lookback window.

### Report Top Artifact Registry Cost Contributors

Ranks projects and SKUs by spend. Raises issues when a project exceeds `ARTIFACT_PROJECT_COST_THRESHOLD_PERCENT` of total artifact spend or when legacy GCR SKUs dominate spend.

### Compare Artifact Registry Spend Month-over-Month

Compares the last three complete calendar months. Raises issues when MoM growth exceeds `ARTIFACT_MOM_GROWTH_THRESHOLD_PERCENT` or when storage costs grow without corresponding transfer/pull activity.

### Detect Artifact Storage Cost Anomalies

Detects daily spikes at `ARTIFACT_COST_SPIKE_MULTIPLIER` times the 7-day average and weekly deviations from the 30-day trend.

### Generate Artifact Registry Spend Optimization Summary

Consolidates findings into recommendations: enable cleanup policies, retire legacy GCR, reduce duplicate tags, and right-size vulnerability scanning. Cross-references high-cost projects for follow-up with `gcp-artifact-registry-governance`.

## Related Bundles

- **gcp-project-cost-health**: Organization-wide GCP cost reporting
- **gcp-artifact-registry-governance**: Operational inventory and cleanup-policy checks

## Limitations

BigQuery billing export does not expose individual repository names. Repository-level attribution requires correlating high-spend projects with the governance bundle's inventory output.
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
#!/usr/bin/env bash
set -euo pipefail
set -x
# -----------------------------------------------------------------------------
# REQUIRED ENV VARS: (optional) GCP_PROJECT_IDS, GCP_BILLING_EXPORT_TABLE
# OPTIONAL: COST_ANALYSIS_LOOKBACK_DAYS, OUTPUT_FORMAT
#
# Query BigQuery billing export for Artifact Registry and legacy GCR SKUs.
# Produces per-project, per-SKU totals with daily/weekly/monthly rollups.
# Outputs artifact_spend_analysis_issues.json
# -----------------------------------------------------------------------------

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=artifact-billing-helpers.sh
source "${SCRIPT_DIR}/artifact-billing-helpers.sh"

OUTPUT_FILE="artifact_spend_analysis_issues.json"
REPORT_FILE="artifact_spend_analysis_report.txt"
JSON_FILE="artifact_spend_analysis_report.json"
CSV_FILE="artifact_spend_analysis_report.csv"
OUTPUT_FORMAT="${OUTPUT_FORMAT:-table}"
issues_json='[]'

read -r START_DATE END_DATE <<< "$(get_date_range)"
LOOKBACK=$(get_lookback_days)

echo "Analyzing Artifact Registry spend from ${START_DATE} to ${END_DATE} (${LOOKBACK}-day lookback)"

if ! BILLING_TABLE=$(ensure_billing_access); then
cat artifact_spend_analysis_issues.json
exit 0
fi

PROJECT_FILTER=$(build_project_filter_sql)

QUERY="
SELECT
project.id AS project_id,
project.name AS project_name,
sku.description AS sku_description,
ROUND(SUM(cost), 4) AS total_cost,
ROUND(SUM(IF(DATE(usage_start_time) >= DATE_SUB('${END_DATE}', INTERVAL 7 DAY), cost, 0)), 4) AS weekly_cost,
ROUND(SUM(IF(DATE(usage_start_time) >= DATE_SUB('${END_DATE}', INTERVAL 30 DAY), cost, 0)), 4) AS monthly_cost,
ROUND(SUM(IF(DATE(usage_start_time) >= DATE_SUB('${END_DATE}', INTERVAL ${LOOKBACK} DAY), cost, 0)), 4) AS lookback_cost
FROM \`${BILLING_TABLE}\`
WHERE DATE(usage_start_time) BETWEEN '${START_DATE}' AND '${END_DATE}'
AND ${ARTIFACT_SKU_FILTER}
${PROJECT_FILTER}
GROUP BY project_id, project_name, sku_description
HAVING total_cost > 0
ORDER BY total_cost DESC
"

if ! RESULT=$(run_bq_query_json "$BILLING_TABLE" "$QUERY" 50000); then
write_access_issue "Cannot Query Artifact Registry Spend for \`${GCP_PROJECT_IDS:-all projects}\`" "BigQuery query for artifact spend failed." "$OUTPUT_FILE"
exit 0
fi

ROW_COUNT=$(echo "$RESULT" | jq 'length')
TOTAL_COST=$(echo "$RESULT" | jq '[.[].total_cost | tonumber] | add // 0')

{
echo "GCP Artifact Registry Spend Analysis"
echo "Period: ${START_DATE} to ${END_DATE}"
echo "Total artifact-related spend: \$${TOTAL_COST}"
echo "Rows (project x SKU): ${ROW_COUNT}"
echo ""
echo "Top contributors:"
echo "$RESULT" | jq -r '.[:15][] | "\(.project_id)\t\(.sku_description)\t$\(.total_cost)"' 2>/dev/null || true
} > "$REPORT_FILE"

echo "$RESULT" | jq '.' > "$JSON_FILE"

if [[ "$OUTPUT_FORMAT" == "csv" || "$OUTPUT_FORMAT" == "all" ]]; then
echo "project_id,project_name,sku_description,total_cost,weekly_cost,monthly_cost,lookback_cost" > "$CSV_FILE"
echo "$RESULT" | jq -r '.[] | [.project_id, .project_name, .sku_description, .total_cost, .weekly_cost, .monthly_cost, .lookback_cost] | @csv' >> "$CSV_FILE"
fi

if [[ "$ROW_COUNT" -eq 0 ]]; then
issues_json=$(echo "$issues_json" | jq \
--arg title "No Artifact Registry Spend Found for \`${GCP_PROJECT_IDS:-configured scope}\`" \
--arg details "No billing rows matched Artifact Registry or legacy Container Registry SKUs in the lookback window." \
--arg severity "3" \
--arg next_steps "Confirm projects use Artifact Registry/GCR and billing export includes recent data. Run gcp-artifact-registry-governance for inventory." \
'. += [{
"title": $title,
"severity": ($severity | tonumber),
"expected": "Projects with active artifact storage should appear in billing export",
"actual": "Zero artifact-related SKU costs in the last '"${LOOKBACK}"' days",
"details": $details,
"next_steps": $next_steps
}]')
fi

echo "$issues_json" > "$OUTPUT_FILE"
echo "Analysis completed. Report: $REPORT_FILE"
cat "$REPORT_FILE"
Loading
Loading