Skip to content

Commit c2d3c73

Browse files
committed
Static Maps: adding some debugging here
1 parent f86e920 commit c2d3c73

3 files changed

Lines changed: 304 additions & 2 deletions

File tree

app/services/static_map_service.py

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -302,8 +302,10 @@ def generate_map(self, resource_id: str, geometry: Any) -> Optional[Path]:
302302
context.add_object(rectangle)
303303

304304
# Render the map (this will auto-center and zoom to fit the objects)
305+
# This step requires outbound HTTP requests to fetch map tiles
305306
# Use render_cairo instead of render_pillow to avoid Pillow 10+ compatibility issues
306307
try:
308+
logger.debug(f"Rendering map for resource {resource_id} (this requires tile downloads)")
307309
cairo_surface = context.render_cairo(self.map_width, self.map_height)
308310
# Convert cairo ImageSurface to Pillow Image
309311
import io
@@ -315,12 +317,37 @@ def generate_map(self, resource_id: str, geometry: Any) -> Optional[Path]:
315317
buf.seek(0)
316318
image = Image.open(buf)
317319
except Exception as e:
318-
logger.warning(f"Failed to render with cairo, trying pillow: {e}")
320+
error_msg = str(e).lower()
321+
# Check for network-related errors
322+
if any(
323+
keyword in error_msg
324+
for keyword in ["connection", "timeout", "network", "unreachable", "refused"]
325+
):
326+
logger.error(
327+
f"Network error rendering map for resource {resource_id}: {e}\n"
328+
"This may indicate that outbound HTTP traffic is blocked by a firewall.\n"
329+
"The static map service requires outbound access to tile servers.\n"
330+
"Run scripts/debug_static_map.py on the server to diagnose network issues."
331+
)
332+
else:
333+
logger.warning(f"Failed to render with cairo, trying pillow: {e}")
319334
# Fallback to pillow if cairo fails (may fail with Pillow 10+)
320335
try:
321336
image = context.render_pillow(self.map_width, self.map_height)
322337
except Exception as pillow_error:
323-
logger.error(f"Both cairo and pillow rendering failed: {pillow_error}")
338+
error_msg = str(pillow_error).lower()
339+
if any(
340+
keyword in error_msg
341+
for keyword in ["connection", "timeout", "network", "unreachable", "refused"]
342+
):
343+
logger.error(
344+
f"Network error rendering map with pillow for resource {resource_id}: {pillow_error}\n"
345+
"This may indicate that outbound HTTP traffic is blocked by a firewall.\n"
346+
"The static map service requires outbound access to tile servers.\n"
347+
"Run scripts/debug_static_map.py on the server to diagnose network issues."
348+
)
349+
else:
350+
logger.error(f"Both cairo and pillow rendering failed: {pillow_error}")
324351
raise
325352

326353
# Save the map to file

scripts/README_debug_static_map.md

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
# Static Map Debugging Guide
2+
3+
## Problem
4+
The static map endpoint (`/resources/{id}/static-map`) may fail on servers with firewalls that block outbound HTTP traffic because it needs to fetch map tiles from external tile servers.
5+
6+
## Quick Diagnosis
7+
8+
Run the diagnostic script on your Kamal server:
9+
10+
```bash
11+
# SSH into your server
12+
kamal app exec -i "python scripts/debug_static_map.py"
13+
```
14+
15+
Or if you have direct SSH access:
16+
17+
```bash
18+
ssh your-server
19+
cd /path/to/app
20+
python scripts/debug_static_map.py
21+
```
22+
23+
## What the Script Tests
24+
25+
1. **Import Check**: Verifies `py-staticmaps` is installed
26+
2. **Network Connectivity**: Tests if the server can reach `basemaps.cartocdn.com` (the tile server)
27+
3. **Map Generation**: Attempts to generate a test static map
28+
29+
## Expected Output
30+
31+
### If Network is Working:
32+
```
33+
✓ Successfully connected to tile server: HTTP 200, received X bytes
34+
✓ Successfully generated static map: /path/to/map.png
35+
```
36+
37+
### If Firewall is Blocking:
38+
```
39+
✗ Failed to connect to tile server: [Errno 61] Connection refused
40+
✗ Network error rendering map: Connection timeout
41+
```
42+
43+
## Solutions
44+
45+
### Option 1: Allow Outbound HTTP/HTTPS (Recommended)
46+
Configure your firewall to allow outbound HTTP (port 80) and HTTPS (port 443) traffic to:
47+
- `*.basemaps.cartocdn.com`
48+
- `*.cartocdn.com`
49+
50+
### Option 2: Use a Proxy Server
51+
If you must restrict outbound traffic, configure an HTTP proxy:
52+
53+
```python
54+
# In your environment or config
55+
import os
56+
os.environ['HTTP_PROXY'] = 'http://your-proxy:8080'
57+
os.environ['HTTPS_PROXY'] = 'http://your-proxy:8080'
58+
```
59+
60+
### Option 3: Pre-generate Maps
61+
Generate static maps on a machine with internet access and copy them to the server.
62+
63+
### Option 4: Use Offline Tile Cache
64+
Set up a local tile server or cache tiles locally (more complex).
65+
66+
## Checking Celery Task Logs
67+
68+
If maps are generated via Celery tasks, check the worker logs:
69+
70+
```bash
71+
# On your server
72+
tail -f logs/celery.log | grep -i "static.*map\|network\|connection"
73+
```
74+
75+
Look for errors like:
76+
- `Connection refused`
77+
- `Connection timeout`
78+
- `Network unreachable`
79+
- `Failed to render`
80+
81+
## Manual Network Test
82+
83+
Test connectivity directly from the server:
84+
85+
```bash
86+
# Test DNS resolution
87+
nslookup basemaps.cartocdn.com
88+
89+
# Test HTTP connection
90+
curl -v http://a.basemaps.cartocdn.com/rastertiles/light_all/1/0/0.png
91+
92+
# Test with timeout
93+
curl --max-time 10 http://a.basemaps.cartocdn.com/rastertiles/light_all/1/0/0.png
94+
```
95+
96+
If these fail, it confirms the firewall is blocking outbound traffic.
97+

scripts/debug_static_map.py

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Debug script to test static map generation and diagnose firewall/network issues.
4+
5+
This script tests:
6+
1. Outbound HTTP connectivity to tile servers
7+
2. Static map generation with a test resource
8+
3. Network error handling
9+
"""
10+
11+
import asyncio
12+
import logging
13+
import sys
14+
from pathlib import Path
15+
16+
# Add project root to path
17+
project_root = Path(__file__).parent.parent
18+
sys.path.insert(0, str(project_root))
19+
20+
from app.services.static_map_service import StaticMapService
21+
22+
logging.basicConfig(
23+
level=logging.DEBUG,
24+
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
25+
)
26+
logger = logging.getLogger(__name__)
27+
28+
29+
def test_tile_server_connectivity():
30+
"""Test if we can reach the Carto tile server."""
31+
import urllib.request
32+
import urllib.error
33+
34+
test_url = "http://a.basemaps.cartocdn.com/rastertiles/light_all/1/0/0.png"
35+
logger.info(f"Testing connectivity to tile server: {test_url}")
36+
37+
try:
38+
req = urllib.request.Request(test_url)
39+
req.add_header("User-Agent", "BTAA-Geospatial-API/1.0")
40+
with urllib.request.urlopen(req, timeout=10) as response:
41+
status = response.getcode()
42+
content_length = len(response.read())
43+
logger.info(
44+
f"✓ Successfully connected to tile server: HTTP {status}, "
45+
f"received {content_length} bytes"
46+
)
47+
return True
48+
except urllib.error.URLError as e:
49+
logger.error(f"✗ Failed to connect to tile server: {e}")
50+
logger.error(" This suggests outbound HTTP traffic may be blocked by firewall")
51+
return False
52+
except Exception as e:
53+
logger.error(f"✗ Unexpected error connecting to tile server: {e}")
54+
return False
55+
56+
57+
def test_static_map_generation():
58+
"""Test static map generation with a sample bounding box."""
59+
logger.info("Testing static map generation...")
60+
61+
# Test with a small bounding box (Minneapolis area)
62+
test_bbox = "ENVELOPE(-93.5, -93.0, 45.0, 44.9)"
63+
test_resource_id = "debug-test-map"
64+
65+
try:
66+
service = StaticMapService()
67+
logger.info(f"Using maps directory: {service.maps_dir}")
68+
69+
# Try to generate a map
70+
map_path = service.generate_map(test_resource_id, test_bbox)
71+
72+
if map_path and map_path.exists():
73+
logger.info(f"✓ Successfully generated static map: {map_path}")
74+
logger.info(f" File size: {map_path.stat().st_size} bytes")
75+
return True
76+
else:
77+
logger.error("✗ Map generation returned None or file doesn't exist")
78+
return False
79+
80+
except Exception as e:
81+
logger.error(f"✗ Error generating static map: {e}", exc_info=True)
82+
return False
83+
84+
85+
def test_py_staticmaps_import():
86+
"""Test if py-staticmaps can be imported and basic functionality works."""
87+
logger.info("Testing py-staticmaps import and basic functionality...")
88+
89+
try:
90+
import staticmaps
91+
92+
logger.info(f"✓ py-staticmaps imported successfully (version: {staticmaps.__version__ if hasattr(staticmaps, '__version__') else 'unknown'})")
93+
94+
# Try creating a context
95+
context = staticmaps.Context()
96+
logger.info("✓ Created staticmaps.Context()")
97+
98+
return True
99+
except ImportError as e:
100+
logger.error(f"✗ Failed to import py-staticmaps: {e}")
101+
logger.error(" Install with: pip install py-staticmaps")
102+
return False
103+
except Exception as e:
104+
logger.error(f"✗ Error testing py-staticmaps: {e}")
105+
return False
106+
107+
108+
def main():
109+
"""Run all diagnostic tests."""
110+
logger.info("=" * 60)
111+
logger.info("Static Map Generation Diagnostic Tool")
112+
logger.info("=" * 60)
113+
logger.info("")
114+
115+
results = {}
116+
117+
# Test 1: Import check
118+
logger.info("Test 1: Checking py-staticmaps import...")
119+
results["import"] = test_py_staticmaps_import()
120+
logger.info("")
121+
122+
# Test 2: Network connectivity
123+
logger.info("Test 2: Testing tile server connectivity...")
124+
results["connectivity"] = test_tile_server_connectivity()
125+
logger.info("")
126+
127+
# Test 3: Map generation
128+
if results["import"]:
129+
logger.info("Test 3: Testing static map generation...")
130+
results["generation"] = test_static_map_generation()
131+
logger.info("")
132+
else:
133+
logger.warning("Skipping map generation test (import failed)")
134+
results["generation"] = False
135+
136+
# Summary
137+
logger.info("=" * 60)
138+
logger.info("Summary")
139+
logger.info("=" * 60)
140+
logger.info(f"Import check: {'✓ PASS' if results['import'] else '✗ FAIL'}")
141+
logger.info(f"Network connectivity: {'✓ PASS' if results['connectivity'] else '✗ FAIL'}")
142+
logger.info(f"Map generation: {'✓ PASS' if results['generation'] else '✗ FAIL'}")
143+
logger.info("")
144+
145+
if not results["connectivity"]:
146+
logger.warning("=" * 60)
147+
logger.warning("NETWORK CONNECTIVITY ISSUE DETECTED")
148+
logger.warning("=" * 60)
149+
logger.warning(
150+
"The tile server connectivity test failed. This suggests that:\n"
151+
"1. Outbound HTTP traffic may be blocked by a firewall\n"
152+
"2. The server may not have internet access\n"
153+
"3. DNS resolution may be failing\n"
154+
"\n"
155+
"Solutions:\n"
156+
"1. Check firewall rules to allow outbound HTTP/HTTPS traffic\n"
157+
"2. Verify DNS resolution: nslookup basemaps.cartocdn.com\n"
158+
"3. Test manual connection: curl http://a.basemaps.cartocdn.com/rastertiles/light_all/1/0/0.png\n"
159+
"4. Consider using a proxy server if outbound traffic must be restricted\n"
160+
)
161+
162+
if results["connectivity"] and not results["generation"]:
163+
logger.warning("=" * 60)
164+
logger.warning("MAP GENERATION ISSUE DETECTED")
165+
logger.warning("=" * 60)
166+
logger.warning(
167+
"Network connectivity works, but map generation failed.\n"
168+
"Check the error messages above for details.\n"
169+
"This may be a py-staticmaps configuration issue.\n"
170+
)
171+
172+
return all(results.values())
173+
174+
175+
if __name__ == "__main__":
176+
success = main()
177+
sys.exit(0 if success else 1)
178+

0 commit comments

Comments
 (0)