Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c15371c236 | |||
|
aaa111dd20
|
|||
|
8e02bfb0a2
|
|||
| 1e34fe77eb | |||
| 30ca9091d2 | |||
|
4dd79e3d73
|
|||
| bdfbf6c22e | |||
| b6a281b5f2 | |||
| e8305441bb | |||
|
dd5c0f3dc0
|
@@ -41,7 +41,7 @@ repos:
|
||||
hooks:
|
||||
- id: golangci-lint-full
|
||||
- repo: https://github.com/gitleaks/gitleaks
|
||||
rev: v8.24.2
|
||||
rev: v8.24.3
|
||||
hooks:
|
||||
- id: gitleaks
|
||||
exclude: '^ctl/generated.go|graph/generated/.*$|^graph/model/models_gen.go|^tools/.*$$'
|
||||
|
||||
@@ -2,6 +2,25 @@
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [0.4.0] - 2025-04-12
|
||||
|
||||
### 🚀 Features
|
||||
|
||||
- *(service)* Implement graceful shutdown for HTTP server
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- *(secrets)* Remove namespace from ExternalSecret definition
|
||||
- *(deps)* Update module github.com/sparetimecoders/goamqp to v0.3.2
|
||||
- *(deps)* Update module github.com/vektah/gqlparser/v2 to v2.5.24
|
||||
- *(deps)* Update module github.com/getsentry/sentry-go to v0.32.0
|
||||
- *(k8s)* Increase CPU request for better performance
|
||||
- *(deps)* Update module gitlab.com/unboundsoftware/eventsourced/amqp to v1.8.1
|
||||
|
||||
### 🚜 Refactor
|
||||
|
||||
- *(deploy)* Remove cpu and memory limits for schemas
|
||||
|
||||
## [0.3.0] - 2025-04-08
|
||||
|
||||
### 🚀 Features
|
||||
|
||||
Vendored
+4
-4
@@ -2,9 +2,9 @@ package cache
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"github.com/apex/log"
|
||||
"github.com/sparetimecoders/goamqp"
|
||||
|
||||
"gitlab.com/unboundsoftware/schemas/domain"
|
||||
@@ -18,7 +18,7 @@ type Cache struct {
|
||||
services map[string]map[string]map[string]struct{}
|
||||
subGraphs map[string]string
|
||||
lastUpdate map[string]string
|
||||
logger log.Interface
|
||||
logger *slog.Logger
|
||||
}
|
||||
|
||||
func (c *Cache) OrganizationByAPIKey(apiKey string) *domain.Organization {
|
||||
@@ -98,7 +98,7 @@ func (c *Cache) Update(msg any, _ goamqp.Headers) (any, error) {
|
||||
case *domain.SubGraph:
|
||||
c.updateSubGraph(m.OrganizationId, m.Ref, m.ID.String(), m.Service, m.ChangedAt)
|
||||
default:
|
||||
c.logger.Warnf("unexpected message received: %+v", msg)
|
||||
c.logger.With("msg", msg).Warn("unexpected message received")
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
@@ -124,7 +124,7 @@ func (c *Cache) addUser(sub string, organization domain.Organization) {
|
||||
}
|
||||
}
|
||||
|
||||
func New(logger log.Interface) *Cache {
|
||||
func New(logger *slog.Logger) *Cache {
|
||||
return &Cache{
|
||||
organizations: make(map[string]domain.Organization),
|
||||
users: make(map[string][]string),
|
||||
|
||||
+18
-16
@@ -2,7 +2,9 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
@@ -17,8 +19,6 @@ import (
|
||||
"github.com/99designs/gqlgen/graphql/handler/transport"
|
||||
"github.com/99designs/gqlgen/graphql/playground"
|
||||
"github.com/alecthomas/kong"
|
||||
"github.com/apex/log"
|
||||
"github.com/apex/log/handlers/json"
|
||||
"github.com/getsentry/sentry-go"
|
||||
sentryhttp "github.com/getsentry/sentry-go/http"
|
||||
"github.com/rs/cors"
|
||||
@@ -32,6 +32,7 @@ import (
|
||||
"gitlab.com/unboundsoftware/schemas/domain"
|
||||
"gitlab.com/unboundsoftware/schemas/graph"
|
||||
"gitlab.com/unboundsoftware/schemas/graph/generated"
|
||||
"gitlab.com/unboundsoftware/schemas/logging"
|
||||
"gitlab.com/unboundsoftware/schemas/middleware"
|
||||
"gitlab.com/unboundsoftware/schemas/store"
|
||||
)
|
||||
@@ -59,9 +60,7 @@ const serviceName = "schemas"
|
||||
func main() {
|
||||
var cli CLI
|
||||
_ = kong.Parse(&cli)
|
||||
log.SetHandler(json.New(os.Stdout))
|
||||
log.SetLevelFromString(cli.LogLevel)
|
||||
logger := log.WithField("service", serviceName)
|
||||
logger := logging.SetupLogger(cli.LogLevel, serviceName, buildVersion)
|
||||
closeEvents := make(chan error)
|
||||
|
||||
if err := start(
|
||||
@@ -70,11 +69,11 @@ func main() {
|
||||
ConnectAMQP,
|
||||
cli,
|
||||
); err != nil {
|
||||
logger.WithError(err).Error("process error")
|
||||
logger.With("error", err).Error("process error")
|
||||
}
|
||||
}
|
||||
|
||||
func start(closeEvents chan error, logger *log.Entry, connectToAmqpFunc func(url string) (Connection, error), cli CLI) error {
|
||||
func start(closeEvents chan error, logger *slog.Logger, connectToAmqpFunc func(url string) (Connection, error), cli CLI) error {
|
||||
if err := setupSentry(logger, cli.SentryConfig); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -123,7 +122,7 @@ func start(closeEvents chan error, logger *log.Entry, connectToAmqpFunc func(url
|
||||
return fmt.Errorf("caching subgraphs: %w", err)
|
||||
}
|
||||
setups := []goamqp.Setup{
|
||||
goamqp.UseLogger(logger.Error),
|
||||
goamqp.UseLogger(func(s string) { logger.Error(s) }),
|
||||
goamqp.CloseListener(closeEvents),
|
||||
goamqp.WithPrefetchLimit(20),
|
||||
goamqp.EventStreamPublisher(publisher),
|
||||
@@ -169,7 +168,7 @@ func start(closeEvents chan error, logger *log.Entry, connectToAmqpFunc func(url
|
||||
defer wg.Done()
|
||||
err := <-closeEvents
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("received close from AMQP")
|
||||
logger.With("error", err).Error("received close from AMQP")
|
||||
rootCancel()
|
||||
}
|
||||
}()
|
||||
@@ -179,8 +178,11 @@ func start(closeEvents chan error, logger *log.Entry, connectToAmqpFunc func(url
|
||||
defer wg.Done()
|
||||
<-rootCtx.Done()
|
||||
|
||||
if err := httpSrv.Close(); err != nil {
|
||||
logger.WithError(err).Error("close http server")
|
||||
shutdownCtx, shutdownRelease := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer shutdownRelease()
|
||||
|
||||
if err := httpSrv.Shutdown(shutdownCtx); err != nil {
|
||||
logger.With("error", err).Error("close http server")
|
||||
}
|
||||
close(sigint)
|
||||
close(closeEvents)
|
||||
@@ -235,10 +237,10 @@ func start(closeEvents chan error, logger *log.Entry, connectToAmqpFunc func(url
|
||||
),
|
||||
))
|
||||
|
||||
logger.Infof("connect to http://localhost:%d/ for GraphQL playground", cli.Port)
|
||||
logger.Info(fmt.Sprintf("connect to http://localhost:%d/ for GraphQL playground", cli.Port))
|
||||
|
||||
if err := httpSrv.ListenAndServe(); err != nil {
|
||||
logger.WithError(err).Error("listen http")
|
||||
if err := httpSrv.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
|
||||
logger.With("error", err).Error("listen http")
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -287,7 +289,7 @@ func healthFunc(w http.ResponseWriter, _ *http.Request) {
|
||||
_, _ = w.Write([]byte("OK"))
|
||||
}
|
||||
|
||||
func setupSentry(logger log.Interface, args SentryConfig) error {
|
||||
func setupSentry(logger *slog.Logger, args SentryConfig) error {
|
||||
if args.Environment == "" {
|
||||
return fmt.Errorf("no Sentry environment supplied, exiting")
|
||||
}
|
||||
@@ -315,7 +317,7 @@ func setupSentry(logger log.Interface, args SentryConfig) error {
|
||||
if err := sentry.Init(cfg); err != nil {
|
||||
return fmt.Errorf("sentry setup: %w", err)
|
||||
}
|
||||
logger.Infof("configured Sentry for env: %s", args.Environment)
|
||||
logger.With("environment", args.Environment).Info("configured Sentry")
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -8,17 +8,17 @@ require (
|
||||
github.com/alecthomas/kong v1.10.0
|
||||
github.com/apex/log v1.9.0
|
||||
github.com/auth0/go-jwt-middleware/v2 v2.3.0
|
||||
github.com/getsentry/sentry-go v0.31.1
|
||||
github.com/getsentry/sentry-go v0.32.0
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2
|
||||
github.com/jmoiron/sqlx v1.4.0
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/pressly/goose/v3 v3.24.2
|
||||
github.com/rs/cors v1.11.1
|
||||
github.com/sparetimecoders/goamqp v0.3.1
|
||||
github.com/sparetimecoders/goamqp v0.3.2
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/vektah/gqlparser/v2 v2.5.23
|
||||
github.com/vektah/gqlparser/v2 v2.5.24
|
||||
github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.169
|
||||
gitlab.com/unboundsoftware/eventsourced/amqp v1.8.0
|
||||
gitlab.com/unboundsoftware/eventsourced/amqp v1.8.1
|
||||
gitlab.com/unboundsoftware/eventsourced/eventsourced v1.19.2
|
||||
gitlab.com/unboundsoftware/eventsourced/pg v1.17.0
|
||||
)
|
||||
|
||||
@@ -46,8 +46,8 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/getsentry/sentry-go v0.31.1 h1:ELVc0h7gwyhnXHDouXkhqTFSO5oslsRDk0++eyE0KJ4=
|
||||
github.com/getsentry/sentry-go v0.31.1/go.mod h1:CYNcMMz73YigoHljQRG+qPF+eMq8gG72XcGN/p71BAY=
|
||||
github.com/getsentry/sentry-go v0.32.0 h1:YKs+//QmwE3DcYtfKRH8/KyOOF/I6Qnx7qYGNHCGmCY=
|
||||
github.com/getsentry/sentry-go v0.32.0/go.mod h1:CYNcMMz73YigoHljQRG+qPF+eMq8gG72XcGN/p71BAY=
|
||||
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
|
||||
github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
@@ -136,8 +136,8 @@ github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9/go.mod h
|
||||
github.com/smartystreets/gunit v1.0.0/go.mod h1:qwPWnhz6pn0NnRBP++URONOVyNkPyr4SauJk4cUOwJs=
|
||||
github.com/sosodev/duration v1.3.1 h1:qtHBDMQ6lvMQsL15g4aopM4HEfOaYuhWBw3NPTtlqq4=
|
||||
github.com/sosodev/duration v1.3.1/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg=
|
||||
github.com/sparetimecoders/goamqp v0.3.1 h1:NCzdyAz84G679HlO+ivhyoI1aMgXEe3qfqpn4EChu1s=
|
||||
github.com/sparetimecoders/goamqp v0.3.1/go.mod h1:PjkgrmsuMVgRbiQDTLs0pCWYrcQgqcUee38JjCDZdlk=
|
||||
github.com/sparetimecoders/goamqp v0.3.2 h1:XdlyUBAJS5RcURw+SnnPjPJJuofddZwQsjAf05VPXvI=
|
||||
github.com/sparetimecoders/goamqp v0.3.2/go.mod h1:W9NRCpWLE+Vruv2dcRSbszNil2O826d2Nv6kAkETW5o=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
@@ -162,16 +162,16 @@ github.com/tj/go-kinesis v0.0.0-20171128231115-08b17f58cb1b/go.mod h1:/yhzCV0xPf
|
||||
github.com/tj/go-spin v1.1.0/go.mod h1:Mg1mzmePZm4dva8Qz60H2lHwmJ2loum4VIrLgVnKwh4=
|
||||
github.com/urfave/cli/v2 v2.27.6 h1:VdRdS98FNhKZ8/Az8B7MTyGQmpIr36O1EHybx/LaZ4g=
|
||||
github.com/urfave/cli/v2 v2.27.6/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ=
|
||||
github.com/vektah/gqlparser/v2 v2.5.23 h1:PurJ9wpgEVB7tty1seRUwkIDa/QH5RzkzraiKIjKLfA=
|
||||
github.com/vektah/gqlparser/v2 v2.5.23/go.mod h1:D1/VCZtV3LPnQrcPBeR/q5jkSQIPti0uYCP/RI0gIeo=
|
||||
github.com/vektah/gqlparser/v2 v2.5.24 h1:Dnip1ilW+nnXmaXL6s6f1w4IaXpAFDLLE1f9SqMegpI=
|
||||
github.com/vektah/gqlparser/v2 v2.5.24/go.mod h1:D1/VCZtV3LPnQrcPBeR/q5jkSQIPti0uYCP/RI0gIeo=
|
||||
github.com/wundergraph/astjson v0.0.0-20250106123708-be463c97e083 h1:8/D7f8gKxTBjW+SZK4mhxTTBVpxcqeBgWF1Rfmltbfk=
|
||||
github.com/wundergraph/astjson v0.0.0-20250106123708-be463c97e083/go.mod h1:eOTL6acwctsN4F3b7YE+eE2t8zcJ/doLm9sZzsxxxrE=
|
||||
github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.169 h1:x8cSom3UvI8PqRB+4DNJKepP3cA5KgBm0vEnEalZyvo=
|
||||
github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.169/go.mod h1:B7eV0Qh8Lop9QzIOQcsvKp3S0ejfC6mgyWoJnI917yQ=
|
||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
|
||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
|
||||
gitlab.com/unboundsoftware/eventsourced/amqp v1.8.0 h1:Ez3UuXvveyV/HlphqWUWZU/05DuhqKfgF9r6tCJigEI=
|
||||
gitlab.com/unboundsoftware/eventsourced/amqp v1.8.0/go.mod h1:HO9ZBFAvBNRhVS3hXPuGMW7zyENDfoqjLCUAzPWw3so=
|
||||
gitlab.com/unboundsoftware/eventsourced/amqp v1.8.1 h1:MGHH2Uxp68J9i4V3/3vApB6gjBUjn6RjiPHhbc8Wsno=
|
||||
gitlab.com/unboundsoftware/eventsourced/amqp v1.8.1/go.mod h1:clGBkdpFWb5/27aLOhJ6+DB15enJf+T4J5lR6X0lqAs=
|
||||
gitlab.com/unboundsoftware/eventsourced/eventsourced v1.19.2 h1:8sCnThNHEPB3BQomcJ7u6fmc2t043fAZSMmVPDDbQOs=
|
||||
gitlab.com/unboundsoftware/eventsourced/eventsourced v1.19.2/go.mod h1:KeLn3U67hxbdFLfeXd0c0LI/r1C5rijbWrfNdARWe98=
|
||||
gitlab.com/unboundsoftware/eventsourced/pg v1.17.0 h1:pUJzMpNPX0GVsffRZXlpKR1d7Ws96KTxJwbLFPpASSc=
|
||||
|
||||
+2
-2
@@ -3,8 +3,8 @@ package graph
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
|
||||
"github.com/apex/log"
|
||||
"gitlab.com/unboundsoftware/eventsourced/eventsourced"
|
||||
|
||||
"gitlab.com/unboundsoftware/schemas/cache"
|
||||
@@ -26,7 +26,7 @@ type Publisher interface {
|
||||
type Resolver struct {
|
||||
EventStore eventsourced.EventStore
|
||||
Publisher Publisher
|
||||
Logger log.Interface
|
||||
Logger *slog.Logger
|
||||
Cache *cache.Cache
|
||||
}
|
||||
|
||||
|
||||
+1
-4
@@ -41,11 +41,8 @@ spec:
|
||||
containers:
|
||||
- name: schemas
|
||||
resources:
|
||||
limits:
|
||||
cpu: "500m"
|
||||
memory: "100Mi"
|
||||
requests:
|
||||
cpu: "10m"
|
||||
cpu: "20m"
|
||||
memory: "20Mi"
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
|
||||
@@ -2,7 +2,6 @@ apiVersion: external-secrets.io/v1beta1
|
||||
kind: ExternalSecret
|
||||
metadata:
|
||||
name: schemas
|
||||
namespace: default
|
||||
spec:
|
||||
refreshInterval: 1h
|
||||
secretStoreRef:
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
package logging
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"os"
|
||||
)
|
||||
|
||||
type Logger interface {
|
||||
Info(msg string, args ...any)
|
||||
Warn(msg string, args ...any)
|
||||
Error(msg string, args ...any)
|
||||
}
|
||||
|
||||
var defaultLogger *slog.Logger
|
||||
|
||||
type contextKey string
|
||||
|
||||
const loggerKey = contextKey("logger")
|
||||
|
||||
func SetupLogger(logLevel, serviceName, buildVersion string) *slog.Logger {
|
||||
var leveler slog.LevelVar
|
||||
|
||||
err := leveler.UnmarshalText([]byte(logLevel))
|
||||
|
||||
defaultLogger = slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
|
||||
AddSource: false,
|
||||
Level: leveler.Level(),
|
||||
ReplaceAttr: nil,
|
||||
})).With("service", serviceName).With("version", buildVersion)
|
||||
|
||||
if err != nil {
|
||||
defaultLogger.With("err", err).Error("Failed to parse log level")
|
||||
os.Exit(1)
|
||||
}
|
||||
slog.SetDefault(defaultLogger)
|
||||
return defaultLogger
|
||||
}
|
||||
|
||||
// ContextWithLogger returns a new Context with the logger attached
|
||||
func ContextWithLogger(ctx context.Context, logger *slog.Logger) context.Context {
|
||||
return context.WithValue(ctx, loggerKey, logger)
|
||||
}
|
||||
|
||||
// LoggerFromContext returns a logger from the passed context or the default logger
|
||||
func LoggerFromContext(ctx context.Context) *slog.Logger {
|
||||
logger := ctx.Value(loggerKey)
|
||||
if l, ok := logger.(*slog.Logger); ok {
|
||||
return l
|
||||
}
|
||||
return defaultLogger
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package logging
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"log/slog"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func NewMockLogger() *MockLogger {
|
||||
logged := &bytes.Buffer{}
|
||||
|
||||
return &MockLogger{
|
||||
logged: logged,
|
||||
logger: slog.New(slog.NewTextHandler(logged, &slog.HandlerOptions{
|
||||
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
|
||||
if a.Key == "time" {
|
||||
return slog.Attr{}
|
||||
}
|
||||
return a
|
||||
},
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
type MockLogger struct {
|
||||
logger *slog.Logger
|
||||
logged *bytes.Buffer
|
||||
}
|
||||
|
||||
func (m *MockLogger) Logger() *slog.Logger {
|
||||
return m.logger
|
||||
}
|
||||
|
||||
func (m *MockLogger) Check(t testing.TB, wantLogged []string) {
|
||||
var gotLogged []string
|
||||
if m.logged.String() != "" {
|
||||
gotLogged = strings.Split(m.logged.String(), "\n")
|
||||
gotLogged = gotLogged[:len(gotLogged)-1]
|
||||
}
|
||||
if len(wantLogged) == 0 {
|
||||
assert.Empty(t, gotLogged)
|
||||
return
|
||||
}
|
||||
assert.Equal(t, wantLogged, gotLogged)
|
||||
}
|
||||
Reference in New Issue
Block a user