Skip to content

Commit dbaea94

Browse files
authored
Merge pull request #485 from NHSDigital/feature/eja-eli-420-automated-prod-capacity-demand-report
eli-420 adding a simple github workflow to extract Cloudwatch charts …
2 parents 33a6185 + 4d472cd commit dbaea94

File tree

3 files changed

+434
-0
lines changed

3 files changed

+434
-0
lines changed
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
name: Monthly Demand and Capacity Report
2+
3+
on:
4+
# Run monthly on the 1st at 9 AM UTC
5+
schedule:
6+
- cron: "0 9 1 * *"
7+
8+
# Allow manual trigger
9+
workflow_dispatch:
10+
11+
permissions:
12+
contents: read
13+
id-token: write # Required for AWS OIDC authentication
14+
15+
jobs:
16+
generate-report:
17+
runs-on: ubuntu-latest
18+
environment: prod
19+
20+
steps:
21+
- name: Checkout code
22+
uses: actions/checkout@v4
23+
24+
- name: Set up Python
25+
uses: actions/setup-python@v5
26+
with:
27+
python-version: "3.11"
28+
29+
- name: "Configure AWS Credentials"
30+
uses: aws-actions/configure-aws-credentials@v5
31+
with:
32+
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/service-roles/github-actions-api-deployment-role
33+
aws-region: eu-west-2
34+
35+
- name: Generate dashboard report
36+
run: |
37+
chmod +x scripts/export_dashboard_image.sh
38+
./scripts/export_dashboard_image.sh Demand_And_Capacity_Prod
39+
env:
40+
AWS_REGION: eu-west-2
41+
42+
- name: Upload report as artifact
43+
uses: actions/upload-artifact@v4
44+
with:
45+
name: capacity-report
46+
path: |
47+
dashboard_exports/*.html
48+
dashboard_exports/*.png
49+
retention-days: 90
50+
51+
- name: Send to Slack
52+
if: success()
53+
env:
54+
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_D_AND_C_WEBHOOK }}
55+
run: |
56+
# Get the latest HTML report
57+
REPORT_FILE=$(ls -t dashboard_exports/dashboard_report_*.html | head -1)
58+
REPORT_NAME=$(basename "$REPORT_FILE")
59+
60+
# GitHub Actions URL
61+
GITHUB_URL="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
62+
63+
# Send Slack notification with simple variables for Workflow Automation
64+
curl -X POST "$SLACK_WEBHOOK_URL" \
65+
-H 'Content-Type: application/json' \
66+
-d @- <<EOF
67+
{
68+
"report_title": "📊 Monthly Demand & Capacity Report - EliD - Prod",
69+
"report_period": "Last 8 weeks",
70+
"generated_date": "$(date +'%Y-%m-%d %H:%M UTC')",
71+
"widgets_count": "7",
72+
"github_url": "$GITHUB_URL",
73+
"report_name": "$REPORT_NAME"
74+
}
75+
EOF

scripts/export_dashboard_image.sh

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
#!/bin/bash
2+
# Script to export CloudWatch dashboard widgets as individual images
3+
4+
set -e
5+
6+
DASHBOARD_NAME="${1:-Demand_And_Capacity_Prod}"
7+
OUTPUT_DIR="dashboard_exports"
8+
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
9+
REGION="${AWS_REGION:-eu-west-2}"
10+
11+
# Create output directory
12+
mkdir -p "$OUTPUT_DIR"
13+
14+
echo "========================================="
15+
echo "CloudWatch Dashboard Image Export"
16+
echo "========================================="
17+
echo "Dashboard: $DASHBOARD_NAME"
18+
echo "Region: $REGION"
19+
echo "Output: $OUTPUT_DIR"
20+
echo ""
21+
22+
# Get dashboard definition
23+
echo "Fetching dashboard definition..."
24+
aws cloudwatch get-dashboard \
25+
--dashboard-name "$DASHBOARD_NAME" \
26+
--region "$REGION" \
27+
--output json > "${OUTPUT_DIR}/dashboard_definition_${TIMESTAMP}.json"
28+
29+
if [ $? -ne 0 ]; then
30+
echo "Error: Failed to fetch dashboard. Check dashboard name and AWS credentials."
31+
exit 1
32+
fi
33+
34+
echo "✓ Dashboard definition saved"
35+
echo ""
36+
37+
# Extract and process widgets using Python
38+
echo "Extracting and capturing widget images..."
39+
40+
export TIMESTAMP
41+
export OUTPUT_DIR
42+
export REGION
43+
44+
python3 - <<'PYTHON_SCRIPT'
45+
import json
46+
import base64
47+
import subprocess
48+
import sys
49+
import os
50+
51+
# Read dashboard definition
52+
with open(f"dashboard_exports/dashboard_definition_{os.environ['TIMESTAMP']}.json") as f:
53+
dashboard_data = json.load(f)
54+
55+
# Parse the dashboard body
56+
dashboard_body = json.loads(dashboard_data['DashboardBody'])
57+
widgets = dashboard_body.get('widgets', [])
58+
59+
print(f"Found {len(widgets)} widgets in dashboard\n")
60+
61+
output_dir = os.environ['OUTPUT_DIR']
62+
region = os.environ['REGION']
63+
64+
for idx, widget in enumerate(widgets, 1):
65+
properties = widget.get('properties', {})
66+
67+
# Get widget title for filename
68+
title = properties.get('title', f'widget_{idx}')
69+
safe_title = "".join(c if c.isalnum() or c in ('-', '_') else '_' for c in title)
70+
71+
# Create metric widget JSON for API call
72+
metric_widget = {
73+
'width': widget.get('width', 6) * 100,
74+
'height': widget.get('height', 6) * 100,
75+
}
76+
77+
# Copy relevant properties
78+
for key in ['metrics', 'view', 'stacked', 'region', 'title', 'period', 'stat', 'yAxis', 'annotations']:
79+
if key in properties:
80+
metric_widget[key] = properties[key]
81+
82+
# Set region if not in widget
83+
if 'region' not in metric_widget:
84+
metric_widget['region'] = region
85+
86+
# Override time range to show last 8 weeks (56 days)
87+
from datetime import datetime, timedelta
88+
end_time = datetime.utcnow()
89+
start_time = end_time - timedelta(weeks=8)
90+
91+
metric_widget['start'] = start_time.strftime('%Y-%m-%dT%H:%M:%S.000Z')
92+
metric_widget['end'] = end_time.strftime('%Y-%m-%dT%H:%M:%S.000Z')
93+
94+
widget_json = json.dumps(metric_widget)
95+
output_file = f"{output_dir}/{idx:02d}_{safe_title}.png"
96+
97+
print(f"[{idx}/{len(widgets)}] Capturing: {title}")
98+
99+
try:
100+
# Call AWS CLI to get widget image
101+
result = subprocess.run(
102+
[
103+
'aws', 'cloudwatch', 'get-metric-widget-image',
104+
'--metric-widget', widget_json,
105+
'--output-format', 'png',
106+
'--region', region,
107+
'--output', 'text'
108+
],
109+
capture_output=True,
110+
text=True,
111+
check=True
112+
)
113+
114+
# Decode base64 and save
115+
image_data = base64.b64decode(result.stdout)
116+
with open(output_file, 'wb') as f:
117+
f.write(image_data)
118+
119+
print(f" ✓ Saved to: {output_file}")
120+
121+
except subprocess.CalledProcessError as e:
122+
print(f" ✗ Failed: {e.stderr}")
123+
except Exception as e:
124+
print(f" ✗ Error: {str(e)}")
125+
126+
print()
127+
128+
print("========================================")
129+
print("Export complete!")
130+
print(f"Images saved to: {output_dir}/")
131+
print("========================================")
132+
133+
PYTHON_SCRIPT
134+
135+
echo ""
136+
echo "Generating HTML report..."
137+
python3 scripts/generate_dashboard_report.py --input "${OUTPUT_DIR}"
138+
139+
echo ""
140+
echo "✓ Complete! Check the ${OUTPUT_DIR}/ directory for:"
141+
echo " - Individual widget images (PNG files)"
142+
echo " - Combined HTML report (dashboard_report_*.html)"

0 commit comments

Comments
 (0)