Shared email alerting, retries, and volume-drop checks for Mesonet workers
Project description
Mesonet Alerts Package
A production-ready shared Python package for email alerting across mesonet microservices. Provides HTML + plaintext email templates, SMTP configuration, DynamoDB persistence, retry helpers, and volume drop detection.
🚀 Quick Start
Installation
Add to your service's pyproject.toml:
[project]
dependencies = [
"mesonet_alerts @ file://../common/mesonet_alerts"
]
Environment Variables
# SMTP Configuration (required for email sending)
ALERTS_SMTP_HOST=localhost
ALERTS_SMTP_PORT=1025
ALERTS_SMTP_USER= # Optional for local dev
ALERTS_SMTP_PASS= # Optional for local dev
ALERTS_FROM="alerts@local.test"
ALERTS_TO="admin@local.test,kevin@local.test"
# Optional persistence
ALERTS_TABLE_NAME=alerts # Enable DynamoDB persistence
# Volume monitoring
EXPECTED_RECORDS_PER_PROVIDER_PER_HOUR=100
Basic Usage
from mesonet_alerts import EmailAlerter, AlertStore, run_with_retries
# Initialize components
emailer = EmailAlerter()
store = AlertStore()
# Send an alert
context = {
"stage": "ingest",
"severity": "ERROR",
"provider": "colorado",
"run_id": "run_123",
"error": "Connection timeout"
}
emailer.send("process_failure", "Alert: Colorado Ingest Failed", context)
# Store alert with deduplication
store.put_alert(
provider="colorado",
stage="ingest",
severity="ERROR",
code="CONNECTION_TIMEOUT",
message="Failed to connect to provider",
dedupe_key="timeout#colorado#run_123"
)
📧 Email Templates
The package includes four pre-built templates with eye-catching HTML + plaintext versions:
process_failure- General processing failuresprovider_empty_data- Empty data warningsharmonize_failure- Data harmonization errorsvolume_drop- Volume drop alerts
Template Variables
All templates support these variables:
context = {
"stage": "ingest", # Processing stage
"severity": "ERROR", # ERROR, WARN, INFO
"provider": "colorado", # Provider name
"run_id": "run_123", # Optional run identifier
"trace_id": "trace_456", # Optional trace identifier
"error": "Error message", # Error details
"attempts": 3, # Number of attempts
"timestamp_iso": "2025-01-15T10:30:00Z",
# Volume drop specific
"expected": 100, # Expected record count
"actual": 75, # Actual record count
"drop_pct": "25.0", # Drop percentage
"window_start": "2025-01-15 13:00 UTC",
"window_end": "2025-01-15 14:00 UTC",
}
🔄 Retry Helpers
Basic Retry Usage
from mesonet_alerts.retry import run_with_retries, ProviderEmptyDataError
def fetch_provider_data():
# Your data fetching logic
data = api_client.get_data()
if not data:
raise ProviderEmptyDataError("No data returned")
return data
def is_retryable_error(e):
return isinstance(e, (ProviderEmptyDataError, ConnectionError))
try:
data = run_with_retries(fetch_provider_data, is_retryable_error, attempts=3)
except ProviderEmptyDataError:
# Handle final failure after retries
pass
Decorator Usage
from mesonet_alerts.retry import retry_on_exceptions
@retry_on_exceptions(ProviderEmptyDataError, ConnectionError)
def fetch_with_auto_retry():
return api_client.get_data()
📊 Volume Drop Detection
from datetime import datetime, timezone, timedelta
from mesonet_alerts.dropcheck import check_and_alert_volume_drop
# Define time window
now = datetime.now(timezone.utc).replace(minute=0, second=0, microsecond=0)
window_end = now
window_start = now - timedelta(hours=1)
# Check for volume drops
check_and_alert_volume_drop(
provider="colorado",
stage="harmonize",
actual_count=75, # Only 75 records processed
expected_count=100, # Expected 100 records
threshold=0.20, # Alert on >20% drop
window_start=window_start,
window_end=window_end,
emailer=emailer,
store=store
)
🏗️ Integration Examples
Ingest Worker Integration
from mesonet_alerts.emailer import EmailAlerter
from mesonet_alerts.store import AlertStore
from mesonet_alerts.retry import run_with_retries, ProviderEmptyDataError
emailer = EmailAlerter()
store = AlertStore()
def _is_retryable(e: Exception) -> bool:
return isinstance(e, ProviderEmptyDataError) or "timeout" in str(e).lower()
def fetch_and_process_with_alerts(provider: str, run_id: str, trace_id: str):
def _do():
data = fetch_from_provider(provider) # your existing call
if not data:
raise ProviderEmptyDataError(f"Empty data from {provider}")
return process_data(data)
try:
return run_with_retries(_do, _is_retryable, attempts=3)
except ProviderEmptyDataError as e:
ctx = {
"stage": "ingest", "severity": "WARN", "provider": provider,
"run_id": run_id, "trace_id": trace_id, "error": str(e), "attempts": 3
}
emailer.send("provider_empty_data", f"[INGEST] Empty data: {provider}", ctx)
store.put_alert(
provider=provider, stage="ingest", severity="WARN",
code="PROVIDER_EMPTY", message="Empty data after retries",
metadata=ctx, dedupe_key=f"empty#{provider}#{run_id}"
)
raise
except Exception as e:
ctx = {
"stage": "ingest", "severity": "ERROR", "provider": provider,
"run_id": run_id, "trace_id": trace_id, "error": str(e), "attempts": 3
}
emailer.send("process_failure", f"[INGEST] Failure: {provider}", ctx)
store.put_alert(
provider=provider, stage="ingest", severity="ERROR",
code="INGEST_FAILURE", message="Ingest failure after retries",
metadata=ctx, dedupe_key=f"ingestfail#{provider}#{run_id}"
)
raise
Harmonize Worker Integration
from datetime import datetime, timezone, timedelta
from mesonet_alerts.emailer import EmailAlerter
from mesonet_alerts.store import AlertStore
from mesonet_alerts.dropcheck import check_and_alert_volume_drop
emailer = EmailAlerter()
store = AlertStore()
# After harmonization run completes
now = datetime.now(timezone.utc).replace(minute=0, second=0, microsecond=0)
window_end = now
window_start = now - timedelta(hours=1)
actual_count = count_harmonized_records(provider, window_start, window_end) # your logic
check_and_alert_volume_drop(
provider=provider,
stage="harmonize",
actual_count=actual_count,
expected_count=None, # use ENV default for now
threshold=0.20,
window_start=window_start,
window_end=window_end,
emailer=emailer,
store=store
)
🧪 Local Development Testing
Prerequisites
-
Build the wheel:
cd micro-services/common/mesonet_alerts python -m build # or uv build
-
Install into workers (already done in pyproject.toml):
dependencies = [ "mesonet-alerts @ file://../common/mesonet_alerts/dist/mesonet_alerts-0.1.0-py3-none-any.whl" ]
Usage (Local Dev)
Terminal 1 - Start Debug SMTP Server:
export $(grep -v '^#' .env.dev | xargs)
./scripts/run_debug_smtp.sh
Terminal 2 - Test Ingest Alerts:
export $(grep -v '^#' .env.dev | xargs)
cd micro-services/mesonet_ingest_worker
python scripts/test_ingest_alerts.py
Terminal 3 - Test Harmonize Alerts:
export $(grep -v '^#' .env.dev | xargs)
cd micro-services/mesonet_harmonize-worker
python scripts/test_harmonize_alerts.py
Expected Output:
- Terminal 1 should print full HTML+text email bodies
- Terminal 2/3 should show "✅ Alert sent" messages
- You should see nicely formatted emails with inline CSS
Environment Variables for Testing
# Override test parameters
TEST_PROVIDER=colorado # Provider name for tests
TEST_ACTUAL=70 # Actual record count (harmonize test)
TEST_EXPECTED=100 # Expected record count (harmonize test)
🗄️ DynamoDB Schema
If ALERTS_TABLE_NAME is set, alerts are persisted with this schema:
Table: alerts
PK: alert_pk (String) = "{provider}#{stage}"
SK: timestamp (String, ISO8601)
Attributes:
- severity (String): ERROR, WARN, INFO
- code (String): PROVIDER_EMPTY, INGEST_FAILURE, etc.
- message (String): Human-readable message
- metadata (Map): Additional context data
- status (String): OPEN (default)
- ttl (Number): Unix timestamp for auto-deletion
- dedupe_key (String): Optional deduplication key
- provider (String): Provider name
- stage (String): Processing stage
🧪 Development
Running Tests
# Install dev dependencies
uv sync --dev
# Run tests
pytest tests/ -v
# With coverage
pytest tests/ --cov=src --cov-report=html
Local SMTP Testing
# Start local SMTP server for testing
python -m smtpd -c DebuggingServer -n localhost:1025
# Or use MailHog (recommended)
docker run -p 1025:1025 -p 8025:8025 mailhog/mailhog
# View emails at http://localhost:8025
🔮 Future Enhancements
The package includes commented hooks for future features:
Database-Backed Configuration
# TODO: Implement in config.py
EmailConfigRepo.get_active_config() # SMTP from DB
RecipientRoutingRepo.get_recipients(provider, severity) # Smart routing
Template Overrides
# TODO: Implement in templates.py
TemplateRepo.get(template_name, format_type) # Custom templates from DB
EventBridge/SNS Integration
# TODO: Implement in store.py
AlertEventPublisher.publish_alert_event(alert_data) # Fan-out to external systems
Provider-Specific Volume Expectations
# TODO: Implement in dropcheck.py
VolumeExpectationRepo.get_expected_volume(provider, stage, hours) # Smart baselines
📋 API Reference
EmailAlerter
__init__(config=None, recipients=None)- Initialize with optional config overridesend(template, subject, context, recipients=None)- Send alert emailresolve_recipients(provider, severity)- Future: smart recipient routing
AlertStore
__init__(table_name=None)- Initialize with optional table nameput_alert(provider, stage, severity, code, message, metadata=None, dedupe_key=None, ttl_seconds=86400)- Store alertget_recent_alerts(provider, stage, hours=24)- Retrieve recent alerts
Retry Functions
run_with_retries(fn, is_retryable, attempts=3, backoffs=[1,3,9])- Execute with retry logicretry_on_exceptions(*exception_types)- Decorator for auto-retryis_network_error(e),is_rate_limit_error(e),is_provider_error(e)- Error classifiers
Volume Drop Detection
check_and_alert_volume_drop(**kwargs)- Check and alert on volume dropsget_volume_trend(provider, stage, hours_back=24, store=None)- Analyze volume trends (placeholder)
🔒 Security Notes
- Credentials are read from environment variables only
- SMTP passwords are not logged
- DynamoDB uses IAM roles for authentication
- All database operations use conditional writes for consistency
- TTL automatically expires old alerts
📄 License
MIT License - see LICENSE file for details.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file mesonet_alerts-0.1.0.tar.gz.
File metadata
- Download URL: mesonet_alerts-0.1.0.tar.gz
- Upload date:
- Size: 22.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1dae1dde84b0009d535525dd744754c186f732b0dc055e6ee4aa9193247e98bd
|
|
| MD5 |
91b2757fa01c434f275ae2ab2ff88c4c
|
|
| BLAKE2b-256 |
7c897bfd169c3e1938192cc1bf9ba2caf21eb0c3223bcedb8744d940dce31af7
|
File details
Details for the file mesonet_alerts-0.1.0-py3-none-any.whl.
File metadata
- Download URL: mesonet_alerts-0.1.0-py3-none-any.whl
- Upload date:
- Size: 20.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
cdde093fc2a1443f2feb4552b37b3f154e6bb1c3a282328b2696aace83869148
|
|
| MD5 |
06174e1c34568b71395c7546f2275f11
|
|
| BLAKE2b-256 |
ee7bf36d9808c652b16f7a9f5e9d4eb20d11ff5988359256c8ca664cfbc21e9d
|