Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 69 additions & 7 deletions app_python/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
DevOps Info Service - FastAPI implementation.

Provides system, runtime, and request information plus a basic health check.
Now emits structured JSON logs for easier aggregation.
"""

import json
import logging
import os
import platform
Expand All @@ -22,11 +24,39 @@
DEBUG: bool = os.getenv("DEBUG", "False").lower() == "true"


# Logging configuration
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
)
class JSONFormatter(logging.Formatter):
"""Format logs as JSON with common fields."""

def format(self, record: logging.LogRecord) -> str: # type: ignore[override]
log_record: dict[str, Any] = {
"timestamp": datetime.now(UTC).isoformat(),
"level": record.levelname,
"message": record.getMessage(),
"logger": record.name,
}

for attr in (
"method",
"path",
"status_code",
"client_ip",
"duration",
):
value = getattr(record, attr, None)
if value is not None:
log_record[attr] = value

if record.exc_info:
log_record["exc_info"] = self.formatException(record.exc_info)

return json.dumps(log_record)


handler = logging.StreamHandler()
handler.setFormatter(JSONFormatter())
root_logger = logging.getLogger()
root_logger.setLevel(logging.INFO)
root_logger.handlers = [handler]
logger = logging.getLogger(__name__)


Expand All @@ -37,6 +67,28 @@
app = FastAPI(title="DevOps Info Service")


@app.middleware("http")
async def log_requests(request: Request, call_next):
"""Log each HTTP request with structured JSON."""
start_time = datetime.now(UTC)
response = await call_next(request)

duration = (datetime.now(UTC) - start_time).total_seconds()

logger.info(
"HTTP request completed",
extra={
"method": request.method,
"path": request.url.path,
"status_code": response.status_code,
"client_ip": request.client.host if request.client else None,
"duration": duration,
},
)

return response


def get_system_info() -> dict[str, Any]:
"""Collect system information."""
return {
Expand Down Expand Up @@ -89,7 +141,14 @@ def get_endpoints() -> list[dict[str, str]]:
@app.get("/")
async def index(request: Request) -> dict[str, Any]:
"""Main endpoint - service and system information."""
logger.info("Handling request for %s %s", request.method, request.url.path)
logger.info(
"Handling request",
extra={
"method": request.method,
"path": request.url.path,
"client_ip": request.client.host if request.client else None,
},
)

system_info = get_system_info()
runtime_info = get_runtime_info()
Expand Down Expand Up @@ -157,5 +216,8 @@ async def unhandled_exception_handler(request: Request, exc: Exception) -> JSONR


if __name__ == "__main__":
logger.info("Starting DevOps Info Service on %s:%s", HOST, PORT)
logger.info(
"Starting DevOps Info Service",
extra={"host": HOST, "port": PORT},
)
uvicorn.run("app:app", host=HOST, port=PORT, reload=DEBUG)
133 changes: 133 additions & 0 deletions monitoring/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
services:
loki:
image: grafana/loki:3.0.0
command: -config.file=/etc/loki/config.yml
ports:
- "3100:3100"
volumes:
- ./loki/config.yml:/etc/loki/config.yml:ro
- loki-data:/loki
networks:
- logging
deploy:
resources:
limits:
cpus: "1.0"
memory: 1G
reservations:
cpus: "0.5"
memory: 512M
healthcheck:
test: ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:3100/ready || exit 1"]
interval: 10s
timeout: 5s
retries: 5
start_period: 15s

promtail:
image: grafana/promtail:3.0.0
command: -config.file=/etc/promtail/config.yml
volumes:
- ./promtail/config.yml:/etc/promtail/config.yml:ro
- /var/log:/var/log:ro
- /var/snap/docker/common/var-lib-docker/containers:/var/lib/docker/containers:ro
- /var/run/docker.sock:/var/run/docker.sock:ro
networks:
- logging
depends_on:
loki:
condition: service_healthy
deploy:
resources:
limits:
cpus: "1.0"
memory: 512M
reservations:
cpus: "0.25"
memory: 256M
healthcheck:
test: ["CMD-SHELL", "bash -c '</dev/tcp/localhost/9080' || exit 1"]
interval: 10s
timeout: 5s
retries: 5
start_period: 15s

grafana:
image: grafana/grafana:12.3.1
ports:
- "3000:3000"
environment:
GF_AUTH_ANONYMOUS_ENABLED: "true"
GF_AUTH_ANONYMOUS_ORG_ROLE: Admin
GF_SECURITY_ALLOW_EMBEDDING: "true"
volumes:
- grafana-data:/var/lib/grafana
networks:
- logging
depends_on:
loki:
condition: service_healthy
deploy:
resources:
limits:
cpus: "1.0"
memory: 1G
reservations:
cpus: "0.5"
memory: 512M
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:3000/api/health || exit 1"]
interval: 10s
timeout: 5s
retries: 5
start_period: 15s

app-python:
build:
context: ../app_python
ports:
- "8000:5000"
networks:
- logging
labels:
logging: "promtail"
app: "devops-python"
deploy:
resources:
limits:
cpus: "0.5"
memory: 512M
reservations:
cpus: "0.25"
memory: 256M

app-go:
build:
context: ../app_go
environment:
HOST: "0.0.0.0"
PORT: "8001"
ports:
- "8001:8001"
networks:
- logging
labels:
logging: "promtail"
app: "devops-go"
deploy:
resources:
limits:
cpus: "0.5"
memory: 512M
reservations:
cpus: "0.25"
memory: 256M

networks:
logging:
driver: bridge

volumes:
loki-data:
grafana-data:

Loading
Loading