Skip to content
Open
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
42 changes: 42 additions & 0 deletions RukosuevaED/alloy/config.alloy
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
prometheus.scrape "app" {
targets = [
{"__address__" = "app:8000"},
]
metrics_path = "/metrics"
scrape_interval = "5s"
scrape_timeout = "3s"
forward_to = [prometheus.remote_write.mimir.receiver]
}

prometheus.remote_write "mimir" {
endpoint {
url = "http://mimir:9009/api/v1/push"
}
}

discovery.docker "containers" {
host = "unix:///var/run/docker.sock"
}

discovery.relabel "containers" {
targets = discovery.docker.containers.targets

rule {
source_labels = ["__meta_docker_container_name"]
regex = "/(.*)"
target_label = "container"
}
}

loki.source.docker "default" {
host = "unix:///var/run/docker.sock"
targets = discovery.relabel.containers.output
relabel_rules = discovery.relabel.containers.rules
forward_to = [loki.write.default.receiver]
}

loki.write "default" {
endpoint {
url = "http://loki:3100/loki/api/v1/push"
}
}
12 changes: 12 additions & 0 deletions RukosuevaED/app/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
FROM python:3.12-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY main.py .

EXPOSE 8000

CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
136 changes: 136 additions & 0 deletions RukosuevaED/app/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import logging
import sys
import time
import uuid
from itertools import count

from fastapi import FastAPI, HTTPException, Request
from prometheus_client import CONTENT_TYPE_LATEST, Counter, Histogram, generate_latest
from pydantic import BaseModel


class JsonLogFormatter(logging.Formatter):
def format(self, record: logging.LogRecord) -> str:
payload = {
"timestamp": self.formatTime(record, "%Y-%m-%dT%H:%M:%S%z"),
"level": record.levelname,
"logger": record.name,
"message": record.getMessage(),
}
for key in ("method", "path", "status_code", "duration_ms", "request_id"):
if hasattr(record, key):
payload[key] = getattr(record, key)
return str(payload).replace("'", '"')


def configure_logging() -> logging.Logger:
handler = logging.StreamHandler(sys.stdout)
handler.setFormatter(JsonLogFormatter())

logger = logging.getLogger("notes_app")
logger.setLevel(logging.INFO)
logger.addHandler(handler)
logger.propagate = False
return logger


logger = configure_logging()

app = FastAPI(title="Notes API")

REQUEST_COUNT = Counter(
"app_requests_total",
"Total number of HTTP requests received by the application",
["method", "path", "status_code"],
)
REQUEST_LATENCY = Histogram(
"app_request_duration_seconds",
"Duration of HTTP requests in seconds",
["method", "path"],
)


@app.middleware("http")
async def metrics_and_logging_middleware(request: Request, call_next):
request_id = str(uuid.uuid4())
start = time.perf_counter()

response = await call_next(request)

duration = time.perf_counter() - start
route = request.scope.get("route")
path_template = route.path if route is not None else request.url.path

REQUEST_COUNT.labels(
method=request.method,
path=path_template,
status_code=response.status_code,
).inc()
REQUEST_LATENCY.labels(method=request.method, path=path_template).observe(duration)

logger.info(
"request completed",
extra={
"method": request.method,
"path": path_template,
"status_code": response.status_code,
"duration_ms": round(duration * 1000, 2),
"request_id": request_id,
},
)
response.headers["X-Request-ID"] = request_id
return response


class NoteIn(BaseModel):
title: str
content: str


class Note(NoteIn):
id: int


_notes: dict[int, Note] = {}
_id_counter = count(1)


@app.get("/health")
def health() -> dict[str, str]:
return {"status": "ok"}


@app.get("/metrics")
def metrics():
from starlette.responses import Response

return Response(generate_latest(), media_type=CONTENT_TYPE_LATEST)


@app.get("/notes", response_model=list[Note])
def list_notes() -> list[Note]:
return list(_notes.values())


@app.post("/notes", response_model=Note, status_code=201)
def create_note(note_in: NoteIn) -> Note:
note_id = next(_id_counter)
note = Note(id=note_id, **note_in.model_dump())
_notes[note_id] = note
logger.info("note created", extra={"path": "/notes", "method": "POST"})
return note


@app.get("/notes/{note_id}", response_model=Note)
def get_note(note_id: int) -> Note:
note = _notes.get(note_id)
if note is None:
raise HTTPException(status_code=404, detail="Note not found")
return note


@app.delete("/notes/{note_id}", status_code=204)
def delete_note(note_id: int) -> None:
if note_id not in _notes:
raise HTTPException(status_code=404, detail="Note not found")
del _notes[note_id]
3 changes: 3 additions & 0 deletions RukosuevaED/app/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
fastapi==0.115.0
uvicorn[standard]==0.30.6
prometheus-client==0.20.0
70 changes: 70 additions & 0 deletions RukosuevaED/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
services:
app:
build: ./app
container_name: notes-app
ports:
- "8000:8000"
restart: unless-stopped

mimir:
image: grafana/mimir:2.14.1
container_name: mimir
command: ["-config.file=/etc/mimir/mimir.yaml"]
volumes:
- ./mimir/mimir.yaml:/etc/mimir/mimir.yaml:ro
- mimir-data:/data
ports:
- "9009:9009"
restart: unless-stopped

loki:
image: grafana/loki:3.2.1
container_name: loki
command: ["-config.file=/etc/loki/loki-config.yaml"]
volumes:
- ./loki/loki-config.yaml:/etc/loki/loki-config.yaml:ro
- loki-data:/loki
ports:
- "3100:3100"
restart: unless-stopped

alloy:
image: grafana/alloy:v1.4.3
container_name: alloy
command:
- "run"
- "--server.http.listen-addr=0.0.0.0:12345"
- "/etc/alloy/config.alloy"
volumes:
- ./alloy/config.alloy:/etc/alloy/config.alloy:ro
- /var/run/docker.sock:/var/run/docker.sock:ro
ports:
- "12345:12345"
depends_on:
- app
- loki
- mimir
restart: unless-stopped

grafana:
image: grafana/grafana:11.2.2
container_name: grafana
environment:
- GF_AUTH_ANONYMOUS_ENABLED=true
- GF_AUTH_ANONYMOUS_ORG_ROLE=Admin
- GF_AUTH_DISABLE_LOGIN_FORM=true
volumes:
- ./grafana/provisioning:/etc/grafana/provisioning:ro
- ./grafana/dashboards:/etc/grafana/dashboards:ro
- grafana-data:/var/lib/grafana
ports:
- "3000:3000"
depends_on:
- loki
- mimir
restart: unless-stopped

volumes:
mimir-data:
loki-data:
grafana-data:
63 changes: 63 additions & 0 deletions RukosuevaED/grafana/dashboards/notes-app.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
{
"title": "Notes App",
"uid": "notes-app",
"timezone": "browser",
"schemaVersion": 39,
"refresh": "10s",
"time": {
"from": "now-15m",
"to": "now"
},
"panels": [
{
"id": 1,
"title": "Requests per second",
"type": "timeseries",
"datasource": { "type": "prometheus", "uid": "mimir" },
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 0 },
"targets": [
{
"expr": "sum by (path, status_code) (rate(app_requests_total[1m]))",
"legendFormat": "{{method}} {{path}} {{status_code}}"
}
]
},
{
"id": 2,
"title": "Request duration (p95)",
"type": "timeseries",
"datasource": { "type": "prometheus", "uid": "mimir" },
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 0 },
"targets": [
{
"expr": "histogram_quantile(0.95, sum by (le, path) (rate(app_request_duration_seconds_bucket[5m])))",
"legendFormat": "{{path}}"
}
]
},
{
"id": 3,
"title": "Total requests",
"type": "stat",
"datasource": { "type": "prometheus", "uid": "mimir" },
"gridPos": { "h": 4, "w": 6, "x": 0, "y": 8 },
"targets": [
{
"expr": "sum(app_requests_total)"
}
]
},
{
"id": 4,
"title": "Application logs",
"type": "logs",
"datasource": { "type": "loki", "uid": "loki" },
"gridPos": { "h": 10, "w": 24, "x": 0, "y": 12 },
"targets": [
{
"expr": "{container=\"notes-app\"}"
}
]
}
]
}
9 changes: 9 additions & 0 deletions RukosuevaED/grafana/provisioning/dashboards/dashboards.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
apiVersion: 1

providers:
- name: default
folder: ""
type: file
updateIntervalSeconds: 30
options:
path: /etc/grafana/dashboards
17 changes: 17 additions & 0 deletions RukosuevaED/grafana/provisioning/datasources/datasources.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
apiVersion: 1

datasources:
- name: Mimir
uid: mimir
type: prometheus
access: proxy
url: http://mimir:9009/prometheus
isDefault: true
jsonData:
httpMethod: POST

- name: Loki
uid: loki
type: loki
access: proxy
url: http://loki:3100
28 changes: 28 additions & 0 deletions RukosuevaED/loki/loki-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
auth_enabled: false

server:
http_listen_port: 3100

common:
path_prefix: /loki
storage:
filesystem:
chunks_directory: /loki/chunks
rules_directory: /loki/rules
replication_factor: 1
ring:
kvstore:
store: inmemory

schema_config:
configs:
- from: 2024-01-01
store: tsdb
object_store: filesystem
schema: v13
index:
prefix: index_
period: 24h

limits_config:
allow_structured_metadata: true
Loading