init: added template

This commit is contained in:
ITQ
2026-02-12 11:36:43 +03:00
parent 030e49edb9
commit 514393e3f8
96 changed files with 14594 additions and 183 deletions
+16
View File
@@ -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
+2
View File
@@ -0,0 +1,2 @@
*.png filter=lfs diff=lfs merge=lfs -text
*.gif filter=lfs diff=lfs merge=lfs -text
+25
View File
@@ -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
View File
@@ -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"]
-60
View File
@@ -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/>
Решение принимается организаторами по совокупности признаков и результатов проверок.
+14
View File
@@ -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
+12
View File
@@ -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
+146
View File
@@ -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
+297
View File
@@ -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
+141
View File
@@ -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
-6
View File
@@ -1,6 +0,0 @@
services:
api:
platform: linux/amd64
build: .
ports:
- "80:80"
+2
View File
@@ -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>500ms
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>500ms
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
@@ -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
+9
View File
@@ -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
+4
View File
@@ -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
+188
View File
@@ -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
+54
View File
@@ -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=
+176
View File
@@ -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
+64
View File
@@ -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;"]
+123
View File
@@ -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
```
View File
+30
View File
@@ -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"
),
]
View File
+186
View File
@@ -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),
]
+43
View File
@@ -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))
+33
View File
@@ -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")
View File
View File
+6
View File
@@ -0,0 +1,6 @@
from django.apps import AppConfig
class CoreConfig(AppConfig):
name = "apps.core"
label = "core"
+50
View File
@@ -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
View File
+6
View File
@@ -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()),
],
),
]
+11
View File
@@ -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")
+17
View File
@@ -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()
+20
View File
@@ -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
+3
View File
@@ -0,0 +1,3 @@
from config.celery import app as celery_app
__all__ = ("celery_app",)
+9
View File
@@ -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()
+10
View File
@@ -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()
+13
View File
@@ -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
+64
View File
@@ -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,
)
+657
View File
@@ -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]
+38
View File
@@ -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
+28
View File
@@ -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
+9
View File
@@ -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()
+69
View File
@@ -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
+23
View File
@@ -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()
View File
+229
View File
@@ -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
+41
View File
@@ -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
+11
View File
@@ -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
+1943
View File
File diff suppressed because it is too large Load Diff
-100
View File
@@ -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.
+19
View File
@@ -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
![backend coverage](../assets/images/backend-coverage.png)
## Running e2e tests
See [tests/e2e/README.md](./e2e/README.md).
+3
View File
@@ -0,0 +1,3 @@
# Below all environment variables and default values
BACKEND_BASE_URL=http://127.0.0.1:13240
+170
View File
@@ -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
+44
View File
@@ -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`
+81
View File
@@ -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)
+94
View File
@@ -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
+3
View File
@@ -0,0 +1,3 @@
[pytest]
log_cli = true
log_level = INFO
+8
View File
@@ -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"
View File
@@ -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)
+38
View File
@@ -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)}"
)