From 056e2ad52992754caa43d081f766262f0b8f95b9 Mon Sep 17 00:00:00 2001 From: alxeg Date: Thu, 5 Mar 2026 11:21:18 +0300 Subject: [PATCH] Initial commit --- .gitignore | 17 ++++ .vscode/launch.json | 20 ++++ cmd/payouts/main.go | 19 ++++ config/payouts.properties | 22 +++++ go.mod | 42 ++++++++ go.sum | 93 ++++++++++++++++++ internal/api/module.go | 95 +++++++++++++++++++ internal/api/version/handler.go | 45 +++++++++ internal/config/app.go | 12 +++ internal/config/module.go | 86 +++++++++++++++++ internal/config/server.go | 20 ++++ internal/log/config/log.go | 14 +++ internal/log/module.go | 56 +++++++++++ internal/service/monitoring/config/metrics.go | 62 ++++++++++++ internal/service/monitoring/metrics.go | 95 +++++++++++++++++++ internal/service/monitoring/module.go | 24 +++++ internal/version/package.go | 6 ++ internal/version/version.txt | 1 + 18 files changed, 729 insertions(+) create mode 100644 .gitignore create mode 100644 .vscode/launch.json create mode 100644 cmd/payouts/main.go create mode 100644 config/payouts.properties create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/api/module.go create mode 100644 internal/api/version/handler.go create mode 100644 internal/config/app.go create mode 100644 internal/config/module.go create mode 100644 internal/config/server.go create mode 100644 internal/log/config/log.go create mode 100644 internal/log/module.go create mode 100644 internal/service/monitoring/config/metrics.go create mode 100644 internal/service/monitoring/metrics.go create mode 100644 internal/service/monitoring/module.go create mode 100644 internal/version/package.go create mode 100644 internal/version/version.txt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d88d613 --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +/bin/* +contrib/dbg/.* +debug +debug.test +__debug_bin +__debug_bin* +*.sublime-project +*.sublime-workspace +*.un~ +*.swp +.idea/ +*.iml +*.log +/logs +static +testdata/ +/payouts.properties diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..1e89f74 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,20 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + + { + "name": "Launch Package", + "type": "go", + "request": "launch", + "mode": "auto", + "cwd": "${workspaceFolder}/config", + "program": "${workspaceFolder}/cmd/payouts", + "env": { + "CONFIG_PATH": "${workspaceFolder}/payouts.properties" + } + } + ] +} \ No newline at end of file diff --git a/cmd/payouts/main.go b/cmd/payouts/main.go new file mode 100644 index 0000000..c411e87 --- /dev/null +++ b/cmd/payouts/main.go @@ -0,0 +1,19 @@ +package main + +import ( + "go.uber.org/fx" + + "payouts/internal/api" + "payouts/internal/config" + "payouts/internal/log" +) + +func main() { + + app := fx.New( + api.Module, + config.Module, + log.Module, + ) + app.Run() +} diff --git a/config/payouts.properties b/config/payouts.properties new file mode 100644 index 0000000..8cec8ab --- /dev/null +++ b/config/payouts.properties @@ -0,0 +1,22 @@ +Server.Port = :8080 +Server.WriteTimeout = 35s +Server.ReadTimeout = 35s +Server.EnablePProfEndpoints = false + +Socket.MaxHttpBufferSize = 2097152 +Socket.PingInterval = 25s +Socket.PingTimeout = 20s +Socket.Debug = false + +# Prometheus settings +Metrics.Endpoint = /metrics +Metrics.HistogramBuckets = 0.001,0.002,0.005,0.01,0.025,0.05,0.1,0.25,0.5,1,2.5,5,10 + +Metrics.Http.HistogramEnabled = true +Metrics.Http.Buckets = 0.001,0.002,0.005,0.01,0.025,0.05,0.1,0.25,0.5,1,2.5,5,10 + +Log.Level = DEBUG +Log.FilePath = ./logs/payouts.log +Log.TextOutput = false +Log.StdoutEnabled = true +Log.FileEnabled = false diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..abbcb6b --- /dev/null +++ b/go.mod @@ -0,0 +1,42 @@ +module payouts + +go 1.24.4 + +require ( + github.com/go-viper/encoding/javaproperties v0.1.0 + github.com/go-viper/mapstructure/v2 v2.5.0 + github.com/gorilla/mux v1.8.1 + github.com/ogier/pflag v0.0.1 + github.com/prometheus/client_golang v1.23.2 + github.com/samber/slog-multi v1.7.1 + github.com/spf13/viper v1.21.0 + go.uber.org/fx v1.24.0 +) + +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/magiconair/properties v1.8.7 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.66.1 // indirect + github.com/prometheus/procfs v0.16.1 // indirect + github.com/sagikazarmark/locafero v0.11.0 // indirect + github.com/samber/lo v1.52.0 // indirect + github.com/samber/slog-common v0.20.0 // indirect + github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect + github.com/spf13/afero v1.15.0 // indirect + github.com/spf13/cast v1.10.0 // indirect + github.com/spf13/pflag v1.0.10 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + go.uber.org/dig v1.19.0 // indirect + go.uber.org/multierr v1.10.0 // indirect + go.uber.org/zap v1.26.0 // indirect + go.yaml.in/yaml/v2 v2.4.2 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/sys v0.35.0 // indirect + golang.org/x/text v0.28.0 // indirect + google.golang.org/protobuf v1.36.8 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..ef0a452 --- /dev/null +++ b/go.sum @@ -0,0 +1,93 @@ +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/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/go-viper/encoding/javaproperties v0.1.0 h1:4pQN/pez/rMy9ITZ++SgLH6VIN3zWzNNuWFHKjrpn6w= +github.com/go-viper/encoding/javaproperties v0.1.0/go.mod h1:LGaThjx5J/GFdQRJscxLMQsYt0XKAM7IW9YzsJTv6jw= +github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= +github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +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/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +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/ogier/pflag v0.0.1 h1:RW6JSWSu/RkSatfcLtogGfFgpim5p7ARQ10ECk5O750= +github.com/ogier/pflag v0.0.1/go.mod h1:zkFki7tvTa0tafRvTBIZTvzYyAu6kQhPZFnshFFPE+g= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +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/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= +github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= +github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= +github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= +github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= +github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw= +github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0= +github.com/samber/slog-common v0.20.0 h1:WaLnm/aCvBJSk5nR5aXZTFBaV0B47A+AEaEOiZDeUnc= +github.com/samber/slog-common v0.20.0/go.mod h1:+Ozat1jgnnE59UAlmNX1IF3IByHsODnnwf9jUcBZ+m8= +github.com/samber/slog-multi v1.7.1 h1:aCLXHRxgU+2v0PVlEOh7phynzM7CRo89ZgFtOwaqVEE= +github.com/samber/slog-multi v1.7.1/go.mod h1:A4KQC99deqfkCDJcL/cO3kX6McX7FffQAx/8QHink+c= +github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= +github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U= +github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= +github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= +github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= +github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= +github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +go.uber.org/dig v1.19.0 h1:BACLhebsYdpQ7IROQ1AGPjrXcP5dF80U3gKoFzbaq/4= +go.uber.org/dig v1.19.0/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE= +go.uber.org/fx v1.24.0 h1:wE8mruvpg2kiiL1Vqd0CC+tr0/24XIB10Iwp2lLWzkg= +go.uber.org/fx v1.24.0/go.mod h1:AmDeGyS+ZARGKM4tlH4FY2Jr63VjbEDJHtqXTGP5hbo= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= +go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= +go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= +go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= +go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= +golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= +golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= +google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= +google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/api/module.go b/internal/api/module.go new file mode 100644 index 0000000..915ad85 --- /dev/null +++ b/internal/api/module.go @@ -0,0 +1,95 @@ +package api + +import ( + "context" + "fmt" + "log/slog" + "net/http" + "net/http/pprof" + + "github.com/gorilla/mux" + "github.com/prometheus/client_golang/prometheus/promhttp" + "go.uber.org/fx" + + "payouts/internal/api/version" + appConfig "payouts/internal/config" + "payouts/internal/service/monitoring" +) + +// Module is a fx module +var Module = fx.Options( + version.Module, + monitoring.Module, + fx.Invoke(RegisterRoutes), +) + +const BaseRoute = "/api/v1" + +// Params represents the module input params +type Params struct { + fx.In + Logger *slog.Logger + + AppConfig *appConfig.App + + Version version.Handler + + Metrics monitoring.Metrics +} + +// RegisterRoutes registers the api routes and starts the http server +func RegisterRoutes(p Params, lc fx.Lifecycle) { + + router := mux.NewRouter() + router.StrictSlash(true) + + router.HandleFunc(version.Route, p.Version.VersionHandler).Methods(http.MethodGet) + + if p.AppConfig.Server.EnablePProfEndpoints { + router.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline) + router.HandleFunc("/debug/pprof/profile", pprof.Profile) + router.HandleFunc("/debug/pprof/symbol", pprof.Symbol) + router.HandleFunc("/debug/pprof/trace", pprof.Trace) + router.NewRoute().PathPrefix("/debug/pprof/").HandlerFunc(pprof.Index) + } + + apiRouter := router.PathPrefix(BaseRoute).Subrouter() + apiRouter.HandleFunc("/test", func(http.ResponseWriter, *http.Request) { + slog.Info("Test called", slog.String("sample", "value")) + + }) + // data + apiRouter.Use(p.Metrics.GetMiddleware()) + + router.Handle(p.AppConfig.Metrics.Endpoint, promhttp.Handler()) + + srv := http.Server{ + Handler: router, + + Addr: p.AppConfig.Server.Port, + WriteTimeout: p.AppConfig.Server.WriteTimeout, + ReadTimeout: p.AppConfig.Server.ReadTimeout, + } + + lc.Append(fx.Hook{ + OnStart: func(c context.Context) error { + go func() { + var err error + + slog.Info(fmt.Sprintf("Starting server on port %s", p.AppConfig.Server.Port)) + if p.AppConfig.Server.Tls.Enabled { + err = srv.ListenAndServeTLS(p.AppConfig.Server.Tls.CertFile, p.AppConfig.Server.Tls.KeyFile) + } else { + err = srv.ListenAndServe() + } + if err != nil { + panic(err) + } + }() + return nil + }, + OnStop: func(ctx context.Context) error { + return srv.Shutdown(ctx) + }, + }) +} diff --git a/internal/api/version/handler.go b/internal/api/version/handler.go new file mode 100644 index 0000000..0cc3e4b --- /dev/null +++ b/internal/api/version/handler.go @@ -0,0 +1,45 @@ +package version + +import ( + "io" + "net/http" + + "go.uber.org/fx" + + // import embedded version + "payouts/internal/version" +) + +// Route version route +const Route = "/version" + +// Module is a fx module +var Module = fx.Options( + fx.Provide(New), +) + +// New constructs a new config Handler. +func New() (Handler, error) { + + return &handler{ + version.Version, + }, nil +} + +// Handler config handler interface +type Handler interface { + VersionHandler(http.ResponseWriter, *http.Request) +} + +type handler struct { + version string +} + +// VersionHandler handles the version requests +func (h *handler) VersionHandler(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + + w.Header().Set("Content-Type", "text/plain") + w.WriteHeader(http.StatusOK) + io.WriteString(w, h.version) +} diff --git a/internal/config/app.go b/internal/config/app.go new file mode 100644 index 0000000..0853a24 --- /dev/null +++ b/internal/config/app.go @@ -0,0 +1,12 @@ +package config + +import ( + logging "payouts/internal/log/config" + monitoring "payouts/internal/service/monitoring/config" +) + +type App struct { + Server Server + Metrics monitoring.Metrics + Log logging.Log +} diff --git a/internal/config/module.go b/internal/config/module.go new file mode 100644 index 0000000..ac6e2b4 --- /dev/null +++ b/internal/config/module.go @@ -0,0 +1,86 @@ +package config + +import ( + "fmt" + "path/filepath" + "strings" + + "github.com/go-viper/encoding/javaproperties" + "github.com/go-viper/mapstructure/v2" + "github.com/ogier/pflag" + "github.com/spf13/viper" + "go.uber.org/fx" + + monitoring "payouts/internal/service/monitoring/config" +) + +const ( + ConfigPathArg = "config-path" + ConfigPathDefault = "./payouts.properties" +) + +var Module = fx.Provide(NewAppConfig) + +func getConfigData(filePath string) (string, string, string) { + dir, file := filepath.Split(filePath) + base := filepath.Base(file) + ext := filepath.Ext(base) + + confPath, _ := filepath.Abs(dir) + confName := strings.TrimSuffix(base, ext) + confType := strings.Trim(ext, ".") + + return confPath, confName, confType +} + +func NewAppConfig() (*App, error) { + mainConfig := &App{} + + configPaths := []string{ConfigPathDefault} + + configPath := pflag.String(ConfigPathArg, "", "") + pflag.Parse() + + configPaths = append(configPaths, *configPath) + + codecRegistry := viper.NewCodecRegistry() + codec := &javaproperties.Codec{} + codecRegistry.RegisterCodec("properties", codec) + codecRegistry.RegisterCodec("props", codec) + codecRegistry.RegisterCodec("prop", codec) + + conf := viper.New() + + for num, path := range configPaths { + if len(path) < 1 { + continue + } + tempConf := viper.NewWithOptions( + viper.WithCodecRegistry(codecRegistry), + ) + + confPath, confName, confType := getConfigData(path) + tempConf.AddConfigPath(confPath) + tempConf.SetConfigName(confName) + tempConf.SetConfigType(confType) + + err := tempConf.ReadInConfig() + if err != nil { + // complain on missed non-default config + if num > 0 { + fmt.Printf("Can't read config from %v, Error: %v\n", path, err) + } + } else { + _ = conf.MergeConfigMap(tempConf.AllSettings()) + } + } + err := conf.Unmarshal(mainConfig, viper.DecodeHook( + mapstructure.ComposeDecodeHookFunc( + mapstructure.TextUnmarshallerHookFunc(), + mapstructure.StringToSliceHookFunc(","), + mapstructure.StringToTimeDurationHookFunc(), + monitoring.CommaSeparatedFloat64SliceHookFunc(), + ))) + + return mainConfig, err +} diff --git a/internal/config/server.go b/internal/config/server.go new file mode 100644 index 0000000..d409396 --- /dev/null +++ b/internal/config/server.go @@ -0,0 +1,20 @@ +package config + +import ( + "time" +) + +type Tls struct { + Enabled bool + CertFile string + KeyFile string +} + +// Server represents the server configiration +type Server struct { + Tls Tls + Port string + WriteTimeout time.Duration + ReadTimeout time.Duration + EnablePProfEndpoints bool +} diff --git a/internal/log/config/log.go b/internal/log/config/log.go new file mode 100644 index 0000000..9584bfa --- /dev/null +++ b/internal/log/config/log.go @@ -0,0 +1,14 @@ +package config + +import "log/slog" + +type Log struct { + Level slog.Level + + FilePath string + + TextOutput bool + StdoutEnabled bool + FileEnabled bool + FluentEnabled bool +} diff --git a/internal/log/module.go b/internal/log/module.go new file mode 100644 index 0000000..77edd8d --- /dev/null +++ b/internal/log/module.go @@ -0,0 +1,56 @@ +package log + +import ( + "log/slog" + "os" + + slogmulti "github.com/samber/slog-multi" + "go.uber.org/fx" + + "payouts/internal/config" +) + +var Module = fx.Options( + fx.Provide(NewLogger), +) + +// Params represents the module input params +type Params struct { + fx.In + + AppConfig *config.App +} + +func NewLogger(p Params) (*slog.Logger, error) { + logConfig := p.AppConfig.Log + opts := &slog.HandlerOptions{ + Level: logConfig.Level, + } + + handlers := []slog.Handler{} + if logConfig.StdoutEnabled { + if logConfig.TextOutput { + handlers = append(handlers, slog.NewTextHandler(os.Stdout, opts)) + } else { + handlers = append(handlers, slog.NewJSONHandler(os.Stdout, opts)) + } + } + + if logConfig.FileEnabled { + file, err := os.OpenFile(logConfig.FilePath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644) + if err != nil { + return nil, err + } + + if logConfig.TextOutput { + handlers = append(handlers, slog.NewTextHandler(file, opts)) + } else { + handlers = append(handlers, slog.NewJSONHandler(file, opts)) + } + } + + logger := slog.New(slogmulti.Fanout(handlers...)) + slog.SetDefault(logger) + + return logger, nil +} diff --git a/internal/service/monitoring/config/metrics.go b/internal/service/monitoring/config/metrics.go new file mode 100644 index 0000000..761aa29 --- /dev/null +++ b/internal/service/monitoring/config/metrics.go @@ -0,0 +1,62 @@ +/* + * Copyright (c) New Cloud Technologies, Ltd., 2013-2026 + * + * You can not use the contents of the file in any way without New Cloud Technologies Ltd. written permission. + * To obtain such a permit, you should contact New Cloud Technologies, Ltd. at https://myoffice.ru/contacts/ + */ + +package config + +import ( + "reflect" + "strconv" + "strings" + + "github.com/go-viper/mapstructure/v2" +) + +type CommaSeparatedFloat64Slice []float64 + +func CommaSeparatedFloat64SliceHookFunc() mapstructure.DecodeHookFuncType { + return func( + f reflect.Type, + t reflect.Type, + data interface{}, + ) (interface{}, error) { + // Check that the data is string + if f.Kind() != reflect.String { + return data, nil + } + + // Check that the target type is our custom type + if t != reflect.TypeOf(CommaSeparatedFloat64Slice{}) { + return data, nil + } + + stringSlice := strings.Split(data.(string), ",") + floatSlice := make([]float64, 0) + for _, str := range stringSlice { + val, err := strconv.ParseFloat(strings.TrimSpace(str), 64) + if err != nil { + return nil, err + } + floatSlice = append(floatSlice, val) + } + + // Return the parsed value + return CommaSeparatedFloat64Slice(floatSlice), nil + } +} + +// HttpMetrics configuration properties for http monitoring +type HttpMetrics struct { + HistogramEnabled bool + Buckets CommaSeparatedFloat64Slice +} + +// Metrics configuration properties for monitoring +type Metrics struct { + Endpoint string + HistogramBuckets CommaSeparatedFloat64Slice + Http HttpMetrics +} diff --git a/internal/service/monitoring/metrics.go b/internal/service/monitoring/metrics.go new file mode 100644 index 0000000..90bf158 --- /dev/null +++ b/internal/service/monitoring/metrics.go @@ -0,0 +1,95 @@ +package monitoring + +import ( + "net/http" + "strconv" + "time" + + "github.com/gorilla/mux" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" + + "payouts/internal/service/monitoring/config" +) + +const ( + METRICS_NAMESPACE = "payouts" +) + +// Metrics represents the metrics service +type Metrics interface { + GetMiddleware() func(next http.Handler) http.Handler +} + +// metrics represents the metrics service implementation +type metrics struct { + httpDuration prometheus.ObserverVec +} + +// NewMetrics instantiates metrics service +func NewMetrics(config config.Metrics) (Metrics, error) { + var httpDuration prometheus.ObserverVec + if config.Http.HistogramEnabled { + httpDuration = CreateHttpHistogram(config.Http.Buckets) + } else { + httpDuration = CreateHttpSummary(config.Http.Buckets) + + } + return &metrics{ + httpDuration: httpDuration, + }, nil +} + +// GetMiddleware returns the middleware to be used in mux router +func (m *metrics) GetMiddleware() func(next http.Handler) http.Handler { + return GetMiddleware(m.httpDuration) +} + +func CreateHttpSummary(buckets []float64) *prometheus.SummaryVec { + return promauto.NewSummaryVec(prometheus.SummaryOpts{ + Name: "http_server_requests_seconds", + Help: "Duration of HTTP requests.", + }, []string{"uri", "method", "code"}) +} + +func CreateHttpHistogram(buckets []float64) *prometheus.HistogramVec { + return promauto.NewHistogramVec(prometheus.HistogramOpts{ + Name: "http_server_requests_seconds", + Help: "Duration of HTTP requests.", + Buckets: buckets, + }, []string{"uri", "method", "code"}) + +} + +func GetMiddleware(histogramVec prometheus.ObserverVec) func(next http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + route := mux.CurrentRoute(r) + path, _ := route.GetPathTemplate() + wd := &writerDelegate{ + ResponseWriter: w, + statusCode: http.StatusOK, + } + + start := time.Now() + defer func() { + duration := time.Since(start).Seconds() + code := strconv.Itoa(wd.statusCode) + histogramVec.WithLabelValues(path, r.Method, code).Observe(duration) + }() + + next.ServeHTTP(wd, r) + }) + } +} + +type writerDelegate struct { + http.ResponseWriter + statusCode int +} + +// override the WriteHeader method +func (w *writerDelegate) WriteHeader(statusCode int) { + w.statusCode = statusCode + w.ResponseWriter.WriteHeader(statusCode) +} diff --git a/internal/service/monitoring/module.go b/internal/service/monitoring/module.go new file mode 100644 index 0000000..b08d6fd --- /dev/null +++ b/internal/service/monitoring/module.go @@ -0,0 +1,24 @@ +package monitoring + +import ( + "go.uber.org/fx" + + "payouts/internal/config" +) + +// Module is a fx module +var Module = fx.Options( + fx.Provide(New), +) + +// Params represents the module input params +type Params struct { + fx.In + + AppConfig *config.App +} + +// New instantiates the metrics service +func New(p Params) (Metrics, error) { + return NewMetrics(p.AppConfig.Metrics) +} diff --git a/internal/version/package.go b/internal/version/package.go new file mode 100644 index 0000000..a58fb16 --- /dev/null +++ b/internal/version/package.go @@ -0,0 +1,6 @@ +package version + +import _ "embed" + +//go:embed version.txt +var Version string diff --git a/internal/version/version.txt b/internal/version/version.txt new file mode 100644 index 0000000..3546645 --- /dev/null +++ b/internal/version/version.txt @@ -0,0 +1 @@ +unknown