diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 20c86c4..6bcb24c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,7 +13,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v4 with: - go-version: "1.25.4" + go-version: "1.25.5" - name: Run tests run: make test diff --git a/Makefile b/Makefile index 1717abf..2446003 100644 --- a/Makefile +++ b/Makefile @@ -15,7 +15,7 @@ compile: .PHONY: test test: - go test -cover -race -v ./... + go test -cover -race ./... .PHONY: build build: diff --git a/README.md b/README.md index 120b5c0..464f9d7 100644 --- a/README.md +++ b/README.md @@ -37,9 +37,23 @@ This will: Then test it: ```bash -kubectl port-forward pod/mokv-0 9800:8400 +➜ kubectl port-forward pod/mokv-0 9800:8400 # In another terminal: -go run cmd/get_servers.go -addr localhost:9800 +➜ go run cmd/test_kv.go -addr localhost:8400 +Getting servers: + - mokv-0.mokv.default.svc.cluster.local:8400 -> is leader: true + - mokv-1.mokv.default.svc.cluster.local:8400 -> is leader: false + - mokv-2.mokv.default.svc.cluster.local:8400 -> is leader: false + +Setting key 'hello' = 'world' +Set OK: true + +Getting key 'hello' +Got: hello = world +``` + +```bash +kubectl scale statefulset mokv --replicas=5 ``` ### Configuration diff --git a/cmd/test_kv.go b/cmd/test_kv.go index fc05924..3f0bc8a 100644 --- a/cmd/test_kv.go +++ b/cmd/test_kv.go @@ -36,7 +36,7 @@ func main() { } // Set - fmt.Println("Setting key 'hello' = 'world'") + fmt.Println("\nSetting key 'hello' = 'world'") setRes, err := client.Set(ctx, &api.SetRequest{Key: "hello", Value: []byte("world")}) if err != nil { log.Fatal(err) @@ -49,5 +49,5 @@ func main() { if err != nil { log.Fatal(err) } - fmt.Printf("Got: %s = %s\n\n", getRes.Key, string(getRes.Value)) + fmt.Printf("Got: %s = %s\n", getRes.Key, string(getRes.Value)) } diff --git a/discovery/mebership.go b/discovery/mebership.go index aae61b5..8007843 100644 --- a/discovery/mebership.go +++ b/discovery/mebership.go @@ -1,8 +1,10 @@ package discovery import ( + stdlog "log" "net" + "github.com/dynamic-calm/mokv/logger" "github.com/rs/zerolog/log" "github.com/hashicorp/memberlist" @@ -59,11 +61,19 @@ func (m *Membership) setupSerf() error { if err != nil { return err } + + // Setup logger + serfLogger := log.With().Str("component", "serf").Logger() + stdLogger := stdlog.New(logger.NewZeroLogWriter(serfLogger), "", 0) + + // Serf config config := serf.DefaultConfig() config.Init() + config.Logger = stdLogger // Memberlist config mlConfig := memberlist.DefaultLocalConfig() + mlConfig.Logger = stdLogger mlConfig.BindAddr = addr.IP.String() mlConfig.BindPort = addr.Port mlConfig.AdvertisePort = addr.Port diff --git a/example/config.yaml b/example/config.yaml index 8e4e904..a872435 100644 --- a/example/config.yaml +++ b/example/config.yaml @@ -5,3 +5,4 @@ rpc-port: 8400 metrics-port: 4000 start-join-addrs: [] bootstrap: true +log-level: "INFO" diff --git a/example/config2.yaml b/example/config2.yaml index 948b576..e84a977 100644 --- a/example/config2.yaml +++ b/example/config2.yaml @@ -6,3 +6,4 @@ metrics-port: 4003 start-join-addrs: - "127.0.0.1:8401" bootstrap: false +log-level: "INFO" diff --git a/example/config3.yaml b/example/config3.yaml index ae5ef8e..8b85632 100644 --- a/example/config3.yaml +++ b/example/config3.yaml @@ -6,3 +6,4 @@ metrics-port: 4006 start-join-addrs: - "127.0.0.1:8401" bootstrap: false +log-level: "INFO" diff --git a/go.mod b/go.mod index d98bcdc..84a80dd 100644 --- a/go.mod +++ b/go.mod @@ -1,10 +1,10 @@ module github.com/dynamic-calm/mokv -go 1.25.4 +go 1.25.5 require ( - github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.2.0 + github.com/hashicorp/go-hclog v1.6.3 github.com/hashicorp/memberlist v0.5.3 github.com/hashicorp/raft v1.7.3 github.com/hashicorp/raft-boltdb/v2 v2.3.1 @@ -33,7 +33,6 @@ require ( github.com/google/btree v1.1.3 // indirect github.com/google/uuid v1.6.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect - github.com/hashicorp/go-hclog v1.6.3 // indirect github.com/hashicorp/go-immutable-radix v1.3.1 // indirect github.com/hashicorp/go-metrics v0.5.4 // indirect github.com/hashicorp/go-msgpack/v2 v2.1.5 // indirect diff --git a/go.sum b/go.sum index a7382b3..69aa559 100644 --- a/go.sum +++ b/go.sum @@ -1,12 +1,10 @@ cel.dev/expr v0.19.0 h1:lXuo+nDhpyJSpWxpPVi5cPUwzKb+dsdOiw6IreM5yt0= cel.dev/expr v0.19.0/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw= -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.112.1 h1:uJSeirPke5UNZHIb4SxfZklVSiWWVqW4oXlETwZziwM= cloud.google.com/go/compute v1.24.0 h1:phWcR2eWzRJaL/kOiJwfFsPs4BaKq1j6vnpZrc1YlVg= cloud.google.com/go/compute/metadata v0.5.2 h1:UxK4uu/Tn+I3p2dYWTfiX4wva7aYlKixAHn3fyqngqo= cloud.google.com/go/compute/metadata v0.5.2/go.mod h1:C66sj2AluDcIqakBq/M8lw8/ybHgOZqin2obFxa/E5k= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/DataDog/zstd v1.5.2/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= github.com/Sereal/Sereal/Go/sereal v0.0.0-20231009093132-b9187f1a92c6/go.mod h1:JwrycNnC8+sZPDyzM3MQ86LvaGzSpfxg885KOOwFRW4= @@ -17,14 +15,12 @@ github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRF github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= github.com/armon/go-metrics v0.4.1 h1:hR91U9KYmb6bLBYLQjyM+3j+rcd/UhE+G78SFnF8gJA= github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4= -github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4= github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.4.1 h1:iKLQ0xPNFxR/2hzXZMrBo8f1j86j5WHzznCCQxV/b8g= github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= @@ -32,8 +28,6 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78 h1:QVw89YDxXxEe+l8gU8ETbOasdwEV+avkR75ZzsVV9WI= github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= @@ -45,12 +39,8 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8Yc github.com/davecgh/go-xdr v0.0.0-20161123171359-e6a2ba005892/go.mod h1:CTDl0pzVzE5DEzZhPfvhY/9sPFMQIxaJ9VAMs9AagrE= github.com/dchest/siphash v1.2.3/go.mod h1:0NvQU092bT0ipiFN++/rXm69QG9tVxLAlQHIXMPAkHc= github.com/dgryski/go-ddmin v0.0.0-20210904190556-96a6d69f1034/go.mod h1:zz4KxBkcXUWKjIcrc+uphJ1gPh/t18ymGm3PmQ+VGTk= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.13.1 h1:vPfJZCkob6yTMEgS+0TwfTUfbHjfy/6vOJ8hUWX/uXE= github.com/envoyproxy/go-control-plane v0.13.1/go.mod h1:X45hY0mufo6Fd0KW3rqsGvQMw58jvjymeCzBU3mWyHw= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v1.1.0 h1:tntQDh69XqOCOZsDz0lVJQez/2L6Uu2PdjCQwWCJ3bM= github.com/envoyproxy/protoc-gen-validate v1.1.0/go.mod h1:sXRDRVmzEbkM7CVcM06s9shE/m23dg3wzjl0UWqJ2q4= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= @@ -74,13 +64,9 @@ github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= @@ -95,7 +81,6 @@ github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6 github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= @@ -107,8 +92,6 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI= -github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8= github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.2.0 h1:kQ0NI7W1B3HwiN5gAYtY+XFItDPbLBwYRxAqbFTyDes= github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.2.0/go.mod h1:zrT2dxOAjNFPRGjTUe2Xmb4q4YdUwVvQFV6xiCSf+z0= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -162,8 +145,6 @@ github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/ github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= -github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= @@ -207,7 +188,6 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= @@ -231,7 +211,6 @@ github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+ github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= @@ -285,7 +264,6 @@ github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpE github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= @@ -297,8 +275,6 @@ github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8 github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.etcd.io/bbolt v1.3.5 h1:XAzx9gjCb0Rxj7EoqcClPD1d5ZBxZJk0jbuoPHenBt0= go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= @@ -316,28 +292,16 @@ go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w= go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= -go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= -go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6 h1:zfMcR1Cs4KNuomFFgGefv5N0czO2XZpUbxGUy8i8ug0= golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6/go.mod h1:46edojNIoXTNOhySWIWdix628clX9ODXwPsQuG6hsK0= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= @@ -346,18 +310,13 @@ golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= @@ -368,16 +327,13 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE= golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -385,7 +341,6 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -406,7 +361,6 @@ golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -437,14 +391,7 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= @@ -453,25 +400,14 @@ golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 h1:9+tzLLstTlPTRyJTh+ah5wIMsBW5c4tQwGTN3thOW9Y= google.golang.org/genproto/googleapis/api v0.0.0-20241202173237-19429a94021a h1:OAiGFfOiA0v9MRYsSidp3ubZaBnteRUyn3xB2ZQ5G/E= google.golang.org/genproto/googleapis/api v0.0.0-20241202173237-19429a94021a/go.mod h1:jehYqy3+AhJU9ve55aNOaSml7wUXjF9x6z2LcCfpAhY= google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba h1:UKgtfRM7Yh93Sya0Fo8ZzhDP4qBckrrxEr2oF5UIVb8= google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ= google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= @@ -487,7 +423,6 @@ google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aO google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= @@ -499,12 +434,8 @@ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/kv/kv.go b/kv/kv.go index 948d64e..9e4e20e 100644 --- a/kv/kv.go +++ b/kv/kv.go @@ -12,6 +12,7 @@ import ( "time" "github.com/dynamic-calm/mokv/api" + "github.com/dynamic-calm/mokv/logger" "github.com/dynamic-calm/mokv/store" "github.com/hashicorp/raft" raftboltdb "github.com/hashicorp/raft-boltdb/v2" @@ -294,6 +295,8 @@ func (kv *KV) setupRaft(dataDir string) error { ) config := raft.DefaultConfig() + raftLogger := log.With().Str("component", "raft").Logger() + config.Logger = logger.NewHCLogWrapper(raftLogger) config.LocalID = kv.cfg.Raft.LocalID config.HeartbeatTimeout = 1 * time.Second config.ElectionTimeout = 3 * time.Second diff --git a/logger/hclog_wrapper.go b/logger/hclog_wrapper.go new file mode 100644 index 0000000..61b297b --- /dev/null +++ b/logger/hclog_wrapper.go @@ -0,0 +1,195 @@ +// Taken from https://github.com/psapezhka/hclog-zerolog/blob/main/wrapper.go +package logger + +import ( + "io" + "log" + + "github.com/hashicorp/go-hclog" + "github.com/rs/zerolog" +) + +// DefaultNameField — field [hclog.Logger] name will be written to. +// +// [hclog] has a concept of logger name and, moreover, name inheritance +// (when you create named logger on top of named logger). +// Logger name acts like a prefix for the log message. +// On the other hand, [zerolog] operates key/value pairs to add context to messages. +// So, we convert the [hclog] logger name to key/value context pair for [zerolog] +// This is a default, can be overridden while creating wrapper with the [NewWithCustomNameField] +const DefaultNameField = "hclog_name" + +type Logger struct { + logger zerolog.Logger + nameField string + name string +} + +// New creates an instance of [Logger] wrapping provided [zerolog.Logger]. +// +// Example of wrapping the default global zerolog logger +// (using hashicorp/raft as an example of lib depending on [hclog.Logger]) +// +// raftLogger := log.With().Str("component", "raft").Logger() +// config := raft.DefaultConfig() +// config.Logger = hclogzerolog.New(raftLogger) +// +// See: +// - https://pkg.go.dev/github.com/hashicorp/raft#Config +func NewHCLogWrapper(logger zerolog.Logger) *Logger { + return &Logger{ + logger: logger.With().Str(DefaultNameField, "").Logger(), + nameField: DefaultNameField, + name: "", + } +} + +// NewWithCustomNameField — does exactly the same as [New] but with the ability to set field (key) +// the [hclog.Logger] name will be written to. +func NewWithCustomNameField(logger zerolog.Logger, nameField string) *Logger { + return &Logger{ + logger: logger.With().Str(nameField, "").Logger(), + nameField: nameField, + name: "", + } +} + +func (l *Logger) Log(level hclog.Level, msg string, args ...any) { + switch level { + case hclog.Trace: + l.logger.Trace().Fields(args).Msg(msg) + case hclog.Debug: + l.logger.Debug().Fields(args).Msg(msg) + case hclog.Info: + l.logger.Info().Fields(args).Msg(msg) + case hclog.Warn: + l.logger.Warn().Fields(args).Msg(msg) + case hclog.Error: + l.logger.Error().Fields(args).Msg(msg) + case hclog.NoLevel: + l.logger.Log().Fields(args).Msg(msg) + case hclog.Off: + // no-op + default: + l.logger.Error().Msgf("Unknown log level: %s", level) + } +} + +func (l *Logger) Trace(format string, args ...any) { + l.logger.Trace().Fields(args).Msg(format) +} + +func (l *Logger) Debug(format string, args ...any) { + l.logger.Debug().Fields(args).Msg(format) +} + +func (l *Logger) Info(format string, args ...any) { + l.logger.Info().Fields(args).Msg(format) +} + +func (l *Logger) Warn(format string, args ...any) { + l.logger.Warn().Fields(args).Msg(format) +} + +func (l *Logger) Error(format string, args ...any) { + l.logger.Error().Fields(args).Msg(format) +} + +func (l *Logger) IsTrace() bool { + return l.logger.GetLevel() == zerolog.TraceLevel +} + +func (l *Logger) IsDebug() bool { + return l.logger.GetLevel() == zerolog.DebugLevel +} + +func (l *Logger) IsInfo() bool { + return l.logger.GetLevel() == zerolog.InfoLevel +} + +func (l *Logger) IsWarn() bool { + return l.logger.GetLevel() == zerolog.WarnLevel +} + +func (l *Logger) IsError() bool { + return l.logger.GetLevel() == zerolog.ErrorLevel +} + +func (l *Logger) ImpliedArgs() []any { + return nil +} + +func (l *Logger) With(args ...any) hclog.Logger { + return &Logger{l.logger.With().Fields(args).Logger(), l.nameField, l.name} +} + +func (l *Logger) Name() string { + return l.name +} + +func (l *Logger) Named(name string) hclog.Logger { + var newName string + if l.name == "" { + newName = name + } else { + newName = l.name + "." + name + } + + return &Logger{l.logger.With().Str(l.nameField, newName).Logger(), l.nameField, newName} +} + +func (l *Logger) ResetNamed(name string) hclog.Logger { + return &Logger{l.logger.With().Str(l.nameField, name).Logger(), l.nameField, name} +} + +func (l *Logger) SetLevel(level hclog.Level) { + switch level { + case hclog.Trace: + l.logger = l.logger.Level(zerolog.TraceLevel) + case hclog.Debug: + l.logger = l.logger.Level(zerolog.DebugLevel) + case hclog.Info: + l.logger = l.logger.Level(zerolog.InfoLevel) + case hclog.Warn: + l.logger = l.logger.Level(zerolog.WarnLevel) + case hclog.Error: + l.logger = l.logger.Level(zerolog.ErrorLevel) + case hclog.Off: + l.logger = l.logger.Level(zerolog.Disabled) + case hclog.NoLevel: + l.logger = l.logger.Level(zerolog.NoLevel) + default: + l.logger.Error().Msgf("Unknown log level: %s", level) + } +} + +func (l *Logger) GetLevel() hclog.Level { + switch l.logger.GetLevel() { + case zerolog.TraceLevel: + return hclog.Trace + case zerolog.DebugLevel: + return hclog.Debug + case zerolog.InfoLevel: + return hclog.Info + case zerolog.WarnLevel: + return hclog.Warn + case zerolog.ErrorLevel, zerolog.FatalLevel, zerolog.PanicLevel: + return hclog.Error + case zerolog.Disabled: + return hclog.Off + case zerolog.NoLevel: + return hclog.NoLevel + default: + l.logger.Error().Msgf("Unknown log level: %s", l.logger.GetLevel()) + + return hclog.NoLevel + } +} + +func (l *Logger) StandardLogger(_ *hclog.StandardLoggerOptions) *log.Logger { + return log.New(l.logger, "", 0) +} + +func (l *Logger) StandardWriter(_ *hclog.StandardLoggerOptions) io.Writer { + return l.logger +} diff --git a/logger/hclog_wrapper_test.go b/logger/hclog_wrapper_test.go new file mode 100644 index 0000000..e28c5ae --- /dev/null +++ b/logger/hclog_wrapper_test.go @@ -0,0 +1,610 @@ +// Taken from https://github.com/psapezhka/hclog-zerolog/blob/main/wrapper_test.go +package logger + +import ( + "bytes" + "encoding/json" + "testing" + + "github.com/hashicorp/go-hclog" + "github.com/rs/zerolog" +) + +const ( + customFieldName = "custom_field" + customFieldValue = "value" + messageToLog = "test message" +) + +type message struct { + Level string `json:"level"` + Message string `json:"message"` + HCLogName string `json:"hclog_name"` + CustomField string `json:"custom_field"` +} + +func (m message) Equal(other message) bool { + return m.Level == other.Level && + m.Message == other.Message && + m.HCLogName == other.HCLogName && + m.CustomField == other.CustomField +} + +func TestNew(t *testing.T) { + buf := &bytes.Buffer{} + hclogLogger := NewHCLogWrapper(zerolog.New(buf)) + + if hclogLogger.nameField != DefaultNameField { + t.Errorf("expected nameField to be %q, got %q", DefaultNameField, hclogLogger.nameField) + } + + if hclogLogger.name != "" { + t.Errorf("expected name to be empty, got %q", hclogLogger.name) + } +} + +func TestNewWithCustomNameField(t *testing.T) { + buf := &bytes.Buffer{} + customNameField := "custom_name" + + hclogLogger := NewWithCustomNameField(zerolog.New(buf), customNameField) + + if hclogLogger.nameField != customNameField { + t.Errorf("expected nameField to be %q, got %q", customNameField, hclogLogger.nameField) + } + + if hclogLogger.name != "" { + t.Errorf("expected name to be empty, got %q", hclogLogger.name) + } +} + +func TestLog(t *testing.T) { + t.Run("logs messages at level", func(t *testing.T) { + logLevels := []struct { + level hclog.Level + expectedLevel string + }{ + {hclog.Trace, "trace"}, + {hclog.Debug, "debug"}, + {hclog.Info, "info"}, + {hclog.Warn, "warn"}, + {hclog.Error, "error"}, + {hclog.NoLevel, ""}, + } + + for _, tt := range logLevels { + t.Run(tt.expectedLevel, func(t *testing.T) { + buf := &bytes.Buffer{} + msg := &message{} + logger := zerolog.New(buf) + hclogLogger := NewHCLogWrapper(logger) + + hclogLogger.Log(tt.level, messageToLog, customFieldName, customFieldValue) + + if err := json.Unmarshal(buf.Bytes(), msg); err != nil { + t.Fatalf("Expected log output to be a valid JSON, got: %s", buf.String()) + } + + wantedMessage := &message{ + Level: tt.expectedLevel, + Message: messageToLog, + HCLogName: "", + CustomField: customFieldValue, + } + + if !msg.Equal(*wantedMessage) { + t.Errorf("expected message to be\n %+v\n got\n %+v", wantedMessage, msg) + } + }) + } + }) + + t.Run("logs messages with unknown level", func(t *testing.T) { + buf := &bytes.Buffer{} + msg := &message{} + logger := zerolog.New(buf) + hclogLogger := NewHCLogWrapper(logger) + + hclogLogger.Log(hclog.Level(999), messageToLog, customFieldName, customFieldValue) + + if err := json.Unmarshal(buf.Bytes(), msg); err != nil { + t.Fatalf("Expected log output to be a valid JSON, got: %s", buf.String()) + } + + wantedMessage := &message{ + Level: "error", + Message: "Unknown log level: unknown", + HCLogName: "", + CustomField: "", + } + + if !msg.Equal(*wantedMessage) { + t.Errorf("expected message to be\n %+v\n got\n %+v", wantedMessage, msg) + } + }) +} + +func TestTrace(t *testing.T) { + t.Run("logs trace level messages", func(t *testing.T) { + buf := &bytes.Buffer{} + msg := &message{} + logger := zerolog.New(buf) + hclogLogger := NewHCLogWrapper(logger) + + hclogLogger.Trace(messageToLog, customFieldName, customFieldValue) + + if err := json.Unmarshal(buf.Bytes(), msg); err != nil { + t.Fatalf("Expected log output to be a valid JSON, got: %s", buf.String()) + } + + wantedMessage := &message{ + Level: "trace", + Message: messageToLog, + HCLogName: "", + CustomField: customFieldValue, + } + + if !msg.Equal(*wantedMessage) { + t.Errorf("expected message to be\n %+v\n got\n %+v", wantedMessage, msg) + } + }) +} + +func TestDebug(t *testing.T) { + t.Run("logs debug level messages", func(t *testing.T) { + buf := &bytes.Buffer{} + msg := &message{} + logger := zerolog.New(buf) + hclogLogger := NewHCLogWrapper(logger) + + hclogLogger.Debug(messageToLog, customFieldName, customFieldValue) + + if err := json.Unmarshal(buf.Bytes(), msg); err != nil { + t.Fatalf("Expected log output to be a valid JSON, got: %s", buf.String()) + } + + wantedMessage := &message{ + Level: "debug", + Message: messageToLog, + HCLogName: "", + CustomField: customFieldValue, + } + + if !msg.Equal(*wantedMessage) { + t.Errorf("expected message to be\n %+v\n got\n %+v", wantedMessage, msg) + } + }) +} + +func TestInfo(t *testing.T) { + t.Run("logs info level messages", func(t *testing.T) { + buf := &bytes.Buffer{} + msg := &message{} + logger := zerolog.New(buf) + hclogLogger := NewHCLogWrapper(logger) + + hclogLogger.Info(messageToLog, customFieldName, customFieldValue) + + if err := json.Unmarshal(buf.Bytes(), msg); err != nil { + t.Fatalf("Expected log output to be a valid JSON, got: %s", buf.String()) + } + + wantedMessage := &message{ + Level: "info", + Message: messageToLog, + HCLogName: "", + CustomField: customFieldValue, + } + + if !msg.Equal(*wantedMessage) { + t.Errorf("expected message to be\n %+v\n got\n %+v", wantedMessage, msg) + } + }) +} + +func TestWarn(t *testing.T) { + t.Run("logs warn level messages", func(t *testing.T) { + buf := &bytes.Buffer{} + msg := &message{} + logger := zerolog.New(buf) + hclogLogger := NewHCLogWrapper(logger) + + hclogLogger.Warn(messageToLog, customFieldName, customFieldValue) + + if err := json.Unmarshal(buf.Bytes(), msg); err != nil { + t.Fatalf("Expected log output to be a valid JSON, got: %s", buf.String()) + } + + wantedMessage := &message{ + Level: "warn", + Message: messageToLog, + HCLogName: "", + CustomField: customFieldValue, + } + + if !msg.Equal(*wantedMessage) { + t.Errorf("expected message to be\n %+v\n got\n %+v", wantedMessage, msg) + } + }) +} + +func TestError(t *testing.T) { + t.Run("logs error level messages", func(t *testing.T) { + buf := &bytes.Buffer{} + msg := &message{} + logger := zerolog.New(buf) + hclogLogger := NewHCLogWrapper(logger) + + hclogLogger.Error(messageToLog, customFieldName, customFieldValue) + + if err := json.Unmarshal(buf.Bytes(), msg); err != nil { + t.Fatalf("Expected log output to be a valid JSON, got: %s", buf.String()) + } + + wantedMessage := &message{ + Level: "error", + Message: messageToLog, + HCLogName: "", + CustomField: customFieldValue, + } + + if !msg.Equal(*wantedMessage) { + t.Errorf("expected message to be\n %+v\n got\n %+v", wantedMessage, msg) + } + }) +} + +func TestIsTrace(t *testing.T) { + t.Run("returns true when logger level is Trace", func(t *testing.T) { + logger := zerolog.New(&bytes.Buffer{}).Level(zerolog.TraceLevel) + hclogLogger := NewHCLogWrapper(logger) + + if !hclogLogger.IsTrace() { + t.Errorf("expected IsTrace to return true, got false") + } + }) + + t.Run("returns false when logger level is not Trace", func(t *testing.T) { + logger := zerolog.New(&bytes.Buffer{}).Level(zerolog.InfoLevel) + hclogLogger := NewHCLogWrapper(logger) + + if hclogLogger.IsTrace() { + t.Errorf("expected IsTrace to return false, got true") + } + }) +} + +func TestIsDebug(t *testing.T) { + t.Run("returns true when logger level is Debug", func(t *testing.T) { + logger := zerolog.New(&bytes.Buffer{}).Level(zerolog.DebugLevel) + hclogLogger := NewHCLogWrapper(logger) + + if !hclogLogger.IsDebug() { + t.Errorf("expected IsDebug to return true, got false") + } + }) + + t.Run("returns false when logger level is not Debug", func(t *testing.T) { + logger := zerolog.New(&bytes.Buffer{}).Level(zerolog.InfoLevel) + hclogLogger := NewHCLogWrapper(logger) + + if hclogLogger.IsDebug() { + t.Errorf("expected IsDebug to return false, got true") + } + }) +} + +func TestIsInfo(t *testing.T) { + t.Run("returns true when logger level is Info", func(t *testing.T) { + logger := zerolog.New(&bytes.Buffer{}).Level(zerolog.InfoLevel) + hclogLogger := NewHCLogWrapper(logger) + + if !hclogLogger.IsInfo() { + t.Errorf("expected IsInfo to return true, got false") + } + }) + + t.Run("returns false when logger level is not Info", func(t *testing.T) { + logger := zerolog.New(&bytes.Buffer{}).Level(zerolog.DebugLevel) + hclogLogger := NewHCLogWrapper(logger) + + if hclogLogger.IsInfo() { + t.Errorf("expected IsInfo to return false, got true") + } + }) +} + +func TestIsWarn(t *testing.T) { + t.Run("returns true when logger level is Warn", func(t *testing.T) { + logger := zerolog.New(&bytes.Buffer{}).Level(zerolog.WarnLevel) + hclogLogger := NewHCLogWrapper(logger) + + if !hclogLogger.IsWarn() { + t.Errorf("expected IsWarn to return true, got false") + } + }) + + t.Run("returns false when logger level is not Warn", func(t *testing.T) { + logger := zerolog.New(&bytes.Buffer{}).Level(zerolog.InfoLevel) + hclogLogger := NewHCLogWrapper(logger) + + if hclogLogger.IsWarn() { + t.Errorf("expected IsWarn to return false, got true") + } + }) +} + +func TestIsError(t *testing.T) { + t.Run("returns true when logger level is Error", func(t *testing.T) { + logger := zerolog.New(&bytes.Buffer{}).Level(zerolog.ErrorLevel) + hclogLogger := NewHCLogWrapper(logger) + + if !hclogLogger.IsError() { + t.Errorf("expected IsError to return true, got false") + } + }) + + t.Run("returns false when logger level is not Error", func(t *testing.T) { + logger := zerolog.New(&bytes.Buffer{}).Level(zerolog.WarnLevel) + hclogLogger := NewHCLogWrapper(logger) + + if hclogLogger.IsError() { + t.Errorf("expected IsError to return false, got true") + } + }) +} + +func TestImpliedArgs(t *testing.T) { + t.Run("returns nil for implied args", func(t *testing.T) { + logger := zerolog.New(&bytes.Buffer{}) + hclogLogger := NewHCLogWrapper(logger) + + impliedArgs := hclogLogger.ImpliedArgs() + + if impliedArgs != nil { + t.Errorf("expected impliedArgs to be nil, got %v", impliedArgs) + } + }) +} + +func TestWith(t *testing.T) { + t.Run("returns a new logger with additional fields", func(t *testing.T) { + buf := &bytes.Buffer{} + logger := zerolog.New(buf) + hclogLogger := NewHCLogWrapper(logger) + + key := "key" + value := "value" + newLogger := hclogLogger.With(key, value) + + // Log a message using the new logger + newLogger.Info("test message") + + // Parse the logged message + var msg map[string]any + if err := json.Unmarshal(buf.Bytes(), &msg); err != nil { + t.Fatalf("Expected log output to be a valid JSON, got: %s", buf.String()) + } + + if msg[key] != value { + t.Errorf("expected field %q to have value %q, got %q", key, value, msg[key]) + } + + if msg["message"] != "test message" { + t.Errorf("expected message to be %q, got %q", "test message", msg["message"]) + } + }) +} + +func TestName(t *testing.T) { + t.Run("returns the logger's name", func(t *testing.T) { + logger := zerolog.New(&bytes.Buffer{}) + hclogLogger := NewHCLogWrapper(logger) + + if hclogLogger.Name() != "" { + t.Errorf("expected name to be empty, got %q", hclogLogger.Name()) + } + + namedLogger := hclogLogger.Named("test") + if namedLogger.Name() != "test" { + t.Errorf("expected name to be %q, got %q", ".test", namedLogger.Name()) + } + + nestedLogger := namedLogger.Named("nested") + if nestedLogger.Name() != "test.nested" { + t.Errorf("expected name to be %q, got %q", ".test.nested", nestedLogger.Name()) + } + }) +} + +func TestResetNamed(t *testing.T) { + t.Run("resets the logger name", func(t *testing.T) { + buf := &bytes.Buffer{} + logger := zerolog.New(buf) + hclogLogger := NewHCLogWrapper(logger).Named("test") + + newName := "reset" + resetLogger := hclogLogger.ResetNamed(newName) + + if resetLogger.Name() != newName { + t.Errorf("expected name to be %q, got %q", newName, resetLogger.Name()) + } + + resetLogger.Info("test message") + + var msg map[string]any + if err := json.Unmarshal(buf.Bytes(), &msg); err != nil { + t.Fatalf("Expected log output to be a valid JSON, got: %s", buf.String()) + } + + if msg[DefaultNameField] != newName { + t.Errorf("expected %q field to be %q, got %q", DefaultNameField, newName, msg[DefaultNameField]) + } + + if msg["message"] != "test message" { + t.Errorf("expected message to be %q, got %q", "test message", msg["message"]) + } + }) +} + +func TestSetLevel(t *testing.T) { + t.Run("sets the logger level correctly", func(t *testing.T) { + tests := []struct { + hclogLevel hclog.Level + expectedLevel zerolog.Level + }{ + {hclog.Trace, zerolog.TraceLevel}, + {hclog.Debug, zerolog.DebugLevel}, + {hclog.Info, zerolog.InfoLevel}, + {hclog.Warn, zerolog.WarnLevel}, + {hclog.Error, zerolog.ErrorLevel}, + {hclog.Off, zerolog.Disabled}, + {hclog.NoLevel, zerolog.NoLevel}, + } + + for _, tt := range tests { + t.Run(tt.hclogLevel.String(), func(t *testing.T) { + logger := zerolog.New(&bytes.Buffer{}) + hclogLogger := NewHCLogWrapper(logger) + + hclogLogger.SetLevel(tt.hclogLevel) + + if hclogLogger.logger.GetLevel() != tt.expectedLevel { + t.Errorf("expected logger level to be %v, got %v", tt.expectedLevel, hclogLogger.logger.GetLevel()) + } + }) + } + }) + + t.Run("handles unknown log levels gracefully", func(t *testing.T) { + buf := &bytes.Buffer{} + msg := &message{} + logger := zerolog.New(buf) + hclogLogger := NewHCLogWrapper(logger) + + hclogLogger.SetLevel(hclog.Level(999)) + + if err := json.Unmarshal(buf.Bytes(), msg); err != nil { + t.Fatalf("Expected log output to be a valid JSON, got: %s", buf.String()) + } + + wantedMessage := &message{ + Level: "error", + Message: "Unknown log level: unknown", + HCLogName: "", + CustomField: "", + } + + if !msg.Equal(*wantedMessage) { + t.Errorf("expected message to be\n %+v\n got\n %+v", wantedMessage, msg) + } + }) +} + +func TestGetLevel(t *testing.T) { + tests := []struct { + zerologLevel zerolog.Level + expectedLevel hclog.Level + }{ + {zerolog.TraceLevel, hclog.Trace}, + {zerolog.DebugLevel, hclog.Debug}, + {zerolog.InfoLevel, hclog.Info}, + {zerolog.WarnLevel, hclog.Warn}, + {zerolog.ErrorLevel, hclog.Error}, + {zerolog.FatalLevel, hclog.Error}, + {zerolog.PanicLevel, hclog.Error}, + {zerolog.Disabled, hclog.Off}, + {zerolog.NoLevel, hclog.NoLevel}, + } + + for _, tt := range tests { + t.Run(tt.zerologLevel.String(), func(t *testing.T) { + logger := zerolog.New(&bytes.Buffer{}).Level(tt.zerologLevel) + hclogLogger := NewHCLogWrapper(logger) + + level := hclogLogger.GetLevel() + + if level != tt.expectedLevel { + t.Errorf("expected level to be %v, got %v", tt.expectedLevel, level) + } + }) + } + + t.Run("handles unknown zerolog levels gracefully", func(t *testing.T) { + buf := &bytes.Buffer{} + msg := &message{} + logger := zerolog.New(buf) + hclogLogger := NewHCLogWrapper(logger) + + hclogLogger.logger = hclogLogger.logger.Level(zerolog.Level(-2)) + + level := hclogLogger.GetLevel() + + if level != hclog.NoLevel { + t.Errorf("expected level to be %v for unknown zerolog level, got %v", hclog.NoLevel, level) + } + + if err := json.Unmarshal(buf.Bytes(), msg); err != nil { + t.Fatalf("Expected log output to be a valid JSON, got: %s", buf.String()) + } + + wantedMessage := &message{ + Level: "error", + Message: "Unknown log level: -2", + HCLogName: "", + CustomField: "", + } + + if !msg.Equal(*wantedMessage) { + t.Errorf("expected message to be\n %+v\n got\n %+v", wantedMessage, msg) + } + }) +} + +func TestStandardLogger(t *testing.T) { + t.Run("returns a standard logger that writes to zerolog", func(t *testing.T) { + buf := &bytes.Buffer{} + logger := zerolog.New(buf) + hclogLogger := NewHCLogWrapper(logger) + + stdLogger := hclogLogger.StandardLogger(nil) + + message := "standard logger message" + stdLogger.Println(message) + + var loggedMessage map[string]any + if err := json.Unmarshal(buf.Bytes(), &loggedMessage); err != nil { + t.Fatalf("Expected log output to be a valid JSON, got: %s", buf.String()) + } + + if loggedMessage["message"] != message { + t.Errorf("expected message to be %q, got %q", message, loggedMessage["message"]) + } + }) +} + +func TestStandardWriter(t *testing.T) { + t.Run("returns a writer that writes to zerolog", func(t *testing.T) { + buf := &bytes.Buffer{} + logger := zerolog.New(buf) + hclogLogger := NewHCLogWrapper(logger) + + writer := hclogLogger.StandardWriter(nil) + + message := "standard writer message\n" + + _, err := writer.Write([]byte(message)) + if err != nil { + t.Fatalf("expected no error while writing, got: %v", err) + } + + var loggedMessage map[string]any + if err := json.Unmarshal(buf.Bytes(), &loggedMessage); err != nil { + t.Fatalf("Expected log output to be a valid JSON, got: %s", buf.String()) + } + + if loggedMessage["message"] != "standard writer message" { + t.Errorf("expected message to be %q, got %q", "standard writer message", loggedMessage["message"]) + } + }) +} diff --git a/logger/logger.go b/logger/logger.go new file mode 100644 index 0000000..c664385 --- /dev/null +++ b/logger/logger.go @@ -0,0 +1,54 @@ +package logger + +import ( + "io" + "os" + "strings" + "time" + + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" +) + +// Config holds logger configuration +type Config struct { + Level string + Development bool + ServiceName string +} + +// Setup configures the global logger. +func Setup(cfg Config, output io.Writer) zerolog.Logger { + // Pretty console output for development + if cfg.Development { + output = zerolog.ConsoleWriter{ + Out: os.Stderr, + TimeFormat: time.RFC3339, + NoColor: false, + } + } + + level, err := zerolog.ParseLevel(strings.ToLower(cfg.Level)) + if err != nil { + level = zerolog.InfoLevel + } + + // Set global level (affects all loggers) + zerolog.SetGlobalLevel(level) + + // Configure global settings + zerolog.TimeFieldFormat = time.RFC3339 + zerolog.DurationFieldUnit = time.Millisecond + + // Build logger with default fields + logger := zerolog.New(output). + With(). + Timestamp(). + Str("service", cfg.ServiceName). + Logger() + + // Set as global logger + log.Logger = logger + + return logger +} diff --git a/logger/std_log_wrapper.go b/logger/std_log_wrapper.go new file mode 100644 index 0000000..0b984c7 --- /dev/null +++ b/logger/std_log_wrapper.go @@ -0,0 +1,32 @@ +package logger + +import ( + "strings" + + "github.com/rs/zerolog" +) + +// ZerologWriter adapts zerolog to io.Writer for standard library log +type ZerologWriter struct { + logger zerolog.Logger +} + +func NewZeroLogWriter(logger zerolog.Logger) *ZerologWriter { + return &ZerologWriter{logger} +} +func (w *ZerologWriter) Write(p []byte) (n int, err error) { + msg := strings.TrimSpace(string(p)) + + switch { + case strings.Contains(msg, "[ERR]") || strings.Contains(msg, "[ERROR]"): + w.logger.Error().Msg(msg) + case strings.Contains(msg, "[WARN]"): + w.logger.Warn().Msg(msg) + case strings.Contains(msg, "[DEBUG]"): + w.logger.Debug().Msg(msg) + default: + w.logger.Info().Msg(msg) + } + + return len(p), nil +} diff --git a/main.go b/main.go index 608ab71..b681164 100644 --- a/main.go +++ b/main.go @@ -4,6 +4,7 @@ import ( "os" "path/filepath" + "github.com/dynamic-calm/mokv/logger" "github.com/dynamic-calm/mokv/mokv" "github.com/rs/zerolog/log" "github.com/spf13/cobra" @@ -14,6 +15,7 @@ const ( defaultBindAddr = "127.0.0.1:8401" defaultRPCPort = 8400 defaultMetricsPort = 4000 + serviceName = "mokv" ) // CLI represents the command-line interface application context and configuration. @@ -22,12 +24,21 @@ type CLI struct { } func main() { + logger.Setup( + logger.Config{ + Level: getEnvOrDefault("LOG_LEVEL", "INFO"), + Development: getEnvOrDefault("ENV", "production") == "development", + ServiceName: serviceName, + }, + os.Stderr, + ) + app := &CLI{} cmd := &cobra.Command{ Use: "mokv", Short: "A distributed key-value store", - PreRunE: app.SetupConfig, - RunE: app.Run, + PreRunE: app.setupConfig, + RunE: app.run, SilenceUsage: true, } @@ -40,9 +51,9 @@ func main() { } } -// SetupConfig initializes the configuration by reading from the config file (if provided) +// setupConfig initializes the configuration by reading from the config file (if provided) // and merging flag values into the application configuration struct. -func (c *CLI) SetupConfig(cmd *cobra.Command, args []string) error { +func (c *CLI) setupConfig(cmd *cobra.Command, args []string) error { c.config = &mokv.Config{} configFile, err := cmd.Flags().GetString("config-file") @@ -66,12 +77,13 @@ func (c *CLI) SetupConfig(cmd *cobra.Command, args []string) error { c.config.StartJoinAddrs = viper.GetStringSlice("start-join-addrs") c.config.Bootstrap = viper.GetBool("bootstrap") c.config.MetricsPort = viper.GetInt("metrics-port") + c.config.LogLevel = viper.GetString("log-level") return nil } // Run executes the main application logic, starting mokv service. -func (c *CLI) Run(cmd *cobra.Command, args []string) error { +func (c *CLI) run(cmd *cobra.Command, args []string) error { ctx := cmd.Context() service, err := mokv.New(c.config, os.Getenv) if err != nil { @@ -96,6 +108,14 @@ func setupFlags(cmd *cobra.Command) error { cmd.Flags().StringSlice("start-join-addrs", nil, "Serf addresses to join.") cmd.Flags().Bool("bootstrap", false, "Bootstrap the cluster.") cmd.Flags().Int("metrics-port", defaultMetricsPort, "Port for metrics server.") + cmd.Flags().String("log-level", "INFO", "Log level.") return viper.BindPFlags(cmd.Flags()) } + +func getEnvOrDefault(key, defaultVal string) string { + if val := os.Getenv(key); val != "" { + return val + } + return defaultVal +} diff --git a/mokv/mokv.go b/mokv/mokv.go index aa2b88c..16c0be4 100644 --- a/mokv/mokv.go +++ b/mokv/mokv.go @@ -38,6 +38,7 @@ type Config struct { MetricsPort int StartJoinAddrs []string Bootstrap bool + LogLevel string } // GetEnv defines a function signature for retrieving environment variables. @@ -71,11 +72,6 @@ func New(cfg *Config, getEnv GetEnv) (*MOKV, error) { raftAdvertiseAddr := fmt.Sprintf("%s:%d", host, cfg.RPCPort) - log.Info(). - Str("listenAddr", rpcAddr). - Str("raftAdvertiseAddr", raftAdvertiseAddr). - Msg("network configuration") - myCmux := cmux.New(listener) // Configure KV store @@ -131,7 +127,7 @@ func New(cfg *Config, getEnv GetEnv) (*MOKV, error) { ), } - grpcServer := server.New(kv, serverOpts...) + grpcServer := server.New(kv, log.Logger, serverOpts...) // Initialize membership membership, err := discovery.NewMembership(kv, discovery.MembershipConfig{ diff --git a/server/server.go b/server/server.go index ccf5cd0..f6b36b3 100644 --- a/server/server.go +++ b/server/server.go @@ -3,12 +3,12 @@ package server import ( "context" "fmt" - "os" "github.com/dynamic-calm/mokv/api" "github.com/dynamic-calm/mokv/kv" - grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware" + "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors" "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/logging" + "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/selector" "github.com/rs/zerolog" "google.golang.org/grpc" "google.golang.org/grpc/codes" @@ -42,21 +42,27 @@ func (kg *kvServerGetter) GetServers() ([]*api.Server, error) { // New creates and configures a new gRPC server instance with logging middleware, // health checks, and the registered KV service. -func New(KV kv.KVI, opts ...grpc.ServerOption) *grpc.Server { - logger := zerolog.New(os.Stderr).With().Timestamp().Logger() +func New(KV kv.KVI, logger zerolog.Logger, opts ...grpc.ServerOption) *grpc.Server { logOpts := []logging.Option{ logging.WithLogOnEvents(logging.FinishCall), logging.WithLevels(logging.DefaultServerCodeToLevel), } // Middleware for streaming and unary requests - opts = append(opts, grpc.StreamInterceptor( - grpc_middleware.ChainStreamServer( - logging.StreamServerInterceptor(interceptorLogger(logger), logOpts...), + opts = append(opts, + grpc.ChainStreamInterceptor( + selector.StreamServerInterceptor( + logging.StreamServerInterceptor(interceptorLogger(logger), logOpts...), + selector.MatchFunc(skipHealthAndReflectionRequests), + ), ), - ), grpc.UnaryInterceptor(grpc_middleware.ChainUnaryServer( - logging.UnaryServerInterceptor(interceptorLogger(logger), logOpts...), - ))) + grpc.ChainUnaryInterceptor( + selector.UnaryServerInterceptor( + logging.UnaryServerInterceptor(interceptorLogger(logger), logOpts...), + selector.MatchFunc(skipHealthAndReflectionRequests), + ), + ), + ) s := grpc.NewServer(opts...) healthSrv := health.NewServer() @@ -145,3 +151,8 @@ func interceptorLogger(l zerolog.Logger) logging.Logger { event.Fields(fields).Msg(msg) }) } + +func skipHealthAndReflectionRequests(_ context.Context, c interceptors.CallMeta) bool { + return c.FullMethod() != "/grpc.health.v1.Health/Check" && + c.FullMethod() != "/grpc.reflection.v1alpha.ServerReflection/ServerReflectionInfo" +} diff --git a/server/server_test.go b/server/server_test.go index c9aaece..9210fb7 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -13,6 +13,7 @@ import ( "github.com/dynamic-calm/mokv/api" "github.com/dynamic-calm/mokv/server" "github.com/dynamic-calm/mokv/store" + "github.com/rs/zerolog/log" grpc "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/credentials/insecure" @@ -30,7 +31,8 @@ func setupTestServer(t *testing.T) (api.KVClient, func()) { } st := store.New() - srv := server.New(st) + + srv := server.New(st, log.Logger) ready := make(chan bool) go func() {