Skip to content

Commit 9185c1b

Browse files
committed
Create report in scheduled task
1 parent 29ec2ef commit 9185c1b

File tree

8 files changed

+407
-92
lines changed

8 files changed

+407
-92
lines changed

api/README.md

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,17 +69,38 @@ Web dashboard interface.
6969

7070
### GET /health
7171

72-
Health check endpoint for monitoring.
72+
Health check endpoint with component-level status.
7373

7474
**Response** (200 OK):
7575
```json
7676
{
77-
"status": "ok",
77+
"status": "healthy",
7878
"service": "muni-status-api",
79-
"timestamp": "2025-12-11T23:00:00.000000"
79+
"timestamp": "2025-12-11T23:00:00.000000",
80+
"components": {
81+
"cache": {
82+
"status": "healthy",
83+
"cache_age_seconds": 45.2,
84+
"is_stale": false,
85+
"last_status": "green"
86+
},
87+
"analytics": {
88+
"status": "healthy",
89+
"total_checks": 20160
90+
}
91+
}
8092
}
8193
```
8294

95+
**Overall Status Values**:
96+
- `healthy`: All components working normally
97+
- `degraded`: Some components have issues but service is functional
98+
- `unhealthy`: Critical failure (not currently returned as 200)
99+
100+
**Component Status**:
101+
- `cache`: Status cache freshness (stale if > 5 minutes old)
102+
- `analytics`: SQLite database connectivity
103+
83104
### GET /status
84105

85106
Current Muni Metro status with best-of-three smoothing logic.

api/api.py

Lines changed: 65 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -173,15 +173,76 @@ def on_get(self, req, resp):
173173

174174

175175
class HealthResource:
176-
"""Health check endpoint."""
176+
"""Health check endpoint with component status."""
177177

178178
def on_get(self, req, resp):
179-
"""Handle GET request to /health"""
179+
"""
180+
Handle GET request to /health
181+
182+
Returns component-level health status:
183+
- cache: Status cache freshness
184+
- analytics: Analytics database status
185+
- overall: Aggregated health (healthy/degraded/unhealthy)
186+
"""
187+
components = {}
188+
overall_status = 'healthy'
189+
190+
# Check status cache
191+
try:
192+
cache_data = read_cache()
193+
if cache_data:
194+
cached_at = datetime.fromisoformat(cache_data.get('cached_at', ''))
195+
cache_age = (datetime.now() - cached_at).total_seconds()
196+
is_stale = cache_age > CACHE_MAX_AGE
197+
198+
components['cache'] = {
199+
'status': 'degraded' if is_stale else 'healthy',
200+
'cache_age_seconds': round(cache_age, 1),
201+
'is_stale': is_stale,
202+
'last_status': cache_data.get('best_status', {}).get('status')
203+
}
204+
if is_stale:
205+
overall_status = 'degraded'
206+
else:
207+
components['cache'] = {
208+
'status': 'unhealthy',
209+
'error': 'No cache data available'
210+
}
211+
overall_status = 'degraded'
212+
except Exception as e:
213+
components['cache'] = {
214+
'status': 'unhealthy',
215+
'error': str(e)
216+
}
217+
overall_status = 'degraded'
218+
219+
# Check analytics database
220+
try:
221+
from lib.analytics import get_db_connection, init_db
222+
init_db() # Ensure tables exist
223+
conn = get_db_connection()
224+
cursor = conn.cursor()
225+
cursor.execute('SELECT COUNT(*) as count FROM status_checks')
226+
count = cursor.fetchone()['count']
227+
conn.close()
228+
229+
components['analytics'] = {
230+
'status': 'healthy',
231+
'total_checks': count
232+
}
233+
except Exception as e:
234+
components['analytics'] = {
235+
'status': 'degraded',
236+
'error': str(e)
237+
}
238+
# Analytics failure doesn't affect overall health critically
239+
180240
resp.status = falcon.HTTP_200
181241
resp.media = {
182-
'status': 'ok',
242+
'status': overall_status,
183243
'service': 'muni-status-api',
184-
'timestamp': datetime.now().isoformat()
244+
'timestamp': datetime.now().isoformat(),
245+
'components': components
185246
}
186247

187248

api/check_status.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
python check_status.py --continuous # Keep checking every 30 seconds
88
python check_status.py --write-cache # Single check, write to cache
99
python check_status.py --continuous --write-cache --interval 60 # Cache mode with custom interval
10+
python check_status.py --generate-reports # Generate analytics reports only
11+
12+
In continuous mode with --write-cache, analytics reports are auto-generated at midnight.
1013
"""
1114

1215
import sys
@@ -186,10 +189,37 @@ def check_status(should_write_cache=False):
186189
return True
187190

188191

192+
def generate_analytics_reports():
193+
"""Generate all analytics reports."""
194+
from lib.analytics import generate_all_reports
195+
196+
print("\nGenerating analytics reports...")
197+
try:
198+
results = generate_all_reports()
199+
for days, result in results.items():
200+
if result['success']:
201+
print(f" {days}-day report: {result['total_checks']} checks, {result['delayed_checks']} delays")
202+
else:
203+
print(f" {days}-day report: FAILED")
204+
return True
205+
except Exception as e:
206+
print(f" Error generating reports: {e}")
207+
return False
208+
209+
189210
def main():
190211
# Parse arguments
191212
continuous = '--continuous' in sys.argv or '-c' in sys.argv
192213
should_write_cache = '--write-cache' in sys.argv
214+
generate_reports_only = '--generate-reports' in sys.argv
215+
216+
# Handle report generation mode
217+
if generate_reports_only:
218+
print("=" * 60)
219+
print("Analytics Report Generator")
220+
print("=" * 60)
221+
generate_analytics_reports()
222+
return
193223

194224
# Parse interval
195225
interval = DEFAULT_INTERVAL
@@ -220,6 +250,7 @@ def main():
220250
count = 0
221251
successful = 0
222252
failed = 0
253+
last_report_hour = -1 # Track when we last generated reports
223254

224255
try:
225256
while True:
@@ -235,6 +266,12 @@ def main():
235266

236267
print(f"\nStats: {successful} successful, {failed} failed")
237268

269+
# Generate analytics reports at midnight (hour 0)
270+
current_hour = datetime.now().hour
271+
if current_hour == 0 and last_report_hour != 0:
272+
generate_analytics_reports()
273+
last_report_hour = current_hour
274+
238275
if count > 1:
239276
print(f"\nWaiting {interval} seconds until next check...")
240277

api/generate_reports_job.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Cloud Run Job script for MuniMetro analytics report generation.
4+
This script is executed by Cloud Scheduler via Cloud Run Jobs (daily at midnight).
5+
"""
6+
7+
import sys
8+
from pathlib import Path
9+
10+
# Add parent directory to path for imports
11+
API_DIR = Path(__file__).resolve().parent
12+
PROJECT_ROOT = API_DIR.parent
13+
sys.path.insert(0, str(PROJECT_ROOT))
14+
15+
from lib.analytics import generate_all_reports, init_db
16+
17+
18+
def main():
19+
"""Generate all analytics reports."""
20+
try:
21+
print("Starting MuniMetro analytics report generation...")
22+
print("-" * 60)
23+
24+
# Ensure database exists
25+
init_db()
26+
27+
# Generate reports for all time periods
28+
results = generate_all_reports()
29+
30+
# Print results
31+
all_success = True
32+
for days, result in results.items():
33+
if result['success']:
34+
print(f" {days}-day report: {result['total_checks']} checks, {result['delayed_checks']} delays")
35+
else:
36+
print(f" {days}-day report: FAILED")
37+
all_success = False
38+
39+
if all_success:
40+
print("\n✓ All reports generated successfully")
41+
sys.exit(0)
42+
else:
43+
print("\n⚠️ Some reports failed to generate", file=sys.stderr)
44+
sys.exit(1)
45+
46+
except Exception as e:
47+
print(f"\n❌ Error during report generation: {e}", file=sys.stderr)
48+
import traceback
49+
traceback.print_exc()
50+
sys.exit(1)
51+
52+
53+
if __name__ == '__main__':
54+
main()

deploy/cloud/README.md

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,15 @@ See [../../CONFIGURATION.md](../../CONFIGURATION.md) for actual deployment confi
77
## Architecture
88

99
```
10-
Cloud Scheduler (every 3 min)
11-
↓ triggers
12-
Checker (Cloud Run Job)
13-
↓ downloads image + predicts status
14-
↓ writes JSON + exits
15-
Cloud Storage (cache file)
16-
↑ reads JSON
17-
API (Cloud Run Service)
10+
Cloud Scheduler (every 3 min) Cloud Scheduler (daily midnight)
11+
↓ triggers ↓ triggers
12+
Checker Job Reports Job
13+
↓ downloads image ↓ generates analytics
14+
↓ detects status ↓ caches reports
15+
↓ writes JSON ↓ exits
16+
Cloud Storage (cache) Cloud Storage (reports cache)
17+
↑ reads ↑ reads
18+
API Service ←───────────────────────────┘
1819
↓ serves to users
1920
Users
2021
```
@@ -83,16 +84,17 @@ export MODEL_VERSION=20251223_224331
8384

8485
**Subsequent deploys**: The script automatically uses the currently deployed model version.
8586

86-
### 3. Setup Scheduler
87+
### 3. Setup Schedulers
8788

8889
```bash
8990
./deploy/cloud/setup-scheduler.sh
9091
```
9192

92-
This script:
93-
- Creates Cloud Scheduler job
94-
- Configures 3-minute interval
95-
- Runs test execution
93+
This script creates two Cloud Scheduler jobs:
94+
- **Status checker** - Runs every 3 minutes, triggers status detection
95+
- **Analytics reports** - Runs daily at midnight UTC, generates cached reports
96+
97+
The analytics reports job is isolated from user requests to prevent report generation issues from affecting the main API.
9698

9799
### 4. Setup Monitoring (Optional but Recommended)
98100

@@ -158,15 +160,16 @@ gcloud logging read 'resource.type="cloud_scheduler_job"' --limit 20
158160

159161
## Cost Estimate
160162

161-
Typical usage costs approximately $1.17/month:
163+
Typical usage costs approximately $1.20/month:
162164

163165
| Service | Usage | Cost |
164166
|---------|-------|------|
165167
| Cloud Run Service (API) | ~1000 requests/day | $0 (free tier) |
166-
| Cloud Run Jobs (Checker) | 21,600 executions/month @ ~10s each | $1.04 |
167-
| Cloud Storage | 1KB file, 75K reads/month | $0.016 |
168-
| Cloud Scheduler | 1 job | $0.10 |
169-
| **Total** | | **~$1.17/month** |
168+
| Cloud Run Jobs (Checker) | 14,400 executions/month @ ~10s each | $0.70 |
169+
| Cloud Run Jobs (Reports) | 30 executions/month @ ~5s each | $0.01 |
170+
| Cloud Storage | Cache files, ~100K reads/month | $0.02 |
171+
| Cloud Scheduler | 2 jobs | $0.20 |
172+
| **Total** | | **~$1.00/month** |
170173

171174
Cloud Run Jobs are more cost-effective than Services for scheduled tasks since execution time is billed without idle costs.
172175

0 commit comments

Comments
 (0)