all repos

onasty @ ecdd5a0f7bd8d3a7948aad8aec355e56acbe07d9

a one-time notes service
20 files changed, 320 insertions(+), 24 deletions(-)
feat: metrics (#24)

* feat: setup prometheus handler in app

* chore: setup prometheus service

* go mod tidy

* feat(metrics): add custom_http_metrics

* feat(http): add middleware to record http metrics

* feat(metrics): record http requests latency

* feat(infra): add grafana

* refactor(logger): add more configuration options

* go mod tidy

* chore(docker): run the app in docker

* chore: setup loki and promtail

* chore(grafana): add datasources

* chore(prometheus): use right address

* fix(transport): count 400 responses as errors, and dont log request_id
twice

* chore(taskfile): build containers on start

* fixup! chore(prometheus): use right address

* refactor(promtail): remove usless pipelines

* refactor(prometheus): update scraping intervals

* Revert "refactor(logger): add more configuration options"

This reverts commit 6fee06063373df746afd88529111eecae0df163c.

* clean up

* chore(loki): dont use alter manager since there's none

* do not set grpc ports

* reformat prometheus config

* refactor: make metrics port consistent with server one

* chore(Taskfile): fix migration commands

* chore(taskfile): update run command

* feat(mail): add migrations

* fix(prometheus): use right port

* chore(docker): fix the tag for core service
Author: Smirnov Oleksandr ss2316544@gmail.com
Committed by: GitHub noreply@github.com
Committed at: 2024-10-05 23:23:51 +0300
Parent: a4b3d8e
M .env.example

@@ -1,7 +1,8 @@

APP_ENV=debug -APP_URL=http://localhost:3000 -SERVER_PORT=3000 +APP_URL=http://localhost:8000 +SERVER_PORT=8000 PASSWORD_SALT=onasty +METRICS_PORT=8001 LOG_LEVEL=debug LOG_FORMAT=text

@@ -13,10 +14,11 @@ JWT_REFRESH_TOKEN_TTL=360d

POSTGRES_USERNAME=onasty POSTGRES_PASSWORD=qwerty -POSTGRES_HOST=127.0.0.1 +POSTGRES_HOST=postgres POSTGRES_PORT=5432 POSTGRES_DATABASE=onasty POSTGRESQL_DSN="postgres://$POSTGRES_USERNAME:$POSTGRES_PASSWORD@$POSTGRES_HOST:$POSTGRES_PORT/$POSTGRES_DATABASE?sslmode=disable" +MIGRATION_DSN="postgres://$POSTGRES_USERNAME:$POSTGRES_PASSWORD@localhost:$POSTGRES_PORT/$POSTGRES_DATABASE?sslmode=disable" MAILGUN_FROM=onasty@mail.com MAILGUN_DOMAI='<domain>'
A Dockerfile

@@ -0,0 +1,15 @@

+FROM golang:1.23.1-alpine AS builder + +WORKDIR /app + +COPY go.mod go.sum ./ +RUN go mod download + +COPY cmd cmd +COPY internal internal + +RUN CGO_ENABLED=0 GOOS=linux go build -ldflags='-w -s' -o /onasty ./cmd/server + +FROM scratch +COPY --from=builder /onasty /onasty +ENTRYPOINT ["/onasty"]
M Taskfile.yml

@@ -11,14 +11,13 @@ build:

- go build -o .bin/onasty ./cmd/server/ run: - - task: build - - .bin/onasty + - docker compose up -d --build core lint: - golangci-lint run docker:up: - - docker compose up -d + - docker compose up -d --build --remove-orphans docker:down: aliases: [docker:stop]
M cmd/server/main.go

@@ -15,6 +15,7 @@ "github.com/olexsmir/onasty/internal/hasher"

"github.com/olexsmir/onasty/internal/jwtutil" "github.com/olexsmir/onasty/internal/logger" "github.com/olexsmir/onasty/internal/mailer" + "github.com/olexsmir/onasty/internal/metrics" "github.com/olexsmir/onasty/internal/service/notesrv" "github.com/olexsmir/onasty/internal/service/usersrv" "github.com/olexsmir/onasty/internal/store/psql/noterepo"

@@ -87,11 +88,22 @@

// http server srv := httpserver.NewServer(cfg.ServerPort, handler.Handler()) go func() { - slog.Debug("starting http server", "port", cfg.ServerPort) + slog.Info("starting http server", "port", cfg.ServerPort) if err := srv.Start(); !errors.Is(err, http.ErrServerClosed) { slog.Error("failed to start http server", "error", err) } }() + + // metrics + if cfg.MetricsEnabled { + mSrv := httpserver.NewServer(cfg.MetricsPort, metrics.Handler()) + go func() { + slog.Info("starting metrics server", "port", cfg.MetricsPort) + if err := mSrv.Start(); !errors.Is(err, http.ErrServerClosed) { + slog.Error("failed to start metrics server", "error", err) + } + }() + } // graceful shutdown quit := make(chan os.Signal, 1)
M docker-compose.yml

@@ -1,4 +1,15 @@

services: + core: + image: onasty:core + container_name: onasty-core + build: + context: . + dockerfile: Dockerfile + env_file: .env + ports: + - 8000:8000 + - 8001:8001 + postgres: image: postgres:16-alpine container_name: onasty-postgres

@@ -10,3 +21,41 @@ volumes:

- .docker/postgres:/var/lib/postgresql/data ports: - 5432:5432 + + prometheus: + image: prom/prometheus + container_name: onasty-prometheus + user: root + volumes: + - ./.docker/prometheus:/prometheus + - ./infra/prometheus:/etc/prometheus + ports: + - 9090:9090 + + grafana: + image: grafana/grafana:11.1.6 + container_name: onasty-grafana + user: root + environment: + - GF_SECURITY_ADMIN_USER=admin + - GF_SECURITY_ADMIN_PASSWORD=admin + volumes: + - ./.docker/grafana:/var/lib/grafana + - ./infra/grafana/datasources.yml:/etc/grafana/provisioning/datasources/datasources.yml + ports: + - 3069:3000 + + loki: + image: grafana/loki:3.2.0 + command: ["--pattern-ingester.enabled=true", "-config.file=/etc/loki/config.yaml"] + ports: + - 3100:3100 + volumes: + - ./infra/loki/config.yaml:/etc/loki/config.yaml + + promtail: + image: grafana/promtail:3.0.0 + command: -config.file=/etc/promtail/config.yaml + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - ./infra/promtail/config.yaml:/etc/promtail/config.yaml
M go.mod

@@ -11,6 +11,7 @@ github.com/henvic/pgq v0.0.3

github.com/jackc/pgx-gofrs-uuid v0.0.0-20230224015001-1d428863c2e2 github.com/jackc/pgx/v5 v5.7.1 github.com/mailgun/mailgun-go/v4 v4.16.0 + github.com/prometheus/client_golang v1.20.4 github.com/stretchr/testify v1.9.0 github.com/testcontainers/testcontainers-go v0.33.0 github.com/testcontainers/testcontainers-go/modules/postgres v0.33.0

@@ -21,9 +22,11 @@ dario.cat/mergo v1.0.0 // indirect

github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 // indirect github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/beorn7/perks v1.0.1 // indirect github.com/bytedance/sonic v1.11.6 // indirect github.com/bytedance/sonic/loader v0.1.1 // indirect github.com/cenkalti/backoff/v4 v4.2.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cloudwego/base64x v0.1.4 // indirect github.com/cloudwego/iasm v0.2.0 // indirect github.com/containerd/log v0.1.0 // indirect

@@ -60,7 +63,7 @@ github.com/jackc/pgtype v1.14.0 // indirect

github.com/jackc/pgx/v4 v4.18.2 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/compress v1.17.4 // indirect + github.com/klauspost/compress v1.17.9 // indirect github.com/klauspost/cpuid/v2 v2.2.7 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/lib/pq v1.10.9 // indirect

@@ -77,12 +80,16 @@ github.com/moby/term v0.5.0 // indirect

github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/morikuni/aec v1.0.0 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.0 // indirect github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.55.0 // indirect + github.com/prometheus/procfs v0.15.1 // indirect github.com/shirou/gopsutil/v3 v3.23.12 // indirect github.com/shoenig/go-m1cpu v0.1.6 // indirect github.com/sirupsen/logrus v1.9.3 // indirect

@@ -96,7 +103,7 @@ go.opentelemetry.io/otel v1.29.0 // indirect

go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 // indirect go.opentelemetry.io/otel/metric v1.29.0 // indirect go.opentelemetry.io/otel/trace v1.29.0 // indirect - go.uber.org/atomic v1.7.0 // indirect + go.uber.org/atomic v1.9.0 // indirect golang.org/x/arch v0.8.0 // indirect golang.org/x/crypto v0.27.0 // indirect golang.org/x/net v0.29.0 // indirect
M go.sum

@@ -9,6 +9,8 @@ github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc=

github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/ahmetb/go-linq v3.0.0+incompatible h1:qQkjjOXKrKOTy83X8OpRmnKflXKQIL/mC/gMVVDMhOA= github.com/ahmetb/go-linq v3.0.0+incompatible/go.mod h1:PFffvbdbtw+QTB0WKRP0cNht7vnCfnGlEpak/DVg5cY= github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=

@@ -17,6 +19,8 @@ github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=

github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=

@@ -169,8 +173,8 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=

github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= -github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= +github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= +github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=

@@ -178,13 +182,15 @@ github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=

github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= -github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=

@@ -227,6 +233,8 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=

github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=

@@ -240,6 +248,14 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=

github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/prometheus/client_golang v1.20.4 h1:Tgh3Yr67PaOv/uTqloMsCEdeuFTatm5zIq5+qNN23vI= +github.com/prometheus/client_golang v1.20.4/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= +github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=

@@ -314,8 +330,8 @@ go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=

go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= -go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= -go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
A infra/grafana/datasources.yml

@@ -0,0 +1,13 @@

+apiVersion: 1 +datasources: + - name: loki + type: loki + access: proxy + url: http://loki:3100 + isDefault: false + + - name: prometheus + type: prometheus + access: proxy + url: http://prometheus:9090 + isDefault: true
A infra/loki/config.yaml

@@ -0,0 +1,29 @@

+auth_enabled: false + +server: + http_listen_port: 3100 + +limits_config: + allow_structured_metadata: false + +common: + path_prefix: /tmp/loki + storage: + filesystem: + chunks_directory: /tmp/loki/chunks + rules_directory: /tmp/loki/rules + replication_factor: 1 + ring: + instance_addr: 127.0.0.1 + kvstore: + store: inmemory + +schema_config: + configs: + - from: 2020-10-24 + store: boltdb-shipper + object_store: filesystem + schema: v11 + index: + prefix: index_ + period: 24h
A infra/prometheus/prometheus.yml

@@ -0,0 +1,13 @@

+global: + scrape_interval: 5s + scrape_timeout: 2s + evaluation_interval: 15s + +scrape_configs: + - job_name: core + metrics_path: /metrics + scheme: http + follow_redirects: true + honor_timestamps: true + static_configs: + - targets: [core:8001]
A infra/promtail/config.yaml

@@ -0,0 +1,20 @@

+server: + http_listen_port: 9080 + +positions: + filename: /tmp/positions.yaml + +clients: + - url: http://loki:3100/loki/api/v1/push + +scrape_configs: + - job_name: containers + docker_sd_configs: + - host: unix:///var/run/docker.sock + relabel_configs: + - source_labels: [__meta_docker_container_name] + target_label: container + regex: '/(.+)' + replacement: '$1' + - source_labels: [__meta_docker_container_id] + target_label: container_id
M internal/config/config.go

@@ -22,6 +22,9 @@ MailgunDomain string

MailgunAPIKey string VerificationTokenTTL time.Duration + MetricsEnabled bool + MetricsPort string + LogLevel string LogFormat string LogShowLine bool

@@ -49,6 +52,9 @@ MailgunAPIKey: getenvOrDefault("MAILGUN_API_KEY", ""),

VerificationTokenTTL: mustParseDurationOrPanic( getenvOrDefault("VERIFICATION_TOKEN_TTL", "24h"), ), + + MetricsPort: getenvOrDefault("METRICS_PORT", "3001"), + MetricsEnabled: getenvOrDefault("METRICS_ENABLED", "true") == "true", LogLevel: getenvOrDefault("LOG_LEVEL", "debug"), LogFormat: getenvOrDefault("LOG_FORMAT", "json"),
M internal/mailer/mailgun.go

@@ -5,6 +5,8 @@ "context"

"log/slog" "github.com/mailgun/mailgun-go/v4" + "github.com/olexsmir/onasty/internal/metrics" + "github.com/olexsmir/onasty/internal/transport/http/reqid" ) var _ Mailer = (*Mailgun)(nil)

@@ -27,10 +29,16 @@ func (m *Mailgun) Send(ctx context.Context, to, subject, content string) error {

msg := m.mg.NewMessage(m.from, subject, "", to) msg.SetHtml(content) + slog.InfoContext(ctx, "email sent", "to", to) + _, _, err := m.mg.Send(ctx, msg) + if err != nil { + metrics.RecordEmailFailed(reqid.GetContext(ctx)) + return err + } - slog.InfoContext(ctx, "email sent", "to", to) slog.DebugContext(ctx, "email sent", "subject", subject, "content", content, "err", err) + metrics.RecordEmailSent() - return err + return nil }
A internal/metrics/http_metrics.go

@@ -0,0 +1,49 @@

+package metrics + +import ( + "time" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" +) + +var ( + successfulHTTPRequest = promauto.NewCounterVec(prometheus.CounterOpts{ //nolint:exhaustruct + Name: "http_successful_requests_total", + Help: "the total number of successful http requests", + ConstLabels: map[string]string{"status": "success"}, + }, []string{"method", "uri"}) + + failedHTTPRequest = promauto.NewCounterVec(prometheus.CounterOpts{ //nolint:exhaustruct + Name: "http_failed_requests_total", + Help: "the total number of failed http requests", + ConstLabels: map[string]string{"status": "failure"}, + }, []string{"method", "uri"}) + + latencyHTTPRequest = promauto.NewHistogramVec(prometheus.HistogramOpts{ //nolint:exhaustruct + Name: "http_request_latency_seconds", + Help: "the latency of http requests in seconds", + Buckets: prometheus.DefBuckets, + }, []string{"method", "uri"}) +) + +func RecordSuccessfulRequestMetric(method, uri string) { + go successfulHTTPRequest.With(prometheus.Labels{ + "method": method, + "uri": uri, + }).Inc() +} + +func RecordFailedRequestMetric(method, uri string) { + go failedHTTPRequest.With(prometheus.Labels{ + "method": method, + "uri": uri, + }).Inc() +} + +func RecordLatencyRequestMetric(method, uri string, latency time.Duration) { + go latencyHTTPRequest.With(prometheus.Labels{ + "method": method, + "uri": uri, + }).Observe(latency.Seconds()) +}
A internal/metrics/mail_metrics.go

@@ -0,0 +1,28 @@

+package metrics + +import ( + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" +) + +var ( + emailSentSuccessfully = promauto.NewCounter(prometheus.CounterOpts{ //nolint:exhaustruct + Name: "mail_sent_total", + Help: "the total number of successfully sent email", + }) + + emailFailedToSend = promauto.NewCounterVec(prometheus.CounterOpts{ //nolint:exhaustruct + Name: "mail_failed_total", + Help: "the total number of email that failed to send", + }, []string{"request_id"}) +) + +func RecordEmailSent() { + go emailSentSuccessfully.Inc() +} + +func RecordEmailFailed(reqid string) { + go emailFailedToSend.With(prometheus.Labels{ + "request_id": reqid, + }).Inc() +}
A internal/metrics/metrics.go

@@ -0,0 +1,13 @@

+package metrics + +import ( + "net/http" + + "github.com/prometheus/client_golang/prometheus/promhttp" +) + +func Handler() http.Handler { + mux := http.NewServeMux() + mux.Handle("GET /metrics", promhttp.Handler()) + return mux +}
M internal/transport/http/apiv1/apiv1.go

@@ -22,6 +22,7 @@ }

} func (a *APIV1) Routes(r *gin.RouterGroup) { + r.Use(a.metricsMiddleware) auth := r.Group("/auth") { auth.POST("/signup", a.signUpHandler)
M internal/transport/http/apiv1/middleware.go

@@ -3,9 +3,11 @@

import ( "context" "strings" + "time" "github.com/gin-gonic/gin" "github.com/gofrs/uuid/v5" + "github.com/olexsmir/onasty/internal/metrics" "github.com/olexsmir/onasty/internal/models" )

@@ -50,6 +52,22 @@ c.Set(userIDCtxKey, uid)

} c.Next() +} + +func (a *APIV1) metricsMiddleware(c *gin.Context) { + start := time.Now() + c.Next() + latency := time.Since(start) + + metrics.RecordLatencyRequestMetric(c.Request.Method, c.Request.RequestURI, latency) + + if c.Writer.Status() >= 200 && c.Writer.Status() < 300 { + metrics.RecordSuccessfulRequestMetric(c.Request.Method, c.Request.RequestURI) + } + + if c.Writer.Status() >= 400 { + metrics.RecordFailedRequestMetric(c.Request.Method, c.Request.RequestURI) + } } //nolint:unused // TODO: remove me later
M internal/transport/http/middlewares.go

@@ -5,7 +5,6 @@ "log/slog"

"time" "github.com/gin-gonic/gin" - "github.com/olexsmir/onasty/internal/transport/http/reqid" ) func (t *Transport) logger() gin.HandlerFunc {

@@ -22,7 +21,7 @@ path = path + "?" + raw

} lvl := slog.LevelInfo - if c.Writer.Status() >= 500 { + if c.Writer.Status() >= 400 { lvl = slog.LevelError }

@@ -30,7 +29,6 @@ slog.LogAttrs(

c.Request.Context(), lvl, c.Errors.ByType(gin.ErrorTypePrivate).String(), - slog.String("request_id", reqid.Get(c)), slog.String("latency", latency.String()), slog.String("method", c.Request.Method), slog.Int("status_code", c.Writer.Status()),
M migrations/Taskfile.yml

@@ -10,13 +10,13 @@ cmds:

- migrate create -ext sql -dir {{.MIGRATIONS_DIR}} {{ .CLI_ARGS }} up: - - migrate -database $POSTGRESQL_DSN -path {{.MIGRATIONS_DIR}} up + - migrate -database $MIGRATION_DSN -path {{.MIGRATIONS_DIR}} up down: - - migrate -database $POSTGRESQL_DSN -path {{.MIGRATIONS_DIR}} down 1 + - migrate -database $MIGRATION_DSN -path {{.MIGRATIONS_DIR}} down 1 drop: - - migrate -database $POSTGRESQL_DSN -path {{.MIGRATIONS_DIR}} drop + - migrate -database $MIGRATION_DSN -path {{.MIGRATIONS_DIR}} drop current-version: - - migrate -database $POSTGRESQL_DSN -path {{.MIGRATIONS_DIR}} version + - migrate -database $MIGRATION_DSN -path {{.MIGRATIONS_DIR}} version