init: added template
This commit is contained in:
@@ -0,0 +1,16 @@
|
|||||||
|
# Base
|
||||||
|
RUSTFS_API_PORT=14602
|
||||||
|
RUSTFS_CONSOLE_PORT=14601
|
||||||
|
|
||||||
|
# Observability
|
||||||
|
GRAFANA_PORT=14603
|
||||||
|
GRAFANA_ALLOY_PORT=14606
|
||||||
|
PGADMIN_PORT=14604
|
||||||
|
VICTORIAMETRICS_PORT=14605
|
||||||
|
OTEL_GRPC_PORT=14607
|
||||||
|
OTEL_HTTP_PORT=14608
|
||||||
|
GRAFANA_PYROSCOPE_PORT=14611
|
||||||
|
|
||||||
|
# Backend
|
||||||
|
DJANGO_PORT=14609
|
||||||
|
DJANGO_STATICFILES_PORT=14610
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
*.png filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.gif filter=lfs diff=lfs merge=lfs -text
|
||||||
+25
@@ -0,0 +1,25 @@
|
|||||||
|
# General
|
||||||
|
.DS_Store
|
||||||
|
__MACOSX/
|
||||||
|
.AppleDouble
|
||||||
|
.LSOverride
|
||||||
|
Icon[
|
||||||
|
]
|
||||||
|
|
||||||
|
# Thumbnails
|
||||||
|
._*
|
||||||
|
|
||||||
|
# Files that might appear in the root of a volume
|
||||||
|
.DocumentRevisions-V100
|
||||||
|
.fseventsd
|
||||||
|
.Spotlight-V100
|
||||||
|
.TemporaryItems
|
||||||
|
.Trashes
|
||||||
|
.VolumeIcon.icns
|
||||||
|
.com.apple.timemachine.donotpresent
|
||||||
|
|
||||||
|
# Directories potentially created on remote AFP share
|
||||||
|
.AppleDB
|
||||||
|
.AppleDesktop
|
||||||
|
Network Trash Folder
|
||||||
|
Temporary Items
|
||||||
-17
@@ -1,17 +0,0 @@
|
|||||||
FROM freepascal/fpc:3.2.2-focal-full AS build
|
|
||||||
WORKDIR /app
|
|
||||||
COPY src/server.pas .
|
|
||||||
RUN fpc -O2 -Xs server.pas
|
|
||||||
|
|
||||||
FROM debian:bookworm-slim
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends libcap2-bin ca-certificates \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
COPY --from=build /app/server /app/server
|
|
||||||
|
|
||||||
RUN setcap 'cap_net_bind_service=+ep' /app/server
|
|
||||||
|
|
||||||
USER 65534:65534
|
|
||||||
EXPOSE 80
|
|
||||||
CMD ["/app/server"]
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
# PROD 2026 - Backend
|
|
||||||
|
|
||||||
|
|
||||||
Используйте данный репозиторий для работы над задачей.
|
|
||||||
|
|
||||||
> [!TIP]
|
|
||||||
> Задание можно найти в отдельном репозитории по ссылке – https://gitlab.prodcontest.com/2026-final-tasks/backend
|
|
||||||
|
|
||||||
|
|
||||||
## Что фиксируется как решение
|
|
||||||
|
|
||||||
Проверяется состояние последнего коммита в ветке `main` на момент дедлайна.
|
|
||||||
|
|
||||||
|
|
||||||
## Технические требования
|
|
||||||
|
|
||||||
Технические ограничения окружения где будет выполняться сборка и запуск решения
|
|
||||||
|
|
||||||
| Параметр | Лимит |
|
|
||||||
|----------|-------|
|
|
||||||
| Время старта | < 180 секунд до готовности /ready |
|
|
||||||
| Память | < 8 GB |
|
|
||||||
| CPU | 4 ядра |
|
|
||||||
| Размер Docker-образа | < 4 GB |
|
|
||||||
| Сеть | Нет доступа в интернет при запуске и проверке |
|
|
||||||
|
|
||||||
Технические требования к решению:
|
|
||||||
|
|
||||||
* Приложение должно запускаться командой `docker compose up -d` в корне репозитория.
|
|
||||||
* Приложение слушает только порт `80`, все сервисы общаются между собой через docker-сеть (локально).
|
|
||||||
|
|
||||||
## Академическая честность и культура общения
|
|
||||||
|
|
||||||
Мы призываем всех участников соблюдать принципы академической честности и культуры общения, подходить к соревнованиям открыто и добросовестно. <br/>
|
|
||||||
Цель олимпиады — не только продемонстрировать свои знания и навыки, но и развиваться как надёжные и ответственные специалисты в будущем.
|
|
||||||
|
|
||||||
### Академическая честность и проверка на заимствования
|
|
||||||
|
|
||||||
Мы проверяем самостоятельность решений:
|
|
||||||
|
|
||||||
- внутренняя проверка организаторов;
|
|
||||||
- внешняя проверка через Codechecker (продукт компании «Антиплагиат»).
|
|
||||||
|
|
||||||
Код из открытых источников использовать можно, но важно указать источник — в комментарии рядом с фрагментом или в `README`.
|
|
||||||
|
|
||||||
Если вы используете LLM/нейросети (например, чтобы сгенерировать фрагменты кода, тесты или документацию), пожалуйста, помечайте это — где уместно, в комментарии рядом с фрагментом или отдельной заметкой в `README`. <br/>
|
|
||||||
Важно: **ответственность за весь код в репозитории несёте вы**, включая сгенерированные фрагменты — вы должны понимать, что именно добавляете, уметь объяснить решение и проверять результат (корректность, безопасность, крайние случаи).
|
|
||||||
|
|
||||||
Это позволит легко проверить и подтвердить самостоятельность вашей работы и снять возможные вопросы при проверке.
|
|
||||||
|
|
||||||
### Культура общения и этика участия
|
|
||||||
|
|
||||||
Олимпиада по промышленному программированию — это про профессиональный подход. Поэтому в репозитории (сообщения коммитов, названия веток, комментарии к коду и обсуждения) мы ожидаем уважительный и деловой тон.
|
|
||||||
|
|
||||||
Мы не приемлем ненормативную лексику, оскорбления и токсичное поведение.
|
|
||||||
|
|
||||||
## Меры при нарушениях
|
|
||||||
|
|
||||||
Если мы фиксируем нарушение академической честности или культуры общения, мы можем аннулировать результат работы (включая дисквалификацию в рамках олимпиады). <br/>
|
|
||||||
Решение принимается организаторами по совокупности признаков и результатов проверок.
|
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
name: lotty
|
||||||
|
|
||||||
|
x-defaults: &defaults
|
||||||
|
project_directory: ./
|
||||||
|
env_file:
|
||||||
|
- ./.env.template
|
||||||
|
|
||||||
|
include:
|
||||||
|
- path: ./deploy/compose/compose.yaml
|
||||||
|
<<: *defaults
|
||||||
|
- path: ./deploy/compose/compose.backend.yaml
|
||||||
|
<<: *defaults
|
||||||
|
- path: ./deploy/compose/compose.observability.yaml
|
||||||
|
<<: *defaults
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
name: lotty
|
||||||
|
|
||||||
|
x-defaults: &defaults
|
||||||
|
project_directory: ./
|
||||||
|
env_file:
|
||||||
|
- ./.env.template
|
||||||
|
|
||||||
|
include:
|
||||||
|
- path: ./deploy/compose/compose.yaml
|
||||||
|
<<: *defaults
|
||||||
|
- path: ./deploy/compose/compose.backend.yaml
|
||||||
|
<<: *defaults
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
services:
|
||||||
|
backend:
|
||||||
|
build:
|
||||||
|
context: ./src/backend
|
||||||
|
dockerfile: Containerfile
|
||||||
|
tags:
|
||||||
|
- lotty-backend:latest
|
||||||
|
pull: true
|
||||||
|
depends_on:
|
||||||
|
backend-initdb:
|
||||||
|
restart: false
|
||||||
|
condition: service_completed_successfully
|
||||||
|
required: true
|
||||||
|
postgresql:
|
||||||
|
restart: false
|
||||||
|
condition: service_healthy
|
||||||
|
required: true
|
||||||
|
valkey:
|
||||||
|
restart: false
|
||||||
|
condition: service_healthy
|
||||||
|
required: true
|
||||||
|
env_file:
|
||||||
|
- path: ./infrastructure/configs/backend/.env.template
|
||||||
|
required: true
|
||||||
|
- path: ./infrastructure/configs/backend/.env
|
||||||
|
required: false
|
||||||
|
ports:
|
||||||
|
- name: web
|
||||||
|
target: 8080
|
||||||
|
published: ${DJANGO_PORT}
|
||||||
|
host_ip: 127.0.0.1
|
||||||
|
protocol: tcp
|
||||||
|
app_protocol: http
|
||||||
|
networks:
|
||||||
|
- default
|
||||||
|
restart: unless-stopped
|
||||||
|
shm_size: 4mb
|
||||||
|
|
||||||
|
backend-initdb:
|
||||||
|
build:
|
||||||
|
context: ./src/backend
|
||||||
|
dockerfile: Containerfile
|
||||||
|
tags:
|
||||||
|
- lotty-backend:latest
|
||||||
|
pull: true
|
||||||
|
entrypoint: ./scripts/initdb
|
||||||
|
depends_on:
|
||||||
|
postgresql:
|
||||||
|
restart: false
|
||||||
|
condition: service_healthy
|
||||||
|
required: true
|
||||||
|
valkey:
|
||||||
|
restart: false
|
||||||
|
condition: service_healthy
|
||||||
|
required: true
|
||||||
|
env_file:
|
||||||
|
- path: ./infrastructure/configs/backend/.env.template
|
||||||
|
required: true
|
||||||
|
- path: ./infrastructure/configs/backend/.env
|
||||||
|
required: false
|
||||||
|
networks:
|
||||||
|
- default
|
||||||
|
restart: no
|
||||||
|
shm_size: 4mb
|
||||||
|
|
||||||
|
backend-staticfiles:
|
||||||
|
build:
|
||||||
|
context: ./src/backend
|
||||||
|
dockerfile: Containerfile.staticfiles
|
||||||
|
tags:
|
||||||
|
- lotty-backend-staticfiles:latest
|
||||||
|
pull: true
|
||||||
|
env_file:
|
||||||
|
- path: ./infrastructure/configs/backend/.env.template
|
||||||
|
required: true
|
||||||
|
- path: ./infrastructure/configs/backend/.env
|
||||||
|
required: false
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "nginx", "-t", "||", "exit 1"]
|
||||||
|
interval: 1m30s
|
||||||
|
timeout: 5s
|
||||||
|
start_period: 5s
|
||||||
|
start_interval: 2s
|
||||||
|
retries: 5
|
||||||
|
ports:
|
||||||
|
- name: web
|
||||||
|
target: 80
|
||||||
|
published: ${DJANGO_STATICFILES_PORT}
|
||||||
|
host_ip: 127.0.0.1
|
||||||
|
protocol: tcp
|
||||||
|
app_protocol: http
|
||||||
|
networks:
|
||||||
|
- default
|
||||||
|
restart: unless-stopped
|
||||||
|
shm_size: 4mb
|
||||||
|
|
||||||
|
backend-celery-worker:
|
||||||
|
build:
|
||||||
|
context: ./src/backend
|
||||||
|
dockerfile: Containerfile
|
||||||
|
tags:
|
||||||
|
- lotty-backend:latest
|
||||||
|
pull: true
|
||||||
|
command: celery -A config worker -l INFO
|
||||||
|
depends_on:
|
||||||
|
valkey:
|
||||||
|
restart: false
|
||||||
|
condition: service_healthy
|
||||||
|
required: true
|
||||||
|
env_file:
|
||||||
|
- path: ./infrastructure/configs/backend/.env.template
|
||||||
|
required: true
|
||||||
|
- path: ./infrastructure/configs/backend/.env
|
||||||
|
required: false
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "celery", "-A", "config", "inspect", "ping"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 10s
|
||||||
|
start_interval: 2s
|
||||||
|
networks:
|
||||||
|
- default
|
||||||
|
restart: unless-stopped
|
||||||
|
shm_size: 4mb
|
||||||
|
|
||||||
|
celery-exporter:
|
||||||
|
image: docker.io/danihodovic/celery-exporter:latest
|
||||||
|
command:
|
||||||
|
- --retry-interval=5
|
||||||
|
depends_on:
|
||||||
|
valkey:
|
||||||
|
restart: false
|
||||||
|
condition: service_healthy
|
||||||
|
required: true
|
||||||
|
env_file:
|
||||||
|
- path: ./infrastructure/configs/celery-exporter/.env.template
|
||||||
|
required: true
|
||||||
|
- path: ./infrastructure/configs/celery-exporter/.env
|
||||||
|
required: false
|
||||||
|
networks:
|
||||||
|
- default
|
||||||
|
profiles:
|
||||||
|
- observability
|
||||||
|
restart: unless-stopped
|
||||||
|
shm_size: 4mb
|
||||||
@@ -0,0 +1,297 @@
|
|||||||
|
services:
|
||||||
|
grafana:
|
||||||
|
image: docker.io/grafana/grafana-oss:12.3.2
|
||||||
|
entrypoint:
|
||||||
|
- /etc/grafana/scripts/entrypoint.sh
|
||||||
|
env_file:
|
||||||
|
- path: ./infrastructure/configs/grafana/.env.template
|
||||||
|
required: true
|
||||||
|
- path: ./infrastructure/configs/grafana/.env
|
||||||
|
required: false
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "wget", "-O", "-", "http://localhost:3000/api/health"]
|
||||||
|
interval: 1m30s
|
||||||
|
timeout: 5s
|
||||||
|
start_period: 5s
|
||||||
|
start_interval: 2s
|
||||||
|
retries: 5
|
||||||
|
ports:
|
||||||
|
- name: web
|
||||||
|
target: 3000
|
||||||
|
published: ${GRAFANA_PORT}
|
||||||
|
host_ip: 127.0.0.1
|
||||||
|
protocol: tcp
|
||||||
|
app_protocol: http
|
||||||
|
networks:
|
||||||
|
- default
|
||||||
|
profiles:
|
||||||
|
- observability
|
||||||
|
restart: unless-stopped
|
||||||
|
shm_size: 4mb
|
||||||
|
volumes:
|
||||||
|
- type: bind
|
||||||
|
source: ./infrastructure/configs/grafana/provisioning
|
||||||
|
target: /etc/grafana/provisioning
|
||||||
|
read_only: true
|
||||||
|
- type: bind
|
||||||
|
source: ./infrastructure/configs/grafana/scripts
|
||||||
|
target: /etc/grafana/scripts
|
||||||
|
read_only: true
|
||||||
|
|
||||||
|
otel-collector:
|
||||||
|
image: docker.io/otel/opentelemetry-collector-contrib:latest
|
||||||
|
command:
|
||||||
|
- --config=/etc/otelcol-contrib/config.yaml
|
||||||
|
configs:
|
||||||
|
- source: otel_collector_config
|
||||||
|
target: /etc/otelcol-contrib/config.yaml
|
||||||
|
ports:
|
||||||
|
- name: otlp-grpc
|
||||||
|
target: 4317
|
||||||
|
published: ${OTEL_GRPC_PORT}
|
||||||
|
host_ip: 127.0.0.1
|
||||||
|
protocol: tcp
|
||||||
|
- name: otlp-http
|
||||||
|
target: 4318
|
||||||
|
published: ${OTEL_HTTP_PORT}
|
||||||
|
host_ip: 127.0.0.1
|
||||||
|
protocol: tcp
|
||||||
|
networks:
|
||||||
|
- default
|
||||||
|
profiles:
|
||||||
|
- observability
|
||||||
|
restart: unless-stopped
|
||||||
|
shm_size: 4mb
|
||||||
|
|
||||||
|
grafana-loki:
|
||||||
|
image: docker.io/grafana/loki:3.6.5
|
||||||
|
command:
|
||||||
|
- --config.file=/etc/loki/local-config.yaml
|
||||||
|
- --config.expand-env=true
|
||||||
|
depends_on:
|
||||||
|
s3:
|
||||||
|
restart: false
|
||||||
|
condition: service_healthy
|
||||||
|
required: true
|
||||||
|
configs:
|
||||||
|
- source: grafana_loki_config
|
||||||
|
target: /etc/loki/local-config.yaml
|
||||||
|
env_file:
|
||||||
|
- path: ./infrastructure/configs/s3/.env.template
|
||||||
|
required: true
|
||||||
|
- path: ./infrastructure/configs/s3/.env
|
||||||
|
required: false
|
||||||
|
networks:
|
||||||
|
- default
|
||||||
|
profiles:
|
||||||
|
- observability
|
||||||
|
restart: unless-stopped
|
||||||
|
shm_size: 4mb
|
||||||
|
|
||||||
|
grafana-tempo:
|
||||||
|
image: docker.io/grafana/tempo:2.10.0
|
||||||
|
command:
|
||||||
|
- --config.file=/etc/tempo/tempo-config.yaml
|
||||||
|
- --config.expand-env=true
|
||||||
|
depends_on:
|
||||||
|
s3:
|
||||||
|
restart: false
|
||||||
|
condition: service_healthy
|
||||||
|
required: true
|
||||||
|
configs:
|
||||||
|
- source: grafana_tempo_config
|
||||||
|
target: /etc/tempo/tempo-config.yaml
|
||||||
|
env_file:
|
||||||
|
- path: ./infrastructure/configs/s3/.env.template
|
||||||
|
required: true
|
||||||
|
- path: ./infrastructure/configs/s3/.env
|
||||||
|
required: false
|
||||||
|
networks:
|
||||||
|
- default
|
||||||
|
profiles:
|
||||||
|
- observability
|
||||||
|
restart: unless-stopped
|
||||||
|
shm_size: 4mb
|
||||||
|
|
||||||
|
grafana-pyroscope:
|
||||||
|
image: docker.io/grafana/pyroscope:1.18.0
|
||||||
|
command:
|
||||||
|
- -config.file=/etc/pyroscope/config.yaml
|
||||||
|
- -config.expand-env=true
|
||||||
|
depends_on:
|
||||||
|
s3:
|
||||||
|
restart: false
|
||||||
|
condition: service_healthy
|
||||||
|
required: true
|
||||||
|
configs:
|
||||||
|
- source: grafana_pyroscope_config
|
||||||
|
target: /etc/pyroscope/config.yaml
|
||||||
|
env_file:
|
||||||
|
- path: ./infrastructure/configs/s3/.env.template
|
||||||
|
required: true
|
||||||
|
- path: ./infrastructure/configs/s3/.env
|
||||||
|
required: false
|
||||||
|
networks:
|
||||||
|
- default
|
||||||
|
profiles:
|
||||||
|
- observability
|
||||||
|
restart: unless-stopped
|
||||||
|
shm_size: 4mb
|
||||||
|
|
||||||
|
vmagent:
|
||||||
|
image: docker.io/victoriametrics/vmagent:v1.135.0
|
||||||
|
command:
|
||||||
|
- -promscrape.config=/etc/vmagent/scrape.yaml
|
||||||
|
- -remoteWrite.url=http://victoriametrics:8428/api/v1/write
|
||||||
|
configs:
|
||||||
|
- source: vmagent_config
|
||||||
|
target: /etc/vmagent/scrape.yaml
|
||||||
|
networks:
|
||||||
|
- default
|
||||||
|
profiles:
|
||||||
|
- observability
|
||||||
|
restart: unless-stopped
|
||||||
|
shm_size: 4mb
|
||||||
|
|
||||||
|
victoriametrics:
|
||||||
|
image: docker.io/victoriametrics/victoria-metrics:v1.134.0
|
||||||
|
command:
|
||||||
|
- -storageDataPath=/var/lib/victoriametrics
|
||||||
|
- -retentionPeriod=30d
|
||||||
|
- -httpListenAddr=:8428
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:8428/-/healthy"]
|
||||||
|
interval: 1m30s
|
||||||
|
timeout: 5s
|
||||||
|
start_period: 5s
|
||||||
|
start_interval: 2s
|
||||||
|
retries: 5
|
||||||
|
ports:
|
||||||
|
- name: web
|
||||||
|
target: 8428
|
||||||
|
published: ${VICTORIAMETRICS_PORT}
|
||||||
|
host_ip: 127.0.0.1
|
||||||
|
protocol: tcp
|
||||||
|
app_protocol: http
|
||||||
|
networks:
|
||||||
|
- default
|
||||||
|
profiles:
|
||||||
|
- observability
|
||||||
|
restart: unless-stopped
|
||||||
|
shm_size: 4mb
|
||||||
|
volumes:
|
||||||
|
- type: volume
|
||||||
|
source: victoriametrics_data
|
||||||
|
target: /var/lib/victoriametrics
|
||||||
|
read_only: false
|
||||||
|
|
||||||
|
redis-exporter:
|
||||||
|
image: docker.io/oliver006/redis_exporter:v1.80.2-alpine
|
||||||
|
depends_on:
|
||||||
|
valkey:
|
||||||
|
restart: false
|
||||||
|
condition: service_healthy
|
||||||
|
required: true
|
||||||
|
env_file:
|
||||||
|
- path: ./infrastructure/configs/redis-exporter/.env.template
|
||||||
|
required: true
|
||||||
|
- path: ./infrastructure/configs/redis-exporter/.env
|
||||||
|
required: false
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "wget", "-O", "-", "http://localhost:9121/metrics"]
|
||||||
|
interval: 1m30s
|
||||||
|
timeout: 5s
|
||||||
|
start_period: 5s
|
||||||
|
start_interval: 2s
|
||||||
|
retries: 5
|
||||||
|
networks:
|
||||||
|
- default
|
||||||
|
profiles:
|
||||||
|
- observability
|
||||||
|
restart: unless-stopped
|
||||||
|
shm_size: 4mb
|
||||||
|
|
||||||
|
postgres-exporter:
|
||||||
|
image: quay.io/prometheuscommunity/postgres-exporter:latest
|
||||||
|
depends_on:
|
||||||
|
postgresql:
|
||||||
|
restart: false
|
||||||
|
condition: service_healthy
|
||||||
|
required: true
|
||||||
|
env_file:
|
||||||
|
- path: ./infrastructure/configs/postgres-exporter/.env.template
|
||||||
|
required: true
|
||||||
|
- path: ./infrastructure/configs/postgres-exporter/.env
|
||||||
|
required: false
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "wget", "-O", "-", "http://localhost:9187/metrics"]
|
||||||
|
interval: 1m30s
|
||||||
|
timeout: 5s
|
||||||
|
start_period: 5s
|
||||||
|
start_interval: 2s
|
||||||
|
retries: 5
|
||||||
|
networks:
|
||||||
|
- default
|
||||||
|
profiles:
|
||||||
|
- observability
|
||||||
|
restart: unless-stopped
|
||||||
|
shm_size: 4mb
|
||||||
|
|
||||||
|
pgadmin:
|
||||||
|
image: docker.io/dpage/pgadmin4:9
|
||||||
|
configs:
|
||||||
|
- source: pgadmin_servers_config
|
||||||
|
target: /pgadmin4/servers.json
|
||||||
|
depends_on:
|
||||||
|
postgresql:
|
||||||
|
restart: false
|
||||||
|
condition: service_healthy
|
||||||
|
required: true
|
||||||
|
env_file:
|
||||||
|
- path: ./infrastructure/configs/pgadmin/.env.template
|
||||||
|
required: true
|
||||||
|
- path: ./infrastructure/configs/pgadmin/.env
|
||||||
|
required: false
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "wget", "-O", "-", "http://localhost:80/misc/ping"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 5s
|
||||||
|
start_period: 5s
|
||||||
|
start_interval: 2s
|
||||||
|
retries: 5
|
||||||
|
ports:
|
||||||
|
- name: web
|
||||||
|
target: 80
|
||||||
|
published: ${PGADMIN_PORT}
|
||||||
|
host_ip: 127.0.0.1
|
||||||
|
protocol: tcp
|
||||||
|
app_protocol: http
|
||||||
|
networks:
|
||||||
|
- default
|
||||||
|
profiles:
|
||||||
|
- observability
|
||||||
|
restart: unless-stopped
|
||||||
|
shm_size: 4mb
|
||||||
|
volumes:
|
||||||
|
- type: volume
|
||||||
|
source: pgadmin_data
|
||||||
|
target: /var/lib/pgadmin
|
||||||
|
read_only: false
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
victoriametrics_data:
|
||||||
|
pgadmin_data:
|
||||||
|
|
||||||
|
configs:
|
||||||
|
otel_collector_config:
|
||||||
|
file: ./infrastructure/configs/otel-collector/config.yaml
|
||||||
|
grafana_loki_config:
|
||||||
|
file: ./infrastructure/configs/grafana-loki/loki.yaml
|
||||||
|
grafana_tempo_config:
|
||||||
|
file: ./infrastructure/configs/grafana-tempo/tempo.yaml
|
||||||
|
grafana_pyroscope_config:
|
||||||
|
file: ./infrastructure/configs/grafana-pyroscope/config.yaml
|
||||||
|
vmagent_config:
|
||||||
|
file: ./infrastructure/configs/victoriametrics/vmagent.yaml
|
||||||
|
pgadmin_servers_config:
|
||||||
|
file: ./infrastructure/configs/pgadmin/servers.json
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
services:
|
||||||
|
postgresql:
|
||||||
|
image: docker.io/postgres:18-alpine
|
||||||
|
configs:
|
||||||
|
- source: postgresql_config
|
||||||
|
target: /etc/postgresql/postgresql.conf
|
||||||
|
env_file:
|
||||||
|
- path: ./infrastructure/configs/postgresql/.env.template
|
||||||
|
required: true
|
||||||
|
- path: ./infrastructure/configs/postgresql/.env
|
||||||
|
required: false
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "sh", "-c", "pg_isready -U postgres -d postgres"]
|
||||||
|
interval: 1m30s
|
||||||
|
timeout: 5s
|
||||||
|
start_period: 5s
|
||||||
|
start_interval: 2s
|
||||||
|
retries: 5
|
||||||
|
networks:
|
||||||
|
- default
|
||||||
|
oom_kill_disable: true
|
||||||
|
restart: unless-stopped
|
||||||
|
shm_size: 128mb
|
||||||
|
volumes:
|
||||||
|
- type: volume
|
||||||
|
source: postgresql_data
|
||||||
|
target: /var/lib/postgresql/data
|
||||||
|
|
||||||
|
valkey:
|
||||||
|
image: docker.io/valkey/valkey:9-alpine
|
||||||
|
command: valkey-server /usr/local/etc/valkey/valkey.conf
|
||||||
|
configs:
|
||||||
|
- source: valkey_config
|
||||||
|
target: /usr/local/etc/valkey/valkey.conf
|
||||||
|
env_file:
|
||||||
|
- path: ./infrastructure/configs/valkey/.env.template
|
||||||
|
required: true
|
||||||
|
- path: ./infrastructure/configs/valkey/.env
|
||||||
|
required: false
|
||||||
|
healthcheck:
|
||||||
|
test:
|
||||||
|
["CMD", "sh", "-c", "valkey-cli -p 6379 --pass valkey ping | grep PONG"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 2s
|
||||||
|
start_period: 5s
|
||||||
|
start_interval: 2s
|
||||||
|
retries: 3
|
||||||
|
networks:
|
||||||
|
- default
|
||||||
|
restart: unless-stopped
|
||||||
|
shm_size: 4mb
|
||||||
|
volumes:
|
||||||
|
- type: volume
|
||||||
|
source: valkey_data
|
||||||
|
target: /data
|
||||||
|
read_only: false
|
||||||
|
|
||||||
|
s3:
|
||||||
|
image: docker.io/rustfs/rustfs:1.0.0-alpha.82
|
||||||
|
env_file:
|
||||||
|
- path: ./infrastructure/configs/rustfs/.env.template
|
||||||
|
required: true
|
||||||
|
- path: ./infrastructure/configs/rustfs/.env
|
||||||
|
required: false
|
||||||
|
healthcheck:
|
||||||
|
test:
|
||||||
|
[
|
||||||
|
"CMD",
|
||||||
|
"sh",
|
||||||
|
"-c",
|
||||||
|
"curl -f http://localhost:9000/health && curl -f http://localhost:9001/rustfs/console/health",
|
||||||
|
]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 5s
|
||||||
|
start_period: 5s
|
||||||
|
start_interval: 5s
|
||||||
|
retries: 3
|
||||||
|
ports:
|
||||||
|
- name: api
|
||||||
|
target: 9000
|
||||||
|
published: ${RUSTFS_API_PORT}
|
||||||
|
host_ip: 127.0.0.1
|
||||||
|
protocol: tcp
|
||||||
|
app_protocol: http
|
||||||
|
- name: console
|
||||||
|
target: 9001
|
||||||
|
published: ${RUSTFS_CONSOLE_PORT}
|
||||||
|
host_ip: 127.0.0.1
|
||||||
|
protocol: tcp
|
||||||
|
app_protocol: http
|
||||||
|
networks:
|
||||||
|
- default
|
||||||
|
restart: unless-stopped
|
||||||
|
shm_size: 4mb
|
||||||
|
volumes:
|
||||||
|
- type: volume
|
||||||
|
source: rustfs_data
|
||||||
|
target: /data
|
||||||
|
|
||||||
|
s3-init:
|
||||||
|
image: docker.io/minio/mc:latest
|
||||||
|
entrypoint: >
|
||||||
|
sh -c "until mc alias set default $$S3_URL $$S3_ACCESS_KEY $$S3_SECRET_KEY; do sleep 1; done &&
|
||||||
|
mc mb default/loki;
|
||||||
|
mc mb default/tempo;
|
||||||
|
mc mb default/pyroscope;
|
||||||
|
mc policy set public default/loki; true"
|
||||||
|
depends_on:
|
||||||
|
s3:
|
||||||
|
restart: false
|
||||||
|
condition: service_healthy
|
||||||
|
required: true
|
||||||
|
env_file:
|
||||||
|
- path: ./infrastructure/configs/s3/.env.template
|
||||||
|
required: true
|
||||||
|
- path: ./infrastructure/configs/s3/.env
|
||||||
|
required: false
|
||||||
|
networks:
|
||||||
|
- default
|
||||||
|
restart: no
|
||||||
|
shm_size: 4mb
|
||||||
|
|
||||||
|
networks:
|
||||||
|
default:
|
||||||
|
driver: bridge
|
||||||
|
name: $COMPOSE_PROJECT_NAME
|
||||||
|
attachable: false
|
||||||
|
enable_ipv4: true
|
||||||
|
enable_ipv6: true
|
||||||
|
internal: false
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgresql_data:
|
||||||
|
valkey_data:
|
||||||
|
rustfs_data:
|
||||||
|
|
||||||
|
configs:
|
||||||
|
postgresql_config:
|
||||||
|
file: ./infrastructure/configs/postgresql/postgresql.conf
|
||||||
|
valkey_config:
|
||||||
|
file: ./infrastructure/configs/valkey/valkey.conf
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
services:
|
|
||||||
api:
|
|
||||||
platform: linux/amd64
|
|
||||||
build: .
|
|
||||||
ports:
|
|
||||||
- "80:80"
|
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# Custom environment files
|
||||||
|
.env
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
DJANGO_SECRET_KEY=very_insecure_key
|
||||||
|
DJANGO_DEBUG=False
|
||||||
|
DJANGO_ALLOWED_HOSTS=*
|
||||||
|
DJANGO_CSRF_TRUSTED_ORIGINS=http://localhost,http://127.0.0.1
|
||||||
|
DJANGO_CORS_ALLOWED_ORIGINS=*
|
||||||
|
DJANGO_INTERNAL_IPS=127.0.0.1
|
||||||
|
DJANGO_LANGUAGE_CODE=en-us
|
||||||
|
DJANGO_STATIC_URL=http://localhost:13241/
|
||||||
|
REDIS_URI=redis://valkey:6379
|
||||||
|
DJANGO_DB_URI=postgresql://postgres:postgres@postgresql/postgres
|
||||||
|
DJANGO_CONN_MAX_AGE=300
|
||||||
|
DJANGO_SILKY_PYTHON_PROFILER=
|
||||||
|
|
||||||
|
DJANGO_CREATE_SUPERUSER=True
|
||||||
|
DJANGO_SUPERUSER_USERNAME=admin
|
||||||
|
DJANGO_SUPERUSER_EMAIL=admin@mail.com
|
||||||
|
DJANGO_SUPERUSER_PASSWORD=admin
|
||||||
|
|
||||||
|
# Observability (OpenTelemetry)
|
||||||
|
|
||||||
|
OTEL_ENABLED=False
|
||||||
|
OTEL_SERVICE_NAME=backend-django
|
||||||
|
OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317
|
||||||
|
OTEL_TRACES_EXPORTER=otlp
|
||||||
|
OTEL_METRICS_EXPORTER=otlp
|
||||||
|
OTEL_LOGS_EXPORTER=otlp
|
||||||
|
|
||||||
|
# Storages (S3)
|
||||||
|
|
||||||
|
AWS_ACCESS_KEY_ID=access
|
||||||
|
AWS_SECRET_ACCESS_KEY=storage-secret
|
||||||
|
AWS_STORAGE_BUCKET_NAME=backend
|
||||||
|
AWS_S3_ENDPOINT_URL=s3:9000
|
||||||
|
AWS_S3_REGION_NAME=local
|
||||||
|
AWS_S3_USE_SSL=False
|
||||||
|
AWS_S3_VERIFY=False
|
||||||
|
|
||||||
|
GUNICORN_WORKERS=4
|
||||||
|
GUNICORN_BIND=0.0.0.0:8080
|
||||||
|
GUNICORN_WORKER_CLASS=uvicorn_worker.UvicornWorker
|
||||||
|
GUNICORN_ACCESS_LOG=-
|
||||||
|
GUNICORN_ERROR_LOG=-
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
CE_BROKER_URL=redis://valkey:6379
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
auth_enabled: false
|
||||||
|
pattern_ingester:
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
server:
|
||||||
|
http_listen_port: 3100
|
||||||
|
|
||||||
|
common:
|
||||||
|
ring:
|
||||||
|
instance_addr: 127.0.0.1
|
||||||
|
kvstore:
|
||||||
|
store: inmemory
|
||||||
|
replication_factor: 1
|
||||||
|
path_prefix: /loki
|
||||||
|
storage:
|
||||||
|
s3:
|
||||||
|
endpoint: ${S3_ENDPOINT}
|
||||||
|
bucketnames: loki
|
||||||
|
access_key_id: ${S3_ACCESS_KEY}
|
||||||
|
secret_access_key: ${S3_SECRET_KEY}
|
||||||
|
insecure: true
|
||||||
|
s3forcepathstyle: true
|
||||||
|
|
||||||
|
schema_config:
|
||||||
|
configs:
|
||||||
|
- from: 2020-05-15
|
||||||
|
store: tsdb
|
||||||
|
object_store: s3
|
||||||
|
schema: v13
|
||||||
|
index:
|
||||||
|
prefix: index_
|
||||||
|
period: 24h
|
||||||
|
|
||||||
|
storage_config:
|
||||||
|
tsdb_shipper:
|
||||||
|
active_index_directory: /loki/index
|
||||||
|
cache_location: /loki/index_cache
|
||||||
|
|
||||||
|
compactor:
|
||||||
|
working_directory: /loki/compactor
|
||||||
|
compaction_interval: 10m
|
||||||
|
|
||||||
|
limits_config:
|
||||||
|
allow_structured_metadata: true
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
server:
|
||||||
|
http_listen_port: 4040
|
||||||
|
|
||||||
|
storage:
|
||||||
|
backend: s3
|
||||||
|
s3:
|
||||||
|
bucket_name: pyroscope
|
||||||
|
endpoint: ${S3_ENDPOINT}
|
||||||
|
access_key_id: ${S3_ACCESS_KEY}
|
||||||
|
secret_access_key: ${S3_SECRET_KEY}
|
||||||
|
insecure: true
|
||||||
|
|
||||||
|
analytics:
|
||||||
|
reporting_enabled: false
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
stream_over_http_enabled: true
|
||||||
|
server:
|
||||||
|
http_listen_port: 3200
|
||||||
|
log_level: info
|
||||||
|
|
||||||
|
query_frontend:
|
||||||
|
search:
|
||||||
|
duration_slo: 5s
|
||||||
|
throughput_bytes_slo: 1.073741824e+09
|
||||||
|
metadata_slo:
|
||||||
|
duration_slo: 5s
|
||||||
|
throughput_bytes_slo: 1.073741824e+09
|
||||||
|
trace_by_id:
|
||||||
|
duration_slo: 5s
|
||||||
|
metrics:
|
||||||
|
max_duration: 120h
|
||||||
|
query_backend_after: 5m
|
||||||
|
duration_slo: 5s
|
||||||
|
throughput_bytes_slo: 1.073741824e+09
|
||||||
|
|
||||||
|
distributor:
|
||||||
|
receivers:
|
||||||
|
otlp:
|
||||||
|
protocols:
|
||||||
|
grpc:
|
||||||
|
endpoint: 0.0.0.0:4317
|
||||||
|
http:
|
||||||
|
endpoint: 0.0.0.0:4318
|
||||||
|
|
||||||
|
ingester:
|
||||||
|
max_block_duration: 5m
|
||||||
|
|
||||||
|
metrics_generator:
|
||||||
|
registry:
|
||||||
|
external_labels:
|
||||||
|
source: tempo
|
||||||
|
storage:
|
||||||
|
path: /var/tempo/generator/wal
|
||||||
|
traces_storage:
|
||||||
|
path: /var/tempo/generator/traces
|
||||||
|
processor:
|
||||||
|
local_blocks:
|
||||||
|
filter_server_spans: false
|
||||||
|
flush_to_storage: true
|
||||||
|
|
||||||
|
storage:
|
||||||
|
trace:
|
||||||
|
backend: s3
|
||||||
|
s3:
|
||||||
|
bucket: tempo
|
||||||
|
endpoint: ${S3_ENDPOINT}
|
||||||
|
access_key: ${S3_ACCESS_KEY}
|
||||||
|
secret_key: ${S3_SECRET_KEY}
|
||||||
|
insecure: true
|
||||||
|
wal:
|
||||||
|
path: /var/tempo/wal
|
||||||
|
local:
|
||||||
|
path: /var/tempo/blocks
|
||||||
|
|
||||||
|
overrides:
|
||||||
|
defaults:
|
||||||
|
metrics_generator:
|
||||||
|
processors: [service-graphs, span-metrics, local-blocks]
|
||||||
|
generate_native_histograms: both
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
GF_SECURITY_ADMIN_PASSWORD=prooooood
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
apiVersion: 1
|
||||||
|
|
||||||
|
contactPoints:
|
||||||
|
- orgId: 1
|
||||||
|
name: Telegram
|
||||||
|
receivers:
|
||||||
|
- uid: aet1srtyc40lca
|
||||||
|
type: telegram
|
||||||
|
settings:
|
||||||
|
bottoken: 7797967907:AAGZuUzzuS4LLb525rDNY52Awc2tvpsLjd4
|
||||||
|
chatid: "-1002555823797"
|
||||||
|
disable_notification: false
|
||||||
|
disable_web_page_preview: false
|
||||||
|
message: '{{ template "telegram.default.message" . }}'
|
||||||
|
parse_mode: Markdown
|
||||||
|
protect_content: false
|
||||||
|
disableResolveMessage: false
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
apiVersion: 1
|
||||||
|
|
||||||
|
groups:
|
||||||
|
- orgId: 1
|
||||||
|
name: Default
|
||||||
|
folder: Backend
|
||||||
|
interval: 10s
|
||||||
|
rules:
|
||||||
|
- uid: aet1xbx1yaupsb
|
||||||
|
title: Backend p99 > 500 ms
|
||||||
|
condition: C
|
||||||
|
data:
|
||||||
|
- refId: A
|
||||||
|
relativeTimeRange:
|
||||||
|
from: 600
|
||||||
|
to: 0
|
||||||
|
datasourceUid: prometheus
|
||||||
|
model:
|
||||||
|
editorMode: code
|
||||||
|
expr: |
|
||||||
|
histogram_quantile(
|
||||||
|
0.99,
|
||||||
|
sum(
|
||||||
|
rate(
|
||||||
|
caddy_http_request_duration_seconds_bucket{instance="proxy:2019",handler="reverse_proxy",host="proxy:8080",job="caddy"}[$__rate_interval]
|
||||||
|
)
|
||||||
|
) by (le)
|
||||||
|
)
|
||||||
|
instant: true
|
||||||
|
intervalMs: 1000
|
||||||
|
legendFormat: __auto
|
||||||
|
maxDataPoints: 43200
|
||||||
|
range: false
|
||||||
|
refId: A
|
||||||
|
- refId: C
|
||||||
|
datasourceUid: __expr__
|
||||||
|
model:
|
||||||
|
conditions:
|
||||||
|
- evaluator:
|
||||||
|
params:
|
||||||
|
- 0.5
|
||||||
|
type: gte
|
||||||
|
operator:
|
||||||
|
type: and
|
||||||
|
query:
|
||||||
|
params:
|
||||||
|
- C
|
||||||
|
reducer:
|
||||||
|
params: []
|
||||||
|
type: last
|
||||||
|
type: query
|
||||||
|
datasource:
|
||||||
|
type: __expr__
|
||||||
|
uid: __expr__
|
||||||
|
expression: A
|
||||||
|
intervalMs: 1000
|
||||||
|
maxDataPoints: 43200
|
||||||
|
refId: C
|
||||||
|
type: threshold
|
||||||
|
dashboardUid: e3a78c36-2f34-4ad6-81d5-284002896829
|
||||||
|
panelId: 32
|
||||||
|
noDataState: OK
|
||||||
|
execErrState: Error
|
||||||
|
for: 10s
|
||||||
|
keepFiringFor: 10s
|
||||||
|
annotations:
|
||||||
|
__dashboardUid__: e3a78c36-2f34-4ad6-81d5-284002896829
|
||||||
|
__panelId__: "32"
|
||||||
|
runbook_url: https://admin.adnova.itqdev.xyz
|
||||||
|
summary: p99 > 500 ms
|
||||||
|
isPaused: false
|
||||||
|
notification_settings:
|
||||||
|
receiver: Telegram
|
||||||
|
- orgId: 1
|
||||||
|
name: Default
|
||||||
|
folder: Postgres
|
||||||
|
interval: 10s
|
||||||
|
rules:
|
||||||
|
- uid: fet1txr4slywwe
|
||||||
|
title: "> 100 QPS on Postgresql"
|
||||||
|
condition: C
|
||||||
|
data:
|
||||||
|
- refId: A
|
||||||
|
relativeTimeRange:
|
||||||
|
from: 600
|
||||||
|
to: 0
|
||||||
|
datasourceUid: prometheus
|
||||||
|
model:
|
||||||
|
editorMode: code
|
||||||
|
expr: |
|
||||||
|
sum(
|
||||||
|
irate(pg_stat_database_xact_commit{datname="postgres",instance="postgres-exporter:9187",job="postgres"}[5m])
|
||||||
|
)
|
||||||
|
+ sum(
|
||||||
|
irate(pg_stat_database_xact_rollback{datname="postgres",instance="postgres-exporter:9187",job="postgres"}[5m])
|
||||||
|
)
|
||||||
|
instant: true
|
||||||
|
intervalMs: 1000
|
||||||
|
legendFormat: __auto
|
||||||
|
maxDataPoints: 43200
|
||||||
|
range: false
|
||||||
|
refId: A
|
||||||
|
- refId: C
|
||||||
|
datasourceUid: __expr__
|
||||||
|
model:
|
||||||
|
conditions:
|
||||||
|
- evaluator:
|
||||||
|
params:
|
||||||
|
- 100
|
||||||
|
type: gte
|
||||||
|
operator:
|
||||||
|
type: and
|
||||||
|
query:
|
||||||
|
params:
|
||||||
|
- C
|
||||||
|
reducer:
|
||||||
|
params: []
|
||||||
|
type: last
|
||||||
|
type: query
|
||||||
|
datasource:
|
||||||
|
type: __expr__
|
||||||
|
uid: __expr__
|
||||||
|
expression: A
|
||||||
|
intervalMs: 1000
|
||||||
|
maxDataPoints: 43200
|
||||||
|
refId: C
|
||||||
|
type: threshold
|
||||||
|
dashboardUid: postgres-overview
|
||||||
|
panelId: 14
|
||||||
|
noDataState: NoData
|
||||||
|
execErrState: Error
|
||||||
|
for: 10s
|
||||||
|
keepFiringFor: 1m
|
||||||
|
annotations:
|
||||||
|
__dashboardUid__: postgres-overview
|
||||||
|
__panelId__: "14"
|
||||||
|
runbook_url: https://admin.adnova.itqdev.xyz
|
||||||
|
summary: Postgresql QPS exceeded 100
|
||||||
|
isPaused: false
|
||||||
|
notification_settings:
|
||||||
|
receiver: Telegram
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
apiVersion: 1
|
||||||
|
|
||||||
|
templates:
|
||||||
|
- orgId: 1
|
||||||
|
name: Telegram
|
||||||
|
template: |
|
||||||
|
{{ define "telegram.default.message" }} {{ if gt (len .Alerts.Firing) 0 }}
|
||||||
|
🔥🚨 *FIRE IN THE HOLE!* 🚨🔥
|
||||||
|
We've got *{{ len .Alerts.Firing }} firing alert(s)* that need your immediate attention!
|
||||||
|
{{ range .Alerts.Firing }}
|
||||||
|
---
|
||||||
|
*Alert:* `{{ .Labels.alertname }}`
|
||||||
|
{{ if .Labels.instance }}*Instance:* `{{ .Labels.instance }}`{{ end }}
|
||||||
|
*Status:* 🔴 *FIRING* since {{ .StartsAt.Format "2006-01-02 15:04:05 MST" }}
|
||||||
|
|
||||||
|
{{ if .Annotations.summary }}*Summary:* {{ .Annotations.summary }}{{ end }}
|
||||||
|
{{ if .Annotations.description }}*Description:* {{ .Annotations.description }}{{ end }}
|
||||||
|
*Labels:*
|
||||||
|
{{ range .Labels.SortedPairs }} • `{{ .Name }}` = `{{ .Value }}`
|
||||||
|
{{ end }}
|
||||||
|
{{ if gt (len .Annotations) 0 }}*Annotations:*
|
||||||
|
{{ range .Annotations.SortedPairs }} • `{{ .Name }}` = `{{ .Value }}`
|
||||||
|
{{ end }}{{ end }}
|
||||||
|
|
||||||
|
{{ if .DashboardURL }}📊 [View Dashboard]({{ .DashboardURL }})
|
||||||
|
{{ end }}{{ if .PanelURL }}📈 [View Panel]({{ .PanelURL }})
|
||||||
|
{{ end }}{{ if .GeneratorURL }}🔗 [Alert Source]({{ .GeneratorURL }})
|
||||||
|
{{ end }}{{ if .SilenceURL }}🤫 [Silence Alert]({{ .SilenceURL }})
|
||||||
|
{{ end }}--- {{ end }} {{ end }}
|
||||||
|
{{ if gt (len .Alerts.Resolved) 0 }}
|
||||||
|
✅🟢 *ALL CLEAR!* 🟢✅
|
||||||
|
Great news! *{{ len .Alerts.Resolved }} alert(s)* have been resolved.
|
||||||
|
{{ range .Alerts.Resolved }}
|
||||||
|
---
|
||||||
|
*Alert:* `{{ .Labels.alertname }}`
|
||||||
|
{{ if .Labels.instance }}*Instance:* `{{ .Labels.instance }}`{{ end }}
|
||||||
|
*Status:* ✅ *RESOLVED* at {{ .EndsAt.Format "2006-01-02 15:04:05 MST" }} (was active since {{ .StartsAt.Format "2006-01-02 15:04:05 MST" }})
|
||||||
|
|
||||||
|
{{ if .Annotations.summary }}*Summary:* {{ .Annotations.summary }}{{ end }}
|
||||||
|
{{ if .Annotations.description }}*Description:* {{ .Annotations.description }}{{ end }}
|
||||||
|
*Labels:*
|
||||||
|
{{ range .Labels.SortedPairs }} • `{{ .Name }}` = `{{ .Value }}`
|
||||||
|
{{ end }}
|
||||||
|
{{ if gt (len .Annotations) 0 }}*Annotations:*
|
||||||
|
{{ range .Annotations.SortedPairs }} • `{{ .Name }}` = `{{ .Value }}`
|
||||||
|
{{ end }}{{ end }}
|
||||||
|
|
||||||
|
{{ if .DashboardURL }}📊 [View Dashboard]({{ .DashboardURL }})
|
||||||
|
{{ end }}{{ if .PanelURL }}📈 [View Panel]({{ .PanelURL }})
|
||||||
|
{{ end }}{{ if .GeneratorURL }}🔗 [Alert Source]({{ .GeneratorURL }}) {{ end }} {{ end }} {{ end }}
|
||||||
|
{{ if or (gt (len .Alerts.Firing) 0) (gt (len .Alerts.Resolved) 0) }}
|
||||||
|
🔔 *Grafana Alertmanager:* [View All Alerts]({{ template "__alertmanagerURL" . }}) 🔔
|
||||||
|
{{ end }}
|
||||||
|
{{ end }}
|
||||||
@@ -0,0 +1,768 @@
|
|||||||
|
{
|
||||||
|
"annotations": {
|
||||||
|
"list": [
|
||||||
|
{
|
||||||
|
"builtIn": 1,
|
||||||
|
"datasource": {
|
||||||
|
"type": "grafana",
|
||||||
|
"uid": "-- Grafana --"
|
||||||
|
},
|
||||||
|
"enable": true,
|
||||||
|
"hide": true,
|
||||||
|
"iconColor": "rgba(0, 211, 255, 1)",
|
||||||
|
"name": "Annotations & Alerts",
|
||||||
|
"type": "dashboard"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"description": "A dashboard that monitors Celery.",
|
||||||
|
"editable": true,
|
||||||
|
"fiscalYearStartMonth": 0,
|
||||||
|
"graphTooltip": 0,
|
||||||
|
"id": 2,
|
||||||
|
"links": [
|
||||||
|
{
|
||||||
|
"tags": ["celery"],
|
||||||
|
"targetBlank": true,
|
||||||
|
"title": "Celery Dashboards",
|
||||||
|
"type": "dashboards"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"liveNow": true,
|
||||||
|
"panels": [
|
||||||
|
{
|
||||||
|
"collapsed": false,
|
||||||
|
"gridPos": {
|
||||||
|
"h": 1,
|
||||||
|
"w": 24,
|
||||||
|
"x": 0,
|
||||||
|
"y": 0
|
||||||
|
},
|
||||||
|
"id": 1,
|
||||||
|
"panels": [],
|
||||||
|
"title": "Tasks",
|
||||||
|
"type": "row"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "datasource",
|
||||||
|
"uid": "-- Mixed --"
|
||||||
|
},
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"custom": {
|
||||||
|
"align": "auto",
|
||||||
|
"cellOptions": {
|
||||||
|
"type": "auto"
|
||||||
|
},
|
||||||
|
"inspect": false
|
||||||
|
},
|
||||||
|
"mappings": [],
|
||||||
|
"noValue": 0,
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"color": "green"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"color": "red",
|
||||||
|
"value": 80
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"unit": "short"
|
||||||
|
},
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"matcher": {
|
||||||
|
"id": "byName",
|
||||||
|
"options": "Success Rate"
|
||||||
|
},
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"id": "unit",
|
||||||
|
"value": "percentunit"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"gridPos": {
|
||||||
|
"h": 8,
|
||||||
|
"w": 16,
|
||||||
|
"x": 0,
|
||||||
|
"y": 1
|
||||||
|
},
|
||||||
|
"id": 2,
|
||||||
|
"options": {
|
||||||
|
"cellHeight": "sm",
|
||||||
|
"footer": {
|
||||||
|
"countRows": false,
|
||||||
|
"enablePagination": true,
|
||||||
|
"fields": "",
|
||||||
|
"reducer": ["sum"],
|
||||||
|
"show": false
|
||||||
|
},
|
||||||
|
"showHeader": true,
|
||||||
|
"sortBy": [
|
||||||
|
{
|
||||||
|
"desc": true,
|
||||||
|
"displayName": "Succeeded"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"pluginVersion": "12.0.2",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "$datasource"
|
||||||
|
},
|
||||||
|
"expr": "sum (\n round(\n increase(\n celery_task_succeeded_total{\n job=\"$job\",\n name=~\"$task\",\n queue_name=~\"$queue_name\"\n }[$__range]\n )\n )\n) by (name)\n/(sum (\n round(\n increase(\n celery_task_succeeded_total{\n job=\"$job\",\n name=~\"$task\",\n queue_name=~\"$queue_name\"\n }[$__range]\n )\n )\n) by (name)\n+sum (\n round(\n increase(\n celery_task_failed_total{\n job=\"$job\",\n name=~\"$task\",\n queue_name=~\"$queue_name\"\n }[$__range]\n )\n )\n) by (name)\n) > -1\n",
|
||||||
|
"format": "table",
|
||||||
|
"instant": true,
|
||||||
|
"refId": "A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "$datasource"
|
||||||
|
},
|
||||||
|
"expr": "sum (\n round(\n increase(\n celery_task_succeeded_total{\n job=\"$job\",\n name=~\"$task\",\n queue_name=~\"$queue_name\"\n }[$__range]\n )\n )\n) by (name) > 0\n",
|
||||||
|
"format": "table",
|
||||||
|
"instant": true,
|
||||||
|
"refId": "B"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "$datasource"
|
||||||
|
},
|
||||||
|
"expr": "sum (\n round(\n increase(\n celery_task_failed_total{\n job=\"$job\",\n name=~\"$task\",\n queue_name=~\"$queue_name\"\n }[$__range]\n )\n )\n) by (name) > 0\n",
|
||||||
|
"format": "table",
|
||||||
|
"instant": true,
|
||||||
|
"refId": "C"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "$datasource"
|
||||||
|
},
|
||||||
|
"expr": "sum (\n round(\n increase(\n celery_task_sent_total{\n job=\"$job\",\n name=~\"$task\",\n queue_name=~\"$queue_name\"\n }[$__range]\n )\n )\n) by (name) > 0\n",
|
||||||
|
"format": "table",
|
||||||
|
"instant": true,
|
||||||
|
"refId": "D"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "$datasource"
|
||||||
|
},
|
||||||
|
"expr": "sum (\n round(\n increase(\n celery_task_received_total{\n job=\"$job\",\n name=~\"$task\",\n queue_name=~\"$queue_name\"\n }[$__range]\n )\n )\n) by (name) > 0\n",
|
||||||
|
"format": "table",
|
||||||
|
"instant": true,
|
||||||
|
"refId": "E"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "$datasource"
|
||||||
|
},
|
||||||
|
"expr": "sum (\n round(\n increase(\n celery_task_rejected_total{\n job=\"$job\",\n name=~\"$task\",\n queue_name=~\"$queue_name\"\n }[$__range]\n )\n )\n) by (name) > 0\n",
|
||||||
|
"format": "table",
|
||||||
|
"instant": true,
|
||||||
|
"refId": "F"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "$datasource"
|
||||||
|
},
|
||||||
|
"expr": "sum (\n round(\n increase(\n celery_task_retried_total{\n job=\"$job\",\n name=~\"$task\",\n queue_name=~\"$queue_name\"\n }[$__range]\n )\n )\n) by (name) > 0\n",
|
||||||
|
"format": "table",
|
||||||
|
"instant": true,
|
||||||
|
"refId": "G"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "$datasource"
|
||||||
|
},
|
||||||
|
"expr": "sum (\n round(\n increase(\n celery_task_revoked_total{\n job=\"$job\",\n name=~\"$task\",\n queue_name=~\"$queue_name\"\n }[$__range]\n )\n )\n) by (name) > 0\n",
|
||||||
|
"format": "table",
|
||||||
|
"instant": true,
|
||||||
|
"refId": "H"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Task Stats",
|
||||||
|
"transformations": [
|
||||||
|
{
|
||||||
|
"id": "merge"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "organize",
|
||||||
|
"options": {
|
||||||
|
"excludeByName": {
|
||||||
|
"Time": true
|
||||||
|
},
|
||||||
|
"indexByName": {
|
||||||
|
"Value #A": 1,
|
||||||
|
"Value #B": 2,
|
||||||
|
"Value #C": 3,
|
||||||
|
"Value #D": 4,
|
||||||
|
"Value #E": 5,
|
||||||
|
"Value #F": 6,
|
||||||
|
"Value #G": 7,
|
||||||
|
"Value #H": 8,
|
||||||
|
"name": 0
|
||||||
|
},
|
||||||
|
"renameByName": {
|
||||||
|
"Value #A": "Success Rate",
|
||||||
|
"Value #B": "Succeeded",
|
||||||
|
"Value #C": "Failed",
|
||||||
|
"Value #D": "Sent",
|
||||||
|
"Value #E": "Received",
|
||||||
|
"Value #F": "Rejected",
|
||||||
|
"Value #G": "Retried",
|
||||||
|
"Value #H": "Revoked",
|
||||||
|
"name": "Name"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "table"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "datasource",
|
||||||
|
"uid": "-- Mixed --"
|
||||||
|
},
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"custom": {
|
||||||
|
"align": "auto",
|
||||||
|
"cellOptions": {
|
||||||
|
"type": "auto"
|
||||||
|
},
|
||||||
|
"inspect": false
|
||||||
|
},
|
||||||
|
"mappings": [],
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"color": "green"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"color": "red",
|
||||||
|
"value": 80
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"unit": "short"
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"gridPos": {
|
||||||
|
"h": 8,
|
||||||
|
"w": 8,
|
||||||
|
"x": 16,
|
||||||
|
"y": 1
|
||||||
|
},
|
||||||
|
"id": 3,
|
||||||
|
"options": {
|
||||||
|
"cellHeight": "sm",
|
||||||
|
"footer": {
|
||||||
|
"countRows": false,
|
||||||
|
"enablePagination": true,
|
||||||
|
"fields": "",
|
||||||
|
"reducer": ["sum"],
|
||||||
|
"show": false
|
||||||
|
},
|
||||||
|
"showHeader": true,
|
||||||
|
"sortBy": [
|
||||||
|
{
|
||||||
|
"desc": true,
|
||||||
|
"displayName": "Value"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"pluginVersion": "12.0.2",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "$datasource"
|
||||||
|
},
|
||||||
|
"expr": "round(\n sum (\n increase(\n celery_task_failed_total{\n job=\"$job\",\n name=~\"$task\",\n queue_name=~\"$queue_name\"\n }[$__range]\n )\n ) by (name, exception) > 0\n)\n",
|
||||||
|
"format": "table",
|
||||||
|
"instant": true,
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Task Exceptions",
|
||||||
|
"transformations": [
|
||||||
|
{
|
||||||
|
"id": "organize",
|
||||||
|
"options": {
|
||||||
|
"excludeByName": {
|
||||||
|
"Time": true,
|
||||||
|
"job": true
|
||||||
|
},
|
||||||
|
"indexByName": {
|
||||||
|
"Value": 2,
|
||||||
|
"exception": 1,
|
||||||
|
"name": 0
|
||||||
|
},
|
||||||
|
"renameByName": {
|
||||||
|
"exception": "Exception",
|
||||||
|
"name": "Task"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "table"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "datasource",
|
||||||
|
"uid": "-- Mixed --"
|
||||||
|
},
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"color": {
|
||||||
|
"mode": "palette-classic"
|
||||||
|
},
|
||||||
|
"custom": {
|
||||||
|
"axisBorderShow": false,
|
||||||
|
"axisCenteredZero": false,
|
||||||
|
"axisColorMode": "text",
|
||||||
|
"axisLabel": "",
|
||||||
|
"axisPlacement": "auto",
|
||||||
|
"barAlignment": 0,
|
||||||
|
"barWidthFactor": 0.6,
|
||||||
|
"drawStyle": "line",
|
||||||
|
"fillOpacity": 0,
|
||||||
|
"gradientMode": "none",
|
||||||
|
"hideFrom": {
|
||||||
|
"legend": false,
|
||||||
|
"tooltip": false,
|
||||||
|
"viz": false
|
||||||
|
},
|
||||||
|
"insertNulls": false,
|
||||||
|
"lineInterpolation": "linear",
|
||||||
|
"lineWidth": 1,
|
||||||
|
"pointSize": 5,
|
||||||
|
"scaleDistribution": {
|
||||||
|
"type": "linear"
|
||||||
|
},
|
||||||
|
"showPoints": "auto",
|
||||||
|
"spanNulls": false,
|
||||||
|
"stacking": {
|
||||||
|
"group": "A",
|
||||||
|
"mode": "none"
|
||||||
|
},
|
||||||
|
"thresholdsStyle": {
|
||||||
|
"mode": "off"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mappings": [],
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"color": "green"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"color": "red",
|
||||||
|
"value": 80
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"unit": "short"
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"gridPos": {
|
||||||
|
"h": 8,
|
||||||
|
"w": 24,
|
||||||
|
"x": 0,
|
||||||
|
"y": 9
|
||||||
|
},
|
||||||
|
"id": 4,
|
||||||
|
"options": {
|
||||||
|
"legend": {
|
||||||
|
"calcs": ["mean", "max"],
|
||||||
|
"displayMode": "table",
|
||||||
|
"placement": "right",
|
||||||
|
"showLegend": true,
|
||||||
|
"sortBy": "Mean",
|
||||||
|
"sortDesc": true
|
||||||
|
},
|
||||||
|
"tooltip": {
|
||||||
|
"hideZeros": false,
|
||||||
|
"mode": "multi",
|
||||||
|
"sort": "desc"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pluginVersion": "12.0.2",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "$datasource"
|
||||||
|
},
|
||||||
|
"expr": "sum (\n round(\n increase(\n celery_task_succeeded_total{\n job=\"$job\",\n name=~\"$task\",\n queue_name=~\"$queue_name\"\n }[$__rate_interval]\n )\n )\n) by (name) > 0\n",
|
||||||
|
"legendFormat": "Succeeded - {{ name }}",
|
||||||
|
"refId": "A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "$datasource"
|
||||||
|
},
|
||||||
|
"expr": "sum (\n round(\n increase(\n celery_task_failed_total{\n job=\"$job\",\n name=~\"$task\",\n queue_name=~\"$queue_name\"\n }[$__rate_interval]\n )\n )\n) by (name) > 0\n",
|
||||||
|
"legendFormat": "Failed - {{ name }}",
|
||||||
|
"refId": "B"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "$datasource"
|
||||||
|
},
|
||||||
|
"expr": "sum (\n round(\n increase(\n celery_task_sent_total{\n job=\"$job\",\n name=~\"$task\",\n queue_name=~\"$queue_name\"\n }[$__rate_interval]\n )\n )\n) by (name) > 0\n",
|
||||||
|
"legendFormat": "Sent - {{ name }}",
|
||||||
|
"refId": "C"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "$datasource"
|
||||||
|
},
|
||||||
|
"expr": "sum (\n round(\n increase(\n celery_task_received_total{\n job=\"$job\",\n name=~\"$task\",\n queue_name=~\"$queue_name\"\n }[$__rate_interval]\n )\n )\n) by (name) > 0\n",
|
||||||
|
"legendFormat": "Received - {{ name }}",
|
||||||
|
"refId": "D"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "$datasource"
|
||||||
|
},
|
||||||
|
"expr": "sum (\n round(\n increase(\n celery_task_retried_total{\n job=\"$job\",\n name=~\"$task\",\n queue_name=~\"$queue_name\"\n }[$__rate_interval]\n )\n )\n) by (name) > 0\n",
|
||||||
|
"legendFormat": "Retried - {{ name }}",
|
||||||
|
"refId": "E"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "$datasource"
|
||||||
|
},
|
||||||
|
"expr": "sum (\n round(\n increase(\n celery_task_revoked_total{\n job=\"$job\",\n name=~\"$task\",\n queue_name=~\"$queue_name\"\n }[$__rate_interval]\n )\n )\n) by (name) > 0\n",
|
||||||
|
"legendFormat": "Revoked - {{ name }}",
|
||||||
|
"refId": "F"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "$datasource"
|
||||||
|
},
|
||||||
|
"expr": "sum (\n round(\n increase(\n celery_task_rejected_total{\n job=\"$job\",\n name=~\"$task\",\n queue_name=~\"$queue_name\"\n }[$__rate_interval]\n )\n )\n) by (name) > 0\n",
|
||||||
|
"legendFormat": "Rejected - {{ name }}",
|
||||||
|
"refId": "G"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Tasks Completed",
|
||||||
|
"type": "timeseries"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "datasource",
|
||||||
|
"uid": "-- Mixed --"
|
||||||
|
},
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"color": {
|
||||||
|
"mode": "palette-classic"
|
||||||
|
},
|
||||||
|
"custom": {
|
||||||
|
"axisBorderShow": false,
|
||||||
|
"axisCenteredZero": false,
|
||||||
|
"axisColorMode": "text",
|
||||||
|
"axisLabel": "",
|
||||||
|
"axisPlacement": "auto",
|
||||||
|
"barAlignment": 0,
|
||||||
|
"barWidthFactor": 0.6,
|
||||||
|
"drawStyle": "line",
|
||||||
|
"fillOpacity": 0,
|
||||||
|
"gradientMode": "none",
|
||||||
|
"hideFrom": {
|
||||||
|
"legend": false,
|
||||||
|
"tooltip": false,
|
||||||
|
"viz": false
|
||||||
|
},
|
||||||
|
"insertNulls": false,
|
||||||
|
"lineInterpolation": "linear",
|
||||||
|
"lineWidth": 1,
|
||||||
|
"pointSize": 5,
|
||||||
|
"scaleDistribution": {
|
||||||
|
"type": "linear"
|
||||||
|
},
|
||||||
|
"showPoints": "auto",
|
||||||
|
"spanNulls": false,
|
||||||
|
"stacking": {
|
||||||
|
"group": "A",
|
||||||
|
"mode": "none"
|
||||||
|
},
|
||||||
|
"thresholdsStyle": {
|
||||||
|
"mode": "off"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mappings": [],
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"color": "green"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"color": "red",
|
||||||
|
"value": 80
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"unit": "short"
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"gridPos": {
|
||||||
|
"h": 8,
|
||||||
|
"w": 24,
|
||||||
|
"x": 0,
|
||||||
|
"y": 17
|
||||||
|
},
|
||||||
|
"id": 5,
|
||||||
|
"options": {
|
||||||
|
"legend": {
|
||||||
|
"calcs": ["mean", "max"],
|
||||||
|
"displayMode": "table",
|
||||||
|
"placement": "right",
|
||||||
|
"showLegend": true,
|
||||||
|
"sortBy": "Mean",
|
||||||
|
"sortDesc": true
|
||||||
|
},
|
||||||
|
"tooltip": {
|
||||||
|
"hideZeros": false,
|
||||||
|
"mode": "multi",
|
||||||
|
"sort": "desc"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pluginVersion": "12.0.2",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "$datasource"
|
||||||
|
},
|
||||||
|
"expr": "sum (\n round(\n increase(\n celery_task_failed_total{\n job=\"$job\",\n name=~\"$task\",\n queue_name=~\"$queue_name\"\n }[$__rate_interval]\n )\n )\n) by (name, exception) > 0\n",
|
||||||
|
"legendFormat": "{{ name }}/{{ exception }}",
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Task Exceptions",
|
||||||
|
"type": "timeseries"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "datasource",
|
||||||
|
"uid": "-- Mixed --"
|
||||||
|
},
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"custom": {
|
||||||
|
"spanNulls": false
|
||||||
|
},
|
||||||
|
"unit": "s"
|
||||||
|
},
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"matcher": {
|
||||||
|
"id": "byName",
|
||||||
|
"options": "P50"
|
||||||
|
},
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"id": "color",
|
||||||
|
"value": {
|
||||||
|
"fixedColor": "green",
|
||||||
|
"mode": "fixed"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matcher": {
|
||||||
|
"id": "byName",
|
||||||
|
"options": "P95"
|
||||||
|
},
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"id": "color",
|
||||||
|
"value": {
|
||||||
|
"fixedColor": "yellow",
|
||||||
|
"mode": "fixed"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matcher": {
|
||||||
|
"id": "byName",
|
||||||
|
"options": "P99"
|
||||||
|
},
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"id": "color",
|
||||||
|
"value": {
|
||||||
|
"fixedColor": "red",
|
||||||
|
"mode": "fixed"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"gridPos": {
|
||||||
|
"h": 8,
|
||||||
|
"w": 24,
|
||||||
|
"x": 0,
|
||||||
|
"y": 25
|
||||||
|
},
|
||||||
|
"id": 6,
|
||||||
|
"options": {
|
||||||
|
"legend": {
|
||||||
|
"calcs": ["mean", "max"],
|
||||||
|
"displayMode": "table",
|
||||||
|
"placement": "right",
|
||||||
|
"showLegend": true,
|
||||||
|
"sortBy": "Mean",
|
||||||
|
"sortDesc": true
|
||||||
|
},
|
||||||
|
"tooltip": {
|
||||||
|
"mode": "multi",
|
||||||
|
"sort": "desc"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pluginVersion": "v11.1.0",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "$datasource"
|
||||||
|
},
|
||||||
|
"expr": "histogram_quantile(0.50,\n sum(\n irate(\n celery_task_runtime_bucket{\n job=\"$job\",\n name=~\"$task\",\n queue_name=~\"$queue_name\"\n }[$__rate_interval]\n ) > 0\n ) by (name, job, le)\n)\n",
|
||||||
|
"legendFormat": "P50 - {{ name }}",
|
||||||
|
"refId": "A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "$datasource"
|
||||||
|
},
|
||||||
|
"expr": "histogram_quantile(0.95,\n sum(\n irate(\n celery_task_runtime_bucket{\n job=\"$job\",\n name=~\"$task\",\n queue_name=~\"$queue_name\"\n }[$__rate_interval]\n ) > 0\n ) by (name, job, le)\n)\n",
|
||||||
|
"legendFormat": "P95 - {{ name }}",
|
||||||
|
"refId": "B"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "$datasource"
|
||||||
|
},
|
||||||
|
"expr": "histogram_quantile(0.99,\n sum(\n irate(\n celery_task_runtime_bucket{\n job=\"$job\",\n name=~\"$task\",\n queue_name=~\"$queue_name\"\n }[$__rate_interval]\n ) > 0\n ) by (name, job, le)\n)\n",
|
||||||
|
"legendFormat": "P99 - {{ name }}",
|
||||||
|
"refId": "C"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Tasks Runtime",
|
||||||
|
"type": "timeseries"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"preload": false,
|
||||||
|
"refresh": "10s",
|
||||||
|
"schemaVersion": 41,
|
||||||
|
"tags": ["celery"],
|
||||||
|
"templating": {
|
||||||
|
"list": [
|
||||||
|
{
|
||||||
|
"label": "Data source",
|
||||||
|
"name": "datasource",
|
||||||
|
"query": "prometheus",
|
||||||
|
"refresh": 1,
|
||||||
|
"type": "datasource"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "${datasource}"
|
||||||
|
},
|
||||||
|
"includeAll": false,
|
||||||
|
"label": "Namespace",
|
||||||
|
"name": "namespace",
|
||||||
|
"query": "label_values(celery_worker_up{}, namespace)",
|
||||||
|
"refresh": 2,
|
||||||
|
"sort": 1,
|
||||||
|
"type": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "${datasource}"
|
||||||
|
},
|
||||||
|
"includeAll": false,
|
||||||
|
"label": "Job",
|
||||||
|
"name": "job",
|
||||||
|
"query": "label_values(celery_worker_up{namespace=\"$namespace\"}, job)",
|
||||||
|
"refresh": 2,
|
||||||
|
"sort": 1,
|
||||||
|
"type": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "${datasource}"
|
||||||
|
},
|
||||||
|
"includeAll": false,
|
||||||
|
"label": "Queue Name",
|
||||||
|
"name": "queue_name",
|
||||||
|
"query": "label_values(celery_task_received_total{namespace=\"$namespace\", job=\"$job\", name!~\"None\"}, queue_name)",
|
||||||
|
"refresh": 2,
|
||||||
|
"sort": 1,
|
||||||
|
"type": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "${datasource}"
|
||||||
|
},
|
||||||
|
"includeAll": false,
|
||||||
|
"label": "Task",
|
||||||
|
"multi": true,
|
||||||
|
"name": "task",
|
||||||
|
"query": "label_values(celery_task_received_total{namespace=\"$namespace\", job=\"$job\", queue_name=~\"$queue_name\", name!~\"None\"}, name)",
|
||||||
|
"refresh": 2,
|
||||||
|
"sort": 1,
|
||||||
|
"type": "query"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"time": {
|
||||||
|
"from": "now-24h",
|
||||||
|
"to": "now"
|
||||||
|
},
|
||||||
|
"timepicker": {},
|
||||||
|
"timezone": "browser",
|
||||||
|
"title": "By Task",
|
||||||
|
"uid": "celery-tasks-by-task",
|
||||||
|
"version": 1,
|
||||||
|
"weekStart": "monday"
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,13 @@
|
|||||||
|
apiVersion: 1
|
||||||
|
|
||||||
|
providers:
|
||||||
|
- name: default
|
||||||
|
orgId: 1
|
||||||
|
folder: ""
|
||||||
|
type: file
|
||||||
|
disableDeletion: false
|
||||||
|
updateIntervalSeconds: 10
|
||||||
|
allowUiUpdates: false
|
||||||
|
options:
|
||||||
|
path: /etc/grafana/provisioning/dashboards
|
||||||
|
foldersFromFilesStructure: true
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
apiVersion: 1
|
||||||
|
prune: true
|
||||||
|
|
||||||
|
datasources:
|
||||||
|
- name: Grafana Loki
|
||||||
|
type: loki
|
||||||
|
access: proxy
|
||||||
|
orgId: 1
|
||||||
|
uid: loki
|
||||||
|
url: http://grafana-loki:3100
|
||||||
|
isDefault: false
|
||||||
|
version: 1
|
||||||
|
editable: false
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
apiVersion: 1
|
||||||
|
prune: true
|
||||||
|
|
||||||
|
datasources:
|
||||||
|
- name: Pyroscope
|
||||||
|
type: grafana-pyroscope-datasource
|
||||||
|
access: proxy
|
||||||
|
orgId: 1
|
||||||
|
uid: pyroscope
|
||||||
|
url: http://grafana-pyroscope:4040
|
||||||
|
isDefault: false
|
||||||
|
version: 1
|
||||||
|
editable: false
|
||||||
|
jsonData:
|
||||||
|
minStep: 15s
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
apiVersion: 1
|
||||||
|
prune: true
|
||||||
|
|
||||||
|
datasources:
|
||||||
|
- name: Grafana Tempo
|
||||||
|
type: tempo
|
||||||
|
access: proxy
|
||||||
|
orgId: 1
|
||||||
|
uid: tempo
|
||||||
|
url: http://grafana-tempo:3200
|
||||||
|
isDefault: false
|
||||||
|
version: 1
|
||||||
|
editable: false
|
||||||
+19
@@ -0,0 +1,19 @@
|
|||||||
|
apiVersion: 1
|
||||||
|
prune: true
|
||||||
|
|
||||||
|
datasources:
|
||||||
|
- name: VictoriaMetrics
|
||||||
|
type: prometheus
|
||||||
|
access: proxy
|
||||||
|
orgId: 1
|
||||||
|
uid: victoriametrics
|
||||||
|
url: http://victoriametrics:8428
|
||||||
|
basicAuth: false
|
||||||
|
basicAuthUser: ""
|
||||||
|
withCredentials: false
|
||||||
|
isDefault: true
|
||||||
|
editable: false
|
||||||
|
jsonData:
|
||||||
|
httpMethod: POST
|
||||||
|
queryTimeout: 10s
|
||||||
|
timeInterval": 10s
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
apiVersion: 1
|
||||||
|
|
||||||
|
apps:
|
||||||
|
- type: yesoreyeram-infinity-datasource
|
||||||
|
orgId: 1
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "Installing Grafana plugins..."
|
||||||
|
grafana cli plugins install yesoreyeram-infinity-datasource
|
||||||
|
|
||||||
|
echo "Starting Grafana..."
|
||||||
|
exec /run.sh
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
KAFKA_NODE_ID=0
|
||||||
|
KAFKA_PROCESS_ROLES=controller,broker
|
||||||
|
KAFKA_CONTROLLER_QUORUM_VOTERS=0@127.0.0.1:9094
|
||||||
|
KAFKA_LISTENERS=PLAINTEXT://:9093,EXTERNAL://:9092,CONTROLLER://:9094
|
||||||
|
KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://kafka:9093,EXTERNAL://localhost:9092
|
||||||
|
KAFKA_CONTROLLER_LISTENER_NAMES=CONTROLLER
|
||||||
|
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP=PLAINTEXT:PLAINTEXT,EXTERNAL:PLAINTEXT,CONTROLLER:PLAINTEXT
|
||||||
|
KAFKA_AUTO_CREATE_TOPICS_ENABLE=true
|
||||||
|
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR=1
|
||||||
|
CLUSTER_ID=q1Sh-9_ISia_zwGINzRvyQ
|
||||||
|
|
||||||
|
KAFKA_TOPIC=default
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
receivers:
|
||||||
|
otlp:
|
||||||
|
protocols:
|
||||||
|
grpc:
|
||||||
|
endpoint: 0.0.0.0:4317
|
||||||
|
http:
|
||||||
|
endpoint: 0.0.0.0:4318
|
||||||
|
|
||||||
|
processors:
|
||||||
|
batch:
|
||||||
|
timeout: 1s
|
||||||
|
send_batch_size: 1024
|
||||||
|
|
||||||
|
exporters:
|
||||||
|
otlphttp/loki:
|
||||||
|
endpoint: http://grafana-loki:3100/otlp
|
||||||
|
tls:
|
||||||
|
insecure: true
|
||||||
|
otlphttp/tempo:
|
||||||
|
endpoint: http://grafana-tempo:4318
|
||||||
|
tls:
|
||||||
|
insecure: true
|
||||||
|
prometheusremotewrite:
|
||||||
|
endpoint: http://victoriametrics:8428/api/v1/write
|
||||||
|
tls:
|
||||||
|
insecure: true
|
||||||
|
|
||||||
|
extensions:
|
||||||
|
health_check:
|
||||||
|
endpoint: 0.0.0.0:13133
|
||||||
|
path: /health
|
||||||
|
response_body:
|
||||||
|
healthy: '{"status": "healthy"}'
|
||||||
|
unhealthy: '{"status": "unhealthy"}'
|
||||||
|
pprof:
|
||||||
|
endpoint: 0.0.0.0:1777
|
||||||
|
zpages:
|
||||||
|
endpoint: 0.0.0.0:55679
|
||||||
|
|
||||||
|
service:
|
||||||
|
extensions: [health_check, pprof, zpages]
|
||||||
|
|
||||||
|
pipelines:
|
||||||
|
traces:
|
||||||
|
receivers: [otlp]
|
||||||
|
processors: [batch]
|
||||||
|
exporters: [otlphttp/tempo]
|
||||||
|
logs:
|
||||||
|
receivers: [otlp]
|
||||||
|
processors: [batch]
|
||||||
|
exporters: [otlphttp/loki]
|
||||||
|
metrics:
|
||||||
|
receivers: [otlp]
|
||||||
|
processors: [batch]
|
||||||
|
exporters: [prometheusremotewrite]
|
||||||
|
|
||||||
|
telemetry:
|
||||||
|
metrics:
|
||||||
|
level: detailed
|
||||||
|
readers:
|
||||||
|
- pull:
|
||||||
|
exporter:
|
||||||
|
prometheus:
|
||||||
|
host: 0.0.0.0
|
||||||
|
port: 8888
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
PGADMIN_DEFAULT_EMAIL=admin@mail.com
|
||||||
|
PGADMIN_DEFAULT_PASSWORD=password
|
||||||
|
PGADMIN_DISABLE_POSTFIX=True
|
||||||
|
PGADMIN_REPLACE_SERVERS_ON_STARTUP=True
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"Servers": {
|
||||||
|
"1": {
|
||||||
|
"Name": "default",
|
||||||
|
"Group": "Servers",
|
||||||
|
"Host": "postgresql",
|
||||||
|
"Port": 5432,
|
||||||
|
"MaintenanceDB": "postgres",
|
||||||
|
"Username": "postgres",
|
||||||
|
"KerberosAuthentication": false,
|
||||||
|
"ConnectionParameters": {
|
||||||
|
"sslmode": "prefer",
|
||||||
|
"connect_timeout": 10
|
||||||
|
},
|
||||||
|
"Tags": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
DATA_SOURCE_URI=postgresql:5432/postgres?sslmode=disable
|
||||||
|
DATA_SOURCE_USER=postgres
|
||||||
|
DATA_SOURCE_PASS=postgres
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
POSTGRES_DB=postgres
|
||||||
|
POSTGRES_USER=postgres
|
||||||
|
POSTGRES_PASSWORD=postgres
|
||||||
@@ -0,0 +1,890 @@
|
|||||||
|
# -----------------------------
|
||||||
|
# PostgreSQL configuration file
|
||||||
|
# -----------------------------
|
||||||
|
#
|
||||||
|
# This file consists of lines of the form:
|
||||||
|
#
|
||||||
|
# name = value
|
||||||
|
#
|
||||||
|
# (The "=" is optional.) Whitespace may be used. Comments are introduced with
|
||||||
|
# "#" anywhere on a line. The complete list of parameter names and allowed
|
||||||
|
# values can be found in the PostgreSQL documentation.
|
||||||
|
#
|
||||||
|
# The commented-out settings shown in this file represent the default values.
|
||||||
|
# Re-commenting a setting is NOT sufficient to revert it to the default value;
|
||||||
|
# you need to reload the server.
|
||||||
|
#
|
||||||
|
# This file is read on server startup and when the server receives a SIGHUP
|
||||||
|
# signal. If you edit the file on a running system, you have to SIGHUP the
|
||||||
|
# server for the changes to take effect, run "pg_ctl reload", or execute
|
||||||
|
# "SELECT pg_reload_conf()". Some parameters, which are marked below,
|
||||||
|
# require a server shutdown and restart to take effect.
|
||||||
|
#
|
||||||
|
# Any parameter can also be given as a command-line option to the server, e.g.,
|
||||||
|
# "postgres -c log_connections=all". Some parameters can be changed at run time
|
||||||
|
# with the "SET" SQL command.
|
||||||
|
#
|
||||||
|
# Memory units: B = bytes Time units: us = microseconds
|
||||||
|
# kB = kilobytes ms = milliseconds
|
||||||
|
# MB = megabytes s = seconds
|
||||||
|
# GB = gigabytes min = minutes
|
||||||
|
# TB = terabytes h = hours
|
||||||
|
# d = days
|
||||||
|
|
||||||
|
|
||||||
|
#------------------------------------------------------------------------------
|
||||||
|
# FILE LOCATIONS
|
||||||
|
#------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# The default values of these variables are driven from the -D command-line
|
||||||
|
# option or PGDATA environment variable, represented here as ConfigDir.
|
||||||
|
|
||||||
|
#data_directory = 'ConfigDir' # use data in another directory
|
||||||
|
# (change requires restart)
|
||||||
|
#hba_file = 'ConfigDir/pg_hba.conf' # host-based authentication file
|
||||||
|
# (change requires restart)
|
||||||
|
#ident_file = 'ConfigDir/pg_ident.conf' # ident configuration file
|
||||||
|
# (change requires restart)
|
||||||
|
|
||||||
|
# If external_pid_file is not explicitly set, no extra PID file is written.
|
||||||
|
#external_pid_file = '' # write an extra PID file
|
||||||
|
# (change requires restart)
|
||||||
|
|
||||||
|
|
||||||
|
#------------------------------------------------------------------------------
|
||||||
|
# CONNECTIONS AND AUTHENTICATION
|
||||||
|
#------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# - Connection Settings -
|
||||||
|
|
||||||
|
#listen_addresses = 'localhost' # what IP address(es) to listen on;
|
||||||
|
# comma-separated list of addresses;
|
||||||
|
# defaults to 'localhost'; use '*' for all
|
||||||
|
# (change requires restart)
|
||||||
|
#port = 5432 # (change requires restart)
|
||||||
|
#max_connections = 100 # (change requires restart)
|
||||||
|
#reserved_connections = 0 # (change requires restart)
|
||||||
|
#superuser_reserved_connections = 3 # (change requires restart)
|
||||||
|
#unix_socket_directories = '/tmp' # comma-separated list of directories
|
||||||
|
# (change requires restart)
|
||||||
|
#unix_socket_group = '' # (change requires restart)
|
||||||
|
#unix_socket_permissions = 0777 # begin with 0 to use octal notation
|
||||||
|
# (change requires restart)
|
||||||
|
#bonjour = off # advertise server via Bonjour
|
||||||
|
# (change requires restart)
|
||||||
|
#bonjour_name = '' # defaults to the computer name
|
||||||
|
# (change requires restart)
|
||||||
|
|
||||||
|
# - TCP settings -
|
||||||
|
# see "man tcp" for details
|
||||||
|
|
||||||
|
#tcp_keepalives_idle = 0 # TCP_KEEPIDLE, in seconds;
|
||||||
|
# 0 selects the system default
|
||||||
|
#tcp_keepalives_interval = 0 # TCP_KEEPINTVL, in seconds;
|
||||||
|
# 0 selects the system default
|
||||||
|
#tcp_keepalives_count = 0 # TCP_KEEPCNT;
|
||||||
|
# 0 selects the system default
|
||||||
|
#tcp_user_timeout = 0 # TCP_USER_TIMEOUT, in milliseconds;
|
||||||
|
# 0 selects the system default
|
||||||
|
|
||||||
|
#client_connection_check_interval = 0 # time between checks for client
|
||||||
|
# disconnection while running queries;
|
||||||
|
# 0 for never
|
||||||
|
|
||||||
|
# - Authentication -
|
||||||
|
|
||||||
|
#authentication_timeout = 1min # 1s-600s
|
||||||
|
#password_encryption = scram-sha-256 # scram-sha-256 or (deprecated) md5
|
||||||
|
#scram_iterations = 4096
|
||||||
|
#md5_password_warnings = on # display md5 deprecation warnings?
|
||||||
|
#oauth_validator_libraries = '' # comma-separated list of trusted validator modules
|
||||||
|
|
||||||
|
# GSSAPI using Kerberos
|
||||||
|
#krb_server_keyfile = 'FILE:${sysconfdir}/krb5.keytab'
|
||||||
|
#krb_caseins_users = off
|
||||||
|
#gss_accept_delegation = off
|
||||||
|
|
||||||
|
# - SSL -
|
||||||
|
|
||||||
|
#ssl = off
|
||||||
|
#ssl_ca_file = ''
|
||||||
|
#ssl_cert_file = 'server.crt'
|
||||||
|
#ssl_crl_file = ''
|
||||||
|
#ssl_crl_dir = ''
|
||||||
|
#ssl_key_file = 'server.key'
|
||||||
|
#ssl_ciphers = 'HIGH:MEDIUM:+3DES:!aNULL' # allowed TLSv1.2 ciphers
|
||||||
|
#ssl_tls13_ciphers = '' # allowed TLSv1.3 cipher suites, blank for default
|
||||||
|
#ssl_prefer_server_ciphers = on
|
||||||
|
#ssl_groups = 'X25519:prime256v1'
|
||||||
|
#ssl_min_protocol_version = 'TLSv1.2'
|
||||||
|
#ssl_max_protocol_version = ''
|
||||||
|
#ssl_dh_params_file = ''
|
||||||
|
#ssl_passphrase_command = ''
|
||||||
|
#ssl_passphrase_command_supports_reload = off
|
||||||
|
|
||||||
|
|
||||||
|
#------------------------------------------------------------------------------
|
||||||
|
# RESOURCE USAGE (except WAL)
|
||||||
|
#------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# - Memory -
|
||||||
|
|
||||||
|
#shared_buffers = 128MB # min 128kB
|
||||||
|
# (change requires restart)
|
||||||
|
#huge_pages = try # on, off, or try
|
||||||
|
# (change requires restart)
|
||||||
|
#huge_page_size = 0 # zero for system default
|
||||||
|
# (change requires restart)
|
||||||
|
#temp_buffers = 8MB # min 800kB
|
||||||
|
#max_prepared_transactions = 0 # zero disables the feature
|
||||||
|
# (change requires restart)
|
||||||
|
# Caution: it is not advisable to set max_prepared_transactions nonzero unless
|
||||||
|
# you actively intend to use prepared transactions.
|
||||||
|
#work_mem = 4MB # min 64kB
|
||||||
|
#hash_mem_multiplier = 2.0 # 1-1000.0 multiplier on hash table work_mem
|
||||||
|
#maintenance_work_mem = 64MB # min 64kB
|
||||||
|
#autovacuum_work_mem = -1 # min 64kB, or -1 to use maintenance_work_mem
|
||||||
|
#logical_decoding_work_mem = 64MB # min 64kB
|
||||||
|
#max_stack_depth = 2MB # min 100kB
|
||||||
|
#shared_memory_type = mmap # the default is the first option
|
||||||
|
# supported by the operating system:
|
||||||
|
# mmap
|
||||||
|
# sysv
|
||||||
|
# windows
|
||||||
|
# (change requires restart)
|
||||||
|
#dynamic_shared_memory_type = posix # the default is usually the first option
|
||||||
|
# supported by the operating system:
|
||||||
|
# posix
|
||||||
|
# sysv
|
||||||
|
# windows
|
||||||
|
# mmap
|
||||||
|
# (change requires restart)
|
||||||
|
#min_dynamic_shared_memory = 0MB # (change requires restart)
|
||||||
|
#vacuum_buffer_usage_limit = 2MB # size of vacuum and analyze buffer access strategy ring;
|
||||||
|
# 0 to disable vacuum buffer access strategy;
|
||||||
|
# range 128kB to 16GB
|
||||||
|
|
||||||
|
# SLRU buffers (change requires restart)
|
||||||
|
#commit_timestamp_buffers = 0 # memory for pg_commit_ts (0 = auto)
|
||||||
|
#multixact_offset_buffers = 16 # memory for pg_multixact/offsets
|
||||||
|
#multixact_member_buffers = 32 # memory for pg_multixact/members
|
||||||
|
#notify_buffers = 16 # memory for pg_notify
|
||||||
|
#serializable_buffers = 32 # memory for pg_serial
|
||||||
|
#subtransaction_buffers = 0 # memory for pg_subtrans (0 = auto)
|
||||||
|
#transaction_buffers = 0 # memory for pg_xact (0 = auto)
|
||||||
|
|
||||||
|
# - Disk -
|
||||||
|
|
||||||
|
#temp_file_limit = -1 # limits per-process temp file space
|
||||||
|
# in kilobytes, or -1 for no limit
|
||||||
|
|
||||||
|
#file_copy_method = copy # copy, clone (if supported by OS)
|
||||||
|
|
||||||
|
#max_notify_queue_pages = 1048576 # limits the number of SLRU pages allocated
|
||||||
|
# for NOTIFY / LISTEN queue
|
||||||
|
|
||||||
|
# - Kernel Resources -
|
||||||
|
|
||||||
|
#max_files_per_process = 1000 # min 64
|
||||||
|
# (change requires restart)
|
||||||
|
|
||||||
|
# - Background Writer -
|
||||||
|
|
||||||
|
#bgwriter_delay = 200ms # 10-10000ms between rounds
|
||||||
|
#bgwriter_lru_maxpages = 100 # max buffers written/round, 0 disables
|
||||||
|
#bgwriter_lru_multiplier = 2.0 # 0-10.0 multiplier on buffers scanned/round
|
||||||
|
#bgwriter_flush_after = 0 # measured in pages, 0 disables
|
||||||
|
|
||||||
|
# - I/O -
|
||||||
|
|
||||||
|
#backend_flush_after = 0 # measured in pages, 0 disables
|
||||||
|
#effective_io_concurrency = 16 # 1-1000; 0 disables issuing multiple simultaneous IO requests
|
||||||
|
#maintenance_io_concurrency = 16 # 1-1000; same as effective_io_concurrency
|
||||||
|
#io_max_combine_limit = 128kB # usually 1-128 blocks (depends on OS)
|
||||||
|
# (change requires restart)
|
||||||
|
#io_combine_limit = 128kB # usually 1-128 blocks (depends on OS)
|
||||||
|
|
||||||
|
#io_method = worker # worker, io_uring, sync
|
||||||
|
# (change requires restart)
|
||||||
|
#io_max_concurrency = -1 # Max number of IOs that one process
|
||||||
|
# can execute simultaneously
|
||||||
|
# -1 sets based on shared_buffers
|
||||||
|
# (change requires restart)
|
||||||
|
#io_workers = 3 # 1-32;
|
||||||
|
|
||||||
|
# - Worker Processes -
|
||||||
|
|
||||||
|
#max_worker_processes = 8 # (change requires restart)
|
||||||
|
#max_parallel_workers_per_gather = 2 # limited by max_parallel_workers
|
||||||
|
#max_parallel_maintenance_workers = 2 # limited by max_parallel_workers
|
||||||
|
#max_parallel_workers = 8 # number of max_worker_processes that
|
||||||
|
# can be used in parallel operations
|
||||||
|
#parallel_leader_participation = on
|
||||||
|
|
||||||
|
|
||||||
|
#------------------------------------------------------------------------------
|
||||||
|
# WRITE-AHEAD LOG
|
||||||
|
#------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# - Settings -
|
||||||
|
|
||||||
|
#wal_level = replica # minimal, replica, or logical
|
||||||
|
# (change requires restart)
|
||||||
|
#fsync = on # flush data to disk for crash safety
|
||||||
|
# (turning this off can cause
|
||||||
|
# unrecoverable data corruption)
|
||||||
|
#synchronous_commit = on # synchronization level;
|
||||||
|
# off, local, remote_write, remote_apply, or on
|
||||||
|
#wal_sync_method = fsync # the default is the first option
|
||||||
|
# supported by the operating system:
|
||||||
|
# open_datasync
|
||||||
|
# fdatasync (default on Linux and FreeBSD)
|
||||||
|
# fsync
|
||||||
|
# fsync_writethrough
|
||||||
|
# open_sync
|
||||||
|
#full_page_writes = on # recover from partial page writes
|
||||||
|
#wal_log_hints = off # also do full page writes of non-critical updates
|
||||||
|
# (change requires restart)
|
||||||
|
#wal_compression = off # enables compression of full-page writes;
|
||||||
|
# off, pglz, lz4, zstd, or on
|
||||||
|
#wal_init_zero = on # zero-fill new WAL files
|
||||||
|
#wal_recycle = on # recycle WAL files
|
||||||
|
#wal_buffers = -1 # min 32kB, -1 sets based on shared_buffers
|
||||||
|
# (change requires restart)
|
||||||
|
#wal_writer_delay = 200ms # 1-10000 milliseconds
|
||||||
|
#wal_writer_flush_after = 1MB # measured in pages, 0 disables
|
||||||
|
#wal_skip_threshold = 2MB
|
||||||
|
|
||||||
|
#commit_delay = 0 # range 0-100000, in microseconds
|
||||||
|
#commit_siblings = 5 # range 0-1000
|
||||||
|
|
||||||
|
# - Checkpoints -
|
||||||
|
|
||||||
|
#checkpoint_timeout = 5min # range 30s-1d
|
||||||
|
#checkpoint_completion_target = 0.9 # checkpoint target duration, 0.0 - 1.0
|
||||||
|
#checkpoint_flush_after = 0 # measured in pages, 0 disables
|
||||||
|
#checkpoint_warning = 30s # 0 disables
|
||||||
|
#max_wal_size = 1GB
|
||||||
|
#min_wal_size = 80MB
|
||||||
|
|
||||||
|
# - Prefetching during recovery -
|
||||||
|
|
||||||
|
#recovery_prefetch = try # prefetch pages referenced in the WAL?
|
||||||
|
#wal_decode_buffer_size = 512kB # lookahead window used for prefetching
|
||||||
|
# (change requires restart)
|
||||||
|
|
||||||
|
# - Archiving -
|
||||||
|
|
||||||
|
#archive_mode = off # enables archiving; off, on, or always
|
||||||
|
# (change requires restart)
|
||||||
|
#archive_library = '' # library to use to archive a WAL file
|
||||||
|
# (empty string indicates archive_command should
|
||||||
|
# be used)
|
||||||
|
#archive_command = '' # command to use to archive a WAL file
|
||||||
|
# placeholders: %p = path of file to archive
|
||||||
|
# %f = file name only
|
||||||
|
# e.g. 'test ! -f "/mnt/server/archivedir/%f" && cp "%p" "/mnt/server/archivedir/%f"'
|
||||||
|
#archive_timeout = 0 # force a WAL file switch after this
|
||||||
|
# number of seconds; 0 disables
|
||||||
|
|
||||||
|
# - Archive Recovery -
|
||||||
|
|
||||||
|
# These are only used in recovery mode.
|
||||||
|
|
||||||
|
#restore_command = '' # command to use to restore an archived WAL file
|
||||||
|
# placeholders: %p = path of file to restore
|
||||||
|
# %f = file name only
|
||||||
|
# e.g. 'cp "/mnt/server/archivedir/%f" "%p"'
|
||||||
|
#archive_cleanup_command = '' # command to execute at every restartpoint
|
||||||
|
#recovery_end_command = '' # command to execute at completion of recovery
|
||||||
|
|
||||||
|
# - Recovery Target -
|
||||||
|
|
||||||
|
# Set these only when performing a targeted recovery.
|
||||||
|
|
||||||
|
#recovery_target = '' # 'immediate' to end recovery as soon as a
|
||||||
|
# consistent state is reached
|
||||||
|
# (change requires restart)
|
||||||
|
#recovery_target_name = '' # the named restore point to which recovery will proceed
|
||||||
|
# (change requires restart)
|
||||||
|
#recovery_target_time = '' # the time stamp up to which recovery will proceed
|
||||||
|
# (change requires restart)
|
||||||
|
#recovery_target_xid = '' # the transaction ID up to which recovery will proceed
|
||||||
|
# (change requires restart)
|
||||||
|
#recovery_target_lsn = '' # the WAL LSN up to which recovery will proceed
|
||||||
|
# (change requires restart)
|
||||||
|
#recovery_target_inclusive = on # Specifies whether to stop:
|
||||||
|
# just after the specified recovery target (on)
|
||||||
|
# just before the recovery target (off)
|
||||||
|
# (change requires restart)
|
||||||
|
#recovery_target_timeline = 'latest' # 'current', 'latest', or timeline ID
|
||||||
|
# (change requires restart)
|
||||||
|
#recovery_target_action = 'pause' # 'pause', 'promote', 'shutdown'
|
||||||
|
# (change requires restart)
|
||||||
|
|
||||||
|
# - WAL Summarization -
|
||||||
|
|
||||||
|
#summarize_wal = off # run WAL summarizer process?
|
||||||
|
#wal_summary_keep_time = '10d' # when to remove old summary files, 0 = never
|
||||||
|
|
||||||
|
|
||||||
|
#------------------------------------------------------------------------------
|
||||||
|
# REPLICATION
|
||||||
|
#------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# - Sending Servers -
|
||||||
|
|
||||||
|
# Set these on the primary and on any standby that will send replication data.
|
||||||
|
|
||||||
|
#max_wal_senders = 10 # max number of walsender processes
|
||||||
|
# (change requires restart)
|
||||||
|
#max_replication_slots = 10 # max number of replication slots
|
||||||
|
# (change requires restart)
|
||||||
|
#wal_keep_size = 0 # in megabytes; 0 disables
|
||||||
|
#max_slot_wal_keep_size = -1 # in megabytes; -1 disables
|
||||||
|
#idle_replication_slot_timeout = 0 # in seconds; 0 disables
|
||||||
|
#wal_sender_timeout = 60s # in milliseconds; 0 disables
|
||||||
|
#track_commit_timestamp = off # collect timestamp of transaction commit
|
||||||
|
# (change requires restart)
|
||||||
|
|
||||||
|
# - Primary Server -
|
||||||
|
|
||||||
|
# These settings are ignored on a standby server.
|
||||||
|
|
||||||
|
#synchronous_standby_names = '' # standby servers that provide sync rep
|
||||||
|
# method to choose sync standbys, number of sync standbys,
|
||||||
|
# and comma-separated list of application_name
|
||||||
|
# from standby(s); '*' = all
|
||||||
|
#synchronized_standby_slots = '' # streaming replication standby server slot
|
||||||
|
# names that logical walsender processes will wait for
|
||||||
|
|
||||||
|
# - Standby Servers -
|
||||||
|
|
||||||
|
# These settings are ignored on a primary server.
|
||||||
|
|
||||||
|
#primary_conninfo = '' # connection string to sending server
|
||||||
|
#primary_slot_name = '' # replication slot on sending server
|
||||||
|
#hot_standby = on # "off" disallows queries during recovery
|
||||||
|
# (change requires restart)
|
||||||
|
#max_standby_archive_delay = 30s # max delay before canceling queries
|
||||||
|
# when reading WAL from archive;
|
||||||
|
# -1 allows indefinite delay
|
||||||
|
#max_standby_streaming_delay = 30s # max delay before canceling queries
|
||||||
|
# when reading streaming WAL;
|
||||||
|
# -1 allows indefinite delay
|
||||||
|
#wal_receiver_create_temp_slot = off # create temp slot if primary_slot_name
|
||||||
|
# is not set
|
||||||
|
#wal_receiver_status_interval = 10s # send replies at least this often
|
||||||
|
# 0 disables
|
||||||
|
#hot_standby_feedback = off # send info from standby to prevent
|
||||||
|
# query conflicts
|
||||||
|
#wal_receiver_timeout = 60s # time that receiver waits for
|
||||||
|
# communication from primary
|
||||||
|
# in milliseconds; 0 disables
|
||||||
|
#wal_retrieve_retry_interval = 5s # time to wait before retrying to
|
||||||
|
# retrieve WAL after a failed attempt
|
||||||
|
#recovery_min_apply_delay = 0 # minimum delay for applying changes during recovery
|
||||||
|
#sync_replication_slots = off # enables slot synchronization on the physical standby from the primary
|
||||||
|
|
||||||
|
# - Subscribers -
|
||||||
|
|
||||||
|
# These settings are ignored on a publisher.
|
||||||
|
|
||||||
|
#max_active_replication_origins = 10 # max number of active replication origins
|
||||||
|
# (change requires restart)
|
||||||
|
#max_logical_replication_workers = 4 # taken from max_worker_processes
|
||||||
|
# (change requires restart)
|
||||||
|
#max_sync_workers_per_subscription = 2 # taken from max_logical_replication_workers
|
||||||
|
#max_parallel_apply_workers_per_subscription = 2 # taken from max_logical_replication_workers
|
||||||
|
|
||||||
|
|
||||||
|
#------------------------------------------------------------------------------
|
||||||
|
# QUERY TUNING
|
||||||
|
#------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# - Planner Method Configuration -
|
||||||
|
|
||||||
|
#enable_async_append = on
|
||||||
|
#enable_bitmapscan = on
|
||||||
|
#enable_gathermerge = on
|
||||||
|
#enable_hashagg = on
|
||||||
|
#enable_hashjoin = on
|
||||||
|
#enable_incremental_sort = on
|
||||||
|
#enable_indexscan = on
|
||||||
|
#enable_indexonlyscan = on
|
||||||
|
#enable_material = on
|
||||||
|
#enable_memoize = on
|
||||||
|
#enable_mergejoin = on
|
||||||
|
#enable_nestloop = on
|
||||||
|
#enable_parallel_append = on
|
||||||
|
#enable_parallel_hash = on
|
||||||
|
#enable_partition_pruning = on
|
||||||
|
#enable_partitionwise_join = off
|
||||||
|
#enable_partitionwise_aggregate = off
|
||||||
|
#enable_presorted_aggregate = on
|
||||||
|
#enable_seqscan = on
|
||||||
|
#enable_sort = on
|
||||||
|
#enable_tidscan = on
|
||||||
|
#enable_group_by_reordering = on
|
||||||
|
#enable_distinct_reordering = on
|
||||||
|
#enable_self_join_elimination = on
|
||||||
|
#enable_eager_aggregate = on
|
||||||
|
|
||||||
|
# - Planner Cost Constants -
|
||||||
|
|
||||||
|
#seq_page_cost = 1.0 # measured on an arbitrary scale
|
||||||
|
#random_page_cost = 4.0 # same scale as above
|
||||||
|
#cpu_tuple_cost = 0.01 # same scale as above
|
||||||
|
#cpu_index_tuple_cost = 0.005 # same scale as above
|
||||||
|
#cpu_operator_cost = 0.0025 # same scale as above
|
||||||
|
#parallel_setup_cost = 1000.0 # same scale as above
|
||||||
|
#parallel_tuple_cost = 0.1 # same scale as above
|
||||||
|
#min_parallel_table_scan_size = 8MB
|
||||||
|
#min_parallel_index_scan_size = 512kB
|
||||||
|
#effective_cache_size = 4GB
|
||||||
|
#min_eager_agg_group_size = 8.0
|
||||||
|
|
||||||
|
#jit_above_cost = 100000 # perform JIT compilation if available
|
||||||
|
# and query more expensive than this;
|
||||||
|
# -1 disables
|
||||||
|
#jit_inline_above_cost = 500000 # inline small functions if query is
|
||||||
|
# more expensive than this; -1 disables
|
||||||
|
#jit_optimize_above_cost = 500000 # use expensive JIT optimizations if
|
||||||
|
# query is more expensive than this;
|
||||||
|
# -1 disables
|
||||||
|
|
||||||
|
# - Genetic Query Optimizer -
|
||||||
|
|
||||||
|
#geqo = on
|
||||||
|
#geqo_threshold = 12
|
||||||
|
#geqo_effort = 5 # range 1-10
|
||||||
|
#geqo_pool_size = 0 # selects default based on effort
|
||||||
|
#geqo_generations = 0 # selects default based on effort
|
||||||
|
#geqo_selection_bias = 2.0 # range 1.5-2.0
|
||||||
|
#geqo_seed = 0.0 # range 0.0-1.0
|
||||||
|
|
||||||
|
# - Other Planner Options -
|
||||||
|
|
||||||
|
#default_statistics_target = 100 # range 1-10000
|
||||||
|
#constraint_exclusion = partition # on, off, or partition
|
||||||
|
#cursor_tuple_fraction = 0.1 # range 0.0-1.0
|
||||||
|
#from_collapse_limit = 8
|
||||||
|
#jit = on # allow JIT compilation
|
||||||
|
#join_collapse_limit = 8 # 1 disables collapsing of explicit
|
||||||
|
# JOIN clauses
|
||||||
|
#plan_cache_mode = auto # auto, force_generic_plan or
|
||||||
|
# force_custom_plan
|
||||||
|
#recursive_worktable_factor = 10.0 # range 0.001-1000000
|
||||||
|
|
||||||
|
|
||||||
|
#------------------------------------------------------------------------------
|
||||||
|
# REPORTING AND LOGGING
|
||||||
|
#------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# - Where to Log -
|
||||||
|
|
||||||
|
#log_destination = 'stderr' # Valid values are combinations of
|
||||||
|
# stderr, csvlog, jsonlog, syslog, and
|
||||||
|
# eventlog, depending on platform.
|
||||||
|
# csvlog and jsonlog require
|
||||||
|
# logging_collector to be on.
|
||||||
|
|
||||||
|
# This is used when logging to stderr:
|
||||||
|
#logging_collector = off # Enable capturing of stderr, jsonlog,
|
||||||
|
# and csvlog into log files. Required
|
||||||
|
# to be on for csvlogs and jsonlogs.
|
||||||
|
# (change requires restart)
|
||||||
|
|
||||||
|
# These are only used if logging_collector is on:
|
||||||
|
#log_directory = 'log' # directory where log files are written,
|
||||||
|
# can be absolute or relative to PGDATA
|
||||||
|
#log_filename = 'postgresql-%Y-%m-%d_%H%M%S.log' # log file name pattern,
|
||||||
|
# can include strftime() escapes
|
||||||
|
#log_file_mode = 0600 # creation mode for log files,
|
||||||
|
# begin with 0 to use octal notation
|
||||||
|
#log_rotation_age = 1d # Automatic rotation of logfiles will
|
||||||
|
# happen after that time. 0 disables.
|
||||||
|
#log_rotation_size = 10MB # Automatic rotation of logfiles will
|
||||||
|
# happen after that much log output.
|
||||||
|
# 0 disables.
|
||||||
|
#log_truncate_on_rotation = off # If on, an existing log file with the
|
||||||
|
# same name as the new log file will be
|
||||||
|
# truncated rather than appended to.
|
||||||
|
# But such truncation only occurs on
|
||||||
|
# time-driven rotation, not on restarts
|
||||||
|
# or size-driven rotation. Default is
|
||||||
|
# off, meaning append to existing files
|
||||||
|
# in all cases.
|
||||||
|
|
||||||
|
# These are relevant when logging to syslog:
|
||||||
|
#syslog_facility = 'LOCAL0'
|
||||||
|
#syslog_ident = 'postgres'
|
||||||
|
#syslog_sequence_numbers = on
|
||||||
|
#syslog_split_messages = on
|
||||||
|
|
||||||
|
# This is only relevant when logging to eventlog (Windows):
|
||||||
|
# (change requires restart)
|
||||||
|
#event_source = 'PostgreSQL'
|
||||||
|
|
||||||
|
# - When to Log -
|
||||||
|
|
||||||
|
#log_min_messages = warning # values in order of decreasing detail:
|
||||||
|
# debug5
|
||||||
|
# debug4
|
||||||
|
# debug3
|
||||||
|
# debug2
|
||||||
|
# debug1
|
||||||
|
# info
|
||||||
|
# notice
|
||||||
|
# warning
|
||||||
|
# error
|
||||||
|
# log
|
||||||
|
# fatal
|
||||||
|
# panic
|
||||||
|
|
||||||
|
#log_min_error_statement = error # values in order of decreasing detail:
|
||||||
|
# debug5
|
||||||
|
# debug4
|
||||||
|
# debug3
|
||||||
|
# debug2
|
||||||
|
# debug1
|
||||||
|
# info
|
||||||
|
# notice
|
||||||
|
# warning
|
||||||
|
# error
|
||||||
|
# log
|
||||||
|
# fatal
|
||||||
|
# panic (effectively off)
|
||||||
|
|
||||||
|
#log_min_duration_statement = -1 # -1 is disabled, 0 logs all statements
|
||||||
|
# and their durations, > 0 logs only
|
||||||
|
# statements running at least this number
|
||||||
|
# of milliseconds
|
||||||
|
|
||||||
|
#log_min_duration_sample = -1 # -1 is disabled, 0 logs a sample of statements
|
||||||
|
# and their durations, > 0 logs only a sample of
|
||||||
|
# statements running at least this number
|
||||||
|
# of milliseconds;
|
||||||
|
# sample fraction is determined by log_statement_sample_rate
|
||||||
|
|
||||||
|
#log_statement_sample_rate = 1.0 # fraction of logged statements exceeding
|
||||||
|
# log_min_duration_sample to be logged;
|
||||||
|
# 1.0 logs all such statements, 0.0 never logs
|
||||||
|
|
||||||
|
|
||||||
|
#log_transaction_sample_rate = 0.0 # fraction of transactions whose statements
|
||||||
|
# are logged regardless of their duration; 1.0 logs all
|
||||||
|
# statements from all transactions, 0.0 never logs
|
||||||
|
|
||||||
|
#log_startup_progress_interval = 10s # Time between progress updates for
|
||||||
|
# long-running startup operations.
|
||||||
|
# 0 disables the feature, > 0 indicates
|
||||||
|
# the interval in milliseconds.
|
||||||
|
|
||||||
|
# - What to Log -
|
||||||
|
|
||||||
|
#debug_print_raw_parse = off
|
||||||
|
#debug_print_parse = off
|
||||||
|
#debug_print_rewritten = off
|
||||||
|
#debug_print_plan = off
|
||||||
|
#debug_pretty_print = on
|
||||||
|
#log_autovacuum_min_duration = 10min # log vacuum activity by autovacuum;
|
||||||
|
# -1 disables, 0 logs all actions and
|
||||||
|
# their durations, > 0 logs only
|
||||||
|
# actions running at least this number
|
||||||
|
# of milliseconds.
|
||||||
|
#log_autoanalyze_min_duration = 10min # log analyze activity by autovacuum;
|
||||||
|
# -1 disables, 0 logs all actions and
|
||||||
|
# their durations, > 0 logs only
|
||||||
|
# actions running at least this number
|
||||||
|
# of milliseconds.
|
||||||
|
#log_checkpoints = on
|
||||||
|
#log_connections = '' # log aspects of connection setup
|
||||||
|
# options include receipt, authentication, authorization,
|
||||||
|
# setup_durations, and all to log all of these aspects
|
||||||
|
#log_disconnections = off
|
||||||
|
#log_duration = off # log statement duration
|
||||||
|
#log_error_verbosity = default # terse, default, or verbose messages
|
||||||
|
#log_hostname = off
|
||||||
|
#log_line_prefix = '%m [%p] ' # special values:
|
||||||
|
# %a = application name
|
||||||
|
# %u = user name
|
||||||
|
# %d = database name
|
||||||
|
# %r = remote host and port
|
||||||
|
# %h = remote host
|
||||||
|
# %L = local address
|
||||||
|
# %b = backend type
|
||||||
|
# %p = process ID
|
||||||
|
# %P = process ID of parallel group leader
|
||||||
|
# %t = timestamp without milliseconds
|
||||||
|
# %m = timestamp with milliseconds
|
||||||
|
# %n = timestamp with milliseconds (as a Unix epoch)
|
||||||
|
# %Q = query ID (0 if none or not computed)
|
||||||
|
# %i = command tag
|
||||||
|
# %e = SQL state
|
||||||
|
# %c = session ID
|
||||||
|
# %l = session line number
|
||||||
|
# %s = session start timestamp
|
||||||
|
# %v = virtual transaction ID
|
||||||
|
# %x = transaction ID (0 if none)
|
||||||
|
# %q = stop here in non-session
|
||||||
|
# processes
|
||||||
|
# %% = '%'
|
||||||
|
# e.g. '<%u%%%d> '
|
||||||
|
#log_lock_waits = on # log lock waits >= deadlock_timeout
|
||||||
|
#log_lock_failures = off # log lock failures
|
||||||
|
#log_recovery_conflict_waits = off # log standby recovery conflict waits
|
||||||
|
# >= deadlock_timeout
|
||||||
|
#log_parameter_max_length = -1 # when logging statements, limit logged
|
||||||
|
# bind-parameter values to N bytes;
|
||||||
|
# -1 means print in full, 0 disables
|
||||||
|
#log_parameter_max_length_on_error = 0 # when logging an error, limit logged
|
||||||
|
# bind-parameter values to N bytes;
|
||||||
|
# -1 means print in full, 0 disables
|
||||||
|
#log_statement = 'none' # none, ddl, mod, all
|
||||||
|
#log_replication_commands = off
|
||||||
|
#log_temp_files = -1 # log temporary files equal or larger
|
||||||
|
# than the specified size in kilobytes;
|
||||||
|
# -1 disables, 0 logs all temp files
|
||||||
|
#log_timezone = 'GMT'
|
||||||
|
|
||||||
|
# - Process Title -
|
||||||
|
|
||||||
|
#cluster_name = '' # added to process titles if nonempty
|
||||||
|
# (change requires restart)
|
||||||
|
#update_process_title = on
|
||||||
|
|
||||||
|
|
||||||
|
#------------------------------------------------------------------------------
|
||||||
|
# STATISTICS
|
||||||
|
#------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# - Cumulative Query and Index Statistics -
|
||||||
|
|
||||||
|
#track_activities = on
|
||||||
|
#track_activity_query_size = 1024 # (change requires restart)
|
||||||
|
#track_counts = on
|
||||||
|
#track_cost_delay_timing = off
|
||||||
|
#track_io_timing = off
|
||||||
|
#track_wal_io_timing = off
|
||||||
|
#track_functions = none # none, pl, all
|
||||||
|
#stats_fetch_consistency = cache # cache, none, snapshot
|
||||||
|
|
||||||
|
|
||||||
|
# - Monitoring -
|
||||||
|
|
||||||
|
#compute_query_id = auto
|
||||||
|
#log_statement_stats = off
|
||||||
|
#log_parser_stats = off
|
||||||
|
#log_planner_stats = off
|
||||||
|
#log_executor_stats = off
|
||||||
|
|
||||||
|
|
||||||
|
#------------------------------------------------------------------------------
|
||||||
|
# VACUUMING
|
||||||
|
#------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# - Automatic Vacuuming -
|
||||||
|
|
||||||
|
#autovacuum = on # Enable autovacuum subprocess? 'on'
|
||||||
|
# requires track_counts to also be on.
|
||||||
|
#autovacuum_worker_slots = 16 # autovacuum worker slots to allocate
|
||||||
|
# (change requires restart)
|
||||||
|
#autovacuum_max_workers = 3 # max number of autovacuum subprocesses
|
||||||
|
#autovacuum_naptime = 1min # time between autovacuum runs
|
||||||
|
#autovacuum_vacuum_threshold = 50 # min number of row updates before
|
||||||
|
# vacuum
|
||||||
|
#autovacuum_vacuum_insert_threshold = 1000 # min number of row inserts
|
||||||
|
# before vacuum; -1 disables insert
|
||||||
|
# vacuums
|
||||||
|
#autovacuum_analyze_threshold = 50 # min number of row updates before
|
||||||
|
# analyze
|
||||||
|
#autovacuum_vacuum_scale_factor = 0.2 # fraction of table size before vacuum
|
||||||
|
#autovacuum_vacuum_insert_scale_factor = 0.2 # fraction of unfrozen pages
|
||||||
|
# before insert vacuum
|
||||||
|
#autovacuum_analyze_scale_factor = 0.1 # fraction of table size before analyze
|
||||||
|
#autovacuum_vacuum_max_threshold = 100000000 # max number of row updates
|
||||||
|
# before vacuum; -1 disables max
|
||||||
|
# threshold
|
||||||
|
#autovacuum_freeze_max_age = 200000000 # maximum XID age before forced vacuum
|
||||||
|
# (change requires restart)
|
||||||
|
#autovacuum_multixact_freeze_max_age = 400000000 # maximum multixact age
|
||||||
|
# before forced vacuum
|
||||||
|
# (change requires restart)
|
||||||
|
#autovacuum_vacuum_cost_delay = 2ms # default vacuum cost delay for
|
||||||
|
# autovacuum, in milliseconds;
|
||||||
|
# -1 means use vacuum_cost_delay
|
||||||
|
#autovacuum_vacuum_cost_limit = -1 # default vacuum cost limit for
|
||||||
|
# autovacuum, -1 means use
|
||||||
|
# vacuum_cost_limit
|
||||||
|
|
||||||
|
# - Cost-Based Vacuum Delay -
|
||||||
|
|
||||||
|
#vacuum_cost_delay = 0 # 0-100 milliseconds (0 disables)
|
||||||
|
#vacuum_cost_page_hit = 1 # 0-10000 credits
|
||||||
|
#vacuum_cost_page_miss = 2 # 0-10000 credits
|
||||||
|
#vacuum_cost_page_dirty = 20 # 0-10000 credits
|
||||||
|
#vacuum_cost_limit = 200 # 1-10000 credits
|
||||||
|
|
||||||
|
# - Default Behavior -
|
||||||
|
|
||||||
|
#vacuum_truncate = on # enable truncation after vacuum
|
||||||
|
|
||||||
|
# - Freezing -
|
||||||
|
|
||||||
|
#vacuum_freeze_table_age = 150000000
|
||||||
|
#vacuum_freeze_min_age = 50000000
|
||||||
|
#vacuum_failsafe_age = 1600000000
|
||||||
|
#vacuum_multixact_freeze_table_age = 150000000
|
||||||
|
#vacuum_multixact_freeze_min_age = 5000000
|
||||||
|
#vacuum_multixact_failsafe_age = 1600000000
|
||||||
|
#vacuum_max_eager_freeze_failure_rate = 0.03 # 0 disables eager scanning
|
||||||
|
|
||||||
|
#------------------------------------------------------------------------------
|
||||||
|
# CLIENT CONNECTION DEFAULTS
|
||||||
|
#------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# - Statement Behavior -
|
||||||
|
|
||||||
|
#client_min_messages = notice # values in order of decreasing detail:
|
||||||
|
# debug5
|
||||||
|
# debug4
|
||||||
|
# debug3
|
||||||
|
# debug2
|
||||||
|
# debug1
|
||||||
|
# log
|
||||||
|
# notice
|
||||||
|
# warning
|
||||||
|
# error
|
||||||
|
#search_path = '"$user", public' # schema names
|
||||||
|
#row_security = on
|
||||||
|
#default_table_access_method = 'heap'
|
||||||
|
#default_tablespace = '' # a tablespace name, '' uses the default
|
||||||
|
#default_toast_compression = 'pglz' # 'pglz' or 'lz4'
|
||||||
|
#temp_tablespaces = '' # a list of tablespace names, '' uses
|
||||||
|
# only default tablespace
|
||||||
|
#check_function_bodies = on
|
||||||
|
#default_transaction_isolation = 'read committed'
|
||||||
|
#default_transaction_read_only = off
|
||||||
|
#default_transaction_deferrable = off
|
||||||
|
#session_replication_role = 'origin'
|
||||||
|
#statement_timeout = 0 # in milliseconds, 0 is disabled
|
||||||
|
#transaction_timeout = 0 # in milliseconds, 0 is disabled
|
||||||
|
#lock_timeout = 0 # in milliseconds, 0 is disabled
|
||||||
|
#idle_in_transaction_session_timeout = 0 # in milliseconds, 0 is disabled
|
||||||
|
#idle_session_timeout = 0 # in milliseconds, 0 is disabled
|
||||||
|
#bytea_output = 'hex' # hex, escape
|
||||||
|
#xmlbinary = 'base64'
|
||||||
|
#xmloption = 'content'
|
||||||
|
#gin_pending_list_limit = 4MB
|
||||||
|
#createrole_self_grant = '' # set and/or inherit
|
||||||
|
#event_triggers = on
|
||||||
|
|
||||||
|
# - Locale and Formatting -
|
||||||
|
|
||||||
|
#datestyle = 'iso, mdy'
|
||||||
|
#intervalstyle = 'postgres'
|
||||||
|
#timezone = 'GMT'
|
||||||
|
#timezone_abbreviations = 'Default' # Select the set of available time zone
|
||||||
|
# abbreviations. Currently, there are
|
||||||
|
# Default
|
||||||
|
# Australia (historical usage)
|
||||||
|
# India
|
||||||
|
# You can create your own file in
|
||||||
|
# share/timezonesets/.
|
||||||
|
#extra_float_digits = 1 # min -15, max 3; any value >0 actually
|
||||||
|
# selects precise output mode
|
||||||
|
#client_encoding = sql_ascii # actually, defaults to database
|
||||||
|
# encoding
|
||||||
|
|
||||||
|
# These settings are initialized by initdb, but they can be changed.
|
||||||
|
#lc_messages = '' # locale for system error message
|
||||||
|
# strings
|
||||||
|
#lc_monetary = 'C' # locale for monetary formatting
|
||||||
|
#lc_numeric = 'C' # locale for number formatting
|
||||||
|
#lc_time = 'C' # locale for time formatting
|
||||||
|
|
||||||
|
#icu_validation_level = warning # report ICU locale validation
|
||||||
|
# errors at the given level
|
||||||
|
|
||||||
|
# default configuration for text search
|
||||||
|
#default_text_search_config = 'pg_catalog.simple'
|
||||||
|
|
||||||
|
# - Shared Library Preloading -
|
||||||
|
|
||||||
|
#local_preload_libraries = ''
|
||||||
|
#session_preload_libraries = ''
|
||||||
|
#shared_preload_libraries = '' # (change requires restart)
|
||||||
|
#jit_provider = 'llvmjit' # JIT library to use
|
||||||
|
|
||||||
|
# - Other Defaults -
|
||||||
|
|
||||||
|
#dynamic_library_path = '$libdir'
|
||||||
|
#extension_control_path = '$system'
|
||||||
|
#gin_fuzzy_search_limit = 0
|
||||||
|
|
||||||
|
|
||||||
|
#------------------------------------------------------------------------------
|
||||||
|
# LOCK MANAGEMENT
|
||||||
|
#------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#deadlock_timeout = 1s
|
||||||
|
#max_locks_per_transaction = 64 # min 10
|
||||||
|
# (change requires restart)
|
||||||
|
#max_pred_locks_per_transaction = 64 # min 10
|
||||||
|
# (change requires restart)
|
||||||
|
#max_pred_locks_per_relation = -2 # negative values mean
|
||||||
|
# (max_pred_locks_per_transaction
|
||||||
|
# / -max_pred_locks_per_relation) - 1
|
||||||
|
#max_pred_locks_per_page = 2 # min 0
|
||||||
|
|
||||||
|
|
||||||
|
#------------------------------------------------------------------------------
|
||||||
|
# VERSION AND PLATFORM COMPATIBILITY
|
||||||
|
#------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# - Previous PostgreSQL Versions -
|
||||||
|
|
||||||
|
#array_nulls = on
|
||||||
|
#backslash_quote = safe_encoding # on, off, or safe_encoding
|
||||||
|
#lo_compat_privileges = off
|
||||||
|
#quote_all_identifiers = off
|
||||||
|
#synchronize_seqscans = on
|
||||||
|
|
||||||
|
# - Other Platforms and Clients -
|
||||||
|
|
||||||
|
#transform_null_equals = off
|
||||||
|
#allow_alter_system = on
|
||||||
|
|
||||||
|
|
||||||
|
#------------------------------------------------------------------------------
|
||||||
|
# ERROR HANDLING
|
||||||
|
#------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#exit_on_error = off # terminate session on any error?
|
||||||
|
#restart_after_crash = on # reinitialize after backend crash?
|
||||||
|
#data_sync_retry = off # retry or panic on failure to fsync
|
||||||
|
# data?
|
||||||
|
# (change requires restart)
|
||||||
|
#recovery_init_sync_method = fsync # fsync, syncfs (Linux 5.8+)
|
||||||
|
|
||||||
|
|
||||||
|
#------------------------------------------------------------------------------
|
||||||
|
# CONFIG FILE INCLUDES
|
||||||
|
#------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# These options allow settings to be loaded from files other than the
|
||||||
|
# default postgresql.conf. Note that these are directives, not variable
|
||||||
|
# assignments, so they can usefully be given more than once.
|
||||||
|
|
||||||
|
#include_dir = '...' # include files ending in '.conf' from
|
||||||
|
# a directory, e.g., 'conf.d'
|
||||||
|
#include_if_exists = '...' # include file only if it exists
|
||||||
|
#include = '...' # include file
|
||||||
|
|
||||||
|
|
||||||
|
#------------------------------------------------------------------------------
|
||||||
|
# CUSTOMIZED OPTIONS
|
||||||
|
#------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Add settings for extensions here
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
REDIS_ADDR=redis://valkey:6379
|
||||||
|
REDIS_USER=default
|
||||||
|
REDIS_PASSWORD=valkey
|
||||||
|
REDIS_EXPORTER_PING_ON_CONNECT=true
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
RUSTFS_VOLUMES=/data
|
||||||
|
|
||||||
|
RUSTFS_ADDRESS=0.0.0.0:9000
|
||||||
|
|
||||||
|
RUSTFS_ACCESS_KEY=access
|
||||||
|
RUSTFS_SECRET_KEY=storage-secret
|
||||||
|
|
||||||
|
RUSTFS_CONSOLE_ENABLE=true
|
||||||
|
RUSTFS_CONSOLE_ADDRESS=0.0.0.0:9001
|
||||||
|
|
||||||
|
RUSTFS_OBS_ENDPOINT=http://otel-collector:4318
|
||||||
|
RUSTFS_OBS_LOGGER_LEVEL=info
|
||||||
|
|
||||||
|
RUSTFS_REGION=local
|
||||||
|
|
||||||
|
#RUSTFS_KMS_ENABLE=true
|
||||||
|
#RUSTFS_KMS_BACKEND=vault
|
||||||
|
#RUSTFS_KMS_KEY_DIR=
|
||||||
|
#RUSTFS_KMS_VAULT_ADDRESS=
|
||||||
|
#RUSTFS_KMS_VAULT_TOKEN=
|
||||||
|
#RUSTFS_KMS_DEFAULT_KEY_ID=
|
||||||
|
|
||||||
|
# Options: GeneralPurpose, AiTraining, DataAnalytics, WebWorkload, IndustrialIoT, SecureStorage
|
||||||
|
RUSTFS_BUFFER_PROFILE=WebWorkload
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
S3_ENDPOINT=s3:9000
|
||||||
|
S3_URL=http://s3:9000
|
||||||
|
S3_ACCESS_KEY=access
|
||||||
|
S3_SECRET_KEY=storage-secret
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
AIOGRAM_BOT_TOKEN=
|
||||||
|
AIOGRAM_BACKEND_URL=http://backend:8080
|
||||||
|
REDIS_URI=redis://redis:6379
|
||||||
|
MINIO_ENDPOINT=http://minio:9000
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
VALKEY_PASSWORD=valkey
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,20 @@
|
|||||||
|
global:
|
||||||
|
scrape_interval: 5s
|
||||||
|
|
||||||
|
scrape_configs:
|
||||||
|
- job_name: postgres
|
||||||
|
static_configs:
|
||||||
|
- targets:
|
||||||
|
- postgres-exporter:9187
|
||||||
|
- job_name: valkey
|
||||||
|
static_configs:
|
||||||
|
- targets:
|
||||||
|
- redis-exporter:9121
|
||||||
|
- job_name: otel-collector
|
||||||
|
static_configs:
|
||||||
|
- targets:
|
||||||
|
- otel-collector:8888
|
||||||
|
- job_name: vmagent
|
||||||
|
static_configs:
|
||||||
|
- targets:
|
||||||
|
- localhost:8429
|
||||||
@@ -0,0 +1,188 @@
|
|||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
share/python-wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
# Usually these files are written by a python script from a template
|
||||||
|
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
*.py,cover
|
||||||
|
.hypothesis/
|
||||||
|
.pytest_cache/
|
||||||
|
cover/
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
*.pot
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
*.log
|
||||||
|
local_settings.py
|
||||||
|
db.sqlite3
|
||||||
|
db.sqlite3-journal
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
.scrapy
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
.pybuilder/
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# IPython
|
||||||
|
profile_default/
|
||||||
|
ipython_config.py
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
# For a library or package, you might want to ignore these files since the code is
|
||||||
|
# intended to run in multiple environments; otherwise, check them in:
|
||||||
|
.python-version
|
||||||
|
|
||||||
|
# pipenv
|
||||||
|
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||||
|
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||||
|
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||||
|
# install all needed dependencies.
|
||||||
|
Pipfile.lock
|
||||||
|
|
||||||
|
# UV
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
||||||
|
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||||
|
# commonly ignored for libraries.
|
||||||
|
# uv.lock
|
||||||
|
|
||||||
|
# poetry
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||||
|
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||||
|
# commonly ignored for libraries.
|
||||||
|
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||||
|
poetry.lock
|
||||||
|
|
||||||
|
# pdm
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||||
|
#pdm.lock
|
||||||
|
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||||
|
# in version control.
|
||||||
|
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
|
||||||
|
.pdm.toml
|
||||||
|
.pdm-python
|
||||||
|
.pdm-build/
|
||||||
|
|
||||||
|
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||||
|
__pypackages__/
|
||||||
|
|
||||||
|
# Celery stuff
|
||||||
|
celerybeat-schedule
|
||||||
|
celerybeat.pid
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
*.sage.py
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
.spyderproject
|
||||||
|
.spyproject
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
.ropeproject
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
/site
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
.mypy_cache/
|
||||||
|
.dmypy.json
|
||||||
|
dmypy.json
|
||||||
|
|
||||||
|
# Pyre type checker
|
||||||
|
.pyre/
|
||||||
|
|
||||||
|
# pytype static type analyzer
|
||||||
|
.pytype/
|
||||||
|
|
||||||
|
# Cython debug symbols
|
||||||
|
cython_debug/
|
||||||
|
|
||||||
|
# PyCharm
|
||||||
|
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||||
|
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||||
|
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||||
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# PyPI configuration file
|
||||||
|
.pypirc
|
||||||
|
|
||||||
|
# Ruff files
|
||||||
|
.ruff_cache
|
||||||
|
|
||||||
|
# Docker files
|
||||||
|
Dockerfile
|
||||||
|
Dockerfile.staticfiles
|
||||||
|
.dockerignore
|
||||||
|
|
||||||
|
# Git files
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
|
||||||
|
# Template env file
|
||||||
|
.env.template
|
||||||
|
|
||||||
|
# Collected static files
|
||||||
|
static
|
||||||
|
|
||||||
|
# Profile files
|
||||||
|
*.prof
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
# Change all vars before going to production and remove all comments (!)
|
||||||
|
# Below all environment variables and default values
|
||||||
|
|
||||||
|
DJANGO_SECRET_KEY=very_insecure_key
|
||||||
|
DJANGO_DEBUG=False
|
||||||
|
DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1
|
||||||
|
DJANGO_CSRF_TRUSTED_ORIGINS=http://localhost,http://127.0.0.1
|
||||||
|
DJANGO_CORS_ALLOWED_ORIGINS=*
|
||||||
|
DJANGO_INTERNAL_IPS=127.0.0.1
|
||||||
|
DJANGO_LANGUAGE_CODE=en-us
|
||||||
|
DJANGO_STATIC_URL=static/
|
||||||
|
REDIS_URI=redis://localhost:6379
|
||||||
|
DJANGO_DB_URI=sqlite:///db.sqlite3
|
||||||
|
DJANGO_CONN_MAX_AGE=300
|
||||||
|
DJANGO_SILKY_ENABLED=True
|
||||||
|
DJANGO_SILKY_PYTHON_PROFILER=False
|
||||||
|
|
||||||
|
|
||||||
|
# Observability (OpenTelemetry)
|
||||||
|
|
||||||
|
OTEL_ENABLED=False
|
||||||
|
OTEL_SERVICE_NAME=backend-django
|
||||||
|
OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317
|
||||||
|
OTEL_TRACES_EXPORTER=otlp
|
||||||
|
OTEL_METRICS_EXPORTER=otlp
|
||||||
|
OTEL_LOGS_EXPORTER=otlp
|
||||||
|
|
||||||
|
|
||||||
|
# Storages (S3)
|
||||||
|
|
||||||
|
AWS_ACCESS_KEY_ID=
|
||||||
|
AWS_SECRET_ACCESS_KEY=
|
||||||
|
AWS_STORAGE_BUCKET_NAME=
|
||||||
|
AWS_S3_ENDPOINT_URL=
|
||||||
|
AWS_S3_REGION_NAME=
|
||||||
|
AWS_S3_USE_SSL=True
|
||||||
|
AWS_S3_VERIFY=True
|
||||||
|
|
||||||
|
|
||||||
|
# Applyable if you installing using docker compose
|
||||||
|
|
||||||
|
GUNICORN_WORKERS=4
|
||||||
|
GUNICORN_BIND=0.0.0.0:8080
|
||||||
|
GUNICORN_WORKER_CLASS=uvicorn_worker.UvicornWorker
|
||||||
|
GUNICORN_ACCESS_LOG=-
|
||||||
|
GUNICORN_ERROR_LOG=-
|
||||||
|
|
||||||
|
RUN_MIGRATIONS=False
|
||||||
|
COLLECT_STATIC=False
|
||||||
|
|
||||||
|
DJANGO_CREATE_SUPERUSER=False
|
||||||
|
DJANGO_SUPERUSER_USERNAME=
|
||||||
|
DJANGO_SUPERUSER_EMAIL=
|
||||||
|
DJANGO_SUPERUSER_PASSWORD=
|
||||||
@@ -0,0 +1,176 @@
|
|||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
share/python-wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
# Usually these files are written by a python script from a template
|
||||||
|
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
*.py,cover
|
||||||
|
.hypothesis/
|
||||||
|
.pytest_cache/
|
||||||
|
cover/
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
*.pot
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
*.log
|
||||||
|
local_settings.py
|
||||||
|
db.sqlite3
|
||||||
|
db.sqlite3-journal
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
.scrapy
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
.pybuilder/
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# IPython
|
||||||
|
profile_default/
|
||||||
|
ipython_config.py
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
# For a library or package, you might want to ignore these files since the code is
|
||||||
|
# intended to run in multiple environments; otherwise, check them in:
|
||||||
|
.python-version
|
||||||
|
|
||||||
|
# pipenv
|
||||||
|
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||||
|
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||||
|
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||||
|
# install all needed dependencies.
|
||||||
|
Pipfile.lock
|
||||||
|
|
||||||
|
# UV
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
||||||
|
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||||
|
# commonly ignored for libraries.
|
||||||
|
# uv.lock
|
||||||
|
|
||||||
|
# poetry
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||||
|
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||||
|
# commonly ignored for libraries.
|
||||||
|
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||||
|
poetry.lock
|
||||||
|
|
||||||
|
# pdm
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||||
|
#pdm.lock
|
||||||
|
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||||
|
# in version control.
|
||||||
|
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
|
||||||
|
.pdm.toml
|
||||||
|
.pdm-python
|
||||||
|
.pdm-build/
|
||||||
|
|
||||||
|
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||||
|
__pypackages__/
|
||||||
|
|
||||||
|
# Celery stuff
|
||||||
|
celerybeat-schedule
|
||||||
|
celerybeat.pid
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
*.sage.py
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
.spyderproject
|
||||||
|
.spyproject
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
.ropeproject
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
/site
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
.mypy_cache/
|
||||||
|
.dmypy.json
|
||||||
|
dmypy.json
|
||||||
|
|
||||||
|
# Pyre type checker
|
||||||
|
.pyre/
|
||||||
|
|
||||||
|
# pytype static type analyzer
|
||||||
|
.pytype/
|
||||||
|
|
||||||
|
# Cython debug symbols
|
||||||
|
cython_debug/
|
||||||
|
|
||||||
|
# PyCharm
|
||||||
|
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||||
|
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||||
|
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||||
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# PyPI configuration file
|
||||||
|
.pypirc
|
||||||
|
|
||||||
|
# Ruff files
|
||||||
|
.ruff_cache
|
||||||
|
|
||||||
|
# Collected static files
|
||||||
|
static
|
||||||
|
|
||||||
|
# Profile files
|
||||||
|
*.prof
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
# Stage 1: Build dependencies
|
||||||
|
FROM docker.io/python:3.13-alpine3.22 AS deps
|
||||||
|
|
||||||
|
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
ENV UV_COMPILE_BYTECODE=1 \
|
||||||
|
UV_PROJECT_ENVIRONMENT=/opt/venv
|
||||||
|
|
||||||
|
COPY pyproject.toml uv.lock ./
|
||||||
|
|
||||||
|
RUN --mount=type=cache,target=/root/.cache/uv \
|
||||||
|
uv sync --frozen --no-install-project --no-dev --no-editable
|
||||||
|
|
||||||
|
|
||||||
|
# Stage 2: Build backend source
|
||||||
|
FROM deps AS backend-builder
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
|
||||||
|
# Stage 3: Build staticfiles
|
||||||
|
FROM deps AS static-builder
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN uv run --no-dev python manage.py collectstatic --noinput
|
||||||
|
|
||||||
|
|
||||||
|
# Stage 3: Runtime application image
|
||||||
|
FROM docker.io/python:3.13-alpine3.22 AS app
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY --from=deps /opt/venv /opt/venv
|
||||||
|
COPY --from=backend-builder /app /app
|
||||||
|
|
||||||
|
RUN chmod +x scripts/entrypoint.sh && \
|
||||||
|
adduser -D -g '' app && \
|
||||||
|
chown -R app:app /app
|
||||||
|
|
||||||
|
ENV PATH="/opt/venv/bin:$PATH" \
|
||||||
|
PYTHONDONTWRITEBYTECODE=1 \
|
||||||
|
PYTHONUNBUFFERED=1 \
|
||||||
|
PYTHONOPTIMIZE=2 \
|
||||||
|
DJANGO_SETTINGS_MODULE=config.settings
|
||||||
|
|
||||||
|
USER app
|
||||||
|
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --start-interval=2s --retries=3 \
|
||||||
|
CMD wget --no-verbose --tries=1 --spider http://127.0.0.1:8080/health?format=json || exit 1
|
||||||
|
|
||||||
|
ENTRYPOINT ["scripts/entrypoint.sh"]
|
||||||
|
|
||||||
|
|
||||||
|
# Stage 4: Staticfiles image
|
||||||
|
FROM docker.io/nginx:1.29-alpine-slim AS staticfiles
|
||||||
|
|
||||||
|
COPY --from=static-builder /app/static /usr/share/nginx/html
|
||||||
|
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
# Lotty Backend
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
Ensure you have the following installed on your system:
|
||||||
|
|
||||||
|
- [Python](https://www.python.org/) (>=3.13,<3.15)
|
||||||
|
- [uv](https://docs.astral.sh/uv/) (latest version recommended)
|
||||||
|
- [Docker](https://www.docker.com/) (for containerized setup, latest version recommended)
|
||||||
|
|
||||||
|
## Basic setup
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
#### Clone the project
|
||||||
|
|
||||||
|
#### Go to the project directory
|
||||||
|
|
||||||
|
#### Customize environment
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.template .env
|
||||||
|
```
|
||||||
|
|
||||||
|
And setup env vars according to your needs.
|
||||||
|
|
||||||
|
#### Install dependencies
|
||||||
|
|
||||||
|
##### For dev environment
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv sync --all-extras
|
||||||
|
```
|
||||||
|
|
||||||
|
##### For prod environment
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv sync --no-dev
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Running
|
||||||
|
|
||||||
|
##### Apply migrations
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run python manage.py migrate
|
||||||
|
```
|
||||||
|
|
||||||
|
##### Start celery worker
|
||||||
|
|
||||||
|
```bash
|
||||||
|
celery -A config worker -l INFO
|
||||||
|
```
|
||||||
|
|
||||||
|
##### Start server
|
||||||
|
|
||||||
|
In dev mode:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run python manage.py runserver
|
||||||
|
```
|
||||||
|
|
||||||
|
In prod mode:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run gunicorn config.wsgi
|
||||||
|
```
|
||||||
|
|
||||||
|
## Containerized setup
|
||||||
|
|
||||||
|
### Clone the project
|
||||||
|
|
||||||
|
### Go to the project directory
|
||||||
|
|
||||||
|
### Build docker image
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build -t lotty-backend .
|
||||||
|
```
|
||||||
|
|
||||||
|
### Customize environment
|
||||||
|
|
||||||
|
Customize environment with `docker run` command (or bind .env file to container), for all environment vars and default values see [.env.template](./.env.template).
|
||||||
|
|
||||||
|
### Run docker image
|
||||||
|
|
||||||
|
#### Backend
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -p 8080:8080 --name lotty-backend lotty-backend
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Celery worker
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run --name lotty-celery-worker lotty-backend celery -A config worker -l INFO
|
||||||
|
```
|
||||||
|
|
||||||
|
Backend will be available on [127.0.0.1:8080](http://127.0.0.1:8080).
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
### Clone the project
|
||||||
|
|
||||||
|
### Go to the project directory
|
||||||
|
|
||||||
|
### Install dependencies
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv sync --all-extras
|
||||||
|
```
|
||||||
|
|
||||||
|
### Run tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run coverage run --source="." manage.py test
|
||||||
|
```
|
||||||
|
|
||||||
|
### Check coverage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run coverage report
|
||||||
|
```
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
from django.urls import path
|
||||||
|
from health_check.views import HealthCheckView
|
||||||
|
|
||||||
|
from api.v1.router import router as api_v1_router
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path("api/v1/", api_v1_router.urls),
|
||||||
|
# Health endpoint
|
||||||
|
path(
|
||||||
|
"health",
|
||||||
|
HealthCheckView.as_view(
|
||||||
|
checks=[
|
||||||
|
"health_check.Memory"
|
||||||
|
],
|
||||||
|
),
|
||||||
|
name="liveness",
|
||||||
|
),
|
||||||
|
# Ready endpoint
|
||||||
|
path(
|
||||||
|
"ready",
|
||||||
|
HealthCheckView.as_view(
|
||||||
|
checks=[
|
||||||
|
"health_check.Cache",
|
||||||
|
"health_check.Database",
|
||||||
|
"health_check.Storage",
|
||||||
|
],
|
||||||
|
),
|
||||||
|
name="readiness"
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,186 @@
|
|||||||
|
import logging
|
||||||
|
from collections.abc import Callable
|
||||||
|
from http import HTTPStatus as status
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import django.core.exceptions
|
||||||
|
import django.http
|
||||||
|
import ninja.errors
|
||||||
|
from django.http import HttpRequest, HttpResponse
|
||||||
|
from ninja import NinjaAPI
|
||||||
|
|
||||||
|
from api.v1.schemas import ApiError, ValidationError
|
||||||
|
from config.errors import ConflictError, ForbiddenError
|
||||||
|
from config.utils import build_error_payload
|
||||||
|
|
||||||
|
logger = logging.getLogger("django")
|
||||||
|
|
||||||
|
|
||||||
|
def create_error_response(
|
||||||
|
request: HttpRequest,
|
||||||
|
code: str,
|
||||||
|
message: str,
|
||||||
|
http_status: int,
|
||||||
|
router: NinjaAPI,
|
||||||
|
details: dict[str, Any] | None = None,
|
||||||
|
) -> HttpResponse:
|
||||||
|
payload = build_error_payload(
|
||||||
|
request=request,
|
||||||
|
code=code,
|
||||||
|
message=message,
|
||||||
|
details=details,
|
||||||
|
)
|
||||||
|
error_data = ApiError.model_validate(payload)
|
||||||
|
return router.create_response(request, error_data, status=http_status)
|
||||||
|
|
||||||
|
|
||||||
|
def handle_validation_error(
|
||||||
|
request: HttpRequest,
|
||||||
|
exc: ninja.errors.ValidationError,
|
||||||
|
router: NinjaAPI,
|
||||||
|
) -> HttpResponse:
|
||||||
|
field_errors_data: list[dict[str, Any]] = []
|
||||||
|
for error in exc.errors:
|
||||||
|
loc = error.get("loc", [])
|
||||||
|
field = ".".join(map(str, loc)) if loc else "non_field_error"
|
||||||
|
field_errors_data.append(
|
||||||
|
{
|
||||||
|
"field": field,
|
||||||
|
"issue": error.get("msg", "Unknown error"),
|
||||||
|
"rejectedValue": error.get("input"),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
payload = build_error_payload(
|
||||||
|
request=request,
|
||||||
|
code="VALIDATION_FAILED",
|
||||||
|
message="Validation failed",
|
||||||
|
field_errors=field_errors_data,
|
||||||
|
)
|
||||||
|
error_data = ValidationError.model_validate(payload)
|
||||||
|
return router.create_response(
|
||||||
|
request, error_data, status=status.UNPROCESSABLE_ENTITY
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def handle_django_validation_error(
|
||||||
|
request: HttpRequest,
|
||||||
|
exc: django.core.exceptions.ValidationError,
|
||||||
|
router: NinjaAPI,
|
||||||
|
code: str = "VALIDATION_FAILED",
|
||||||
|
http_status: int = status.UNPROCESSABLE_ENTITY,
|
||||||
|
) -> HttpResponse:
|
||||||
|
field_errors_data: list[dict[str, Any]] = []
|
||||||
|
if hasattr(exc, "error_dict"):
|
||||||
|
for field, errors in exc.error_dict.items():
|
||||||
|
field_errors_data.extend(
|
||||||
|
{
|
||||||
|
"field": field,
|
||||||
|
"issue": str(error.message),
|
||||||
|
"rejectedValue": None,
|
||||||
|
}
|
||||||
|
for error in errors
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
field_errors_data.extend(
|
||||||
|
{
|
||||||
|
"field": "non_field_error",
|
||||||
|
"issue": str(error.message),
|
||||||
|
"rejectedValue": None,
|
||||||
|
}
|
||||||
|
for error in exc.error_list
|
||||||
|
)
|
||||||
|
|
||||||
|
payload = build_error_payload(
|
||||||
|
request=request,
|
||||||
|
code=code,
|
||||||
|
message="Validation failed"
|
||||||
|
if code == "VALIDATION_FAILED"
|
||||||
|
else "Conflict",
|
||||||
|
field_errors=field_errors_data,
|
||||||
|
)
|
||||||
|
error_data = ValidationError.model_validate(payload)
|
||||||
|
return router.create_response(request, error_data, status=http_status)
|
||||||
|
|
||||||
|
|
||||||
|
def handle_authentication_error(
|
||||||
|
request: HttpRequest,
|
||||||
|
exc: ninja.errors.AuthenticationError,
|
||||||
|
router: NinjaAPI,
|
||||||
|
) -> HttpResponse:
|
||||||
|
return create_error_response(
|
||||||
|
request,
|
||||||
|
code="UNAUTHENTICATED",
|
||||||
|
message="Authentication required",
|
||||||
|
http_status=status.UNAUTHORIZED,
|
||||||
|
router=router,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def handle_forbidden_error(
|
||||||
|
request: HttpRequest,
|
||||||
|
exc: ForbiddenError,
|
||||||
|
router: NinjaAPI,
|
||||||
|
) -> HttpResponse:
|
||||||
|
return create_error_response(
|
||||||
|
request,
|
||||||
|
code="FORBIDDEN",
|
||||||
|
message=exc.message,
|
||||||
|
http_status=status.FORBIDDEN,
|
||||||
|
router=router,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def handle_not_found_error(
|
||||||
|
request: HttpRequest,
|
||||||
|
exc: Exception,
|
||||||
|
router: NinjaAPI,
|
||||||
|
) -> HttpResponse:
|
||||||
|
return create_error_response(
|
||||||
|
request,
|
||||||
|
code="NOT_FOUND",
|
||||||
|
message="Resource not found",
|
||||||
|
http_status=status.NOT_FOUND,
|
||||||
|
router=router,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def handle_conflict_error(
|
||||||
|
request: HttpRequest,
|
||||||
|
exc: ConflictError,
|
||||||
|
router: NinjaAPI,
|
||||||
|
) -> HttpResponse:
|
||||||
|
return handle_django_validation_error(
|
||||||
|
request,
|
||||||
|
exc.validation_error,
|
||||||
|
router,
|
||||||
|
code="CONFLICT",
|
||||||
|
http_status=status.CONFLICT,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def handle_unknown_exception(
|
||||||
|
request: HttpRequest,
|
||||||
|
exc: Exception,
|
||||||
|
router: NinjaAPI,
|
||||||
|
) -> HttpResponse:
|
||||||
|
logger.error("Internal server error: %s", exc, exc_info=True) # noqa: LOG014
|
||||||
|
|
||||||
|
return create_error_response(
|
||||||
|
request,
|
||||||
|
code="INTERNAL_SERVER_ERROR",
|
||||||
|
message="An unexpected error occurred",
|
||||||
|
http_status=status.INTERNAL_SERVER_ERROR,
|
||||||
|
router=router,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
exception_handlers: list[tuple[Any, Callable[..., Any]]] = [
|
||||||
|
(ninja.errors.ValidationError, handle_validation_error),
|
||||||
|
(django.core.exceptions.ValidationError, handle_django_validation_error),
|
||||||
|
(ninja.errors.AuthenticationError, handle_authentication_error),
|
||||||
|
(ForbiddenError, handle_forbidden_error),
|
||||||
|
(django.http.Http404, handle_not_found_error),
|
||||||
|
(ConflictError, handle_conflict_error),
|
||||||
|
(Exception, handle_unknown_exception),
|
||||||
|
]
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
from functools import partial
|
||||||
|
from typing import Any, override
|
||||||
|
|
||||||
|
import orjson
|
||||||
|
from django.http import HttpRequest
|
||||||
|
from ninja import NinjaAPI, Schema
|
||||||
|
from ninja.renderers import BaseRenderer
|
||||||
|
|
||||||
|
from api.v1 import handlers
|
||||||
|
|
||||||
|
|
||||||
|
class ORJSONRenderer(BaseRenderer):
|
||||||
|
media_type: str | None = "application/json"
|
||||||
|
|
||||||
|
@override
|
||||||
|
def render(
|
||||||
|
self, request: HttpRequest, data: Any, *, response_status: int
|
||||||
|
) -> Any:
|
||||||
|
return orjson.dumps(data, default=self.default)
|
||||||
|
|
||||||
|
def default(self, obj: Any) -> Any:
|
||||||
|
if isinstance(obj, Schema):
|
||||||
|
return obj.model_dump(by_alias=True)
|
||||||
|
raise TypeError
|
||||||
|
|
||||||
|
|
||||||
|
router = NinjaAPI(
|
||||||
|
title="Lotty API",
|
||||||
|
version="1",
|
||||||
|
description="API docs for Lotty A/B platform",
|
||||||
|
openapi_url="/docs/openapi.json",
|
||||||
|
renderer=ORJSONRenderer(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# router.add_router(
|
||||||
|
# "health",
|
||||||
|
# health_router,
|
||||||
|
# )
|
||||||
|
|
||||||
|
|
||||||
|
for exception, handler in handlers.exception_handlers:
|
||||||
|
router.add_exception_handler(exception, partial(handler, router=router))
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from ninja import Schema
|
||||||
|
from pydantic import ConfigDict, Field
|
||||||
|
|
||||||
|
|
||||||
|
class FieldError(Schema):
|
||||||
|
model_config = ConfigDict(populate_by_name=True)
|
||||||
|
|
||||||
|
field: str = Field(
|
||||||
|
...,
|
||||||
|
description="Field name with error (can be nested)",
|
||||||
|
)
|
||||||
|
issue: str = Field(..., description="Problem description")
|
||||||
|
rejected_value: Any = Field(
|
||||||
|
None, alias="rejectedValue", description="Value that failed validation"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ApiError(Schema):
|
||||||
|
model_config = ConfigDict(populate_by_name=True)
|
||||||
|
|
||||||
|
code: str
|
||||||
|
message: str
|
||||||
|
trace_id: str = Field(..., alias="traceId")
|
||||||
|
timestamp: datetime
|
||||||
|
path: str
|
||||||
|
details: dict[str, Any] | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class ValidationError(ApiError):
|
||||||
|
field_errors: list[FieldError] = Field(..., alias="fieldErrors")
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class CoreConfig(AppConfig):
|
||||||
|
name = "apps.core"
|
||||||
|
label = "core"
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import uuid
|
||||||
|
from typing import Any, override
|
||||||
|
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
from config.errors import ConflictError
|
||||||
|
|
||||||
|
|
||||||
|
class BaseModel(models.Model):
|
||||||
|
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
abstract = True
|
||||||
|
|
||||||
|
@override
|
||||||
|
def save(self, *args: Any, **kwargs: Any) -> None:
|
||||||
|
self.validate()
|
||||||
|
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
def validate(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
validate_unique: bool = True,
|
||||||
|
validate_constraints: bool = True,
|
||||||
|
include: list[models.Field[Any, Any]] | None = None,
|
||||||
|
) -> None:
|
||||||
|
self.full_clean(
|
||||||
|
validate_unique=False,
|
||||||
|
validate_constraints=False,
|
||||||
|
exclude=(
|
||||||
|
field.name
|
||||||
|
for field in set(self._meta.get_fields()) - set(include)
|
||||||
|
)
|
||||||
|
if include
|
||||||
|
else None,
|
||||||
|
)
|
||||||
|
|
||||||
|
if validate_unique:
|
||||||
|
try:
|
||||||
|
self.validate_unique()
|
||||||
|
except ValidationError as e:
|
||||||
|
raise ConflictError(e) from None
|
||||||
|
|
||||||
|
if validate_constraints:
|
||||||
|
try:
|
||||||
|
self.validate_constraints()
|
||||||
|
except ValidationError as e:
|
||||||
|
raise ConflictError(e) from None
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class UsersConfig(AppConfig):
|
||||||
|
default_auto_field = "django.db.models.BigAutoField"
|
||||||
|
name = "apps.users"
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
# Generated by Django 5.2.11 on 2026-02-10 20:37
|
||||||
|
|
||||||
|
import django.contrib.auth.models
|
||||||
|
import django.contrib.auth.validators
|
||||||
|
import django.utils.timezone
|
||||||
|
import uuid
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("auth", "0012_alter_user_first_name_max_length"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="User",
|
||||||
|
fields=[
|
||||||
|
("password", models.CharField(max_length=128, verbose_name="password")),
|
||||||
|
(
|
||||||
|
"last_login",
|
||||||
|
models.DateTimeField(
|
||||||
|
blank=True, null=True, verbose_name="last login"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"is_superuser",
|
||||||
|
models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
help_text="Designates that this user has all permissions without explicitly assigning them.",
|
||||||
|
verbose_name="superuser status",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"username",
|
||||||
|
models.CharField(
|
||||||
|
error_messages={
|
||||||
|
"unique": "A user with that username already exists."
|
||||||
|
},
|
||||||
|
help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.",
|
||||||
|
max_length=150,
|
||||||
|
unique=True,
|
||||||
|
validators=[
|
||||||
|
django.contrib.auth.validators.UnicodeUsernameValidator()
|
||||||
|
],
|
||||||
|
verbose_name="username",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"first_name",
|
||||||
|
models.CharField(
|
||||||
|
blank=True, max_length=150, verbose_name="first name"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"last_name",
|
||||||
|
models.CharField(
|
||||||
|
blank=True, max_length=150, verbose_name="last name"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"email",
|
||||||
|
models.EmailField(
|
||||||
|
blank=True, max_length=254, verbose_name="email address"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"is_staff",
|
||||||
|
models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
help_text="Designates whether the user can log into this admin site.",
|
||||||
|
verbose_name="staff status",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"is_active",
|
||||||
|
models.BooleanField(
|
||||||
|
default=True,
|
||||||
|
help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.",
|
||||||
|
verbose_name="active",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"date_joined",
|
||||||
|
models.DateTimeField(
|
||||||
|
default=django.utils.timezone.now, verbose_name="date joined"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.UUIDField(
|
||||||
|
default=uuid.uuid4,
|
||||||
|
editable=False,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"groups",
|
||||||
|
models.ManyToManyField(
|
||||||
|
blank=True,
|
||||||
|
help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.",
|
||||||
|
related_name="user_set",
|
||||||
|
related_query_name="user",
|
||||||
|
to="auth.group",
|
||||||
|
verbose_name="groups",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"user_permissions",
|
||||||
|
models.ManyToManyField(
|
||||||
|
blank=True,
|
||||||
|
help_text="Specific permissions for this user.",
|
||||||
|
related_name="user_set",
|
||||||
|
related_query_name="user",
|
||||||
|
to="auth.permission",
|
||||||
|
verbose_name="user permissions",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"verbose_name": "user",
|
||||||
|
"verbose_name_plural": "users",
|
||||||
|
"swappable": "AUTH_USER_MODEL",
|
||||||
|
},
|
||||||
|
managers=[
|
||||||
|
("objects", django.contrib.auth.models.UserManager()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
from django.contrib.auth.models import AbstractUser
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from apps.core.models import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class User(AbstractUser, BaseModel):
|
||||||
|
class Meta:
|
||||||
|
swappable = "AUTH_USER_MODEL"
|
||||||
|
verbose_name = _("user")
|
||||||
|
verbose_name_plural = _("users")
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import uuid
|
||||||
|
|
||||||
|
from django.db.models import QuerySet
|
||||||
|
|
||||||
|
from apps.users.models import User
|
||||||
|
|
||||||
|
|
||||||
|
def user_get_by_id(user_id: str) -> User | None:
|
||||||
|
try:
|
||||||
|
uuid.UUID(user_id)
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
return User.objects.filter(id=user_id).first()
|
||||||
|
|
||||||
|
|
||||||
|
def user_list() -> QuerySet[User]:
|
||||||
|
return User.objects.all()
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from apps.users.models import User
|
||||||
|
|
||||||
|
|
||||||
|
def user_create(
|
||||||
|
*,
|
||||||
|
username: str,
|
||||||
|
email: str,
|
||||||
|
password: str | None = None,
|
||||||
|
**extra_fields: Any,
|
||||||
|
) -> User:
|
||||||
|
user = User(username=username, email=email, **extra_fields)
|
||||||
|
if password is not None:
|
||||||
|
user.set_password(password)
|
||||||
|
else:
|
||||||
|
user.set_unusable_password()
|
||||||
|
|
||||||
|
user.save()
|
||||||
|
return user
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
from config.celery import app as celery_app
|
||||||
|
|
||||||
|
__all__ = ("celery_app",)
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
"""ASGI config for Lotty."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.core.asgi import get_asgi_application
|
||||||
|
|
||||||
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
|
||||||
|
|
||||||
|
application = get_asgi_application()
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
from celery import Celery
|
||||||
|
|
||||||
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
|
||||||
|
|
||||||
|
app = Celery("lotty")
|
||||||
|
|
||||||
|
app.config_from_object("django.conf:settings", namespace="CELERY")
|
||||||
|
app.autodiscover_tasks()
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
from http import HTTPStatus as status
|
||||||
|
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
|
||||||
|
|
||||||
|
class ConflictError(Exception):
|
||||||
|
def __init__(self, validation_error: ValidationError) -> None:
|
||||||
|
self.validation_error = validation_error
|
||||||
|
|
||||||
|
|
||||||
|
class ForbiddenError(Exception):
|
||||||
|
def __init__(self, message: str = status.FORBIDDEN.phrase) -> None:
|
||||||
|
self.message = message
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
from http import HTTPStatus as status
|
||||||
|
|
||||||
|
from django.http import HttpRequest, JsonResponse
|
||||||
|
|
||||||
|
from config.utils import build_error_payload
|
||||||
|
|
||||||
|
|
||||||
|
def create_error_response(
|
||||||
|
request: HttpRequest,
|
||||||
|
code: str,
|
||||||
|
message: str,
|
||||||
|
http_status: int,
|
||||||
|
) -> JsonResponse:
|
||||||
|
payload = build_error_payload(request, code, message)
|
||||||
|
|
||||||
|
return JsonResponse(status=http_status, data=payload)
|
||||||
|
|
||||||
|
|
||||||
|
def handler400(
|
||||||
|
request: HttpRequest,
|
||||||
|
exception: Exception | None = None,
|
||||||
|
) -> JsonResponse:
|
||||||
|
return create_error_response(
|
||||||
|
request,
|
||||||
|
code="BAD_REQUEST",
|
||||||
|
message=status.BAD_REQUEST.phrase,
|
||||||
|
http_status=status.BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def handler403(
|
||||||
|
request: HttpRequest,
|
||||||
|
exception: Exception | None = None,
|
||||||
|
) -> JsonResponse:
|
||||||
|
return create_error_response(
|
||||||
|
request,
|
||||||
|
code="FORBIDDEN",
|
||||||
|
message=status.FORBIDDEN.phrase,
|
||||||
|
http_status=status.FORBIDDEN,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def handler404(
|
||||||
|
request: HttpRequest,
|
||||||
|
exception: Exception | None = None,
|
||||||
|
) -> JsonResponse:
|
||||||
|
return create_error_response(
|
||||||
|
request,
|
||||||
|
code="NOT_FOUND",
|
||||||
|
message=status.NOT_FOUND.phrase,
|
||||||
|
http_status=status.NOT_FOUND,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def handler500(
|
||||||
|
request: HttpRequest,
|
||||||
|
exception: Exception | None = None,
|
||||||
|
) -> JsonResponse:
|
||||||
|
return create_error_response(
|
||||||
|
request,
|
||||||
|
code="INTERNAL_SERVER_ERROR",
|
||||||
|
message=status.INTERNAL_SERVER_ERROR.phrase,
|
||||||
|
http_status=status.INTERNAL_SERVER_ERROR,
|
||||||
|
)
|
||||||
@@ -0,0 +1,657 @@
|
|||||||
|
"""Django settings for Lotty."""
|
||||||
|
|
||||||
|
import contextlib
|
||||||
|
import logging
|
||||||
|
from collections.abc import Callable
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
|
import django_stubs_ext
|
||||||
|
import environ
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
|
||||||
|
|
||||||
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
|
env = environ.Env()
|
||||||
|
environ.Env.read_env(BASE_DIR / ".env")
|
||||||
|
|
||||||
|
django_stubs_ext.monkeypatch()
|
||||||
|
|
||||||
|
|
||||||
|
# Common settings
|
||||||
|
|
||||||
|
DEBUG = env("DJANGO_DEBUG", default=False)
|
||||||
|
|
||||||
|
ALLOWED_HOSTS = env.list(
|
||||||
|
"DJANGO_ALLOWED_HOSTS",
|
||||||
|
default=["localhost", "127.0.0.1"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Caching
|
||||||
|
|
||||||
|
REDIS_URI = env("REDIS_URI", default="redis://localhost:6379")
|
||||||
|
|
||||||
|
CACHES = {
|
||||||
|
"default": {
|
||||||
|
"BACKEND": "django_prometheus.cache.backends.redis.RedisCache",
|
||||||
|
"LOCATION": REDIS_URI,
|
||||||
|
"TIMEOUT": None,
|
||||||
|
"KEY_PREFIX": "backend",
|
||||||
|
"VERSION": 1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Celery
|
||||||
|
|
||||||
|
CELERY_BROKER_URL = REDIS_URI
|
||||||
|
|
||||||
|
CELERY_RESULT_BACKEND = REDIS_URI
|
||||||
|
|
||||||
|
CELERY_TIMEZONE = "UTC"
|
||||||
|
|
||||||
|
CELERY_WORKER_SEND_TASK_EVENTS = True
|
||||||
|
|
||||||
|
CELERY_TASK_SEND_SENT_EVENT = True
|
||||||
|
|
||||||
|
CELERY_TASK_TRACK_STARTED = True
|
||||||
|
|
||||||
|
|
||||||
|
# Database
|
||||||
|
|
||||||
|
DB_URI = env.db_url("DJANGO_DB_URI", default="sqlite:///db.sqlite3")
|
||||||
|
DB_URI["ENGINE"] = DB_URI["ENGINE"].replace(
|
||||||
|
"django.db.backends", "django_prometheus.db.backends"
|
||||||
|
)
|
||||||
|
|
||||||
|
DATABASES = {
|
||||||
|
"default": {
|
||||||
|
**DB_URI,
|
||||||
|
"CONN_MAX_AGE": env.int("DJANGO_CONN_MAX_AGE", default=300),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
||||||
|
|
||||||
|
|
||||||
|
# Password validation
|
||||||
|
|
||||||
|
AUTH_PASSWORD_VALIDATORS = [
|
||||||
|
{
|
||||||
|
"NAME": (
|
||||||
|
"django.contrib.auth.password_validation.UserAttributeSimilarityValidator"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"NAME": (
|
||||||
|
"django.contrib.auth.password_validation.MinimumLengthValidator"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"NAME": (
|
||||||
|
"django.contrib.auth.password_validation.CommonPasswordValidator"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"NAME": (
|
||||||
|
"django.contrib.auth.password_validation.NumericPasswordValidator"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# Static files (CSS, JavaScript, Images)
|
||||||
|
|
||||||
|
STATIC_ROOT = BASE_DIR / "static"
|
||||||
|
|
||||||
|
STATIC_URL = env("DJANGO_STATIC_URL", default="static/")
|
||||||
|
|
||||||
|
STATICFILES_DIRS: list[str] = []
|
||||||
|
|
||||||
|
STATICFILES_STORAGE = "django.contrib.staticfiles.storage.StaticFilesStorage"
|
||||||
|
|
||||||
|
STATICFILES_FINDERS = [
|
||||||
|
"django.contrib.staticfiles.finders.FileSystemFinder",
|
||||||
|
"django.contrib.staticfiles.finders.AppDirectoriesFinder",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# Files
|
||||||
|
|
||||||
|
FILE_UPLOAD_MAX_MEMORY_SIZE = 2621440
|
||||||
|
|
||||||
|
# S3 (django-storages)
|
||||||
|
|
||||||
|
AWS_ACCESS_KEY_ID = env("AWS_ACCESS_KEY_ID", default=None)
|
||||||
|
|
||||||
|
AWS_SECRET_ACCESS_KEY = env("AWS_SECRET_ACCESS_KEY", default=None)
|
||||||
|
|
||||||
|
AWS_STORAGE_BUCKET_NAME = env("AWS_STORAGE_BUCKET_NAME", default=None)
|
||||||
|
|
||||||
|
AWS_S3_ENDPOINT_URL = env("AWS_S3_ENDPOINT_URL", default=None)
|
||||||
|
|
||||||
|
AWS_S3_REGION_NAME = env("AWS_S3_REGION_NAME", default=None)
|
||||||
|
|
||||||
|
AWS_S3_USE_SSL = env.bool("AWS_S3_USE_SSL", default=True)
|
||||||
|
|
||||||
|
AWS_S3_VERIFY = env.bool("AWS_S3_VERIFY", default=True)
|
||||||
|
|
||||||
|
AWS_S3_FILE_OVERWRITE = False
|
||||||
|
|
||||||
|
STORAGES = {
|
||||||
|
"default": {
|
||||||
|
"BACKEND": "storages.backends.s3.S3Storage",
|
||||||
|
},
|
||||||
|
"staticfiles": {
|
||||||
|
"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Cors
|
||||||
|
|
||||||
|
CORS_ALLOWED_ORIGINS_FROM_ENV = env.list(
|
||||||
|
"DJANGO_CORS_ALLOWED_ORIGINS", default=["*"]
|
||||||
|
)
|
||||||
|
|
||||||
|
if CORS_ALLOWED_ORIGINS_FROM_ENV == ["*"]:
|
||||||
|
CORS_ALLOW_ALL_ORIGINS = True
|
||||||
|
else:
|
||||||
|
CORS_ALLOWED_ORIGINS = CORS_ALLOWED_ORIGINS_FROM_ENV
|
||||||
|
|
||||||
|
|
||||||
|
# Forms
|
||||||
|
|
||||||
|
FORM_RENDERER = "django.forms.renderers.DjangoTemplates"
|
||||||
|
|
||||||
|
FORMS_URLFIELD_ASSUME_HTTPS = False
|
||||||
|
|
||||||
|
|
||||||
|
# Internationalization
|
||||||
|
|
||||||
|
DATE_FORMAT = "N j, Y"
|
||||||
|
|
||||||
|
DATE_INPUT_FORMATS = [
|
||||||
|
"%Y-%m-%d", # '2006-10-25'
|
||||||
|
"%m/%d/%Y", # '10/25/2006'
|
||||||
|
"%m/%d/%y", # '10/25/06'
|
||||||
|
"%b %d %Y", # 'Oct 25 2006'
|
||||||
|
"%b %d, %Y", # 'Oct 25, 2006'
|
||||||
|
"%d %b %Y", # '25 Oct 2006'
|
||||||
|
"%d %b, %Y", # '25 Oct, 2006'
|
||||||
|
"%B %d %Y", # 'October 25 2006'
|
||||||
|
"%B %d, %Y", # 'October 25, 2006'
|
||||||
|
"%d %B %Y", # '25 October 2006'
|
||||||
|
"%d %B, %Y", # '25 October, 2006'
|
||||||
|
]
|
||||||
|
|
||||||
|
DATETIME_FORMAT = "N j, Y, H:i:s"
|
||||||
|
|
||||||
|
DATETIME_INPUT_FORMATS = [
|
||||||
|
"%Y-%m-%d %H:%M:%S", # '2006-10-25 14:30:59'
|
||||||
|
"%Y-%m-%d %H:%M:%S.%f", # '2006-10-25 14:30:59.000200'
|
||||||
|
"%Y-%m-%d %H:%M", # '2006-10-25 14:30'
|
||||||
|
"%m/%d/%Y %H:%M:%S", # '10/25/2006 14:30:59'
|
||||||
|
"%m/%d/%Y %H:%M:%S.%f", # '10/25/2006 14:30:59.000200'
|
||||||
|
"%m/%d/%Y %H:%M", # '10/25/2006 14:30'
|
||||||
|
"%m/%d/%y %H:%M:%S", # '10/25/06 14:30:59'
|
||||||
|
"%m/%d/%y %H:%M:%S.%f", # '10/25/06 14:30:59.000200'
|
||||||
|
"%m/%d/%y %H:%M", # '10/25/06 14:30'
|
||||||
|
]
|
||||||
|
|
||||||
|
DECIMAL_SEPARATOR = "."
|
||||||
|
|
||||||
|
FIRST_DAY_OF_WEEK = 1
|
||||||
|
|
||||||
|
FORMAT_MODULE_PATH: str | None = None
|
||||||
|
|
||||||
|
LANGUAGE_CODE = env("DJANGO_LANGUAGE_CODE", default="en-us")
|
||||||
|
|
||||||
|
LANGUAGES = [("en", _("English")), ("ru", _("Russian"))]
|
||||||
|
|
||||||
|
LOCALE_PATHS: list[str] = []
|
||||||
|
|
||||||
|
MONTH_DAY_FORMAT = "F j"
|
||||||
|
|
||||||
|
NUMBER_GROUPING = 0
|
||||||
|
|
||||||
|
SHORT_DATE_FORMAT = "m/d/Y"
|
||||||
|
|
||||||
|
SHORT_DATETIME_FORMAT = "m/d/Y H:i:s"
|
||||||
|
|
||||||
|
THOUSAND_SEPARATOR = ","
|
||||||
|
|
||||||
|
TIME_FORMAT = "H:i:s"
|
||||||
|
|
||||||
|
TIME_INPUT_FORMATS = [
|
||||||
|
"%H:%M:%S", # '14:30:59'
|
||||||
|
"%H:%M:%S.%f", # '14:30:59.000200'
|
||||||
|
"%H:%M", # '14:30'
|
||||||
|
]
|
||||||
|
|
||||||
|
TIME_ZONE = "UTC"
|
||||||
|
|
||||||
|
USE_I18N = True
|
||||||
|
|
||||||
|
USE_THOUSAND_SEPARATOR = True
|
||||||
|
|
||||||
|
USE_TZ = True
|
||||||
|
|
||||||
|
YEAR_MONTH_FORMAT = "F Y"
|
||||||
|
|
||||||
|
|
||||||
|
# HTTP
|
||||||
|
|
||||||
|
DATA_UPLOAD_MAX_MEMORY_SIZE: int | None = None
|
||||||
|
|
||||||
|
DATA_UPLOAD_MAX_NUMBER_FIELDS: int | None = None
|
||||||
|
|
||||||
|
DATA_UPLOAD_MAX_NUMBER_FILES: int | None = None
|
||||||
|
|
||||||
|
DEFAULT_CHARSET = "utf-8"
|
||||||
|
|
||||||
|
FORCE_SCRIPT_NAME: str | None = None
|
||||||
|
|
||||||
|
INTERNAL_IPS = env.list(
|
||||||
|
"DJANGO_INTERNAL_IPS",
|
||||||
|
default=["127.0.0.1"],
|
||||||
|
)
|
||||||
|
|
||||||
|
MIDDLEWARE = [
|
||||||
|
"django_prometheus.middleware.PrometheusBeforeMiddleware",
|
||||||
|
"django_guid.middleware.guid_middleware",
|
||||||
|
"corsheaders.middleware.CorsMiddleware",
|
||||||
|
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||||
|
"django.middleware.csrf.CsrfViewMiddleware",
|
||||||
|
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||||
|
"django.contrib.messages.middleware.MessageMiddleware",
|
||||||
|
"django_prometheus.middleware.PrometheusAfterMiddleware",
|
||||||
|
]
|
||||||
|
|
||||||
|
SIGNING_BACKEND = "django.core.signing.TimestampSigner"
|
||||||
|
|
||||||
|
USE_X_FORWARDED_HOST = False
|
||||||
|
|
||||||
|
USE_X_FORWARDED_PORT = False
|
||||||
|
|
||||||
|
WSGI_APPLICATION = "config.wsgi.application"
|
||||||
|
|
||||||
|
AUTH_USER_MODEL = "users.User"
|
||||||
|
|
||||||
|
|
||||||
|
# Logging
|
||||||
|
|
||||||
|
LOGGER_NAME = "lotty"
|
||||||
|
|
||||||
|
LOGGER = logging.getLogger(LOGGER_NAME)
|
||||||
|
|
||||||
|
LOGGING_FILTERS = {
|
||||||
|
"require_debug_true": {
|
||||||
|
"()": "django.utils.log.RequireDebugTrue",
|
||||||
|
},
|
||||||
|
"require_debug_false": {
|
||||||
|
"()": "django.utils.log.RequireDebugFalse",
|
||||||
|
},
|
||||||
|
"correlation_id": {
|
||||||
|
"()": "django_guid.log_filters.CorrelationId",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
LOGGING_FORMATTERS = {
|
||||||
|
"json": {
|
||||||
|
"()": "pythonjsonlogger.jsonlogger.JsonFormatter",
|
||||||
|
"format": (
|
||||||
|
"{levelname}{correlation_id}{asctime}{name}{pathname}{lineno}{message}"
|
||||||
|
),
|
||||||
|
"style": "{",
|
||||||
|
},
|
||||||
|
"text": {
|
||||||
|
"()": "colorlog.ColoredFormatter",
|
||||||
|
"format": (
|
||||||
|
"{log_color}[{levelname}]{reset} "
|
||||||
|
"{light_black}{asctime} {name} | {pathname}:{lineno}{reset}\n"
|
||||||
|
"{bold_black}{message}{reset}"
|
||||||
|
),
|
||||||
|
"log_colors": {
|
||||||
|
"DEBUG": "bold_green",
|
||||||
|
"INFO": "bold_cyan",
|
||||||
|
"WARNING": "bold_yellow",
|
||||||
|
"ERROR": "bold_red",
|
||||||
|
"CRITICAL": "bold_purple",
|
||||||
|
},
|
||||||
|
"style": "{",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
LOGGING_HANDLERS = {
|
||||||
|
"console_debug": {
|
||||||
|
"class": "logging.StreamHandler",
|
||||||
|
"level": "DEBUG",
|
||||||
|
"filters": ["require_debug_true"],
|
||||||
|
"formatter": "text",
|
||||||
|
},
|
||||||
|
"console_prod": {
|
||||||
|
"class": "logging.StreamHandler",
|
||||||
|
"level": "INFO",
|
||||||
|
"filters": ["require_debug_false", "correlation_id"],
|
||||||
|
"formatter": "json",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
LOGGING_LOGGERS = {
|
||||||
|
"django": {
|
||||||
|
"handlers": ["console_debug", "console_prod"],
|
||||||
|
"level": "INFO" if DEBUG else "ERROR",
|
||||||
|
"propagate": False,
|
||||||
|
},
|
||||||
|
"django.request": {
|
||||||
|
"handlers": ["console_debug", "console_prod"],
|
||||||
|
"level": "INFO" if DEBUG else "ERROR",
|
||||||
|
"propagate": False,
|
||||||
|
},
|
||||||
|
"django.server": {
|
||||||
|
"handlers": ["console_debug"],
|
||||||
|
"level": "INFO",
|
||||||
|
"filters": ["require_debug_true"],
|
||||||
|
"propagate": False,
|
||||||
|
},
|
||||||
|
"django.template": {"handlers": []},
|
||||||
|
"django.db.backends.schema": {"handlers": []},
|
||||||
|
"django.security": {"handlers": [], "propagate": True},
|
||||||
|
"django.db.backends": {
|
||||||
|
"handlers": ["console_debug"],
|
||||||
|
"filters": ["require_debug_true"],
|
||||||
|
"level": "DEBUG",
|
||||||
|
"propagate": False,
|
||||||
|
},
|
||||||
|
"health-check": {
|
||||||
|
"handlers": ["console_debug", "console_prod"],
|
||||||
|
"level": "INFO" if DEBUG else "ERROR",
|
||||||
|
"propagate": False,
|
||||||
|
},
|
||||||
|
LOGGER_NAME: {
|
||||||
|
"handlers": ["console_debug", "console_prod"],
|
||||||
|
"level": "DEBUG" if DEBUG else "INFO",
|
||||||
|
"propagate": False,
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"handlers": ["console_debug", "console_prod"],
|
||||||
|
"level": "INFO" if DEBUG else "ERROR",
|
||||||
|
"propagate": False,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
LOGGING = {
|
||||||
|
"version": 1,
|
||||||
|
"disable_existing_loggers": False,
|
||||||
|
"filters": LOGGING_FILTERS,
|
||||||
|
"formatters": LOGGING_FORMATTERS,
|
||||||
|
"handlers": LOGGING_HANDLERS,
|
||||||
|
"loggers": LOGGING_LOGGERS,
|
||||||
|
}
|
||||||
|
|
||||||
|
LOGGING_CONFIG = "logging.config.dictConfig"
|
||||||
|
|
||||||
|
|
||||||
|
# Models
|
||||||
|
|
||||||
|
ABSOLUTE_URL_OVERRIDES: dict[str, Callable[..., Any]] = {}
|
||||||
|
|
||||||
|
FIXTURE_DIRS: list[str] = []
|
||||||
|
|
||||||
|
INSTALLED_APPS = [
|
||||||
|
# Build-in apps
|
||||||
|
"django.contrib.admin",
|
||||||
|
"django.contrib.auth",
|
||||||
|
"django.contrib.contenttypes",
|
||||||
|
"django.contrib.sessions",
|
||||||
|
"django.contrib.messages",
|
||||||
|
"django.contrib.staticfiles",
|
||||||
|
# Healthcheck
|
||||||
|
"health_check",
|
||||||
|
"health_check.db",
|
||||||
|
"health_check.cache",
|
||||||
|
"health_check.storage",
|
||||||
|
"health_check.contrib.migrations",
|
||||||
|
"health_check.contrib.celery",
|
||||||
|
"health_check.contrib.celery_ping",
|
||||||
|
# Third-party apps
|
||||||
|
"corsheaders",
|
||||||
|
"django_extensions",
|
||||||
|
"django_guid",
|
||||||
|
"django_prometheus",
|
||||||
|
"ninja",
|
||||||
|
"storages",
|
||||||
|
# Internal apps
|
||||||
|
"apps.core",
|
||||||
|
"apps.users",
|
||||||
|
# API apps
|
||||||
|
]
|
||||||
|
|
||||||
|
# GUID
|
||||||
|
|
||||||
|
DJANGO_GUID = {
|
||||||
|
"GUID_HEADER_NAME": "Correlation-ID",
|
||||||
|
"VALIDATE_GUID": True,
|
||||||
|
"RETURN_HEADER": True,
|
||||||
|
"EXPOSE_HEADER": True,
|
||||||
|
"INTEGRATIONS": [],
|
||||||
|
"IGNORE_URLS": [],
|
||||||
|
"UUID_LENGTH": 32,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Security
|
||||||
|
|
||||||
|
LANGUAGE_COOKIE_AGE = 31449600
|
||||||
|
|
||||||
|
LANGUAGE_COOKIE_DOMAIN: str | None = None
|
||||||
|
|
||||||
|
LANGUAGE_COOKIE_HTTPONLY = False
|
||||||
|
|
||||||
|
LANGUAGE_COOKIE_NAME = "django_language"
|
||||||
|
|
||||||
|
LANGUAGE_COOKIE_PATH = "/"
|
||||||
|
|
||||||
|
LANGUAGE_COOKIE_SAMESITE = "Lax"
|
||||||
|
|
||||||
|
LANGUAGE_COOKIE_SECURE = False
|
||||||
|
|
||||||
|
SECURE_PROXY_SSL_HEADER: tuple[str, str] | None = None
|
||||||
|
|
||||||
|
CSRF_COOKIE_AGE = 31449600
|
||||||
|
|
||||||
|
CSRF_COOKIE_DOMAIN: str | None = None
|
||||||
|
|
||||||
|
CSRF_COOKIE_HTTPONLY = False
|
||||||
|
|
||||||
|
CSRF_COOKIE_NAME = "djangocsrftoken"
|
||||||
|
|
||||||
|
CSRF_COOKIE_PATH = "/"
|
||||||
|
|
||||||
|
CSRF_COOKIE_SAMESITE = "Lax"
|
||||||
|
|
||||||
|
CSRF_COOKIE_SECURE = False
|
||||||
|
|
||||||
|
CSRF_FAILURE_VIEW = "django.views.csrf.csrf_failure"
|
||||||
|
|
||||||
|
CSRF_HEADER_NAME = "HTTP_X_CSRFTOKEN"
|
||||||
|
|
||||||
|
CSRF_TRUSTED_ORIGINS = env.list(
|
||||||
|
"DJANGO_CSRF_TRUSTED_ORIGINS",
|
||||||
|
default=["http://localhost", "http://127.0.0.1"],
|
||||||
|
)
|
||||||
|
|
||||||
|
CSRF_USE_SESSIONS = False
|
||||||
|
|
||||||
|
SECRET_KEY = env("DJANGO_SECRET_KEY", default="very_insecure_key")
|
||||||
|
|
||||||
|
SECRET_KEY_FALLBACKS: list[str] = []
|
||||||
|
|
||||||
|
|
||||||
|
# Auth
|
||||||
|
|
||||||
|
LOGIN_REDIRECT_URL = "/admin/"
|
||||||
|
|
||||||
|
LOGIN_URL = "/admin/"
|
||||||
|
|
||||||
|
|
||||||
|
# Sessions
|
||||||
|
|
||||||
|
SESSION_CACHE_ALIAS = "default"
|
||||||
|
|
||||||
|
SESSION_COOKIE_AGE = 1209600
|
||||||
|
|
||||||
|
SESSION_COOKIE_DOMAIN: str | None = None
|
||||||
|
|
||||||
|
SESSION_COOKIE_HTTPONLY = True
|
||||||
|
|
||||||
|
SESSION_COOKIE_NAME = "djangosessionid"
|
||||||
|
|
||||||
|
SESSION_COOKIE_PATH = "/"
|
||||||
|
|
||||||
|
SESSION_COOKIE_SAMESITE = "Lax"
|
||||||
|
|
||||||
|
SESSION_COOKIE_SECURE = False
|
||||||
|
|
||||||
|
SESSION_ENGINE = "django.contrib.sessions.backends.db"
|
||||||
|
|
||||||
|
SESSION_EXPIRE_AT_BROWSER_CLOSE = False
|
||||||
|
|
||||||
|
SESSION_FILE_PATH: str | None = None
|
||||||
|
|
||||||
|
SESSION_SAVE_EVERY_REQUEST = False
|
||||||
|
|
||||||
|
SESSION_SERIALIZER = "django.contrib.sessions.serializers.JSONSerializer"
|
||||||
|
|
||||||
|
|
||||||
|
# Templates
|
||||||
|
|
||||||
|
TEMPLATES = [
|
||||||
|
{
|
||||||
|
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
||||||
|
"DIRS": [],
|
||||||
|
"APP_DIRS": True,
|
||||||
|
"OPTIONS": {
|
||||||
|
"autoescape": True,
|
||||||
|
"context_processors": [
|
||||||
|
"django.template.context_processors.debug",
|
||||||
|
"django.template.context_processors.request",
|
||||||
|
"django.contrib.auth.context_processors.auth",
|
||||||
|
"django.contrib.messages.context_processors.messages",
|
||||||
|
],
|
||||||
|
"debug": DEBUG,
|
||||||
|
"string_if_invalid": "",
|
||||||
|
"file_charset": "utf-8",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
if not DEBUG:
|
||||||
|
TEMPLATES[0]["OPTIONS"]["loaders"] = [ # type: ignore[index]
|
||||||
|
(
|
||||||
|
"django.template.loaders.cached.Loader",
|
||||||
|
[
|
||||||
|
"django.template.loaders.filesystem.Loader",
|
||||||
|
"django.template.loaders.app_directories.Loader",
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
|
TEMPLATES[0]["APP_DIRS"] = False
|
||||||
|
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
|
||||||
|
TEST_NON_SERIALIZED_APPS: list[str] = []
|
||||||
|
|
||||||
|
TEST_RUNNER = "django.test.runner.DiscoverRunner"
|
||||||
|
|
||||||
|
|
||||||
|
# URLs
|
||||||
|
|
||||||
|
ROOT_URLCONF = "config.urls"
|
||||||
|
|
||||||
|
|
||||||
|
# debug-toolbar
|
||||||
|
|
||||||
|
DEBUG_TOOLBAR_ENABLED = False
|
||||||
|
|
||||||
|
with contextlib.suppress(Exception):
|
||||||
|
import debug_toolbar # noqa: F401
|
||||||
|
|
||||||
|
DEBUG_TOOLBAR_ENABLED = True
|
||||||
|
|
||||||
|
DEBUG_TOOLBAR_CONFIG = {"SHOW_COLLAPSED": True, "UPDATE_ON_FETCH": True}
|
||||||
|
|
||||||
|
if DEBUG and DEBUG_TOOLBAR_ENABLED:
|
||||||
|
INSTALLED_APPS.append("debug_toolbar")
|
||||||
|
MIDDLEWARE.append("debug_toolbar.middleware.DebugToolbarMiddleware")
|
||||||
|
|
||||||
|
|
||||||
|
# Prometheus
|
||||||
|
|
||||||
|
PROMETHEUS_LATENCY_BUCKETS = (
|
||||||
|
0.005,
|
||||||
|
0.01,
|
||||||
|
0.025,
|
||||||
|
0.05,
|
||||||
|
0.075,
|
||||||
|
0.1,
|
||||||
|
0.25,
|
||||||
|
0.5,
|
||||||
|
0.75,
|
||||||
|
1.0,
|
||||||
|
2.5,
|
||||||
|
5.0,
|
||||||
|
7.5,
|
||||||
|
10.0,
|
||||||
|
25.0,
|
||||||
|
50.0,
|
||||||
|
75.0,
|
||||||
|
float("inf"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# django-silk
|
||||||
|
|
||||||
|
SILKY_ENABLED = env.bool("DJANGO_SILKY_ENABLED", default=False)
|
||||||
|
|
||||||
|
SILKY_PYTHON_PROFILER = env.bool("DJANGO_SILKY_PYTHON_PROFILER", default=False)
|
||||||
|
|
||||||
|
SILKY_PYTHON_PROFILER_BINARY = True
|
||||||
|
|
||||||
|
SILKY_PYTHON_PROFILER_RESULT_PATH = "./profiles"
|
||||||
|
|
||||||
|
SILKY_PYTHON_PROFILER_EXTENDED_FILE_NAME = True
|
||||||
|
|
||||||
|
SILKY_AUTHENTICATION = True
|
||||||
|
|
||||||
|
SILKY_AUTHORISATION = True
|
||||||
|
|
||||||
|
|
||||||
|
def is_allowed_to_use_profiling(user: "User") -> bool:
|
||||||
|
return user.is_staff
|
||||||
|
|
||||||
|
|
||||||
|
SILKY_PERMISSIONS = is_allowed_to_use_profiling
|
||||||
|
|
||||||
|
SILKY_MAX_RECORDED_REQUESTS = 10**3
|
||||||
|
|
||||||
|
SILKY_MAX_RECORDED_REQUESTS_CHECK_PERCENT = 10
|
||||||
|
|
||||||
|
SILKY_MAX_REQUEST_BODY_SIZE = 128
|
||||||
|
|
||||||
|
SILKY_INTERCEPT_PERCENT = 25
|
||||||
|
|
||||||
|
SILKY_META = True
|
||||||
|
|
||||||
|
SILKY_DYNAMIC_PROFILING: list[Any] = []
|
||||||
|
|
||||||
|
if DEBUG and SILKY_ENABLED:
|
||||||
|
INSTALLED_APPS.append("silk")
|
||||||
|
MIDDLEWARE = ["silk.middleware.SilkyMiddleware", *MIDDLEWARE]
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
"""URL configuration for Lotty."""
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.contrib import admin
|
||||||
|
from django.urls import include, path
|
||||||
|
|
||||||
|
from config import handlers
|
||||||
|
|
||||||
|
admin.site.site_title = "Lotty"
|
||||||
|
admin.site.site_header = "Lotty"
|
||||||
|
admin.site.index_title = "Lotty"
|
||||||
|
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
# Admin urls
|
||||||
|
path("admin/", admin.site.urls),
|
||||||
|
# API urls
|
||||||
|
path("", include("api.urls")),
|
||||||
|
# Prometheus urls
|
||||||
|
path("", include("django_prometheus.urls")),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
if settings.DEBUG and settings.DEBUG_TOOLBAR_ENABLED:
|
||||||
|
from debug_toolbar.toolbar import debug_toolbar_urls
|
||||||
|
|
||||||
|
urlpatterns += debug_toolbar_urls()
|
||||||
|
|
||||||
|
if settings.DEBUG and settings.SILKY_ENABLED:
|
||||||
|
urlpatterns.append(path("silk/", include("silk.urls", namespace="silk")))
|
||||||
|
|
||||||
|
handler400 = handlers.handler400
|
||||||
|
|
||||||
|
handler403 = handlers.handler403
|
||||||
|
|
||||||
|
handler404 = handlers.handler404
|
||||||
|
|
||||||
|
handler500 = handlers.handler500
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import uuid
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from django.http import HttpRequest
|
||||||
|
from django.utils.timezone import now
|
||||||
|
from django_guid import get_guid
|
||||||
|
|
||||||
|
|
||||||
|
def build_error_payload(
|
||||||
|
request: HttpRequest,
|
||||||
|
code: str,
|
||||||
|
message: str,
|
||||||
|
details: dict[str, Any] | None = None,
|
||||||
|
field_errors: list[dict[str, Any]] | None = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
trace_id = get_guid() or str(uuid.uuid4())
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"code": code,
|
||||||
|
"message": message,
|
||||||
|
"traceId": str(trace_id),
|
||||||
|
"timestamp": now(),
|
||||||
|
"path": request.path,
|
||||||
|
"details": details,
|
||||||
|
}
|
||||||
|
if field_errors is not None:
|
||||||
|
payload["fieldErrors"] = field_errors
|
||||||
|
return payload
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
"""WSGI config for Lotty."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.core.wsgi import get_wsgi_application
|
||||||
|
|
||||||
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
|
||||||
|
|
||||||
|
application = get_wsgi_application()
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
#!/usr/bin/env just --justfile
|
||||||
|
|
||||||
|
[group('help')]
|
||||||
|
[private]
|
||||||
|
default:
|
||||||
|
@ just --list --list-heading $'justfile manual page:\n'
|
||||||
|
|
||||||
|
# show help
|
||||||
|
[group('help')]
|
||||||
|
help: default
|
||||||
|
|
||||||
|
# runs service
|
||||||
|
[group('run')]
|
||||||
|
run:
|
||||||
|
@ uv run python manage.py runserver
|
||||||
|
|
||||||
|
style:
|
||||||
|
just format
|
||||||
|
just lint
|
||||||
|
just mypy
|
||||||
|
|
||||||
|
check:
|
||||||
|
just style
|
||||||
|
just test
|
||||||
|
just test-coverage
|
||||||
|
|
||||||
|
# lints codebase using golangci-lint
|
||||||
|
[group('lint')]
|
||||||
|
lint:
|
||||||
|
@ uv run ruff check .
|
||||||
|
|
||||||
|
# lints and fixes codebase using ruff
|
||||||
|
[group('lint')]
|
||||||
|
fix:
|
||||||
|
@ uv run ruff check . --fix
|
||||||
|
|
||||||
|
# formats codebase using ruff
|
||||||
|
[group('lint')]
|
||||||
|
format:
|
||||||
|
@ uv run ruff format .
|
||||||
|
|
||||||
|
alias fmt := format
|
||||||
|
|
||||||
|
# lints codebase using mypy
|
||||||
|
[group('lint')]
|
||||||
|
mypy:
|
||||||
|
@ uv run mypy .
|
||||||
|
|
||||||
|
# run tests
|
||||||
|
[group('test')]
|
||||||
|
test:
|
||||||
|
@ uv run python manage.py test
|
||||||
|
|
||||||
|
# run tests with coverage report
|
||||||
|
[group('test')]
|
||||||
|
test-coverage:
|
||||||
|
@ uv run coverage run --source="." manage.py test
|
||||||
|
|
||||||
|
# generates migrations
|
||||||
|
[group('generate')]
|
||||||
|
generate-migrations:
|
||||||
|
@ uv run python manage.py makemigrations
|
||||||
|
|
||||||
|
# applies migrations
|
||||||
|
[group('generate')]
|
||||||
|
apply-migrations:
|
||||||
|
@ uv run python manage.py migrate
|
||||||
|
|
||||||
|
alias m := apply-migrations
|
||||||
Executable
+23
@@ -0,0 +1,23 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# ruff: noqa: PLC0415
|
||||||
|
"""Django's command-line utility for administrative tasks."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
"""Run administrative tasks."""
|
||||||
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
|
||||||
|
try:
|
||||||
|
from django.core.management import execute_from_command_line
|
||||||
|
except ImportError as exc:
|
||||||
|
e = """Couldn't import Django. Are you sure it's installed and "
|
||||||
|
"available on your PYTHONPATH environment variable? Did you "
|
||||||
|
"forget to activate a virtual environment?"""
|
||||||
|
raise ImportError(e) from exc
|
||||||
|
execute_from_command_line(sys.argv)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,229 @@
|
|||||||
|
[project]
|
||||||
|
dependencies = [
|
||||||
|
"celery>=5.5.0,<6.0.0",
|
||||||
|
"colorlog>=6.9.0,<7.0.0",
|
||||||
|
"django-cors-headers>=4.7.0,<5.0.0",
|
||||||
|
"django-environ>=0.12.0,<1.0.0",
|
||||||
|
"django-extensions>=4.1.0,<5.0.0",
|
||||||
|
"django-guid>=3.5.1,<4.0.0",
|
||||||
|
"django-health-check>=3.18.3,<4.0.0",
|
||||||
|
"django-storages[s3]>=1.14,<2.0",
|
||||||
|
"django-ninja>=1.3.0,<2.0.0",
|
||||||
|
"django-prometheus>=2.4.1,<3.0.0",
|
||||||
|
"django-redis>=6.0.0,<7.0.0",
|
||||||
|
"django-silk[formatting]>=5.4.0,<6.0.0",
|
||||||
|
"django-stubs-ext>=5.1.3,<6.0.0",
|
||||||
|
"gunicorn>=23.0.0,<24.0.0",
|
||||||
|
"httpx>=0.28.1,<0.29.0",
|
||||||
|
"opentelemetry-api>=1.35.0",
|
||||||
|
"opentelemetry-distro>=0.56b0",
|
||||||
|
"opentelemetry-exporter-otlp>=1.35.0",
|
||||||
|
"opentelemetry-exporter-zipkin-proto-http>=1.11.1",
|
||||||
|
"opentelemetry-instrumentation-asyncio>=0.56b0",
|
||||||
|
"opentelemetry-instrumentation-celery>=0.56b0",
|
||||||
|
"opentelemetry-instrumentation-dbapi>=0.56b0",
|
||||||
|
"opentelemetry-instrumentation-django>=0.56b0",
|
||||||
|
"opentelemetry-instrumentation-httpx>=0.56b0",
|
||||||
|
"opentelemetry-instrumentation-psycopg2>=0.56b0",
|
||||||
|
"opentelemetry-instrumentation-requests>=0.56b0",
|
||||||
|
"opentelemetry-instrumentation-sqlite3>=0.56b0",
|
||||||
|
"opentelemetry-instrumentation-threading>=0.56b0",
|
||||||
|
"opentelemetry-instrumentation-urllib>=0.56b0",
|
||||||
|
"opentelemetry-instrumentation-urllib3>=0.56b0",
|
||||||
|
"opentelemetry-instrumentation-wsgi>=0.56b0",
|
||||||
|
"opentelemetry-sdk>=1.35.0",
|
||||||
|
"orjson>=3.10.15,<4.0.0",
|
||||||
|
"pillow>=11.1.0,<12.0.0",
|
||||||
|
"psycopg2-binary>=2.9.10,<3.0.0",
|
||||||
|
"pydantic>=2.10.5,<3.0.0",
|
||||||
|
"pyjwt>=2.10.1,<3.0.0",
|
||||||
|
"python-json-logger>=3.2.1,<4.0.0",
|
||||||
|
"pytz>=2024.2,<2025.0",
|
||||||
|
"redis>=6.2.0,<7.0.0",
|
||||||
|
"uvicorn[standard]>=0.34.0,<1.0.0",
|
||||||
|
"uvicorn-worker>=0.2.0,<1.0.0",
|
||||||
|
]
|
||||||
|
name = "lotty-backend"
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.13,<3.15"
|
||||||
|
version = "0.1.0"
|
||||||
|
|
||||||
|
[dependency-groups]
|
||||||
|
dev = [
|
||||||
|
"coverage",
|
||||||
|
"django-debug-toolbar>=5.2,<5.3",
|
||||||
|
"django-stubs[compatible-mypy]",
|
||||||
|
"mypy",
|
||||||
|
"ruff",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.ruff]
|
||||||
|
builtins = []
|
||||||
|
cache-dir = ".ruff_cache"
|
||||||
|
target-version = "py313"
|
||||||
|
exclude = [
|
||||||
|
"**/migrations/*.py",
|
||||||
|
".bzr",
|
||||||
|
".direnv",
|
||||||
|
".eggs",
|
||||||
|
".git",
|
||||||
|
".git-rewrite",
|
||||||
|
".hg",
|
||||||
|
".mypy_cache",
|
||||||
|
".nox",
|
||||||
|
".pants.d",
|
||||||
|
".pytype",
|
||||||
|
".ruff_cache",
|
||||||
|
".svn",
|
||||||
|
".tox",
|
||||||
|
".venv",
|
||||||
|
"__pypackages__",
|
||||||
|
"_build",
|
||||||
|
"buck-out",
|
||||||
|
"dist",
|
||||||
|
"node_modules",
|
||||||
|
"venv",
|
||||||
|
]
|
||||||
|
extend-exclude = []
|
||||||
|
extend-include = []
|
||||||
|
fix = false
|
||||||
|
fix-only = false
|
||||||
|
force-exclude = true
|
||||||
|
include = ["**/pyproject.toml", "*.ipynb", "*.py", "*.pyi"]
|
||||||
|
indent-width = 4
|
||||||
|
line-length = 79
|
||||||
|
namespace-packages = []
|
||||||
|
output-format = "full"
|
||||||
|
preview = true
|
||||||
|
required-version = ">=0.8.4"
|
||||||
|
respect-gitignore = true
|
||||||
|
show-fixes = true
|
||||||
|
src = [".", "src"]
|
||||||
|
unsafe-fixes = false
|
||||||
|
|
||||||
|
[tool.ruff.analyze]
|
||||||
|
detect-string-imports = true
|
||||||
|
direction = "Dependencies"
|
||||||
|
exclude = []
|
||||||
|
include-dependencies = {}
|
||||||
|
preview = false
|
||||||
|
|
||||||
|
[tool.ruff.lint]
|
||||||
|
select = [
|
||||||
|
"A", # flake8-builtins
|
||||||
|
"B", # flake8-bugbear
|
||||||
|
"C4", # flake8-comprehensions
|
||||||
|
"C90", # maccabe
|
||||||
|
"COM", # flake8-commas
|
||||||
|
"D", # pydocstyle
|
||||||
|
"DTZ", # flake8-datetimez
|
||||||
|
"E", # pycodestyle
|
||||||
|
"ERA", # flake8-eradicate
|
||||||
|
"EXE", # flake8-executable
|
||||||
|
"F", # pyflakes
|
||||||
|
"FA", # flake8-future-annotations
|
||||||
|
"FBT", # flake8-boolean-trap
|
||||||
|
"FLY", # pyflint
|
||||||
|
"FLY", # pyflint
|
||||||
|
"FURB", # refurb
|
||||||
|
"G", # flake8-logging-format
|
||||||
|
"I", # isort
|
||||||
|
"ICN", # flake8-import-conventions
|
||||||
|
"ISC", # flake8-implicit-str-concat
|
||||||
|
"LOG", # flake8-logging
|
||||||
|
"N", # pep8-naming
|
||||||
|
"PERF", # perflint
|
||||||
|
"PIE", # flake8-pie
|
||||||
|
"PL", # pylint
|
||||||
|
"PT", # flake8-pytest-style
|
||||||
|
"PTH", # flake8-use-pathlib
|
||||||
|
"Q", # flake8-quotes
|
||||||
|
"RET", # flake8-return
|
||||||
|
"RSE", # flake8-raise
|
||||||
|
"RUF", # ruff
|
||||||
|
"S", # flake8-bandit
|
||||||
|
"SIM", # flake8-simpify
|
||||||
|
"SLF", # flake8-self
|
||||||
|
"SLOT", # flake8-slots
|
||||||
|
"T100", # flake8-debugger
|
||||||
|
"TRY", # tryceratops
|
||||||
|
"UP", # pyupgrade
|
||||||
|
"W", # pycodestyle
|
||||||
|
"YTT", # flake8-2020
|
||||||
|
]
|
||||||
|
ignore = [
|
||||||
|
"PLR1702",
|
||||||
|
"A005", # allow to shadow stdlib and builtin module names
|
||||||
|
"COM812", # trailing comma, conflicts with `ruff format`
|
||||||
|
# Different doc rules that we don't really care about:
|
||||||
|
"D",
|
||||||
|
"ISC001", # implicit string concat conflicts with `ruff format`
|
||||||
|
"ISC003", # prefer explicit string concat over implicit concat
|
||||||
|
"PLR09", # we have our own complexity rules
|
||||||
|
"PLR2004", # do not report magic numbers
|
||||||
|
"PLR6301", # do not require classmethod / staticmethod when self not used
|
||||||
|
"TRY003", # long exception messages from `tryceratops`
|
||||||
|
"N813",
|
||||||
|
"S106",
|
||||||
|
"ERA",
|
||||||
|
"PT022",
|
||||||
|
"RUF001",
|
||||||
|
"RUF012",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.ruff.lint.isort]
|
||||||
|
case-sensitive = true
|
||||||
|
combine-as-imports = true
|
||||||
|
|
||||||
|
[tool.ruff.format]
|
||||||
|
docstring-code-format = true
|
||||||
|
docstring-code-line-length = 79
|
||||||
|
exclude = []
|
||||||
|
indent-style = "space"
|
||||||
|
line-ending = "lf"
|
||||||
|
preview = false
|
||||||
|
quote-style = "double"
|
||||||
|
skip-magic-trailing-comma = false
|
||||||
|
|
||||||
|
[tool.mypy]
|
||||||
|
strict = true
|
||||||
|
strict_bytes = true
|
||||||
|
local_partial_types = true
|
||||||
|
warn_unreachable = true
|
||||||
|
ignore_missing_imports = true
|
||||||
|
plugins = ["mypy_django_plugin.main"]
|
||||||
|
enable_error_code = [
|
||||||
|
"truthy-bool",
|
||||||
|
"truthy-iterable",
|
||||||
|
"redundant-expr",
|
||||||
|
"unused-awaitable",
|
||||||
|
"ignore-without-code",
|
||||||
|
"possibly-undefined",
|
||||||
|
"redundant-self",
|
||||||
|
"explicit-override",
|
||||||
|
"mutable-override",
|
||||||
|
"unimported-reveal",
|
||||||
|
"deprecated",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[tool.mypy.overrides]]
|
||||||
|
module = ["*.migrations.*"]
|
||||||
|
ignore_errors = true
|
||||||
|
|
||||||
|
[tool.django-stubs]
|
||||||
|
django_settings_module = "config.settings"
|
||||||
|
strict_settings = false
|
||||||
|
|
||||||
|
[tool.coverage.run]
|
||||||
|
omit = [
|
||||||
|
"config/asgi.py",
|
||||||
|
"config/errors.py",
|
||||||
|
"config/handlers.py",
|
||||||
|
"config/settings.py",
|
||||||
|
"config/urls.py",
|
||||||
|
"config/wsgi.py",
|
||||||
|
"manage.py",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.coverage.report]
|
||||||
|
skip_covered = true
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
if [ "$RUN_MIGRATIONS" = "true" ]; then
|
||||||
|
echo "Running migrations..."
|
||||||
|
python manage.py migrate --noinput
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$COLLECT_STATIC" = "true" ]; then
|
||||||
|
echo "Collecting static files..."
|
||||||
|
python manage.py collectstatic --noinput
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$OTEL_ENABLED" = "true" ]; then
|
||||||
|
echo "Starting with OpenTelemetry instrumentation..."
|
||||||
|
export OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED="true"
|
||||||
|
export OTEL_TRACES_EXPORTER="${OTEL_TRACES_EXPORTER:-otlp}"
|
||||||
|
export OTEL_METRICS_EXPORTER="${OTEL_METRICS_EXPORTER:-otlp}"
|
||||||
|
export OTEL_LOGS_EXPORTER="${OTEL_LOGS_EXPORTER:-otlp}"
|
||||||
|
export OTEL_EXPORTER_OTLP_ENDPOINT="${OTEL_EXPORTER_OTLP_ENDPOINT:-http://otel-collector:4317}"
|
||||||
|
|
||||||
|
exec opentelemetry-instrument \
|
||||||
|
--service_name "${OTEL_SERVICE_NAME:-backend-django}" \
|
||||||
|
gunicorn config.asgi:application \
|
||||||
|
--workers "${GUNICORN_WORKERS:-4}" \
|
||||||
|
--worker-class "${GUNICORN_WORKER_CLASS:-uvicorn_worker.UvicornWorker}" \
|
||||||
|
--bind "${GUNICORN_BIND:-0.0.0.0:8080}" \
|
||||||
|
--access-logfile "${GUNICORN_ACCESS_LOG:--}" \
|
||||||
|
--error-logfile "${GUNICORN_ERROR_LOG:--}" \
|
||||||
|
--access-logformat '{"remote_ip": "%(h)s", "request_id": "%({X-Request-Id}i)s", "response_code": "%(s)s", "request_method": "%(m)s", "request_path": "%(U)s", "request_timetaken": "%(D)s"}'
|
||||||
|
else
|
||||||
|
echo "Starting without OpenTelemetry instrumentation..."
|
||||||
|
exec gunicorn config.asgi:application \
|
||||||
|
--workers "${GUNICORN_WORKERS:-4}" \
|
||||||
|
--worker-class "${GUNICORN_WORKER_CLASS:-uvicorn_worker.UvicornWorker}" \
|
||||||
|
--bind "${GUNICORN_BIND:-0.0.0.0:8080}" \
|
||||||
|
--access-logfile "${GUNICORN_ACCESS_LOG:--}" \
|
||||||
|
--error-logfile "${GUNICORN_ERROR_LOG:--}" \
|
||||||
|
--access-logformat '{"remote_ip": "%(h)s", "request_id": "%({X-Request-Id}i)s", "response_code": "%(s)s", "request_method": "%(m)s", "request_path": "%(U)s", "request_timetaken": "%(D)s"}'
|
||||||
|
fi
|
||||||
Executable
+11
@@ -0,0 +1,11 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
python manage.py migrate
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "Migration failed"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$DJANGO_CREATE_SUPERUSER" = "True" ]; then
|
||||||
|
python manage.py createsuperuser --noinput --username "$DJANGO_SUPERUSER_USERNAME" --email "$DJANGO_SUPERUSER_EMAIL" || true
|
||||||
|
fi
|
||||||
Generated
+1943
File diff suppressed because it is too large
Load Diff
-100
@@ -1,100 +0,0 @@
|
|||||||
program HealthServer;
|
|
||||||
|
|
||||||
{$mode objfpc}{$H+}
|
|
||||||
|
|
||||||
uses
|
|
||||||
cthreads,
|
|
||||||
fphttpserver, httpdefs, sysutils;
|
|
||||||
|
|
||||||
const
|
|
||||||
LOTTY =
|
|
||||||
'╔═════════════════════════════════════════════════════════════════════╗' + LineEnding +
|
|
||||||
'║ ║' + LineEnding +
|
|
||||||
'║ ⠀⠀⠀⠀⢀⡀⣄⢀⡄⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣀⣤⣴⡶⠶⠶⠖⠒⠒⠒⠲⠶⠶⢶⣤⣤⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡀⠀⠀⠀⠀⠀⠀ ║' + LineEnding +
|
|
||||||
'║ ⠀⠀⠀⠰⣮⡿⠛⠻⠷⣯⣶⣀⡀⠀⠀⠀⠀⣀⣴⠾⠛⠉⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠉⠛⠶⣤⡀⠀⠀⠀⠀⠀⣀⣤⣾⣼⣿⣷⣿⣠⠀⠀⠀⠀ ║' + LineEnding +
|
|
||||||
'║ ⠀⠀⠀⢛⣿⡄⠀⠀⠀⠀⠙⢿⣶⣂⣀⣤⡾⠋⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⠷⣄⠀⣀⣶⣿⠟⠋⠁⠀⠀⢹⣿⠆⠀⠀⠀ ║' + LineEnding +
|
|
||||||
'║ ⠀⠀⠀⠀⠾⣻⣦⡀⠀⠀⠀⠀⠙⣿⡿⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⢻⣿⠋⠁⠀⠀⠀⢀⣴⢿⡍⠀⠀⠀⠀ ║' + LineEnding +
|
|
||||||
'║ ⠀⠀⠀⠀⠀⠈⠸⢻⡷⣦⣄⡀⣰⠏⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠹⣦⣠⣤⣴⣾⢿⠝⠃⠀⠀⠀⠀⠀ ║' + LineEnding +
|
|
||||||
'║ ⠀⠀⠀⣀⢀⠀⡀⠀⠈⠋⠿⢹⡏⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢹⣿⠃⠋⠉⠀⢀⠀⡀⣀⠀⠀⠀ ║' + LineEnding +
|
|
||||||
'║ ⣴⣮⣷⣿⣾⣾⣧⣿⣼⣶⣆⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢿⣴⣄⣷⣿⡾⠷⠿⠾⣾⣧⡄ ║' + LineEnding +
|
|
||||||
'║ ⠶⢿⡋⠀⠀⠀⠀⠀⠀⠉⠉⢻⡏⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⡟⠋⠁⠀⠀⠀⠀⠀⢀⣿⠶ ║' + LineEnding +
|
|
||||||
'║ ⠉⠽⣷⣦⣄⣀⣀⠀⠀⠀⣀⣸⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⡇⠀⠀⠀⢀⣀⣤⣴⣿⡏⠁ ║' + LineEnding +
|
|
||||||
'║ ⠀⠀⠁⠋⠻⠹⠿⠟⡿⠻⠻⠟⣿⠀⠀⠀⠀⠀⣴⣾⣿⣶⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣴⣿⣷⣦⠀⠀⠀⠀⣾⢻⠟⡿⠻⠛⠏⠛⠈⠁⠀⠀ ║' + LineEnding +
|
|
||||||
'║ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⣄⣤⣰⣿⣆⠀⠀⠀⠀⣿⣿⣿⣿⡇⠀⠀⠀⠀⢀⠀⠀⠀⠀⠀⣀⠀⠀⠀⠀⢸⣿⣿⣿⣿⠃⠀⠀⣰⣿⣖⣴⢀⡀⡀⠀⠀⠀⠀⠀⠀ ║' + LineEnding +
|
|
||||||
'║ ⠀⠀⠀⠀⠀⣀⢰⣼⣷⠿⠛⠋⠉⠹⣦⡀⠀⠀⠈⠛⠛⠋⠀⠀⠀⠀⠀⠉⠛⠛⠛⠛⠛⠉⠀⠀⠀⠀⠀⠉⠛⠛⠁⠀⠀⣴⠏⠉⠙⠛⠿⢾⣧⣶⣠⠀⠀⠀⠀ ║' + LineEnding +
|
|
||||||
'║ ⠀⠀⠀⠀⣰⣾⠟⠉⠀⠀⠀⠀⠀⣠⣾⡷⣦⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣤⢞⣷⣤⡀⠀⠀⠀⠀⠈⠙⢿⣄⡀⠀⠀ ║' + LineEnding +
|
|
||||||
'║ ⠀⠀⠀⠈⣽⣯⡀⠀⠀⢀⣠⣴⣿⡝⠊⠀⠈⠙⠶⣤⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⣤⠶⠋⠁⠀⠑⠋⣿⢶⣤⣤⣀⣀⣠⣼⢯⡅⠀⠀ ║' + LineEnding +
|
|
||||||
'║ ⠀⠀⠀⠀⠐⠋⠿⢻⠿⡏⠷⠙⠁⠀⠀⠀⠀⠀⠀⠀⠉⠛⠳⠶⣤⣤⣄⣀⣀⣀⣀⣀⣀⣀⣠⣤⣤⠶⠚⠋⠉⠀⠀⠀⠀⠀⠀⠀⠈⠐⠃⠘⠋⠗⠙⠁⠀⠀⠀ ║' + LineEnding +
|
|
||||||
'║ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⡿⠁⠈⠉⠉⠉⠉⠉⠉⠉⠁⠀⢹⡆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ ║' + LineEnding +
|
|
||||||
'║ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣾⠃⠉⠉⠙⣆⠀⠀⠀⢰⠋⠉⠉⠀⢿⡀⠀⠀⠀╭─────────────────────╮ ║' + LineEnding +
|
|
||||||
'║ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⡏⠀⠐⠓⠒⠃⠀⠀⠀⠈⠓⠚⠂⠀⠸⣇⠀⠀⠀│ LOTTY says: │ ║' + LineEnding +
|
|
||||||
'║ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣾⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⠀⠀⠀│ "Теперь вы PROD!" │ ║' + LineEnding +
|
|
||||||
'║ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⠀⠀⠀╰─────────────────────╯ ║' + LineEnding +
|
|
||||||
'║ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢹⡄⠀⣠⠚⠙⡆⠀⠀⠀⢰⠋⠑⣆⠀⢰⠇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ ║' + LineEnding +
|
|
||||||
'║ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠻⣄⠀⠀⠰⠁⠀⠀⠀⠈⠃⠀⢀⣰⠏⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ ║' + LineEnding +
|
|
||||||
'║ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠛⢶⣤⣤⣤⣤⣤⣤⣤⡶⠋⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ ║' + LineEnding +
|
|
||||||
'║ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⢻⣇⠀⠀⠀⢸⡏⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ ║' + LineEnding +
|
|
||||||
'║ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⠀⣿⠀⠀⢀⡿⢰⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ ║' + LineEnding +
|
|
||||||
'║ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⡄⣿⠀⠀⣼⢇⡾⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ ║' + LineEnding +
|
|
||||||
'║ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⢷⣿⠀⢰⣿⡞⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ ║' + LineEnding +
|
|
||||||
'║ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⢿⣤⠿⠋⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ ║' + LineEnding +
|
|
||||||
'║ ║' + LineEnding +
|
|
||||||
'║ ██████╗ ██████╗ ██████╗ ██████╗ ║' + LineEnding +
|
|
||||||
'║ ██╔══██╗██╔══██╗██╔═══██╗██╔══██╗ ║' + LineEnding +
|
|
||||||
'║ ██████╔╝██████╔╝██║ ██║██║ ██║ ║' + LineEnding +
|
|
||||||
'║ ██╔═══╝ ██╔══██╗██║ ██║██║ ██║ ║' + LineEnding +
|
|
||||||
'║ ██║ ██║ ██║╚██████╔╝██████╔╝ ║' + LineEnding +
|
|
||||||
'║ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ║' + LineEnding +
|
|
||||||
'║ ║' + LineEnding +
|
|
||||||
'║ ║' + LineEnding +
|
|
||||||
'║ Status: HEALTHY ║' + LineEnding +
|
|
||||||
'║ ║' + LineEnding +
|
|
||||||
'║ ║' + LineEnding +
|
|
||||||
'║ ║' + LineEnding +
|
|
||||||
'║ ║' + LineEnding +
|
|
||||||
'║ © Powered by Watlon ║' + LineEnding +
|
|
||||||
'╚═════════════════════════════════════════════════════════════════════╝' + LineEnding;
|
|
||||||
|
|
||||||
type
|
|
||||||
THandler = class
|
|
||||||
procedure HandleRequest(Sender: TObject; var Req: TFPHTTPConnectionRequest;
|
|
||||||
var Res: TFPHTTPConnectionResponse);
|
|
||||||
end;
|
|
||||||
|
|
||||||
procedure THandler.HandleRequest(Sender: TObject; var Req: TFPHTTPConnectionRequest;
|
|
||||||
var Res: TFPHTTPConnectionResponse);
|
|
||||||
begin
|
|
||||||
Res.ContentType := 'text/plain; charset=utf-8';
|
|
||||||
|
|
||||||
if (Req.URI = '/ping') or (Req.URI = '/ready') or (Req.URI = '/health') then
|
|
||||||
begin
|
|
||||||
Res.Code := 200;
|
|
||||||
Res.Content := LOTTY;
|
|
||||||
end
|
|
||||||
else
|
|
||||||
begin
|
|
||||||
Res.Code := 404;
|
|
||||||
Res.Content := 'not found' + LineEnding;
|
|
||||||
end;
|
|
||||||
end;
|
|
||||||
|
|
||||||
var
|
|
||||||
Server: TFPHTTPServer;
|
|
||||||
Handler: THandler;
|
|
||||||
|
|
||||||
begin
|
|
||||||
Handler := THandler.Create;
|
|
||||||
Server := TFPHTTPServer.Create(nil);
|
|
||||||
try
|
|
||||||
Server.Port := 80;
|
|
||||||
Server.Threaded := True;
|
|
||||||
Server.OnRequest := @Handler.HandleRequest;
|
|
||||||
Server.Active := True;
|
|
||||||
|
|
||||||
WriteLn('Listening on :80');
|
|
||||||
while True do Sleep(3600 * 1000);
|
|
||||||
finally
|
|
||||||
Server.Free;
|
|
||||||
Handler.Free;
|
|
||||||
end;
|
|
||||||
end.
|
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# Lotty Tests
|
||||||
|
|
||||||
|
There is `unit` and `e2e` tests available, `unit` tests are placed all around `backend` service folder and `e2e` tests placed [here](./e2e/).
|
||||||
|
|
||||||
|
## Running unit tests
|
||||||
|
|
||||||
|
### Backend service
|
||||||
|
|
||||||
|
See [services/backend/README.md](../src/backend/README.md#testing).
|
||||||
|
|
||||||
|
## Unit tests coverage
|
||||||
|
|
||||||
|
### Backend service
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Running e2e tests
|
||||||
|
|
||||||
|
See [tests/e2e/README.md](./e2e/README.md).
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# Below all environment variables and default values
|
||||||
|
|
||||||
|
BACKEND_BASE_URL=http://127.0.0.1:13240
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
share/python-wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
# Usually these files are written by a python script from a template
|
||||||
|
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
*.py,cover
|
||||||
|
.hypothesis/
|
||||||
|
.pytest_cache/
|
||||||
|
cover/
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
*.pot
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
*.log
|
||||||
|
local_settings.py
|
||||||
|
db.sqlite3
|
||||||
|
db.sqlite3-journal
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
.scrapy
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
.pybuilder/
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# IPython
|
||||||
|
profile_default/
|
||||||
|
ipython_config.py
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
# For a library or package, you might want to ignore these files since the code is
|
||||||
|
# intended to run in multiple environments; otherwise, check them in:
|
||||||
|
.python-version
|
||||||
|
|
||||||
|
# pipenv
|
||||||
|
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||||
|
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||||
|
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||||
|
# install all needed dependencies.
|
||||||
|
Pipfile.lock
|
||||||
|
|
||||||
|
# UV
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
||||||
|
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||||
|
# commonly ignored for libraries.
|
||||||
|
uv.lock
|
||||||
|
|
||||||
|
# poetry
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||||
|
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||||
|
# commonly ignored for libraries.
|
||||||
|
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||||
|
poetry.lock
|
||||||
|
|
||||||
|
# pdm
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||||
|
#pdm.lock
|
||||||
|
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||||
|
# in version control.
|
||||||
|
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
|
||||||
|
.pdm.toml
|
||||||
|
.pdm-python
|
||||||
|
.pdm-build/
|
||||||
|
|
||||||
|
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||||
|
__pypackages__/
|
||||||
|
|
||||||
|
# Celery stuff
|
||||||
|
celerybeat-schedule
|
||||||
|
celerybeat.pid
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
*.sage.py
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
.spyderproject
|
||||||
|
.spyproject
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
.ropeproject
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
/site
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
.mypy_cache/
|
||||||
|
.dmypy.json
|
||||||
|
dmypy.json
|
||||||
|
|
||||||
|
# Pyre type checker
|
||||||
|
.pyre/
|
||||||
|
|
||||||
|
# pytype static type analyzer
|
||||||
|
.pytype/
|
||||||
|
|
||||||
|
# Cython debug symbols
|
||||||
|
cython_debug/
|
||||||
|
|
||||||
|
# PyCharm
|
||||||
|
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||||
|
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||||
|
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||||
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# PyPI configuration file
|
||||||
|
.pypirc
|
||||||
|
|
||||||
|
# Ruff files
|
||||||
|
.ruff_cache
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
# E2E tests for Lotty
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
Ensure you have the following installed on your system:
|
||||||
|
|
||||||
|
- [Python](https://www.python.org/) (>=3.10,<3.14)
|
||||||
|
- [uv](https://docs.astral.sh/uv/) (latest version recommended)
|
||||||
|
- [Docker](https://www.docker.com/) (latest version recommended)
|
||||||
|
- [Docker compose](https://docs.docker.com/compose/) (latest version recommended)
|
||||||
|
|
||||||
|
## Warning
|
||||||
|
|
||||||
|
Please note that containers will use ports from 13240 to 13248, so there is must be no listeners on this ports range.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
### Clone the project
|
||||||
|
|
||||||
|
### Go to the project directory
|
||||||
|
|
||||||
|
### Install dependencies
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv sync --no-dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### Customize environment (optional)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.template .env
|
||||||
|
```
|
||||||
|
|
||||||
|
And setup env vars according to your needs.
|
||||||
|
|
||||||
|
### Run tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run pytest .
|
||||||
|
```
|
||||||
|
|
||||||
|
### Results
|
||||||
|
|
||||||
|
You will see something like `n passed in Ns`
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from collections.abc import Generator
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
import pytest
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
BACKEND_BASE_URL = os.getenv("BACKEND_BASE_URL", "http://127.0.0.1:13240")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session", autouse=True)
|
||||||
|
def docker_compose() -> Generator[None]:
|
||||||
|
# btw, this is just in case you've forgotten to shut down compose :)
|
||||||
|
subprocess.run(
|
||||||
|
executable="docker",
|
||||||
|
args=[
|
||||||
|
"docker",
|
||||||
|
"compose",
|
||||||
|
"--profile",
|
||||||
|
"loadtest",
|
||||||
|
"--profile",
|
||||||
|
"minio",
|
||||||
|
"--profile",
|
||||||
|
"observability",
|
||||||
|
"--profile",
|
||||||
|
"proxy",
|
||||||
|
"--profile",
|
||||||
|
"telegram_bot",
|
||||||
|
"down",
|
||||||
|
],
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
subprocess.run(
|
||||||
|
executable="docker",
|
||||||
|
args=[
|
||||||
|
"docker",
|
||||||
|
"compose",
|
||||||
|
"--project-name",
|
||||||
|
"template-testing",
|
||||||
|
"--profile",
|
||||||
|
"minio",
|
||||||
|
"up",
|
||||||
|
"-d",
|
||||||
|
"--build",
|
||||||
|
"--force-recreate",
|
||||||
|
"--remove-orphans",
|
||||||
|
],
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
time.sleep(5)
|
||||||
|
|
||||||
|
yield
|
||||||
|
|
||||||
|
subprocess.run(
|
||||||
|
executable="docker",
|
||||||
|
args=[
|
||||||
|
"docker",
|
||||||
|
"compose",
|
||||||
|
"--project-name",
|
||||||
|
"template-testing",
|
||||||
|
"--profile",
|
||||||
|
"minio",
|
||||||
|
"down",
|
||||||
|
"-v",
|
||||||
|
],
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
def client() -> Generator[httpx.Client]:
|
||||||
|
with httpx.Client(base_url=BACKEND_BASE_URL, timeout=10.0) as client:
|
||||||
|
yield client
|
||||||
|
|
||||||
|
|
||||||
|
def pytest_collection_modifyitems(items: list[pytest.Item]) -> None:
|
||||||
|
items.sort(key=lambda item: "test_health" not in item.nodeid)
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
[project]
|
||||||
|
dependencies = ["httpx>=0.28.1", "pytest>=8.3.4", "python-dotenv>=1.0.1"]
|
||||||
|
name = "lotty-tests-e2e"
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.10,<3.14"
|
||||||
|
version = "0.1.0"
|
||||||
|
|
||||||
|
[dependency-groups]
|
||||||
|
dev = ["ruff"]
|
||||||
|
|
||||||
|
[tool.ruff]
|
||||||
|
builtins = []
|
||||||
|
cache-dir = ".ruff_cache"
|
||||||
|
exclude = [
|
||||||
|
".bzr",
|
||||||
|
".direnv",
|
||||||
|
".eggs",
|
||||||
|
".git",
|
||||||
|
".git-rewrite",
|
||||||
|
".hg",
|
||||||
|
".mypy_cache",
|
||||||
|
".nox",
|
||||||
|
".pants.d",
|
||||||
|
".pytype",
|
||||||
|
".ruff_cache",
|
||||||
|
".svn",
|
||||||
|
".tox",
|
||||||
|
".venv",
|
||||||
|
"__pypackages__",
|
||||||
|
"_build",
|
||||||
|
"buck-out",
|
||||||
|
"dist",
|
||||||
|
"migrations",
|
||||||
|
"node_modules",
|
||||||
|
"venv",
|
||||||
|
]
|
||||||
|
extend-exclude = []
|
||||||
|
extend-include = []
|
||||||
|
fix = false
|
||||||
|
fix-only = false
|
||||||
|
force-exclude = true
|
||||||
|
include = ["**/pyproject.toml", "*.ipynb", "*.py", "*.pyi"]
|
||||||
|
indent-width = 4
|
||||||
|
line-length = 79
|
||||||
|
namespace-packages = []
|
||||||
|
output-format = "full"
|
||||||
|
preview = false
|
||||||
|
required-version = ">=0.8.4"
|
||||||
|
respect-gitignore = true
|
||||||
|
show-fixes = true
|
||||||
|
src = [".", "src"]
|
||||||
|
target-version = "py310"
|
||||||
|
unsafe-fixes = false
|
||||||
|
|
||||||
|
[tool.ruff.analyze]
|
||||||
|
detect-string-imports = true
|
||||||
|
direction = "Dependencies"
|
||||||
|
exclude = []
|
||||||
|
include-dependencies = {}
|
||||||
|
preview = false
|
||||||
|
|
||||||
|
[tool.ruff.format]
|
||||||
|
docstring-code-format = true
|
||||||
|
docstring-code-line-length = 79
|
||||||
|
exclude = []
|
||||||
|
indent-style = "space"
|
||||||
|
line-ending = "lf"
|
||||||
|
preview = false
|
||||||
|
quote-style = "double"
|
||||||
|
skip-magic-trailing-comma = false
|
||||||
|
|
||||||
|
[tool.ruff.lint]
|
||||||
|
allowed-confusables = ["ℹ"]
|
||||||
|
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
|
||||||
|
exclude = ["tests.py"]
|
||||||
|
explicit-preview-rules = false
|
||||||
|
extend-fixable = []
|
||||||
|
extend-per-file-ignores = {}
|
||||||
|
extend-safe-fixes = []
|
||||||
|
extend-select = []
|
||||||
|
extend-unsafe-fixes = []
|
||||||
|
external = []
|
||||||
|
fixable = ["ALL"]
|
||||||
|
ignore = ["ANN401", "ARG", "COM812", "D", "FBT001", "FBT002", "N813", "S101"]
|
||||||
|
logger-objects = []
|
||||||
|
per-file-ignores = {}
|
||||||
|
preview = false
|
||||||
|
select = ["ALL"]
|
||||||
|
task-tags = ["FIXME", "HACK", "TODO", "WORKOUT"]
|
||||||
|
typing-modules = []
|
||||||
|
unfixable = []
|
||||||
|
|
||||||
|
[tool.ruff.lint.pylint]
|
||||||
|
max-args = 6
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
[pytest]
|
||||||
|
log_cli = true
|
||||||
|
log_level = INFO
|
||||||
Executable
+8
@@ -0,0 +1,8 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
GREEN='\033[1;32m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
uvx ruff format .
|
||||||
|
uvx ruff check . --fix
|
||||||
|
printf "${GREEN}Linters/formatters runned${NC}\n"
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import logging
|
||||||
|
import time
|
||||||
|
from http import HTTPStatus as status
|
||||||
|
|
||||||
|
from httpx import Client
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def test_generate_ad_text(client: Client) -> None:
|
||||||
|
"""
|
||||||
|
Tests integration between:
|
||||||
|
- backend
|
||||||
|
- redis
|
||||||
|
- yandexgpt
|
||||||
|
- celery
|
||||||
|
"""
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"advertiser_name": "Центральный Университет",
|
||||||
|
"ad_title": "Всероссийский кейс-чемпионат DEADLINE",
|
||||||
|
}
|
||||||
|
response = client.post("/generate/ad_text", json=payload)
|
||||||
|
assert response.status_code == status.OK
|
||||||
|
|
||||||
|
response_data = response.json()
|
||||||
|
assert "task_id" in response_data, "Missing task_id in response"
|
||||||
|
|
||||||
|
task_id = response_data["task_id"]
|
||||||
|
start_time = time.time()
|
||||||
|
|
||||||
|
while True:
|
||||||
|
result_response = client.get(f"/generate/ad_text/{task_id}/result")
|
||||||
|
assert result_response.status_code in (status.OK, status.NOT_FOUND)
|
||||||
|
result_data = result_response.json()
|
||||||
|
|
||||||
|
if (
|
||||||
|
result_data.get("status") == "SUCCESS"
|
||||||
|
and result_response.status_code == status.OK
|
||||||
|
):
|
||||||
|
assert isinstance(result_data.get("result"), str), (
|
||||||
|
"Result must be a string"
|
||||||
|
)
|
||||||
|
elapsed_time = time.time() - start_time
|
||||||
|
logger.info(
|
||||||
|
"Task %s completed in %.2f seconds", task_id, elapsed_time
|
||||||
|
)
|
||||||
|
logger.info("Generated Ad Text: %s", result_data["result"])
|
||||||
|
break
|
||||||
|
|
||||||
|
time.sleep(1)
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import logging
|
||||||
|
from http import HTTPStatus as status
|
||||||
|
|
||||||
|
from httpx import Client
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def test_healthcheck(client: Client) -> None:
|
||||||
|
"""
|
||||||
|
Tests integration between:
|
||||||
|
- redis
|
||||||
|
- celery
|
||||||
|
- postgres
|
||||||
|
- minio
|
||||||
|
- yandexgpt
|
||||||
|
"""
|
||||||
|
|
||||||
|
response = client.get("/health?format=json")
|
||||||
|
assert response.status_code == status.OK
|
||||||
|
|
||||||
|
response_data = response.json()
|
||||||
|
|
||||||
|
unhealthy_services = [
|
||||||
|
service
|
||||||
|
for service, status in response_data.items()
|
||||||
|
if status != "working"
|
||||||
|
]
|
||||||
|
|
||||||
|
for service in unhealthy_services:
|
||||||
|
logger.error(
|
||||||
|
"Service %s unhealthy: %s", service, response_data[service]
|
||||||
|
)
|
||||||
|
|
||||||
|
assert not unhealthy_services, (
|
||||||
|
f"Some services are unhealthy: {', '.join(unhealthy_services)}"
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user