Compare commits
10 Commits
f1bb00ef09
...
311e5902c9
| Author | SHA1 | Date | |
|---|---|---|---|
| 311e5902c9 | |||
| 8d908a0502 | |||
| 84ec54a893 | |||
| 145028af37 | |||
| 6f4090fcb3 | |||
| 802dfc97a2 | |||
| e0929aff56 | |||
| 90020de1ec | |||
| 4885dad4ab | |||
| 05ca5ac787 |
@ -1,2 +0,0 @@
|
|||||||
POSTGRES_USER=developer
|
|
||||||
POSTGRES_PW=secret
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
POSTGRES_USER=developer
|
|
||||||
POSTGRES_PW=secret
|
|
||||||
6
.env.example
Normal file
6
.env.example
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
POSTGRES_USER=developer
|
||||||
|
POSTGRES_PASSWORD=secret
|
||||||
|
DSN=database:5432/meteologica?sslmode=disable
|
||||||
|
|
||||||
|
URL_SERVICE_A=http://service_a:8080
|
||||||
|
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,3 +1,4 @@
|
|||||||
.sqlfluff
|
.sqlfluff
|
||||||
*.out
|
*.out
|
||||||
apuntes.md
|
apuntes.md
|
||||||
|
.env
|
||||||
|
|||||||
@ -4,26 +4,41 @@ services:
|
|||||||
image: postgres:17.6-alpine3.22
|
image: postgres:17.6-alpine3.22
|
||||||
environment:
|
environment:
|
||||||
- POSTGRES_USER=${POSTGRES_USER}
|
- POSTGRES_USER=${POSTGRES_USER}
|
||||||
- POSTGRES_PASSWORD=${POSTGRES_PW}
|
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
|
||||||
- POSTGRES_DB=meteologica
|
- POSTGRES_DB=meteologica
|
||||||
ports:
|
ports:
|
||||||
- "5432:5432"
|
- "5432:5432"
|
||||||
restart: always
|
restart: always
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d meteologica"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
service_a:
|
service_a:
|
||||||
build:
|
build:
|
||||||
context: ./service_a
|
context: .
|
||||||
dockerfile: Dockerfile
|
dockerfile: ./service_a/Dockerfile
|
||||||
container_name: service_a
|
container_name: service_a
|
||||||
|
environment:
|
||||||
|
- URL_SERVICE_A=${URL_SERVICE_A}
|
||||||
|
- DSN=${DSN}
|
||||||
|
- POSTGRES_USER=${POSTGRES_USER}
|
||||||
|
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
|
||||||
ports:
|
ports:
|
||||||
- "8080:8080"
|
- "8080:8080"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
database:
|
||||||
|
condition: service_healthy
|
||||||
|
|
||||||
service_b:
|
service_b:
|
||||||
build:
|
build:
|
||||||
context: ./service_b
|
context: .
|
||||||
dockerfile: Dockerfile
|
dockerfile: ./service_b/Dockerfile
|
||||||
container_name: service_b
|
container_name: service_b
|
||||||
|
environment:
|
||||||
|
- URL_SERVICE_A=${URL_SERVICE_A}
|
||||||
ports:
|
ports:
|
||||||
- "8090:8090"
|
- "8090:8090"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|||||||
31
pkg/app.go
Normal file
31
pkg/app.go
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
package pkg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func LoadEnvFile(envDirectory string) error {
|
||||||
|
file, err := os.Open(envDirectory)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
scanner := bufio.NewScanner(file)
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := scanner.Text()
|
||||||
|
if len(line) == 0 || strings.HasPrefix(line, "#") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
parts := strings.SplitN(line, "=", 2)
|
||||||
|
if len(parts) != 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
key := strings.TrimSpace(parts[0])
|
||||||
|
value := strings.TrimSpace(parts[1])
|
||||||
|
os.Setenv(key, value)
|
||||||
|
}
|
||||||
|
return scanner.Err()
|
||||||
|
}
|
||||||
6
pkg/errors.go
Normal file
6
pkg/errors.go
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
package pkg
|
||||||
|
|
||||||
|
var (
|
||||||
|
SQLSTATE_25P02 = "25P02"
|
||||||
|
SQLSTATE_23505 = "23505"
|
||||||
|
)
|
||||||
3
pkg/go.mod
Normal file
3
pkg/go.mod
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
module pkg
|
||||||
|
|
||||||
|
go 1.25.2
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package domains
|
package pkg
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
@ -1,3 +1,3 @@
|
|||||||
package app
|
package pkg
|
||||||
|
|
||||||
type H map[string]any
|
type H map[string]any
|
||||||
@ -2,10 +2,13 @@ FROM golang:1.25.2-alpine3.22 AS builder
|
|||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY go.mod go.sum ./
|
COPY pkg/ ./pkg/
|
||||||
COPY server/ ./server/
|
COPY service_a/go.mod service_a/go.sum ./service_a/
|
||||||
COPY internal/ ./internal/
|
COPY service_a/server/ ./service_a/server/
|
||||||
COPY assets/ ./assets/
|
COPY service_a/internal/ ./service_a/internal/
|
||||||
|
COPY service_a/assets/ ./service_a/assets/
|
||||||
|
|
||||||
|
WORKDIR /app/service_a
|
||||||
|
|
||||||
RUN go mod download
|
RUN go mod download
|
||||||
|
|
||||||
@ -13,13 +16,13 @@ RUN go test ./... -v
|
|||||||
|
|
||||||
RUN rm -rf ./assets/
|
RUN rm -rf ./assets/
|
||||||
|
|
||||||
RUN go build -o /app/service_a ./server/main.go
|
RUN go build -o /app/bin/service_a ./server/main.go
|
||||||
|
|
||||||
FROM alpine:latest
|
FROM alpine:latest
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY --from=builder /app/service_a /app/service_a
|
COPY --from=builder /app/bin/service_a /app/service_a
|
||||||
|
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
|
|
||||||
|
|||||||
@ -3,16 +3,21 @@ module servicea
|
|||||||
go 1.25.2
|
go 1.25.2
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/golang-migrate/migrate/v4 v4.19.0
|
||||||
github.com/jackc/pgx/v5 v5.7.6
|
github.com/jackc/pgx/v5 v5.7.6
|
||||||
github.com/stretchr/testify v1.11.1
|
github.com/stretchr/testify v1.11.1
|
||||||
|
pkg v0.0.0-00010101000000-000000000000
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||||
|
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||||
github.com/kr/text v0.2.0 // indirect
|
github.com/kr/text v0.2.0 // indirect
|
||||||
|
github.com/lib/pq v1.10.9 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||||
golang.org/x/crypto v0.37.0 // indirect
|
golang.org/x/crypto v0.37.0 // indirect
|
||||||
@ -20,3 +25,5 @@ require (
|
|||||||
golang.org/x/text v0.24.0 // indirect
|
golang.org/x/text v0.24.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
|
replace pkg => ../pkg
|
||||||
|
|||||||
@ -1,7 +1,40 @@
|
|||||||
|
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
|
||||||
|
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||||
|
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||||
|
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||||
|
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
|
||||||
|
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
|
||||||
|
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
|
||||||
|
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
|
||||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
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/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/dhui/dktest v0.4.6 h1:+DPKyScKSEp3VLtbMDHcUq6V5Lm5zfZZVb0Sk7Ahom4=
|
||||||
|
github.com/dhui/dktest v0.4.6/go.mod h1:JHTSYDtKkvFNFHJKqCzVzqXecyv+tKt8EzceOmQOgbU=
|
||||||
|
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||||
|
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||||
|
github.com/docker/docker v28.3.3+incompatible h1:Dypm25kh4rmk49v1eiVbsAtpAsYURjYkaKubwuBdxEI=
|
||||||
|
github.com/docker/docker v28.3.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||||
|
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
|
||||||
|
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
|
||||||
|
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||||
|
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||||
|
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||||
|
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||||
|
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||||
|
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
|
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
|
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||||
|
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||||
|
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||||
|
github.com/golang-migrate/migrate/v4 v4.19.0 h1:RcjOnCGz3Or6HQYEJ/EEVLfWnmw9KnoigPSjzhCuaSE=
|
||||||
|
github.com/golang-migrate/migrate/v4 v4.19.0/go.mod h1:9dyEcu+hO+G9hPSw8AIg50yg622pXJsoHItQnDGZkI0=
|
||||||
|
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||||
|
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
||||||
|
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||||
|
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
||||||
|
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||||
@ -14,6 +47,20 @@ 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.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||||
|
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||||
|
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||||
|
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||||
|
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
|
||||||
|
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
|
||||||
|
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
||||||
|
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||||
|
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=
|
||||||
|
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
|
||||||
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
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/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||||
@ -23,10 +70,22 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
|
|||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
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/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
|
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||||
|
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk=
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8=
|
||||||
|
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
|
||||||
|
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
|
||||||
|
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
|
||||||
|
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
|
||||||
|
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
|
||||||
|
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
|
||||||
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
|
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
|
||||||
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
|
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
|
||||||
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
|
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
|
||||||
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||||
|
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
|
||||||
|
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
|
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
|
||||||
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
|
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
|||||||
@ -2,8 +2,16 @@ package app
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"embed"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
mig "github.com/golang-migrate/migrate/v4"
|
||||||
|
_ "github.com/golang-migrate/migrate/v4/database/postgres"
|
||||||
|
"github.com/golang-migrate/migrate/v4/source/iofs"
|
||||||
"github.com/jackc/pgx/v5/pgxpool"
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
_ "github.com/jackc/pgx/v5/stdlib"
|
_ "github.com/jackc/pgx/v5/stdlib"
|
||||||
)
|
)
|
||||||
@ -22,3 +30,36 @@ func NewPGXPool(datasource string) *pgxpool.Pool {
|
|||||||
slog.Info("connected to database", "datasource", datasource)
|
slog.Info("connected to database", "datasource", datasource)
|
||||||
return dbPool
|
return dbPool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Migrate(database embed.FS) {
|
||||||
|
dbConn, err := sql.Open("pgx", os.Getenv("DATASOURCE"))
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("error opening database connection", "error", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer dbConn.Close()
|
||||||
|
|
||||||
|
d, err := iofs.New(database, "database/migrations")
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("error creating migration source", "error", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
m, err := mig.NewWithSourceInstance("iofs", d, fmt.Sprintf("postgres://%s:%s@%s", os.Getenv("POSTGRES_USER"), os.Getenv("POSTGRES_PASSWORD"), os.Getenv("DSN")))
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("error creating migration instance", "error", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = m.Up()
|
||||||
|
if err != nil && !errors.Is(err, mig.ErrNoChange) {
|
||||||
|
slog.Error("cannot migrate", "error", err)
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
if errors.Is(err, mig.ErrNoChange) {
|
||||||
|
slog.Info("migration has no changes")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Info("migration done")
|
||||||
|
}
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import (
|
|||||||
"encoding/csv"
|
"encoding/csv"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"servicea/internal/app"
|
"pkg"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@ -52,6 +52,10 @@ func (c *CSV) Parse(r io.Reader) ([]MeteoData, []RejectedMeteoData, error) {
|
|||||||
return nil, nil, fmt.Errorf("%w: invalid separator detected, expected semicolon (;)", ErrCannotParseFile)
|
return nil, nil, fmt.Errorf("%w: invalid separator detected, expected semicolon (;)", ErrCannotParseFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for i := range header {
|
||||||
|
header[i] = strings.TrimSpace(header[i])
|
||||||
|
}
|
||||||
|
|
||||||
var meteoDataList []MeteoData
|
var meteoDataList []MeteoData
|
||||||
var rejectedDataList []RejectedMeteoData
|
var rejectedDataList []RejectedMeteoData
|
||||||
|
|
||||||
@ -70,7 +74,7 @@ func (c *CSV) Parse(r io.Reader) ([]MeteoData, []RejectedMeteoData, error) {
|
|||||||
|
|
||||||
rowValue := strings.Join(row, ";")
|
rowValue := strings.Join(row, ";")
|
||||||
|
|
||||||
record := make(app.H)
|
record := make(pkg.H)
|
||||||
for i, value := range row {
|
for i, value := range row {
|
||||||
if i < len(header) {
|
if i < len(header) {
|
||||||
record[header[i]] = value
|
record[header[i]] = value
|
||||||
@ -100,7 +104,7 @@ func (c *CSV) Parse(r io.Reader) ([]MeteoData, []RejectedMeteoData, error) {
|
|||||||
return meteoDataList, rejectedDataList, nil
|
return meteoDataList, rejectedDataList, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func normalize(record app.H) (*MeteoData, error) {
|
func normalize(record pkg.H) (*MeteoData, error) {
|
||||||
meteoData := &MeteoData{}
|
meteoData := &MeteoData{}
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
@ -138,7 +142,7 @@ func normalize(record app.H) (*MeteoData, error) {
|
|||||||
return meteoData, nil
|
return meteoData, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseDate(record app.H, key string, errMissing error) (time.Time, error) {
|
func parseDate(record pkg.H, key string, errMissing error) (time.Time, error) {
|
||||||
if str, ok := record[key].(string); ok && str != "" {
|
if str, ok := record[key].(string); ok && str != "" {
|
||||||
t, err := time.Parse("2006/01/02", str)
|
t, err := time.Parse("2006/01/02", str)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -149,14 +153,14 @@ func parseDate(record app.H, key string, errMissing error) (time.Time, error) {
|
|||||||
return time.Time{}, errMissing
|
return time.Time{}, errMissing
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseString(record app.H, key string, errMissing error) (string, error) {
|
func parseString(record pkg.H, key string, errMissing error) (string, error) {
|
||||||
if str, ok := record[key].(string); ok && str != "" {
|
if str, ok := record[key].(string); ok && str != "" {
|
||||||
return str, nil
|
return str, nil
|
||||||
}
|
}
|
||||||
return "", errMissing
|
return "", errMissing
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseFloatField(record app.H, key string, errMissing error) (float32, error) {
|
func parseFloatField(record pkg.H, key string, errMissing error) (float32, error) {
|
||||||
if str, ok := record[key].(string); ok && str != "" {
|
if str, ok := record[key].(string); ok && str != "" {
|
||||||
str = strings.Replace(str, ",", ".", 1)
|
str = strings.Replace(str, ",", ".", 1)
|
||||||
f, err := strconv.ParseFloat(str, 32)
|
f, err := strconv.ParseFloat(str, 32)
|
||||||
@ -168,7 +172,7 @@ func parseFloatField(record app.H, key string, errMissing error) (float32, error
|
|||||||
return 0, errMissing
|
return 0, errMissing
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseIntField(record app.H, key string, errMissing error) (int, error) {
|
func parseIntField(record pkg.H, key string, errMissing error) (int, error) {
|
||||||
if str, ok := record[key].(string); ok && str != "" {
|
if str, ok := record[key].(string); ok && str != "" {
|
||||||
str = strings.TrimSpace(str)
|
str = strings.TrimSpace(str)
|
||||||
i, err := strconv.Atoi(str)
|
i, err := strconv.Atoi(str)
|
||||||
|
|||||||
@ -32,7 +32,7 @@ func Test_CSV_ParseFile(t *testing.T) {
|
|||||||
assert.Equal(t, float32(11.55), record.MaxTemp)
|
assert.Equal(t, float32(11.55), record.MaxTemp)
|
||||||
assert.Equal(t, float32(6.25), record.MinTemp)
|
assert.Equal(t, float32(6.25), record.MinTemp)
|
||||||
assert.Equal(t, float32(0), record.Rainfall)
|
assert.Equal(t, float32(0), record.Rainfall)
|
||||||
assert.Equal(t, float32(10), record.Cloudiness)
|
assert.Equal(t, int(10), record.Cloudiness)
|
||||||
},
|
},
|
||||||
validateRejected: func(t *testing.T, rejected []meteo.RejectedMeteoData) {
|
validateRejected: func(t *testing.T, rejected []meteo.RejectedMeteoData) {
|
||||||
assert.Empty(t, rejected)
|
assert.Empty(t, rejected)
|
||||||
@ -61,7 +61,7 @@ func Test_CSV_ParseFile(t *testing.T) {
|
|||||||
},
|
},
|
||||||
validateRejected: func(t *testing.T, rejected []meteo.RejectedMeteoData) {
|
validateRejected: func(t *testing.T, rejected []meteo.RejectedMeteoData) {
|
||||||
assert.Equal(t, 1, len(rejected))
|
assert.Equal(t, 1, len(rejected))
|
||||||
assert.Contains(t, rejected[0].Reason, "missing or invalid city field")
|
assert.Contains(t, rejected[0].Reason, "missing or invalid location")
|
||||||
assert.Equal(t, "2025/10/12;11,55;6,25;0;10", rejected[0].RowValue)
|
assert.Equal(t, "2025/10/12;11,55;6,25;0;10", rejected[0].RowValue)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -87,7 +87,7 @@ func Test_CSV_ParseFile(t *testing.T) {
|
|||||||
},
|
},
|
||||||
validateRejected: func(t *testing.T, rejected []meteo.RejectedMeteoData) {
|
validateRejected: func(t *testing.T, rejected []meteo.RejectedMeteoData) {
|
||||||
assert.Equal(t, 1, len(rejected))
|
assert.Equal(t, 1, len(rejected))
|
||||||
assert.Contains(t, rejected[0].Reason, "missing or invalid max temp field")
|
assert.Contains(t, rejected[0].Reason, "missing or invalid max temp")
|
||||||
assert.Equal(t, "2025/10/12;Madrid;;6,25;0;10", rejected[0].RowValue)
|
assert.Equal(t, "2025/10/12;Madrid;;6,25;0;10", rejected[0].RowValue)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -101,7 +101,7 @@ func Test_CSV_ParseFile(t *testing.T) {
|
|||||||
},
|
},
|
||||||
validateRejected: func(t *testing.T, rejected []meteo.RejectedMeteoData) {
|
validateRejected: func(t *testing.T, rejected []meteo.RejectedMeteoData) {
|
||||||
assert.Equal(t, 1, len(rejected))
|
assert.Equal(t, 1, len(rejected))
|
||||||
assert.Contains(t, rejected[0].Reason, "missing or invalid city field")
|
assert.Contains(t, rejected[0].Reason, "missing or invalid location")
|
||||||
assert.Equal(t, "2025/10/12;;11,55;6,25;0;10", rejected[0].RowValue)
|
assert.Equal(t, "2025/10/12;;11,55;6,25;0;10", rejected[0].RowValue)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -116,7 +116,7 @@ func Test_CSV_ParseFile(t *testing.T) {
|
|||||||
},
|
},
|
||||||
validateRejected: func(t *testing.T, rejected []meteo.RejectedMeteoData) {
|
validateRejected: func(t *testing.T, rejected []meteo.RejectedMeteoData) {
|
||||||
assert.Equal(t, 1, len(rejected))
|
assert.Equal(t, 1, len(rejected))
|
||||||
assert.Contains(t, rejected[0].Reason, "missing or invalid date field")
|
assert.Contains(t, rejected[0].Reason, "missing or invalid date")
|
||||||
assert.Equal(t, ";Madrid;11,55;6,25;0;10", rejected[0].RowValue)
|
assert.Equal(t, ";Madrid;11,55;6,25;0;10", rejected[0].RowValue)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@ -8,13 +8,12 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
"servicea/internal/app"
|
"pkg"
|
||||||
"servicea/internal/domains"
|
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Handler struct {
|
type Handler struct {
|
||||||
domains.BaseHandler
|
pkg.BaseHandler
|
||||||
s *Service
|
s *Service
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -27,20 +26,23 @@ func NewHandler(service *Service) *Handler {
|
|||||||
func (h *Handler) IngestCSV(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) IngestCSV(w http.ResponseWriter, r *http.Request) {
|
||||||
err := r.ParseMultipartForm(10 << 20)
|
err := r.ParseMultipartForm(10 << 20)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, ErrParsingForm.Error(), http.StatusBadRequest)
|
slog.Error(ErrParsingForm.Error(), "error", err)
|
||||||
|
h.ToJSON(w, http.StatusBadRequest, pkg.H{"error": ErrParsingForm})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
file, header, err := r.FormFile("file")
|
file, header, err := r.FormFile("file")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, ErrRetrievingFile.Error(), http.StatusBadRequest)
|
slog.Error(ErrRetrievingFile.Error(), "error", err)
|
||||||
|
h.ToJSON(w, http.StatusBadRequest, pkg.H{"error": ErrRetrievingFile})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
|
|
||||||
content, err := io.ReadAll(file)
|
content, err := io.ReadAll(file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, ErrReadingFile.Error(), http.StatusInternalServerError)
|
slog.Error(ErrReadingFile.Error(), "error", err)
|
||||||
|
h.ToJSON(w, http.StatusInternalServerError, pkg.H{"error": ErrReadingFile})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -57,13 +59,13 @@ func (h *Handler) IngestCSV(w http.ResponseWriter, r *http.Request) {
|
|||||||
slog.Error(ErrCannotParseFile.Error(),
|
slog.Error(ErrCannotParseFile.Error(),
|
||||||
"filename", header.Filename,
|
"filename", header.Filename,
|
||||||
"error", err)
|
"error", err)
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
h.ToJSON(w, http.StatusConflict, pkg.H{"error": err})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
fileStats.ElapsedMS = int(time.Since(start).Milliseconds())
|
fileStats.ElapsedMS = int(time.Since(start).Milliseconds())
|
||||||
h.s.UpdateElapsedMS(r.Context(), fileStats.BatchID, fileStats.ElapsedMS)
|
h.s.UpdateElapsedMS(r.Context(), fileStats.BatchID, fileStats.ElapsedMS)
|
||||||
|
|
||||||
slog.Info("CSV file processed",
|
slog.Info("csv file processed",
|
||||||
"filename", header.Filename,
|
"filename", header.Filename,
|
||||||
"rows_inserted", fileStats.RowsInserted,
|
"rows_inserted", fileStats.RowsInserted,
|
||||||
"rows_rejected", fileStats.RowsRejected,
|
"rows_rejected", fileStats.RowsRejected,
|
||||||
@ -71,7 +73,7 @@ func (h *Handler) IngestCSV(w http.ResponseWriter, r *http.Request) {
|
|||||||
"file_checksum", fileStats.FileChecksum,
|
"file_checksum", fileStats.FileChecksum,
|
||||||
)
|
)
|
||||||
|
|
||||||
h.ToJSON(w, http.StatusOK, app.H{"stats": fileStats})
|
h.ToJSON(w, http.StatusOK, pkg.H{"stats": fileStats})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) IngestExcel(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) IngestExcel(w http.ResponseWriter, r *http.Request) {
|
||||||
@ -79,7 +81,9 @@ func (h *Handler) IngestExcel(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) GetCities(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) GetCities(w http.ResponseWriter, r *http.Request) {
|
||||||
h.ToJSON(w, http.StatusOK, app.H{"cities": h.s.GetCities(r.Context())})
|
cities := h.s.GetCities(r.Context())
|
||||||
|
slog.Info("cities retrieved", "count", len(cities))
|
||||||
|
h.ToJSON(w, http.StatusOK, pkg.H{"cities": cities})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) GetMeteoData(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) GetMeteoData(w http.ResponseWriter, r *http.Request) {
|
||||||
@ -94,19 +98,19 @@ func (h *Handler) GetMeteoData(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := params.Validate(); err != nil {
|
if err := params.Validate(); err != nil {
|
||||||
slog.Error("Error validating struct", "error", err)
|
slog.Error("error validating struct", "error", err)
|
||||||
h.ToJSON(w, http.StatusBadRequest, app.H{"error": err.Error()})
|
h.ToJSON(w, http.StatusBadRequest, pkg.H{"error": err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
meteoData, err := h.s.GetMeteoData(r.Context(), params)
|
meteoData, err := h.s.GetMeteoData(r.Context(), params)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error(ErrReadingData.Error(), "error", err)
|
slog.Error(ErrReadingData.Error(), "error", err)
|
||||||
h.ToJSON(w, http.StatusNotFound, app.H{"error": ErrReadingData.Error()})
|
h.ToJSON(w, http.StatusNotFound, pkg.H{"error": ErrReadingData.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
slog.Info("Data retrieved", "location", params.Location)
|
slog.Info("data retrieved", "location", params.Location)
|
||||||
|
|
||||||
h.ToJSON(w, http.StatusOK, app.H{"meteo_data": meteoData})
|
h.ToJSON(w, http.StatusOK, pkg.H{"meteo_data": meteoData})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,8 @@ package meteo
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"pkg"
|
||||||
|
"strings"
|
||||||
|
|
||||||
b "github.com/jackc/pgx/v5"
|
b "github.com/jackc/pgx/v5"
|
||||||
"github.com/jackc/pgx/v5/pgxpool"
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
@ -76,6 +78,9 @@ func (pgx *pgxRepo) insertBatch(ctx context.Context, tx b.Tx, fileChecksum strin
|
|||||||
var batchID int
|
var batchID int
|
||||||
err := tx.QueryRow(ctx, insertBatch, 0, fileChecksum).Scan(&batchID)
|
err := tx.QueryRow(ctx, insertBatch, 0, fileChecksum).Scan(&batchID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if strings.Contains(err.Error(), pkg.SQLSTATE_23505) {
|
||||||
|
return 0, ErrRecordAlreadyExists
|
||||||
|
}
|
||||||
return 0, fmt.Errorf("error inserting batch: %w", err)
|
return 0, fmt.Errorf("error inserting batch: %w", err)
|
||||||
}
|
}
|
||||||
return batchID, nil
|
return batchID, nil
|
||||||
@ -102,6 +107,9 @@ func (pgx *pgxRepo) insertAcceptedMeteoData(ctx context.Context, tx b.Tx, batchI
|
|||||||
rowsInserted++
|
rowsInserted++
|
||||||
if err != nil {
|
if err != nil {
|
||||||
results.Close()
|
results.Close()
|
||||||
|
if strings.Contains(err.Error(), pkg.SQLSTATE_23505) {
|
||||||
|
return 0, ErrRecordAlreadyExists
|
||||||
|
}
|
||||||
return 0, fmt.Errorf("error executing batch command %d: %w", i, err)
|
return 0, fmt.Errorf("error executing batch command %d: %w", i, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,17 +1,31 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"embed"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"pkg"
|
||||||
"servicea/internal/app"
|
"servicea/internal/app"
|
||||||
"servicea/internal/domains/meteo"
|
"servicea/internal/domains/meteo"
|
||||||
"servicea/internal/router"
|
"servicea/internal/router"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
//go:embed database/migrations
|
||||||
|
var database embed.FS
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
err := pkg.LoadEnvFile("./../.env")
|
||||||
|
if err != nil {
|
||||||
|
slog.Warn("error loading env file", "error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
pool := app.NewPGXPool("postgres://developer:secret@localhost:5432/meteologica?sslmode=disable")
|
pool := app.NewPGXPool(fmt.Sprintf("postgres://%s:%s@%s", os.Getenv("POSTGRES_USER"), os.Getenv("POSTGRES_PASSWORD"), os.Getenv("DSN")))
|
||||||
|
app.Migrate(database)
|
||||||
|
|
||||||
mux := router.SetupRoutes()
|
mux := router.SetupRoutes()
|
||||||
|
|
||||||
|
|||||||
@ -2,17 +2,21 @@ FROM golang:1.25.2-alpine3.22 AS builder
|
|||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY go.mod ./
|
COPY pkg/ ./pkg/
|
||||||
COPY server/ ./server/
|
COPY service_b/go.mod service_b/go.sum ./service_b/
|
||||||
|
COPY service_b/server/ ./service_b/server/
|
||||||
|
COPY service_b/internal/ ./service_b/internal/
|
||||||
|
|
||||||
|
WORKDIR /app/service_b
|
||||||
|
|
||||||
RUN go mod download
|
RUN go mod download
|
||||||
RUN go build -o /app/service_b ./server/main.go
|
RUN go build -o /app/bin/service_b ./server/main.go
|
||||||
|
|
||||||
FROM alpine:latest
|
FROM alpine:latest
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY --from=builder /app/service_b /app/service_b
|
COPY --from=builder /app/bin/service_b /app/service_b
|
||||||
|
|
||||||
EXPOSE 8090
|
EXPOSE 8090
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +1,17 @@
|
|||||||
module serviceb
|
module serviceb
|
||||||
|
|
||||||
go 1.25.2
|
go 1.25.2
|
||||||
|
|
||||||
|
replace pkg => ../pkg
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/cenkalti/backoff/v5 v5.0.3
|
||||||
|
pkg v0.0.0-00010101000000-000000000000
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
|
github.com/dgraph-io/ristretto/v2 v2.3.0 // indirect
|
||||||
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
|
golang.org/x/sys v0.35.0 // indirect
|
||||||
|
)
|
||||||
|
|||||||
10
service_b/go.sum
Normal file
10
service_b/go.sum
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
||||||
|
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||||
|
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/dgraph-io/ristretto/v2 v2.3.0 h1:qTQ38m7oIyd4GAed/QkUZyPFNMnvVWyazGXRwvOt5zk=
|
||||||
|
github.com/dgraph-io/ristretto/v2 v2.3.0/go.mod h1:gpoRV3VzrEY1a9dWAYV6T1U7YzfgttXdd/ZzL1s9OZM=
|
||||||
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
|
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||||
|
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
1
service_b/internal/app/app.go
Normal file
1
service_b/internal/app/app.go
Normal file
@ -0,0 +1 @@
|
|||||||
|
package app
|
||||||
104
service_b/internal/domains/meteo/domain.go
Normal file
104
service_b/internal/domains/meteo/domain.go
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
package meteo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MeteoDataFromServiceA struct {
|
||||||
|
Timestamp time.Time `csv:"fecha" json:"timestamp"`
|
||||||
|
Location string `csv:"ciudad" json:"location"`
|
||||||
|
MaxTemp float32 `csv:"temperatura maxima" json:"max_temp"`
|
||||||
|
MinTemp float32 `csv:"temperatura minima" json:"min_temp"`
|
||||||
|
Rainfall float32 `csv:"precipitacion" json:"rainfall"`
|
||||||
|
Cloudiness int `csv:"nubosidad" json:"cloudiness"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MeteoDataPerDay struct {
|
||||||
|
MaxTemp float32 `json:"max_temp"`
|
||||||
|
MinTemp float32 `json:"min_temp"`
|
||||||
|
AvgTemp float32 `json:"avg_temp"`
|
||||||
|
Rainfall float32 `json:"rainfall"`
|
||||||
|
Cloudiness int `json:"cloudiness"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mtpd *MeteoDataPerDay) ConvertValue() {
|
||||||
|
mtpd.MaxTemp = mtpd.MaxTemp*9/5 + 32
|
||||||
|
mtpd.MinTemp = mtpd.MinTemp*9/5 + 32
|
||||||
|
mtpd.AvgTemp = mtpd.AvgTemp*9/5 + 32
|
||||||
|
}
|
||||||
|
|
||||||
|
type Rolling7Data struct {
|
||||||
|
AvgTemp float32 `json:"avg_temp"`
|
||||||
|
AvgCloudiness int `json:"avg_cloudiness"`
|
||||||
|
SumRainfall float32 `json:"sum_rainfall"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MeteoData struct {
|
||||||
|
Location string `json:"location"`
|
||||||
|
Unit Unit `json:"unit"`
|
||||||
|
From string `json:"from"`
|
||||||
|
Days *[]MeteoDataPerDay `json:"days,omitempty"`
|
||||||
|
Rolling7 *Rolling7Data `json:"rolling7,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mt *MeteoData) ComputeCacheKey() string {
|
||||||
|
return fmt.Sprintf("meteo:%s:%s", mt.Location, mt.From)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Unit string
|
||||||
|
|
||||||
|
const (
|
||||||
|
UnitC Unit = "C"
|
||||||
|
UnitF Unit = "F"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Agg string
|
||||||
|
|
||||||
|
const (
|
||||||
|
AggDaily Agg = "daily"
|
||||||
|
AggRolling7 Agg = "rolling7"
|
||||||
|
)
|
||||||
|
|
||||||
|
type GetMeteoData struct {
|
||||||
|
Location string
|
||||||
|
Date string
|
||||||
|
Days int
|
||||||
|
Unit Unit
|
||||||
|
Agg Agg
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mt *GetMeteoData) Validate() error {
|
||||||
|
if mt.Date == "" {
|
||||||
|
mt.Date = time.Now().Format("2006-01-02")
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := time.Parse("2006-01-02", mt.Date); err != nil {
|
||||||
|
return ErrMissingOrInvalidDate
|
||||||
|
}
|
||||||
|
|
||||||
|
if mt.Days == 0 {
|
||||||
|
mt.Days = 5
|
||||||
|
}
|
||||||
|
if mt.Days < 1 || mt.Days > 10 {
|
||||||
|
return ErrInvalidDays
|
||||||
|
}
|
||||||
|
|
||||||
|
if mt.Unit == "" {
|
||||||
|
mt.Unit = UnitC
|
||||||
|
}
|
||||||
|
|
||||||
|
if mt.Agg == "" {
|
||||||
|
mt.Agg = AggDaily
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrCityNotFound = errors.New("city not found")
|
||||||
|
ErrReadingData = errors.New("error reading data")
|
||||||
|
ErrMissingOrInvalidDate = errors.New("missing or invalid date")
|
||||||
|
ErrInvalidDays = errors.New("days must be between 1 and 10")
|
||||||
|
)
|
||||||
47
service_b/internal/domains/meteo/handlers.go
Normal file
47
service_b/internal/domains/meteo/handlers.go
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
package meteo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
"pkg"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Handler struct {
|
||||||
|
pkg.BaseHandler
|
||||||
|
s *Service
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHandler(service *Service) *Handler {
|
||||||
|
return &Handler{
|
||||||
|
s: service,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) GetMeteoData(w http.ResponseWriter, r *http.Request) {
|
||||||
|
locationValue := r.PathValue("city")
|
||||||
|
queryParams := r.URL.Query()
|
||||||
|
|
||||||
|
params := GetMeteoData{
|
||||||
|
Location: locationValue,
|
||||||
|
Date: queryParams.Get("date"),
|
||||||
|
Days: h.ParamToInt(queryParams.Get("days"), 5),
|
||||||
|
Unit: Unit(queryParams.Get("unit")),
|
||||||
|
Agg: Agg(queryParams.Get("agg")),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := params.Validate(); err != nil {
|
||||||
|
slog.Error("error validating struct", "error", err)
|
||||||
|
h.ToJSON(w, http.StatusBadRequest, pkg.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := h.s.GetWeatherByCity(r.Context(), params)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("error", "err", err)
|
||||||
|
h.ToJSON(w, http.StatusInternalServerError, pkg.H{"error": err})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Info("data retrieved", "location", params.Location)
|
||||||
|
h.ToJSON(w, http.StatusOK, data)
|
||||||
|
}
|
||||||
7
service_b/internal/domains/meteo/router.go
Normal file
7
service_b/internal/domains/meteo/router.go
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
package meteo
|
||||||
|
|
||||||
|
import "net/http"
|
||||||
|
|
||||||
|
func RegisterRoutes(mux *http.ServeMux, handler *Handler) {
|
||||||
|
mux.HandleFunc("GET /weather/{city}", handler.GetMeteoData)
|
||||||
|
}
|
||||||
155
service_b/internal/domains/meteo/service.go
Normal file
155
service_b/internal/domains/meteo/service.go
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
package meteo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/cenkalti/backoff/v5"
|
||||||
|
"github.com/dgraph-io/ristretto/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Service struct {
|
||||||
|
cache *ristretto.Cache[string, string]
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewService() *Service {
|
||||||
|
cache, err := ristretto.NewCache(&ristretto.Config[string, string]{
|
||||||
|
NumCounters: 1024,
|
||||||
|
MaxCost: 1 << 30,
|
||||||
|
BufferItems: 64,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("cannot init cache", "err", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Service{
|
||||||
|
cache: cache,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) GetWeatherByCity(ctx context.Context, params GetMeteoData) (MeteoData, error) {
|
||||||
|
fromDate, err := time.Parse("2006-01-02", params.Date)
|
||||||
|
if err != nil {
|
||||||
|
return MeteoData{}, err
|
||||||
|
}
|
||||||
|
toDate := fromDate.AddDate(0, 0, params.Days-1)
|
||||||
|
|
||||||
|
operation := func() (*http.Response, error) {
|
||||||
|
url := fmt.Sprintf("%s/data?city=%s&from=%s&to=%s", os.Getenv("URL_SERVICE_A"),
|
||||||
|
params.Location, params.Date, toDate.Format("2006-01-02"))
|
||||||
|
|
||||||
|
slog.Info("url", "url", url)
|
||||||
|
|
||||||
|
resp, err := http.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode == http.StatusBadRequest {
|
||||||
|
resp.Body.Close()
|
||||||
|
return nil, backoff.Permanent(errors.New("bad request"))
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := backoff.Retry(ctx, operation, backoff.WithBackOff(backoff.NewExponentialBackOff()))
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("somethin happened")
|
||||||
|
return MeteoData{}, err
|
||||||
|
}
|
||||||
|
defer result.Body.Close()
|
||||||
|
|
||||||
|
body, err := io.ReadAll(result.Body)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("error reading response body", "err", err)
|
||||||
|
return MeteoData{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var serviceAResponse struct {
|
||||||
|
MeteoData []MeteoDataFromServiceA `json:"meteo_data"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(body, &serviceAResponse); err != nil {
|
||||||
|
slog.Error("error unmarshaling response", "err", err)
|
||||||
|
return MeteoData{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(serviceAResponse.MeteoData) == 0 {
|
||||||
|
return MeteoData{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if params.Agg == AggDaily {
|
||||||
|
return s.processDailyData(serviceAResponse.MeteoData, params)
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.processRolling7Data(serviceAResponse.MeteoData, params)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) processDailyData(data []MeteoDataFromServiceA, params GetMeteoData) (MeteoData, error) {
|
||||||
|
days := make([]MeteoDataPerDay, 0, len(data))
|
||||||
|
|
||||||
|
for _, d := range data {
|
||||||
|
avgTemp := (d.MaxTemp + d.MinTemp) / 2
|
||||||
|
day := MeteoDataPerDay{
|
||||||
|
MaxTemp: d.MaxTemp,
|
||||||
|
MinTemp: d.MinTemp,
|
||||||
|
AvgTemp: avgTemp,
|
||||||
|
Rainfall: d.Rainfall,
|
||||||
|
Cloudiness: d.Cloudiness,
|
||||||
|
}
|
||||||
|
|
||||||
|
if params.Unit == UnitF {
|
||||||
|
day.ConvertValue()
|
||||||
|
}
|
||||||
|
|
||||||
|
days = append(days, day)
|
||||||
|
}
|
||||||
|
|
||||||
|
return MeteoData{
|
||||||
|
Location: params.Location,
|
||||||
|
Unit: params.Unit,
|
||||||
|
From: params.Date,
|
||||||
|
Days: &days,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) processRolling7Data(data []MeteoDataFromServiceA, params GetMeteoData) (MeteoData, error) {
|
||||||
|
if len(data) < 7 {
|
||||||
|
return MeteoData{}, errors.New("insufficient data for rolling 7-day calculation")
|
||||||
|
}
|
||||||
|
|
||||||
|
var sumTemp, sumRainfall float32
|
||||||
|
var sumCloudiness int
|
||||||
|
|
||||||
|
for i := len(data) - 7; i < len(data); i++ {
|
||||||
|
avgTemp := (data[i].MaxTemp + data[i].MinTemp) / 2
|
||||||
|
sumTemp += avgTemp
|
||||||
|
sumRainfall += data[i].Rainfall
|
||||||
|
sumCloudiness += data[i].Cloudiness
|
||||||
|
}
|
||||||
|
|
||||||
|
rolling7 := &Rolling7Data{
|
||||||
|
AvgTemp: sumTemp / 7,
|
||||||
|
AvgCloudiness: sumCloudiness / 7,
|
||||||
|
SumRainfall: sumRainfall,
|
||||||
|
}
|
||||||
|
|
||||||
|
if params.Unit == UnitF {
|
||||||
|
rolling7.AvgTemp = rolling7.AvgTemp*9/5 + 32
|
||||||
|
}
|
||||||
|
|
||||||
|
return MeteoData{
|
||||||
|
Location: params.Location,
|
||||||
|
Unit: params.Unit,
|
||||||
|
From: params.Date,
|
||||||
|
Rolling7: rolling7,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
16
service_b/internal/router/router.go
Normal file
16
service_b/internal/router/router.go
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func SetupRoutes() *http.ServeMux {
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
|
||||||
|
mux.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
fmt.Fprintf(w, "hello world")
|
||||||
|
})
|
||||||
|
|
||||||
|
return mux
|
||||||
|
}
|
||||||
@ -2,17 +2,39 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"pkg"
|
||||||
|
"serviceb/internal/domains/meteo"
|
||||||
|
"serviceb/internal/router"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func init() {
|
||||||
mux := http.NewServeMux()
|
err := pkg.LoadEnvFile("./../.env")
|
||||||
|
if err != nil {
|
||||||
mux.HandleFunc("GET /hello", func(w http.ResponseWriter, r *http.Request) {
|
slog.Warn("error loading env file", "error", err)
|
||||||
log.Println("Received request on /hello endpoint")
|
}
|
||||||
fmt.Fprintf(w, "Hello world from service B")
|
}
|
||||||
})
|
|
||||||
|
func main() {
|
||||||
http.ListenAndServe(":8090", mux)
|
mux := router.SetupRoutes()
|
||||||
|
|
||||||
|
meteoService := meteo.NewService()
|
||||||
|
meteoHandler := meteo.NewHandler(meteoService)
|
||||||
|
meteo.RegisterRoutes(mux, meteoHandler)
|
||||||
|
|
||||||
|
server := http.Server{
|
||||||
|
Addr: ":8090",
|
||||||
|
Handler: mux,
|
||||||
|
ReadTimeout: 15 * time.Second,
|
||||||
|
WriteTimeout: 15 * time.Second,
|
||||||
|
IdleTimeout: 60 * time.Second,
|
||||||
|
ReadHeaderTimeout: 5 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Info("server starting on :8090")
|
||||||
|
if err := server.ListenAndServe(); err != nil {
|
||||||
|
panic(fmt.Sprintf("server failed, error %s", err))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user