Skip to content

Commit ca471e1

Browse files
challgrenclaude
andcommitted
Add comprehensive health checks to Docker container
- Enhanced health check script with multiple check points - Added /api/health endpoint for monitoring integration - Configured proper Docker HEALTHCHECK parameters - Implemented graceful degradation for non-critical failures - Added health monitoring documentation Health checks now monitor: - Python process status - Web server responsiveness - TAR1090 connection - Log file access - Request failure rates 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent a54364b commit ca471e1

4 files changed

Lines changed: 170 additions & 15 deletions

File tree

Dockerfile

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,5 +55,9 @@ RUN chmod +x /etc/s6-overlay/s6-rc.d/aircraft-circle/run && \
5555
EXPOSE 8888
5656

5757
# Health check
58-
HEALTHCHECK --start-period=30s --interval=30s --timeout=5s --retries=3 \
58+
# Start period: 60s to allow service to fully start
59+
# Interval: 30s for regular checks
60+
# Timeout: 10s to allow for TAR1090 connection check
61+
# Retries: 3 failures before marking unhealthy
62+
HEALTHCHECK --start-period=60s --interval=30s --timeout=10s --retries=3 \
5963
CMD python3 /scripts/healthcheck.py || exit 1

README.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,51 @@ python app.py --server http://your-tar1090:8080 --web
111111
| `MIN_GRID_LEGS` | Minimum parallel legs | `3` |
112112
| `MIN_LEG_LENGTH` | Minimum leg length (km) | `2.0` |
113113

114+
## 🏥 Health Monitoring
115+
116+
### Docker Health Check
117+
118+
The container includes comprehensive health checks that monitor:
119+
120+
- Python process status
121+
- Web server responsiveness (when enabled)
122+
- API endpoint availability
123+
- TAR1090 connection status
124+
- Log file writability
125+
126+
Health check configuration:
127+
128+
- **Start Period**: 60 seconds (allows service to fully initialize)
129+
- **Check Interval**: 30 seconds
130+
- **Timeout**: 10 seconds per check
131+
- **Retries**: 3 failures before marking unhealthy
132+
133+
### Health API Endpoint
134+
135+
Access health status at: `http://localhost:8888/api/health`
136+
137+
```json
138+
{
139+
"status": "healthy",
140+
"timestamp": 1234567890.123,
141+
"checks": {
142+
"web_server": true,
143+
"tar1090_connection": true,
144+
"last_update": 1234567890.123,
145+
"aircraft_count": 115,
146+
"active_circles": 2,
147+
"active_grids": 1,
148+
"total_requests": 1000,
149+
"failed_requests": 5
150+
}
151+
}
152+
```
153+
154+
Status values:
155+
156+
- `healthy` - All systems operational
157+
- `degraded` - Service running but with issues (e.g., TAR1090 connection lost)
158+
114159
## 🖥️ Web Interface
115160

116161
### Live View

app.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2537,6 +2537,40 @@ def send_static(path):
25372537
def get_patterns():
25382538
return jsonify(self.get_pattern_data_json())
25392539

2540+
@app.route('/api/health')
2541+
def health_check():
2542+
"""Health check endpoint for monitoring."""
2543+
# Count active patterns
2544+
circling_count = len(self.get_circling_aircraft())
2545+
grid_count = len(self.get_grid_aircraft())
2546+
2547+
health_status = {
2548+
'status': 'healthy',
2549+
'timestamp': time.time(),
2550+
'checks': {
2551+
'web_server': True,
2552+
'tar1090_connection': self.last_update is not None,
2553+
'last_update': self.last_update.timestamp() if self.last_update else 0,
2554+
'aircraft_count': len(self.aircraft),
2555+
'active_circles': circling_count,
2556+
'active_grids': grid_count,
2557+
'total_requests': self.total_requests,
2558+
'failed_requests': self.failed_requests
2559+
}
2560+
}
2561+
2562+
# Check if we haven't received updates in a while (5 minutes)
2563+
if self.last_update and (time.time() - self.last_update.timestamp()) > 300:
2564+
health_status['status'] = 'degraded'
2565+
health_status['checks']['tar1090_connection'] = False
2566+
2567+
# Check if too many requests are failing
2568+
if self.total_requests > 10 and (self.failed_requests / self.total_requests) > 0.5:
2569+
health_status['status'] = 'degraded'
2570+
health_status['checks']['high_failure_rate'] = True
2571+
2572+
return jsonify(health_status)
2573+
25402574
@app.route('/history')
25412575
def history():
25422576
return render_template_string(HISTORY_HTML_TEMPLATE)

rootfs/scripts/healthcheck.py

Lines changed: 86 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,42 @@
33

44
import sys
55
import os
6+
import json
67
import requests
78
from pathlib import Path
9+
import time
810

911
def check_web_server():
1012
"""Check if web server is responding."""
11-
if os.environ.get('ENABLE_WEB', 'true').lower() == 'true':
12-
port = os.environ.get('WEB_PORT', '8888')
13-
try:
14-
response = requests.get(f'http://localhost:{port}/data.json', timeout=3)
15-
return response.status_code == 200
16-
except:
13+
if os.environ.get('ENABLE_WEB', 'true').lower() != 'true':
14+
return True # Web server disabled, skip check
15+
16+
port = os.environ.get('WEB_PORT', '8888')
17+
try:
18+
# Check health endpoint
19+
response = requests.get(f'http://localhost:{port}/api/health', timeout=3)
20+
if response.status_code != 200:
21+
return False
22+
23+
# Verify response is valid JSON and check status
24+
data = response.json()
25+
if not isinstance(data, dict):
1726
return False
18-
return True
27+
28+
# Check the reported status
29+
status = data.get('status', 'unknown')
30+
if status == 'healthy':
31+
return True
32+
elif status == 'degraded':
33+
print(f"Warning: Service is degraded - {data.get('checks', {})}")
34+
return True # Still return True for degraded state
35+
else:
36+
print(f"Error: Service status is {status}")
37+
return False
38+
39+
except Exception as e:
40+
print(f"Web server check error: {e}")
41+
return False
1942

2043
def check_process():
2144
"""Check if main process is running."""
@@ -25,23 +48,72 @@ def check_process():
2548
result = subprocess.run(['pgrep', '-f', 'python3.*app.py'],
2649
capture_output=True, text=True)
2750
return result.returncode == 0
28-
except:
51+
except Exception as e:
52+
print(f"Process check error: {e}")
53+
return False
54+
55+
def check_tar1090_connection():
56+
"""Check if we can connect to TAR1090."""
57+
tar1090_url = os.environ.get('TAR1090_URL', 'http://tar1090:80')
58+
try:
59+
# Allow this check to fail gracefully as TAR1090 might be temporarily unavailable
60+
response = requests.get(f'{tar1090_url}/data/aircraft.json', timeout=5)
61+
if response.status_code == 200:
62+
data = response.json()
63+
# Check if we got valid aircraft data
64+
if 'aircraft' in data or 'ac' in data:
65+
return True
66+
# Don't fail health check if TAR1090 is temporarily unavailable
67+
# Just log a warning
68+
print(f"Warning: TAR1090 at {tar1090_url} not responding (status: {response.status_code})")
69+
return True # Return True to not fail the container
70+
except requests.exceptions.RequestException as e:
71+
print(f"Warning: Cannot connect to TAR1090 at {tar1090_url}: {e}")
72+
return True # Return True to not fail the container
73+
except Exception as e:
74+
print(f"Warning: TAR1090 check error: {e}")
75+
return True # Return True to not fail the container
76+
77+
def check_log_files():
78+
"""Check if log files are writable."""
79+
try:
80+
# Check if we can write to log directory
81+
log_dir = Path('/app')
82+
if not log_dir.exists():
83+
return False
84+
85+
# Test write access
86+
test_file = log_dir / '.healthcheck_test'
87+
test_file.write_text(str(time.time()))
88+
test_file.unlink()
89+
return True
90+
except Exception as e:
91+
print(f"Log file check error: {e}")
2992
return False
3093

3194
def main():
3295
"""Run health checks."""
96+
# Define checks with their names and criticality
3397
checks = [
34-
("Process", check_process()),
35-
("Web Server", check_web_server()),
98+
("Process", check_process(), True), # Critical
99+
("Web Server", check_web_server(), True), # Critical
100+
("Log Files", check_log_files(), True), # Critical
101+
("TAR1090 Connection", check_tar1090_connection(), False), # Non-critical
36102
]
37103

38-
failed = [name for name, status in checks if not status]
104+
# Separate critical and non-critical failures
105+
critical_failures = [name for name, status, critical in checks if not status and critical]
106+
warnings = [name for name, status, critical in checks if not status and not critical]
107+
108+
# Print status
109+
if warnings:
110+
print(f"Health check warnings: {', '.join(warnings)}")
39111

40-
if failed:
41-
print(f"Health check failed: {', '.join(failed)}")
112+
if critical_failures:
113+
print(f"Health check FAILED: {', '.join(critical_failures)}")
42114
sys.exit(1)
43115

44-
print("Health check passed")
116+
print("Health check PASSED")
45117
sys.exit(0)
46118

47119
if __name__ == "__main__":

0 commit comments

Comments
 (0)