Openstatus www.openstatus.dev

add logging to private-regions app (#1759)

* add logging to private-regions app

* Update apps/private-location/internal/server/routes.go

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update apps/private-location/internal/server/server.go

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update apps/private-location/internal/server/routes.go

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update apps/private-location/internal/server/routes.go

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* fix it

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

authored by

Thibault Le Ouay
Copilot
and committed by
GitHub
343ee0cd 4c0f4c00

+360 -22
+7 -4
apps/private-location/cmd/server/main.go
··· 12 12 "github.com/openstatushq/openstatus/apps/private-location/internal/server" 13 13 ) 14 14 15 - func gracefulShutdown(apiServer *http.Server, done chan bool) { 15 + func gracefulShutdown(apiServer *http.Server, cleanup func(context.Context), done chan bool) { 16 16 // Create context that listens for the interrupt signal from the OS. 17 17 ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) 18 18 defer stop() ··· 20 20 // Listen for the interrupt signal. 21 21 <-ctx.Done() 22 22 23 - log.Println("shutting down gracefully, press Ctrl+C again to force") 23 + fmt.Println("shutting down gracefully, press Ctrl+C again to force") 24 24 stop() // Allow Ctrl+C to force shutdown 25 25 26 26 // The context is used to inform the server it has 5 seconds to finish ··· 31 31 log.Printf("Server forced to shutdown with error: %v", err) 32 32 } 33 33 34 + // Cleanup log provider 35 + cleanup(ctx) 36 + 34 37 log.Println("Server exiting") 35 38 36 39 // Notify the main goroutine that the shutdown is complete ··· 39 42 40 43 func main() { 41 44 42 - server := server.NewServer() 45 + server, cleanup := server.NewServer() 43 46 44 47 // Create a done channel to signal when the shutdown is complete 45 48 done := make(chan bool, 1) 46 49 47 50 // Run graceful shutdown in a separate goroutine 48 - go gracefulShutdown(server, done) 51 + go gracefulShutdown(server, cleanup, done) 49 52 50 53 err := server.ListenAndServe() 51 54 if err != nil && err != http.ErrServerClosed {
+22 -1
apps/private-location/go.mod
··· 6 6 connectrpc.com/connect v1.19.1 7 7 github.com/go-chi/chi/v5 v5.2.3 8 8 github.com/go-chi/render v1.0.3 9 + github.com/google/uuid v1.6.0 9 10 github.com/jmoiron/sqlx v1.4.0 10 11 github.com/joho/godotenv v1.5.1 11 12 github.com/mattn/go-sqlite3 v1.14.22 ··· 13 14 github.com/rs/zerolog v1.34.0 14 15 github.com/stretchr/testify v1.11.1 15 16 github.com/tursodatabase/libsql-client-go v0.0.0-20240902231107-85af5b9d094d 17 + go.opentelemetry.io/contrib/bridges/otelslog v0.14.0 18 + go.opentelemetry.io/otel v1.39.0 19 + go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.15.0 20 + go.opentelemetry.io/otel/log v0.15.0 21 + go.opentelemetry.io/otel/sdk v1.39.0 22 + go.opentelemetry.io/otel/sdk/log v0.15.0 16 23 google.golang.org/protobuf v1.36.10 17 24 ) 18 25 19 26 require ( 20 27 github.com/ajg/form v1.5.1 // indirect 21 28 github.com/antlr4-go/antlr/v4 v4.13.0 // indirect 29 + github.com/cenkalti/backoff/v5 v5.0.3 // indirect 30 + github.com/cespare/xxhash/v2 v2.3.0 // indirect 22 31 github.com/coder/websocket v1.8.12 // indirect 23 32 github.com/davecgh/go-spew v1.1.1 // indirect 33 + github.com/go-logr/logr v1.4.3 // indirect 34 + github.com/go-logr/stdr v1.2.2 // indirect 35 + github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect 24 36 github.com/mattn/go-colorable v0.1.13 // indirect 25 37 github.com/mattn/go-isatty v0.0.20 // indirect 26 38 github.com/pmezard/go-difflib v1.0.0 // indirect 39 + go.opentelemetry.io/auto/sdk v1.2.1 // indirect 40 + go.opentelemetry.io/otel/metric v1.39.0 // indirect 41 + go.opentelemetry.io/otel/trace v1.39.0 // indirect 42 + go.opentelemetry.io/proto/otlp v1.9.0 // indirect 27 43 golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect 28 - golang.org/x/sys v0.29.0 // indirect 44 + golang.org/x/net v0.47.0 // indirect 45 + golang.org/x/sys v0.39.0 // indirect 46 + golang.org/x/text v0.31.0 // indirect 47 + google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 // indirect 48 + google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect 49 + google.golang.org/grpc v1.77.0 // indirect 29 50 gopkg.in/yaml.v3 v3.0.1 // indirect 30 51 )
+61 -3
apps/private-location/go.sum
··· 6 6 github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= 7 7 github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI= 8 8 github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g= 9 + github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= 10 + github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= 11 + github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 12 + github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 9 13 github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo= 10 14 github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= 11 15 github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= ··· 15 19 github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= 16 20 github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4= 17 21 github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0= 22 + github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 23 + github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= 24 + github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 25 + github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 26 + github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 18 27 github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= 19 28 github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= 20 29 github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 30 + github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 31 + github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 21 32 github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 22 33 github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 34 + github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 35 + github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 36 + github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg= 37 + github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4= 23 38 github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= 24 39 github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= 25 40 github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= 26 41 github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 42 + github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 43 + github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 44 + github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 45 + github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 27 46 github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= 28 47 github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 29 48 github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= ··· 39 58 github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 40 59 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 41 60 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 61 + github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= 62 + github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= 42 63 github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= 43 64 github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= 44 65 github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= ··· 46 67 github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= 47 68 github.com/tursodatabase/libsql-client-go v0.0.0-20240902231107-85af5b9d094d h1:dOMI4+zEbDI37KGb0TI44GUAwxHF9cMsIoDTJ7UmgfU= 48 69 github.com/tursodatabase/libsql-client-go v0.0.0-20240902231107-85af5b9d094d/go.mod h1:l8xTsYB90uaVdMHXMCxKKLSgw5wLYBwBKKefNIUnm9s= 70 + go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= 71 + go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= 72 + go.opentelemetry.io/contrib/bridges/otelslog v0.14.0 h1:eypSOd+0txRKCXPNyqLPsbSfA0jULgJcGmSAdFAnrCM= 73 + go.opentelemetry.io/contrib/bridges/otelslog v0.14.0/go.mod h1:CRGvIBL/aAxpQU34ZxyQVFlovVcp67s4cAmQu8Jh9mc= 74 + go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= 75 + go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= 76 + go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.15.0 h1:EKpiGphOYq3CYnIe2eX9ftUkyU+Y8Dtte8OaWyHJ4+I= 77 + go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.15.0/go.mod h1:nWFP7C+T8TygkTjJ7mAyEaFaE7wNfms3nV/vexZ6qt0= 78 + go.opentelemetry.io/otel/log v0.15.0 h1:0VqVnc3MgyYd7QqNVIldC3dsLFKgazR6P3P3+ypkyDY= 79 + go.opentelemetry.io/otel/log v0.15.0/go.mod h1:9c/G1zbyZfgu1HmQD7Qj84QMmwTp2QCQsZH1aeoWDE4= 80 + go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= 81 + go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= 82 + go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= 83 + go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= 84 + go.opentelemetry.io/otel/sdk/log v0.15.0 h1:WgMEHOUt5gjJE93yqfqJOkRflApNif84kxoHWS9VVHE= 85 + go.opentelemetry.io/otel/sdk/log v0.15.0/go.mod h1:qDC/FlKQCXfH5hokGsNg9aUBGMJQsrUyeOiW5u+dKBQ= 86 + go.opentelemetry.io/otel/sdk/log/logtest v0.14.0 h1:Ijbtz+JKXl8T2MngiwqBlPaHqc4YCaP/i13Qrow6gAM= 87 + go.opentelemetry.io/otel/sdk/log/logtest v0.14.0/go.mod h1:dCU8aEL6q+L9cYTqcVOk8rM9Tp8WdnHOPLiBgp0SGOA= 88 + go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= 89 + go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= 90 + go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= 91 + go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= 92 + go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= 93 + go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= 49 94 golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= 50 95 golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= 96 + golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= 97 + golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= 51 98 golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 52 99 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 53 100 golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 54 - golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= 55 - golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 101 + golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= 102 + golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 103 + golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= 104 + golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= 105 + gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= 106 + gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= 107 + google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls= 108 + google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto= 109 + google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww= 110 + google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= 111 + google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= 112 + google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= 56 113 google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= 57 114 google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= 58 - gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 59 115 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 116 + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 117 + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 60 118 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 61 119 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+74
apps/private-location/internal/logs/logs.go
··· 1 + package logs 2 + 3 + import ( 4 + "log/slog" 5 + "math/rand/v2" 6 + "time" 7 + ) 8 + 9 + func ShouldSample(event map[string]any) bool { 10 + statusCode, _ := event["status_code"].(int) 11 + durationMs, _ := event["duration_ms"].(int) 12 + // Always capture: server errors 13 + if statusCode >= 500 { 14 + return true 15 + } 16 + 17 + // Always capture: explicit errors 18 + if _, hasError := event["error"]; hasError { 19 + return true 20 + } 21 + 22 + // Always capture: slow requests (above p99 - 2s threshold) 23 + if durationMs > 2000 { 24 + return true 25 + } 26 + 27 + // Higher sampling for client errors (4xx) - 100% 28 + if statusCode >= 400 && statusCode < 500 { 29 + return true 30 + } 31 + 32 + // Random sample successful, fast requests at 20% 33 + return rand.Float64() < 0.2 34 + } 35 + 36 + // MapToAttrs converts a map[string]any to a slice of slog.Attr 37 + func MapToAttrs(m map[string]any) []slog.Attr { 38 + attrs := make([]slog.Attr, 0, len(m)) 39 + for k, v := range m { 40 + attrs = append(attrs, toAttr(k, v)) 41 + } 42 + return attrs 43 + } 44 + 45 + func toAttr(key string, value any) slog.Attr { 46 + switch v := value.(type) { 47 + case string: 48 + return slog.String(key, v) 49 + case int: 50 + return slog.Int(key, v) 51 + case int64: 52 + return slog.Int64(key, v) 53 + case float64: 54 + return slog.Float64(key, v) 55 + case bool: 56 + return slog.Bool(key, v) 57 + case time.Time: 58 + return slog.Time(key, v) 59 + case time.Duration: 60 + return slog.Duration(key, v) 61 + case map[string]any: 62 + return slog.Group(key, MapToAny(v)...) 63 + default: 64 + return slog.Any(key, v) 65 + } 66 + } 67 + 68 + func MapToAny(m map[string]any) []any { 69 + args := make([]any, 0, len(m)*2) 70 + for k, v := range m { 71 + args = append(args, toAttr(k, v)) 72 + } 73 + return args 74 + }
+112 -3
apps/private-location/internal/server/routes.go
··· 1 1 package server 2 2 3 3 import ( 4 + "context" 5 + "log/slog" 4 6 "net/http" 5 7 "os" 6 8 "time" 7 - 8 - _ "github.com/joho/godotenv/autoload" 9 9 10 10 "github.com/go-chi/chi/v5" 11 11 "github.com/go-chi/render" 12 + "github.com/google/uuid" 12 13 "github.com/jmoiron/sqlx" 14 + _ "github.com/joho/godotenv/autoload" 15 + "github.com/openstatushq/openstatus/apps/private-location/internal/logs" 13 16 "github.com/openstatushq/openstatus/apps/private-location/internal/tinybird" 14 17 v1 "github.com/openstatushq/openstatus/apps/private-location/proto/private_location/v1" 15 18 ) 16 19 17 - // Monitor represents a monitoring job. 20 + type contextKey string 21 + 22 + const ( 23 + requestIDKey contextKey = "request_id" 24 + eventKey contextKey = "event" 25 + ) 26 + 27 + // responseWriter wraps http.ResponseWriter to capture the status code 28 + type responseWriter struct { 29 + http.ResponseWriter 30 + status int 31 + } 32 + 33 + func (rw *responseWriter) WriteHeader(code int) { 34 + rw.status = code 35 + rw.ResponseWriter.WriteHeader(code) 36 + } 37 + 38 + func (rw *responseWriter) Write(b []byte) (int, error) { 39 + if rw.status == 0 { 40 + rw.status = http.StatusOK 41 + } 42 + return rw.ResponseWriter.Write(b) 43 + } 44 + 45 + // Logger returns a Chi middleware that logs request details 46 + func Logger() func(next http.Handler) http.Handler { 47 + return func(next http.Handler) http.Handler { 48 + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 49 + startTime := time.Now() 50 + 51 + // Generate or get request ID 52 + requestID := r.Header.Get("X-Request-ID") 53 + if requestID == "" { 54 + requestID = uuid.New().String() 55 + } 56 + 57 + // Build wide event context at request start 58 + scheme := "http" 59 + if r.TLS != nil { 60 + scheme = "https" 61 + } 62 + fullURL := scheme + "://" + r.Host + r.RequestURI 63 + 64 + event := map[string]any{ 65 + "timestamp": startTime.Format(time.RFC3339), 66 + "request_id": requestID, 67 + "method": r.Method, 68 + "path": r.URL.Path, 69 + "url": fullURL, 70 + "user_agent": r.Header.Get("User-Agent"), 71 + "content_type": r.Header.Get("Content-Type"), 72 + } 73 + 74 + // Store in context 75 + ctx := context.WithValue(r.Context(), requestIDKey, requestID) 76 + ctx = context.WithValue(ctx, eventKey, event) 77 + r = r.WithContext(ctx) 78 + 79 + // Wrap response writer to capture status code 80 + wrapped := &responseWriter{ResponseWriter: w, status: 0} 81 + 82 + // Process request 83 + next.ServeHTTP(wrapped, r) 84 + 85 + // After request - capture response details 86 + duration := time.Since(startTime).Milliseconds() 87 + status := wrapped.status 88 + if status == 0 { 89 + status = http.StatusOK 90 + } 91 + 92 + event["status_code"] = status 93 + event["duration_ms"] = duration 94 + 95 + if status >= 400 { 96 + event["outcome"] = "error" 97 + } else { 98 + event["outcome"] = "success" 99 + } 100 + 101 + if logs.ShouldSample(event) { 102 + attrs := logs.MapToAttrs(event) 103 + slog.LogAttrs(r.Context(), slog.LevelInfo, "request done", attrs...) 104 + } 105 + }) 106 + } 107 + } 108 + 109 + // GetRequestID retrieves the request ID from context 110 + func GetRequestID(ctx context.Context) string { 111 + if id, ok := ctx.Value(requestIDKey).(string); ok { 112 + return id 113 + } 114 + return "" 115 + } 116 + 117 + // GetEvent retrieves the event map from context 118 + func GetEvent(ctx context.Context) map[string]any { 119 + if event, ok := ctx.Value(eventKey).(map[string]any); ok { 120 + return event 121 + } 122 + return nil 123 + } 18 124 19 125 type privateLocationHandler struct { 20 126 db *sqlx.DB ··· 31 137 // RegisterRoutes sets up the HTTP routes for the server. 32 138 func (s *Server) RegisterRoutes() http.Handler { 33 139 r := chi.NewRouter() 140 + 141 + r.Use(Logger()) 142 + 34 143 r.Get("/health", s.healthHandler) 35 144 36 145 tinyBirdToken := os.Getenv("TINYBIRD_TOKEN")
+84 -11
apps/private-location/internal/server/server.go
··· 1 1 package server 2 2 3 3 import ( 4 - // "database/sql" 4 + "context" 5 5 "fmt" 6 + "log/slog" 6 7 "net/http" 7 8 "os" 8 9 "strconv" ··· 10 11 11 12 "github.com/jmoiron/sqlx" 12 13 _ "github.com/joho/godotenv/autoload" 14 + "go.opentelemetry.io/contrib/bridges/otelslog" 15 + "go.opentelemetry.io/otel/attribute" 16 + "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp" 17 + "go.opentelemetry.io/otel/log/global" 18 + sdklog "go.opentelemetry.io/otel/sdk/log" 19 + "go.opentelemetry.io/otel/sdk/resource" 20 + semconv "go.opentelemetry.io/otel/semconv/v1.21.0" 13 21 14 22 "github.com/openstatushq/openstatus/apps/private-location/internal/database" 15 23 ) 16 24 17 25 type Server struct { 18 - port int 19 - 20 - db *sqlx.DB 26 + port int 27 + db *sqlx.DB 28 + logger *slog.Logger 29 + logProvider *sdklog.LoggerProvider 21 30 } 22 31 23 - func NewServer() *http.Server { 32 + // NewServer returns an HTTP server and a cleanup function to shutdown the log provider. 33 + func NewServer() (*http.Server, func(context.Context)) { 24 34 port, _ := strconv.Atoi(os.Getenv("PORT")) 25 - NewServer := &Server{ 26 - port: port, 27 35 28 - db: database.New(), 36 + logger, logProvider := setupLogger() 37 + 38 + newServer := &Server{ 39 + port: port, 40 + db: database.New(), 41 + logger: logger, 42 + logProvider: logProvider, 29 43 } 30 44 31 45 // Declare Server config 32 46 server := &http.Server{ 33 - Addr: fmt.Sprintf(":%d", NewServer.port), 34 - Handler: NewServer.RegisterRoutes(), 47 + Addr: fmt.Sprintf(":%d", newServer.port), 48 + Handler: newServer.RegisterRoutes(), 35 49 IdleTimeout: time.Minute, 36 50 ReadTimeout: 10 * time.Second, 37 51 WriteTimeout: 30 * time.Second, 38 52 } 39 53 40 - return server 54 + // Return cleanup function for graceful shutdown 55 + cleanup := func(ctx context.Context) { 56 + if logProvider != nil { 57 + logProvider.Shutdown(ctx) 58 + } 59 + } 60 + 61 + return server, cleanup 62 + } 63 + 64 + func setupLogger() (*slog.Logger, *sdklog.LoggerProvider) { 65 + ctx := context.Background() 66 + 67 + axiomToken := env("AXIOM_TOKEN", "") 68 + axiomDataset := env("AXIOM_DATASET", "dev") 69 + 70 + // If no Axiom token, return a standard logger 71 + if axiomToken == "" { 72 + logger := slog.Default() 73 + return logger, nil 74 + } 75 + 76 + res := resource.NewWithAttributes( 77 + semconv.SchemaURL, 78 + semconv.ServiceNameKey.String("openstatus-private-location"), 79 + semconv.ServiceVersionKey.String("1.0.0"), 80 + attribute.String("environment", "production"), 81 + ) 82 + 83 + // Set up OTLP log exporter for Axiom 84 + exporter, err := otlploghttp.New(ctx, 85 + otlploghttp.WithEndpointURL("https://eu-central-1.aws.edge.axiom.co/v1/logs"), 86 + otlploghttp.WithHeaders(map[string]string{ 87 + "Authorization": "Bearer " + axiomToken, 88 + "X-Axiom-Dataset": axiomDataset, 89 + }), 90 + ) 91 + if err != nil { 92 + fmt.Fprintf(os.Stderr, "failed to create OTLP exporter: %v\n", err) 93 + return slog.Default(), nil 94 + } 95 + 96 + // Create log provider with resource and batch processor 97 + logProvider := sdklog.NewLoggerProvider( 98 + sdklog.WithResource(res), 99 + sdklog.WithProcessor(sdklog.NewBatchProcessor(exporter)), 100 + ) 101 + 102 + global.SetLoggerProvider(logProvider) 103 + logger := otelslog.NewLogger("openstatus-private-location") 104 + slog.SetDefault(logger) 105 + 106 + return logger, logProvider 107 + } 108 + 109 + func env(key, fallback string) string { 110 + if value, ok := os.LookupEnv(key); ok { 111 + return value 112 + } 113 + return fallback 41 114 }