From a84f3b0745e350858e7200fedb563e8445517763 Mon Sep 17 00:00:00 2001 From: Yazeed AlKhalaf Date: Wed, 8 Jan 2025 22:28:49 +0300 Subject: [PATCH] add logging --- compose.override.yaml | 12 +++- compose.yaml | 33 +++++++++ falak/cmd/main.go | 20 +++++- falak/falak.env.example | 5 +- falak/go.mod | 14 ++-- falak/go.sum | 28 ++++---- falak/pkg/interceptors/logging.go | 6 +- falak/pkg/lokilogger/logger.go | 109 ++++++++++++++++++++++++++++++ falak/pkg/util/config.go | 39 ++++++----- 9 files changed, 218 insertions(+), 48 deletions(-) create mode 100644 falak/pkg/lokilogger/logger.go diff --git a/compose.override.yaml b/compose.override.yaml index 0c0e3de..fe14451 100644 --- a/compose.override.yaml +++ b/compose.override.yaml @@ -25,6 +25,8 @@ services: - "traefik.http.routers.falak.entrypoints=web" - "traefik.http.services.falak.loadbalancer.server.port=${FALAK_PORT}" - "traefik.http.services.falak.loadbalancer.server.scheme=h2c" + ports: + - "9090:50064" website: labels: @@ -41,13 +43,17 @@ services: - "traefik.http.routers.grafana.entrypoints=web" - "traefik.http.services.grafana.loadbalancer.server.port=3000" + loki: + ports: + - "3100:3100" + plantuml: image: plantuml/plantuml-server:jetty labels: - "traefik.enable=true" - "traefik.docker.network=symmetrical-spoon_web" - - "traefik.http.routers.grafana.rule=Host(`plantuml.localhost`)" - - "traefik.http.routers.grafana.entrypoints=web" - - "traefik.http.services.grafana.loadbalancer.server.port=8080" + - "traefik.http.routers.plantuml.rule=Host(`plantuml.localhost`)" + - "traefik.http.routers.plantuml.entrypoints=web" + - "traefik.http.services.plantuml.loadbalancer.server.port=8080" networks: - web diff --git a/compose.yaml b/compose.yaml index 62c52f0..1c11bf1 100644 --- a/compose.yaml +++ b/compose.yaml @@ -50,6 +50,8 @@ services: depends_on: postgresdb: condition: service_healthy + loki: + condition: service_started networks: - web - internal @@ -63,15 +65,46 @@ services: grafana: image: grafana/grafana:latest + entrypoint: + - sh + - -euc + - | + mkdir -p /etc/grafana/provisioning/datasources + cat < /etc/grafana/provisioning/datasources/ds.yaml + apiVersion: 1 + datasources: + - name: Loki + type: loki + access: proxy + orgId: 1 + url: http://loki:3100 + basicAuth: false + isDefault: true + version: 1 + editable: false + EOF + /run.sh networks: - web - internal volumes: - grafana-storage:/var/lib/grafana + depends_on: + loki: + condition: service_started + + loki: + image: grafana/loki:3.3.2 + command: -config.file=/etc/loki/local-config.yaml + networks: + - internal + volumes: + - loki-storage:/loki volumes: postgres_data: grafana-storage: + loki-storage: networks: web: diff --git a/falak/cmd/main.go b/falak/cmd/main.go index 794e380..8eb6507 100644 --- a/falak/cmd/main.go +++ b/falak/cmd/main.go @@ -25,6 +25,7 @@ import ( googleclient "github.com/jadwalapp/symmetrical-spoon/falak/pkg/google/client" "github.com/jadwalapp/symmetrical-spoon/falak/pkg/httpclient" "github.com/jadwalapp/symmetrical-spoon/falak/pkg/interceptors" + "github.com/jadwalapp/symmetrical-spoon/falak/pkg/lokilogger" "github.com/jadwalapp/symmetrical-spoon/falak/pkg/store" "github.com/jadwalapp/symmetrical-spoon/falak/pkg/tokens" "github.com/jadwalapp/symmetrical-spoon/falak/pkg/util" @@ -40,13 +41,26 @@ import ( const dbDriverName = "pgx" func main() { - log.Logger = zerolog.New(os.Stderr).With().Timestamp().Logger() - config, err := util.LoadFalakConfig(".") if err != nil { log.Fatal().Msgf("cannot load config: %v", err) } + // ======== LOKI CLIENT ======== + lokiHookConfig := lokilogger.LokiConfig{ + PushIntervalSeconds: int64(config.LokiPushIntervalSeconds), + MaxBatchSize: config.LokiMaxBatchSize, + LokiEndpoint: config.LokiEndpoint, + ServiceName: "falak", + } + lokiClient := lokilogger.NewLokiClient(&lokiHookConfig) + // ======== LOKI CLIENT ======== + + // ======== LOGGER ======== + multiLevelWriters := zerolog.MultiLevelWriter(os.Stdout, lokiClient) + log.Logger = zerolog.New(multiLevelWriters).With().Timestamp().Logger() + // ======== LOGGER ======== + // ======== DATABASE ======== dbSource := util.CreateDbSource( config.DBUser, @@ -145,7 +159,7 @@ func main() { // ======== INTERCEPTORS ======== interceptorsForServer := connect.WithInterceptors( - interceptors.LoggingInterceptor(), + interceptors.LoggingInterceptor(lokiClient), interceptors.EnsureValidTokenInterceptor(tokens, apiMetadata), interceptors.LangInterceptor(apiMetadata), ) diff --git a/falak/falak.env.example b/falak/falak.env.example index 35e9b7a..7811185 100644 --- a/falak/falak.env.example +++ b/falak/falak.env.example @@ -15,4 +15,7 @@ SMTP_PASSWORD= DOMAIN=http://localhost:50064 RESEND_API_KEY= GOOGLE_CLIENT_BASE_URL= -GOOGLE_OAUTH_CLIENT_ID= \ No newline at end of file +GOOGLE_OAUTH_CLIENT_ID= +LOKI_ENDPOINT=http://loki:3100 +LOKI_PUSH_INTERVAL_SECONDS=1 +LOKI_MAX_BATCH_SIZE=10 \ No newline at end of file diff --git a/falak/go.mod b/falak/go.mod index fa59c18..651afbd 100644 --- a/falak/go.mod +++ b/falak/go.mod @@ -45,15 +45,15 @@ require ( github.com/spf13/pflag v1.0.5 // indirect github.com/stoewer/go-strcase v1.3.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect - go.uber.org/atomic v1.9.0 // indirect - go.uber.org/multierr v1.9.0 // indirect - golang.org/x/crypto v0.28.0 // indirect + go.uber.org/atomic v1.11.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + golang.org/x/crypto v0.31.0 // indirect golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c // indirect - golang.org/x/sync v0.8.0 // indirect - golang.org/x/sys v0.26.0 // indirect - golang.org/x/text v0.19.0 // indirect + golang.org/x/sync v0.10.0 // indirect + golang.org/x/sys v0.28.0 // indirect + golang.org/x/text v0.21.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20241007155032-5fefd90f89a9 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/falak/go.sum b/falak/go.sum index 3cb29b9..aebb0ab 100644 --- a/falak/go.sum +++ b/falak/go.sum @@ -149,29 +149,29 @@ go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2 go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8= go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4= go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ= -go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= -go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= -go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= -golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= -golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY= golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8= golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= -golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= -golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= -golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= -golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= google.golang.org/genproto/googleapis/api v0.0.0-20241007155032-5fefd90f89a9 h1:T6rh4haD3GVYsgEfWExoCZA2o2FmbNyKpTuAxbEFPTg= google.golang.org/genproto/googleapis/api v0.0.0-20241007155032-5fefd90f89a9/go.mod h1:wp2WsuBYj6j8wUdo3ToZsdxxixbvQNAHqVJrTgi5E5M= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9 h1:QCqS/PdaHTSWGvupk2F/ehwHtGc0/GYkT+3GAcR1CCc= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 h1:X58yt85/IXCx0Y3ZwN6sEIKZzQtDEYaBWrDvErdXrRE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI= google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/falak/pkg/interceptors/logging.go b/falak/pkg/interceptors/logging.go index 3e9c381..d17867b 100644 --- a/falak/pkg/interceptors/logging.go +++ b/falak/pkg/interceptors/logging.go @@ -7,12 +7,13 @@ import ( "connectrpc.com/connect" "github.com/google/uuid" + "github.com/jadwalapp/symmetrical-spoon/falak/pkg/lokilogger" "github.com/rs/zerolog" ) type traceIDKey struct{} -func LoggingInterceptor() connect.UnaryInterceptorFunc { +func LoggingInterceptor(lokiClient *lokilogger.LokiClient) connect.UnaryInterceptorFunc { return func(next connect.UnaryFunc) connect.UnaryFunc { return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) { traceID, ok := ctx.Value(traceIDKey{}).(string) @@ -21,7 +22,8 @@ func LoggingInterceptor() connect.UnaryInterceptorFunc { ctx = context.WithValue(ctx, traceIDKey{}, traceID) } - logger := zerolog.New(os.Stderr).With(). + multiLevelWriters := zerolog.MultiLevelWriter(os.Stdout, lokiClient) + logger := zerolog.New(multiLevelWriters).With(). Timestamp(). Str("method", req.Spec().Procedure). Str("trace_id", traceID). diff --git a/falak/pkg/lokilogger/logger.go b/falak/pkg/lokilogger/logger.go new file mode 100644 index 0000000..46b31e8 --- /dev/null +++ b/falak/pkg/lokilogger/logger.go @@ -0,0 +1,109 @@ +package lokilogger + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "time" +) + +type LokiConfig struct { + PushIntervalSeconds int64 + MaxBatchSize int + LokiEndpoint string + ServiceName string +} + +type lokiStream struct { + Stream map[string]string `json:"stream"` + Values [][]string `json:"values"` +} + +type lokiLogEvent struct { + Streams []lokiStream `json:"streams"` +} + +type LokiClient struct { + config *LokiConfig + logs [][]string + done chan bool +} + +func (l *LokiClient) bgRun() { + for { + l.sendLogsToLoki() + + select { + case <-l.done: + return + default: + time.Sleep(time.Second * time.Duration(l.config.PushIntervalSeconds)) + } + } +} + +func (l *LokiClient) sendLogsToLoki() { + if len(l.logs) == 0 { + return + } + + var logsToSend [][]string + if len(l.logs) > l.config.MaxBatchSize { + logsToSend = l.logs[:l.config.MaxBatchSize] + l.logs = l.logs[l.config.MaxBatchSize:] + } else { + logsToSend = l.logs + l.logs = [][]string{} + } + + data, err := json.Marshal(lokiLogEvent{ + Streams: []lokiStream{ + { + Stream: map[string]string{ + "service": l.config.ServiceName, + }, + Values: logsToSend, + }, + }, + }) + if err != nil { + fmt.Printf("Error marshalling logs: %v\n", err) + return + } + + req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("%s/loki/api/v1/push", l.config.LokiEndpoint), bytes.NewBuffer(data)) + if err != nil { + fmt.Printf("Error creating request: %v\n", err) + return + } + + req.Header.Set("Content-Type", "application/json") + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + fmt.Printf("Error sending logs: %v\n", err) + return + } + defer resp.Body.Close() +} + +func (l *LokiClient) Write(p []byte) (n int, err error) { + l.logs = append(l.logs, []string{ + fmt.Sprintf("%d", time.Now().UnixNano()), + string(p), + }) + + return len(p), nil +} + +func NewLokiClient(config *LokiConfig) *LokiClient { + client := &LokiClient{ + config: config, + logs: [][]string{}, + done: make(chan bool), + } + go client.bgRun() + return client +} diff --git a/falak/pkg/util/config.go b/falak/pkg/util/config.go index 755e0e5..befeb9a 100644 --- a/falak/pkg/util/config.go +++ b/falak/pkg/util/config.go @@ -5,24 +5,27 @@ import "github.com/spf13/viper" // FalakConfig stores all configuration of the application. // The values are read by viper from a config file or environment variables. type FalakConfig struct { - Port string `mapstructure:"PORT"` - JWTPublicKey string `mapstructure:"JWT_PUBLIC_KEY"` - JWTPrivateKey string `mapstructure:"JWT_PRIVATE_KEY"` - DBUser string `mapstructure:"DB_USER"` - DBPassword string `mapstructure:"DB_PASSWORD"` - DBHost string `mapstructure:"DB_HOST"` - DBPort string `mapstructure:"DB_PORT"` - DBName string `mapstructure:"DB_NAME"` - DBSSLMode string `mapstructure:"DB_SSL_MODE"` - EmailerName string `mapstructure:"EMAILER_NAME"` - SMTPHost string `mapstructure:"SMTP_HOST"` - SMTPPort string `mapstructure:"SMTP_PORT"` - SMTPUSername string `mapstructure:"SMTP_USERNAME"` - SMTPPasword string `mapstructure:"SMTP_PASSWORD"` - Domain string `mapstructure:"DOMAIN"` - ResendApiKey string `mapstructure:"RESEND_API_KEY"` - GoogleClientBaseUrl string `mapstructure:"GOOGLE_CLIENT_BASE_URL"` - GoogleOAuthClientId string `mapstructure:"GOOGLE_OAUTH_CLIENT_ID"` + Port string `mapstructure:"PORT"` + JWTPublicKey string `mapstructure:"JWT_PUBLIC_KEY"` + JWTPrivateKey string `mapstructure:"JWT_PRIVATE_KEY"` + DBUser string `mapstructure:"DB_USER"` + DBPassword string `mapstructure:"DB_PASSWORD"` + DBHost string `mapstructure:"DB_HOST"` + DBPort string `mapstructure:"DB_PORT"` + DBName string `mapstructure:"DB_NAME"` + DBSSLMode string `mapstructure:"DB_SSL_MODE"` + EmailerName string `mapstructure:"EMAILER_NAME"` + SMTPHost string `mapstructure:"SMTP_HOST"` + SMTPPort string `mapstructure:"SMTP_PORT"` + SMTPUSername string `mapstructure:"SMTP_USERNAME"` + SMTPPasword string `mapstructure:"SMTP_PASSWORD"` + Domain string `mapstructure:"DOMAIN"` + ResendApiKey string `mapstructure:"RESEND_API_KEY"` + GoogleClientBaseUrl string `mapstructure:"GOOGLE_CLIENT_BASE_URL"` + GoogleOAuthClientId string `mapstructure:"GOOGLE_OAUTH_CLIENT_ID"` + LokiEndpoint string `mapstructure:"LOKI_ENDPOINT"` + LokiPushIntervalSeconds int `mapstructure:"LOKI_PUSH_INTERVAL_SECONDS"` + LokiMaxBatchSize int `mapstructure:"LOKI_MAX_BATCH_SIZE"` } // LoadFalakConfig reads configuration from the provided path or environment variables.