Finish step1
This commit is contained in:
parent
24e1e7aae2
commit
eff48933d3
15
Dockerfile
Normal file
15
Dockerfile
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
FROM python:3.11-slim
|
||||||
|
|
||||||
|
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||||
|
PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY requirements.txt .
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
EXPOSE 8000
|
||||||
|
|
||||||
|
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||||
195
README.md
Normal file
195
README.md
Normal file
@ -0,0 +1,195 @@
|
|||||||
|
# Open-Meteo Coordinates Service
|
||||||
|
|
||||||
|
A FastAPI-based microservice that queries the Open-Meteo Geocoding API to retrieve coordinates for various cities. The service includes caching, Prometheus metrics, and Grafana dashboards for monitoring.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **RESTful API** for retrieving city coordinates
|
||||||
|
- **Intelligent caching** to reduce external API calls
|
||||||
|
- **Prometheus metrics** for observability
|
||||||
|
- **Pre-configured Grafana dashboards** with 5 panels
|
||||||
|
- **Docker Compose** setup for easy deployment
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Docker
|
||||||
|
- Docker Compose
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
1. **Clone the repository**
|
||||||
|
```bash
|
||||||
|
cd open-meteo-service
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Start all services**
|
||||||
|
```bash
|
||||||
|
docker compose up --build
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Access the services**
|
||||||
|
- API: http://localhost:8000/docs
|
||||||
|
- Prometheus: http://localhost:9090
|
||||||
|
- Grafana: http://localhost:3000 (admin/admin)
|
||||||
|
|
||||||
|
## API Documentation
|
||||||
|
|
||||||
|
### Endpoints
|
||||||
|
|
||||||
|
#### `GET /coordinates`
|
||||||
|
Retrieve coordinates for all configured cities.
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"source": "cache",
|
||||||
|
"data": {
|
||||||
|
"Tel Aviv": {
|
||||||
|
"name": "Tel Aviv",
|
||||||
|
"latitude": 32.08088,
|
||||||
|
"longitude": 34.78057,
|
||||||
|
"country": "Israel"
|
||||||
|
},
|
||||||
|
...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `GET /coordinates/{city}`
|
||||||
|
Retrieve coordinates for a specific city.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
- `city` (path) - City name (e.g., "Ashkelon", "London")
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8000/coordinates/Paris
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"source": "open-meteo",
|
||||||
|
"data": {
|
||||||
|
"name": "Paris",
|
||||||
|
"latitude": 48.85341,
|
||||||
|
"longitude": 2.3488,
|
||||||
|
"country": "France"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `GET /metrics`
|
||||||
|
Prometheus metrics endpoint exposing service metrics.
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8000/metrics
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `GET /healthz`
|
||||||
|
Health check endpoint.
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "ok"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Metrics
|
||||||
|
|
||||||
|
The service exposes the following Prometheus metrics:
|
||||||
|
|
||||||
|
### HTTP Metrics
|
||||||
|
- **`http_requests_total`** - Counter of total HTTP requests
|
||||||
|
- Labels: `endpoint`, `method`, `status`
|
||||||
|
|
||||||
|
- **`http_request_duration_seconds`** - Histogram of request durations
|
||||||
|
- Labels: `endpoint`, `method`
|
||||||
|
|
||||||
|
### Cache Metrics
|
||||||
|
- **`coordinates_cache_hits_total`** - Counter of cache hits
|
||||||
|
- **`coordinates_cache_misses_total`** - Counter of cache misses
|
||||||
|
|
||||||
|
### External API Metrics
|
||||||
|
- **`openmeteo_api_calls_total`** - Counter of calls to Open-Meteo Geocoding API
|
||||||
|
- Labels: `city`
|
||||||
|
|
||||||
|
## Grafana Dashboard
|
||||||
|
|
||||||
|
The pre-configured dashboard includes 5 panels:
|
||||||
|
|
||||||
|
1. **Request Rate** - Requests per second by endpoint
|
||||||
|
2. **Request Duration p95** - 95th percentile latency
|
||||||
|
3. **Cache Hits vs Misses** - Cache effectiveness
|
||||||
|
4. **Open-Meteo Calls by City** - External API usage per city
|
||||||
|
5. **Requests by Status** - HTTP status code distribution
|
||||||
|
|
||||||
|
Access the dashboard at http://localhost:3000 after logging in with `admin/admin`.
|
||||||
|
|
||||||
|
## Caching
|
||||||
|
|
||||||
|
The service uses a local JSON file (`coordinates_cache.json`) to cache city coordinates:
|
||||||
|
- Reduces external API calls
|
||||||
|
- Shared across all API endpoints
|
||||||
|
- Persists between requests (not container restarts)
|
||||||
|
- Automatically updated when new cities are queried
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
### Project Structure
|
||||||
|
```
|
||||||
|
.
|
||||||
|
├── app/
|
||||||
|
│ ├── main.py # FastAPI application
|
||||||
|
│ ├── service.py # Business logic & caching
|
||||||
|
│ └── metrics.py # Prometheus metrics definitions
|
||||||
|
├── grafana/
|
||||||
|
│ ├── provisioning/ # Auto-configured datasources & dashboards
|
||||||
|
│ └── dashboards/ # Dashboard JSON definitions
|
||||||
|
├── docker-compose.yml # Service orchestration
|
||||||
|
├── Dockerfile # Python app container
|
||||||
|
├── prometheus.yml # Prometheus scrape configuration
|
||||||
|
└── requirements.txt # Python dependencies
|
||||||
|
```
|
||||||
|
|
||||||
|
### Stop Services
|
||||||
|
```bash
|
||||||
|
docker compose down
|
||||||
|
```
|
||||||
|
|
||||||
|
### View Logs
|
||||||
|
```bash
|
||||||
|
docker compose logs -f open-meteo-service
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rebuild After Code Changes
|
||||||
|
```bash
|
||||||
|
docker compose up --build
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
- `CACHE_FILE` - Path to cache file (default: `coordinates_cache.json`)
|
||||||
|
|
||||||
|
### Scrape Interval
|
||||||
|
Edit `prometheus.yml` to adjust the scrape interval (default: 15s).
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
Generate test traffic to populate metrics:
|
||||||
|
```bash
|
||||||
|
# Test all endpoints
|
||||||
|
curl http://localhost:8000/coordinates
|
||||||
|
curl http://localhost:8000/coordinates/Paris
|
||||||
|
curl http://localhost:8000/coordinates/London
|
||||||
|
|
||||||
|
# Generate load
|
||||||
|
for i in {1..10}; do curl -s http://localhost:8000/coordinates > /dev/null; done
|
||||||
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
36
app/main.py
Normal file
36
app/main.py
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
from fastapi import FastAPI, Response
|
||||||
|
from prometheus_client import generate_latest, CONTENT_TYPE_LATEST
|
||||||
|
|
||||||
|
from .service import get_all_coordinates, get_coordinates_for_city
|
||||||
|
from .metrics import RequestTimer
|
||||||
|
|
||||||
|
app = FastAPI(title="Open-Meteo Coordinates Service")
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/coordinates")
|
||||||
|
def coordinates():
|
||||||
|
with RequestTimer(endpoint="/coordinates", method="GET") as t:
|
||||||
|
try:
|
||||||
|
return get_all_coordinates()
|
||||||
|
except Exception:
|
||||||
|
t.set_status("500")
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/coordinates/{city}")
|
||||||
|
def coordinates_for_city(city: str):
|
||||||
|
with RequestTimer(endpoint="/coordinates/{city}", method="GET") as t:
|
||||||
|
try:
|
||||||
|
return get_coordinates_for_city(city)
|
||||||
|
except Exception:
|
||||||
|
t.set_status("500")
|
||||||
|
raise
|
||||||
|
|
||||||
|
@app.get("/metrics")
|
||||||
|
def metrics():
|
||||||
|
return Response(generate_latest(), media_type=CONTENT_TYPE_LATEST)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/healthz")
|
||||||
|
def healthz():
|
||||||
|
return {"status": "ok"}
|
||||||
54
app/metrics.py
Normal file
54
app/metrics.py
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import time
|
||||||
|
from prometheus_client import Counter, Histogram
|
||||||
|
|
||||||
|
HTTP_REQUESTS_TOTAL = Counter(
|
||||||
|
"http_requests_total",
|
||||||
|
"Total HTTP requests",
|
||||||
|
["endpoint", "method", "status"],
|
||||||
|
)
|
||||||
|
|
||||||
|
HTTP_REQUEST_DURATION_SECONDS = Histogram(
|
||||||
|
"http_request_duration_seconds",
|
||||||
|
"HTTP request duration in seconds",
|
||||||
|
["endpoint", "method"],
|
||||||
|
)
|
||||||
|
|
||||||
|
CACHE_HITS_TOTAL = Counter(
|
||||||
|
"coordinates_cache_hits_total",
|
||||||
|
"Total cache hits for coordinates",
|
||||||
|
)
|
||||||
|
|
||||||
|
CACHE_MISSES_TOTAL = Counter(
|
||||||
|
"coordinates_cache_misses_total",
|
||||||
|
"Total cache misses for coordinates",
|
||||||
|
)
|
||||||
|
|
||||||
|
OPENMETEO_CALLS_TOTAL = Counter(
|
||||||
|
"openmeteo_api_calls_total",
|
||||||
|
"Total calls made to Open-Meteo Geocoding API",
|
||||||
|
["city"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class RequestTimer:
|
||||||
|
"""Small helper to measure request duration and emit metrics."""
|
||||||
|
def __init__(self, endpoint: str, method: str):
|
||||||
|
self.endpoint = endpoint
|
||||||
|
self.method = method
|
||||||
|
self.start = None
|
||||||
|
self.status = "200"
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
self.start = time.time()
|
||||||
|
return self
|
||||||
|
|
||||||
|
def set_status(self, status: str):
|
||||||
|
self.status = status
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc, tb):
|
||||||
|
HTTP_REQUESTS_TOTAL.labels(
|
||||||
|
endpoint=self.endpoint, method=self.method, status=self.status
|
||||||
|
).inc()
|
||||||
|
HTTP_REQUEST_DURATION_SECONDS.labels(
|
||||||
|
endpoint=self.endpoint, method=self.method
|
||||||
|
).observe(time.time() - self.start)
|
||||||
78
app/service.py
Normal file
78
app/service.py
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import json
|
||||||
|
import os
|
||||||
|
from typing import Dict, Any
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from .metrics import CACHE_HITS_TOTAL, CACHE_MISSES_TOTAL, OPENMETEO_CALLS_TOTAL
|
||||||
|
|
||||||
|
API_URL = "https://geocoding-api.open-meteo.com/v1/search"
|
||||||
|
CITIES = ["Tel Aviv", "Beer Sheva", "Jerusalem", "Szeged"]
|
||||||
|
|
||||||
|
CACHE_FILE = os.environ.get("CACHE_FILE", "coordinates_cache.json")
|
||||||
|
|
||||||
|
|
||||||
|
def _fetch_coordinates(city: str) -> Dict[str, Any]:
|
||||||
|
OPENMETEO_CALLS_TOTAL.labels(city=city).inc()
|
||||||
|
|
||||||
|
params = {"name": city, "count": 1}
|
||||||
|
r = requests.get(API_URL, params=params, timeout=10)
|
||||||
|
r.raise_for_status()
|
||||||
|
data = r.json()
|
||||||
|
|
||||||
|
if "results" not in data or not data["results"]:
|
||||||
|
raise ValueError(f"No results found for {city}")
|
||||||
|
|
||||||
|
result = data["results"][0]
|
||||||
|
return {
|
||||||
|
"name": result.get("name"),
|
||||||
|
"latitude": result.get("latitude"),
|
||||||
|
"longitude": result.get("longitude"),
|
||||||
|
"country": result.get("country"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _load_cache() -> Dict[str, Any] | None:
|
||||||
|
if os.path.exists(CACHE_FILE):
|
||||||
|
with open(CACHE_FILE, "r", encoding="utf-8") as f:
|
||||||
|
return json.load(f)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _save_cache(data: Dict[str, Any]) -> None:
|
||||||
|
with open(CACHE_FILE, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(data, f, indent=2)
|
||||||
|
|
||||||
|
|
||||||
|
def get_all_coordinates() -> Dict[str, Any]:
|
||||||
|
cached = _load_cache()
|
||||||
|
if cached:
|
||||||
|
CACHE_HITS_TOTAL.inc()
|
||||||
|
return {"source": "cache", "data": cached}
|
||||||
|
|
||||||
|
CACHE_MISSES_TOTAL.inc()
|
||||||
|
|
||||||
|
results: Dict[str, Any] = {}
|
||||||
|
for city in CITIES:
|
||||||
|
results[city] = _fetch_coordinates(city)
|
||||||
|
|
||||||
|
_save_cache(results)
|
||||||
|
return {"source": "open-meteo", "data": results}
|
||||||
|
|
||||||
|
|
||||||
|
def get_coordinates_for_city(city: str) -> Dict[str, Any]:
|
||||||
|
cached = _load_cache()
|
||||||
|
if cached and city in cached:
|
||||||
|
CACHE_HITS_TOTAL.inc()
|
||||||
|
return {"source": "cache", "data": cached[city]}
|
||||||
|
|
||||||
|
CACHE_MISSES_TOTAL.inc()
|
||||||
|
|
||||||
|
result = _fetch_coordinates(city)
|
||||||
|
|
||||||
|
# Update cache with the new city
|
||||||
|
if cached is None:
|
||||||
|
cached = {}
|
||||||
|
cached[city] = result
|
||||||
|
_save_cache(cached)
|
||||||
|
|
||||||
|
return {"source": "open-meteo", "data": result}
|
||||||
54
docker-compose.yml
Normal file
54
docker-compose.yml
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
open-meteo-service:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
image: open-meteo-service:local
|
||||||
|
container_name: open-meteo-service
|
||||||
|
ports:
|
||||||
|
- "8000:8000"
|
||||||
|
networks:
|
||||||
|
- monitoring
|
||||||
|
|
||||||
|
prometheus:
|
||||||
|
image: prom/prometheus:latest
|
||||||
|
container_name: prometheus
|
||||||
|
ports:
|
||||||
|
- "9090:9090"
|
||||||
|
volumes:
|
||||||
|
- ./prometheus.yml:/etc/prometheus/prometheus.yml
|
||||||
|
- prometheus_data:/prometheus
|
||||||
|
command:
|
||||||
|
- "--config.file=/etc/prometheus/prometheus.yml"
|
||||||
|
- "--storage.tsdb.path=/prometheus"
|
||||||
|
networks:
|
||||||
|
- monitoring
|
||||||
|
depends_on:
|
||||||
|
- open-meteo-service
|
||||||
|
|
||||||
|
grafana:
|
||||||
|
image: grafana/grafana:latest
|
||||||
|
container_name: grafana
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
environment:
|
||||||
|
- GF_SECURITY_ADMIN_PASSWORD=admin
|
||||||
|
- GF_SECURITY_ADMIN_USER=admin
|
||||||
|
volumes:
|
||||||
|
- grafana_data:/var/lib/grafana
|
||||||
|
- ./grafana/provisioning:/etc/grafana/provisioning
|
||||||
|
- ./grafana/dashboards:/var/lib/grafana/dashboards
|
||||||
|
networks:
|
||||||
|
- monitoring
|
||||||
|
depends_on:
|
||||||
|
- prometheus
|
||||||
|
|
||||||
|
networks:
|
||||||
|
monitoring:
|
||||||
|
driver: bridge
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
prometheus_data:
|
||||||
|
grafana_data:
|
||||||
107
grafana/dashboards/open-meteo-service.json
Normal file
107
grafana/dashboards/open-meteo-service.json
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
{
|
||||||
|
"uid": "open-meteo-service",
|
||||||
|
"title": "Open-Meteo Service",
|
||||||
|
"timezone": "browser",
|
||||||
|
"schemaVersion": 38,
|
||||||
|
"version": 1,
|
||||||
|
"refresh": "10s",
|
||||||
|
"time": {
|
||||||
|
"from": "now-15m",
|
||||||
|
"to": "now"
|
||||||
|
},
|
||||||
|
"panels": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"type": "timeseries",
|
||||||
|
"title": "Request Rate",
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "prometheus"
|
||||||
|
},
|
||||||
|
"gridPos": { "x": 0, "y": 0, "w": 12, "h": 8 },
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"expr": "sum(rate(http_requests_total[5m])) by (endpoint, method)",
|
||||||
|
"legendFormat": "{{endpoint}} {{method}}",
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"type": "timeseries",
|
||||||
|
"title": "Request Duration p95",
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "prometheus"
|
||||||
|
},
|
||||||
|
"gridPos": { "x": 12, "y": 0, "w": 12, "h": 8 },
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"expr": "histogram_quantile(0.95, sum(rate(http_request_duration_seconds_bucket[5m])) by (le, endpoint, method))",
|
||||||
|
"legendFormat": "{{endpoint}} {{method}}",
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"type": "timeseries",
|
||||||
|
"title": "Cache Hits vs Misses",
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "prometheus"
|
||||||
|
},
|
||||||
|
"gridPos": { "x": 0, "y": 8, "w": 12, "h": 8 },
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"expr": "rate(coordinates_cache_hits_total[5m])",
|
||||||
|
"legendFormat": "hits",
|
||||||
|
"refId": "A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "rate(coordinates_cache_misses_total[5m])",
|
||||||
|
"legendFormat": "misses",
|
||||||
|
"refId": "B"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"type": "timeseries",
|
||||||
|
"title": "Open-Meteo Calls by City",
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "prometheus"
|
||||||
|
},
|
||||||
|
"gridPos": { "x": 12, "y": 8, "w": 12, "h": 8 },
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"expr": "sum(rate(openmeteo_api_calls_total[5m])) by (city)",
|
||||||
|
"legendFormat": "{{city}}",
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"type": "timeseries",
|
||||||
|
"title": "Requests by Status",
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "prometheus"
|
||||||
|
},
|
||||||
|
"gridPos": { "x": 0, "y": 16, "w": 24, "h": 8 },
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"expr": "sum(rate(http_requests_total[5m])) by (status)",
|
||||||
|
"legendFormat": "{{status}}",
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"templating": {
|
||||||
|
"list": []
|
||||||
|
}
|
||||||
|
}
|
||||||
10
grafana/provisioning/dashboards/dashboard.yml
Normal file
10
grafana/provisioning/dashboards/dashboard.yml
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
apiVersion: 1
|
||||||
|
|
||||||
|
providers:
|
||||||
|
- name: default
|
||||||
|
type: file
|
||||||
|
disableDeletion: false
|
||||||
|
editable: true
|
||||||
|
updateIntervalSeconds: 10
|
||||||
|
options:
|
||||||
|
path: /var/lib/grafana/dashboards
|
||||||
9
grafana/provisioning/datasources/datasource.yml
Normal file
9
grafana/provisioning/datasources/datasource.yml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
apiVersion: 1
|
||||||
|
|
||||||
|
datasources:
|
||||||
|
- name: Prometheus
|
||||||
|
type: prometheus
|
||||||
|
access: proxy
|
||||||
|
url: http://prometheus:9090
|
||||||
|
isDefault: true
|
||||||
|
uid: prometheus
|
||||||
66
main.py
66
main.py
@ -1,66 +0,0 @@
|
|||||||
import requests
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
|
|
||||||
API_URL = "https://geocoding-api.open-meteo.com/v1/search"
|
|
||||||
CITIES = ["Tel Aviv", "Beersheba", "Jerusalem", "Szeged"]
|
|
||||||
CACHE_FILE = "coordinates_cache.json"
|
|
||||||
|
|
||||||
|
|
||||||
def fetch_coordinates(city):
|
|
||||||
params = {
|
|
||||||
"name": city,
|
|
||||||
"count": 1
|
|
||||||
}
|
|
||||||
response = requests.get(API_URL, params=params)
|
|
||||||
response.raise_for_status()
|
|
||||||
data = response.json()
|
|
||||||
|
|
||||||
if "results" not in data:
|
|
||||||
raise ValueError(f"No results found for {city}")
|
|
||||||
|
|
||||||
result = data["results"][0]
|
|
||||||
|
|
||||||
return {
|
|
||||||
"name": result["name"],
|
|
||||||
"latitude": result["latitude"],
|
|
||||||
"longitude": result["longitude"],
|
|
||||||
"country": result["country"]
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def load_cache():
|
|
||||||
if os.path.exists(CACHE_FILE):
|
|
||||||
with open(CACHE_FILE, "r") as f:
|
|
||||||
return json.load(f)
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def save_cache(data):
|
|
||||||
with open(CACHE_FILE, "w") as f:
|
|
||||||
json.dump(data, f, indent=4)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
# Try loading cached data first
|
|
||||||
cached_data = load_cache()
|
|
||||||
if cached_data:
|
|
||||||
print("Loaded from cache:")
|
|
||||||
print(json.dumps(cached_data, indent=4))
|
|
||||||
return
|
|
||||||
|
|
||||||
# If no cache, fetch from API
|
|
||||||
print("Fetching from Open-Meteo API...")
|
|
||||||
results = {}
|
|
||||||
|
|
||||||
for city in CITIES:
|
|
||||||
results[city] = fetch_coordinates(city)
|
|
||||||
|
|
||||||
save_cache(results)
|
|
||||||
|
|
||||||
print("Saved to cache:")
|
|
||||||
print(json.dumps(results, indent=4))
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
12
prometheus.yml
Normal file
12
prometheus.yml
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
global:
|
||||||
|
scrape_interval: 15s
|
||||||
|
|
||||||
|
scrape_configs:
|
||||||
|
- job_name: "prometheus"
|
||||||
|
static_configs:
|
||||||
|
- targets: ["prometheus:9090"]
|
||||||
|
|
||||||
|
- job_name: "open-meteo-service"
|
||||||
|
metrics_path: "/metrics"
|
||||||
|
static_configs:
|
||||||
|
- targets: ["open-meteo-service:8000"]
|
||||||
4
requirements.txt
Normal file
4
requirements.txt
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
fastapi==0.115.8
|
||||||
|
uvicorn[standard]==0.30.6
|
||||||
|
requests==2.32.3
|
||||||
|
prometheus-client==0.21.1
|
||||||
Loading…
x
Reference in New Issue
Block a user