From db957d3dced1ab69d1c1bc1b30f6761e9ecf7bea Mon Sep 17 00:00:00 2001 From: Dylan Bourque Date: Fri, 17 Nov 2023 15:08:14 -0600 Subject: [PATCH 1/3] feat(logging): update server to always log errors update Go version to 1.21 bump GH actions to use Go 1.21 use `log/slog` instead of `golang.org/x/exp/slog` refactor logging code to support debug, error, and info logs ensure internal errors are always logged --- .github/workflows/check-goreleaser.yml | 11 +- .github/workflows/go.yml | 2 +- .github/workflows/release.yml | 2 +- client_config.go | 2 +- debug.go | 85 ------------ go.mod | 3 +- go.sum | 14 +- internal/log/logger.go | 182 +++++++++++++++++++++++++ internal/server/grpc.go | 49 ++++--- internal/server/server.go | 62 ++++++--- internal/store/options.go | 18 ++- internal/store/pg.go | 22 ++- log.go | 29 ++++ main.go | 6 +- pathfinder.go | 2 +- perseusapi/buf.lock | 6 +- query.go | 8 +- update.go | 4 +- 18 files changed, 342 insertions(+), 165 deletions(-) delete mode 100644 debug.go create mode 100644 internal/log/logger.go create mode 100644 log.go diff --git a/.github/workflows/check-goreleaser.yml b/.github/workflows/check-goreleaser.yml index 4756d10..a0024e1 100644 --- a/.github/workflows/check-goreleaser.yml +++ b/.github/workflows/check-goreleaser.yml @@ -9,16 +9,15 @@ jobs: goreleaser-check: runs-on: ubuntu-latest steps: - - - name: Checkout + - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 - - - name: Set up Go + - name: Set up Go uses: actions/setup-go@v4 - - - name: Run 'goreleaser check' + with: + go-version: 1.21 + - name: Run 'goreleaser check' uses: goreleaser/goreleaser-action@v5 with: version: latest diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index dad5801..d635af2 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -14,7 +14,7 @@ jobs: - uses: actions/setup-go@v4 with: - go-version: 1.19 + go-version: 1.21 - name: lint uses: golangci/golangci-lint-action@v3.7.0 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 14f08b5..829028e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -22,7 +22,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v4 with: - go-version: 1.19 + go-version: 1.21 - name: Login to GitHub Container Registry uses: docker/login-action@v3 with: diff --git a/client_config.go b/client_config.go index d04943a..706f0b3 100644 --- a/client_config.go +++ b/client_config.go @@ -110,7 +110,7 @@ func (conf *clientConfig) dialServer() (client perseusapi.PerseusServiceClient, } else { dialOpts = append(dialOpts, grpc.WithTransportCredentials(credentials.NewTLS(nil))) } - debugLog("connecting to Perseus server", "addr", conf.serverAddr, "useTLS", !conf.disableTLS) + logger.Debug("connecting to Perseus server", "addr", conf.serverAddr, "useTLS", !conf.disableTLS) ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() conn, err := grpc.DialContext(ctx, conf.serverAddr, dialOpts...) diff --git a/debug.go b/debug.go deleted file mode 100644 index 59a7f3a..0000000 --- a/debug.go +++ /dev/null @@ -1,85 +0,0 @@ -package main - -import ( - "context" - "fmt" - "os" - "runtime" - "runtime/debug" - "strings" - "sync" - "time" - - "golang.org/x/exp/slog" -) - -var ( - debugMode bool - initLogOnce sync.Once - logger *slog.Logger -) - -// debugLog writes the provided message and key/value pairs to stdout using structured logging -func debugLog(msg string, kvs ...any) { - if !debugMode { - return - } - initLogOnce.Do(func() { - opts := slog.HandlerOptions{ - AddSource: true, - Level: slog.LevelDebug, - ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr { - switch a.Key { - case slog.SourceKey: - // trim "source" down to relative path within this module - val := a.Value.String() - if idx := strings.Index(val, "github.com/CrowdStrike/perseus/"); idx != -1 { - a.Value = slog.StringValue(val[idx+31:]) - } - case slog.LevelKey: - // don't output "level" since we're only ever generating debug logs - a = slog.Attr{} - default: - } - return a - }, - } - if inK8S() { - logger = slog.New(opts.NewJSONHandler(os.Stdout)) - } else { - logger = slog.New(opts.NewTextHandler(os.Stdout)) - } - }) - - // golang.org/x/exp is still technically unstable and we'd rather eat a panic than crash - defer func() { - if r := recover(); r != nil { - fmt.Fprintf(os.Stderr, "panic caught while writing debug log\n\t%v\n%s", r, string(debug.Stack())) - } - }() - - ctx := context.Background() - var pcs [1]uintptr - runtime.Callers(2, pcs[:]) - rec := slog.NewRecord(time.Now().UTC(), slog.LevelDebug, msg, pcs[0]) - - if len(kvs) > 0 { - attrs := make([]slog.Attr, 0, len(kvs)/2) - for i := 0; i < len(kvs); i += 2 { - k, ok := kvs[i].(string) - if !ok { - k = fmt.Sprintf("%v", kvs[i]) - } - attrs = append(attrs, slog.Any(k, kvs[i+1])) - } - rec.AddAttrs(attrs...) - } - - // TODO: what should we do if this call to logger.Handler().Handle() fails? - _ = logger.Handler().Handle(ctx, rec) -} - -func inK8S() bool { - _, ok := os.LookupEnv("KUBERNETES_SERVICE_HOST") - return ok -} diff --git a/go.mod b/go.mod index a24a32b..a42e1ba 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/CrowdStrike/perseus -go 1.18 +go 1.21 require ( github.com/Masterminds/squirrel v1.5.4 @@ -16,7 +16,6 @@ require ( github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.8.4 github.com/theckman/yacspin v0.13.12 - golang.org/x/exp v0.0.0-20230321023759-10a507213a29 golang.org/x/mod v0.14.0 google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17 google.golang.org/grpc v1.59.0 diff --git a/go.sum b/go.sum index b716214..0f098e4 100644 --- a/go.sum +++ b/go.sum @@ -13,7 +13,9 @@ github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371/go.mod h1:EjA github.com/acomagu/bufpipe v1.0.4 h1:e3H4WUzM3npvo5uv95QuJM3cQspFNtFBzvJ2oNjKIDQ= github.com/acomagu/bufpipe v1.0.4/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ2sYmHc4= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= 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/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= @@ -33,16 +35,19 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU= +github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY= +github.com/gliderlabs/ssh v0.3.5/go.mod h1:8XB4KraRrX39qHhT6yxPsHedjA08I/uBVwj4xC+/+z4= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU= github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= github.com/go-git/go-git/v5 v5.10.0 h1:F0x3xXrAWmhwtzoCokU4IMPcBdncG+HAAqi9FcOOjbQ= github.com/go-git/go-git/v5 v5.10.0/go.mod h1:1FOZ/pQnqw24ghP2n7cunVl0ON55BsjPYvhWHvZGhoo= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= @@ -60,6 +65,7 @@ github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= @@ -126,10 +132,12 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxv github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw= github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk= @@ -159,6 +167,7 @@ github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= +github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -178,6 +187,7 @@ github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= +github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= @@ -247,8 +257,6 @@ golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58 golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= -golang.org/x/exp v0.0.0-20230321023759-10a507213a29 h1:ooxPy7fPvB4kwsA2h+iBNHkAbp/4JxTSwCmvdjEYmug= -golang.org/x/exp v0.0.0-20230321023759-10a507213a29/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= @@ -308,6 +316,7 @@ golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek= +golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -356,6 +365,7 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 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= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= diff --git a/internal/log/logger.go b/internal/log/logger.go new file mode 100644 index 0000000..d5e7aba --- /dev/null +++ b/internal/log/logger.go @@ -0,0 +1,182 @@ +package log + +import ( + "context" + "encoding/json" + "fmt" + "log/slog" + "os" + "runtime" + "strings" + "sync" + "time" +) + +// New initializes and returns a new [Logger] using [level] to dynamically determine the active +// verbosity level. +func New(level slog.Leveler) *Logger { + opts := slog.HandlerOptions{ + AddSource: true, + Level: level, + ReplaceAttr: replaceRecordAttributes, + } + + var logger *slog.Logger + if inK8S() { + logger = slog.New(slog.NewJSONHandler(os.Stdout, &opts)) + } else { + logger = slog.New(slog.NewTextHandler(os.Stdout, &opts)) + } + return &Logger{ + logger: logger, + } +} + +// Logger wraps a [slog.Logger] to provide a streamlined API and consistent behavior for the Perseus +// application. +type Logger struct { + logger *slog.Logger +} + +// Info logs a message at INFO level with the specified message and attributes. +func (l *Logger) Info(msg string, kvs ...any) { + ctx, h := context.Background(), l.logger.Handler() + if h.Enabled(ctx, slog.LevelInfo) { + rec := getLogRecord(slog.LevelInfo, msg, kvs...) + if err := h.Handle(ctx, rec); err != nil { + dumpLogHandlerError(err, rec) + } + } +} + +// Debug logs a message at DEBUG level with the specified message and attributes. +func (l *Logger) Debug(msg string, kvs ...any) { + ctx, h := context.Background(), l.logger.Handler() + if h.Enabled(ctx, slog.LevelDebug) { + rec := getLogRecord(slog.LevelDebug, msg, kvs...) + if err := h.Handle(ctx, rec); err != nil { + dumpLogHandlerError(err, rec) + } + } +} + +// Error logs a message at ERROR level with the specified message and attributes. If [err] is not nil, +// it will be logged in an additional attribute called "err". +func (l *Logger) Error(err error, msg string, kvs ...any) { + ctx, h := context.Background(), l.logger.Handler() + if h.Enabled(ctx, slog.LevelError) { + rec := getLogRecord(slog.LevelError, msg, kvs...) + if err != nil { + rec.Add(slog.String("err", err.Error())) + } + if err := h.Handle(ctx, rec); err != nil { + dumpLogHandlerError(err, rec) + } + } +} + +// getLogRecord builds a [slog.Record] with the provided level and message. [kvs], if provided, must +// be a list of tuples where the first item is the string key for the attribute and the second is the +// value. These are used to construct [slog.Any] attributes. +func getLogRecord(lvl slog.Level, msg string, kvs ...any) slog.Record { + // skip runtime.Callers(), getLogRecord(), and Info()/Debug()/Error() + var pcs [1]uintptr + runtime.Callers(3, pcs[:]) + + rec := slog.NewRecord(time.Now().UTC(), lvl, msg, pcs[0]) + if len(kvs) > 0 { + attrs := make([]slog.Attr, 0, len(kvs)/2) + for i := 0; i < len(kvs); i += 2 { + k, ok := kvs[i].(string) + if !ok { + k = fmt.Sprintf("%v", kvs[i]) + } + attrs = append(attrs, slog.Any(k, kvs[i+1])) + } + rec.AddAttrs(attrs...) + } + return rec +} + +// replaceRecordAttributes massages the log record attributes before output +func replaceRecordAttributes(_ []string, a slog.Attr) slog.Attr { + switch a.Key { + case slog.SourceKey: + // trim "source" down to relative path within this module + val := a.Value.Any().(*slog.Source) + if idx := strings.Index(val.File, "github.com/CrowdStrike/perseus/"); idx != -1 { + val.File = val.File[idx+31:] + } + default: + } + return a +} + +var ( + loadRunningInK8sOnce sync.Once + runningInK8s bool +) + +// inK8S returns a boolean value that indicates whether or not the current process is running inside +// of Kubernetes. This is used configure the log output to be either logfmt or JSON. +func inK8S() bool { + loadRunningInK8sOnce.Do(func() { + _, runningInK8s = os.LookupEnv("KUBERNETES_SERVICE_HOST") + }) + return runningInK8s +} + +// dumpLogHandlerError is called when invoking the underlying [slog.Handler] returns an error to write +// the error and additional details to [os.Stderr] in the appropriate format (JSON vs key/value) based +// on the environment. +func dumpLogHandlerError(err error, rec slog.Record) { + var source string + rec.Attrs(func(a slog.Attr) bool { + switch a.Key { + case slog.SourceKey: + src := a.Value.Any().(*slog.Source) + source = fmt.Sprintf("%s:%d", src.File, src.Line) + default: + } + return true + }) + // generate a JSON "log" if we're running in k8s, logfmt style k/v pairs otherwise + logData := map[string]string{ + "time": rec.Time.Format(time.RFC3339), + "level": "ERROR", + "source": source, + "msg": "failure invoking slog.Handler.Handle()", + "originalMsg": rec.Message, + "error": err.Error(), + } + if inK8S() { + output, _ := json.Marshal(logData) + _, _ = os.Stderr.Write(output) + } else { + // output keys in an explicit order for consistency + var ( + sb strings.Builder + keys = []string{"time", "level", "source", "msg", "orginalMsg", "error"} + ) + for i, k := range keys { + if i > 0 { + sb.WriteRune(' ') + } + v := logData[k] + // use snake-case instead of Pascal case for logfmt output + if k == "originalMsg" { + k = "original_msg" + } + sb.WriteString(k + "=") + switch k { + case "time", "level", "source": + // these keys won't have embedded quotes or backslashes + sb.WriteString(v) + default: + sb.WriteString("\"" + strings.ReplaceAll(v, "\"", "\\\"") + "\"") + } + } + _, _ = os.Stderr.WriteString(sb.String()) + } + _, _ = os.Stderr.WriteString("\n") +} diff --git a/internal/server/grpc.go b/internal/server/grpc.go index 73af41d..d851c0f 100644 --- a/internal/server/grpc.go +++ b/internal/server/grpc.go @@ -35,7 +35,7 @@ func newGRPCServer(store store.Store) perseusapi.PerseusServiceServer { } func (s *grpcServer) CreateModule(ctx context.Context, req *perseusapi.CreateModuleRequest) (*perseusapi.CreateModuleResponse, error) { - debugLog("CreateModule() called", "module", req.GetModule().GetName(), "versions", req.GetModule().GetVersions()) + log.Debug("CreateModule() called", "module", req.GetModule().GetName(), "versions", req.GetModule().GetVersions()) m := req.GetModule() if m.GetName() == "" { @@ -61,27 +61,25 @@ func (s *grpcServer) CreateModule(ctx context.Context, req *perseusapi.CreateMod } } - err := s.store.SaveModule(ctx, m.GetName(), "", m.GetVersions()...) - if err != nil { - debugLog("unable to save module", "module", m.GetName(), "err", err) + if err := s.store.SaveModule(ctx, m.GetName(), "", m.GetVersions()...); err != nil { + log.Error(err, "error saving new module", "module", m.GetName(), "versions", m.GetVersions()) return nil, status.Errorf(codes.Internal, fmt.Sprintf("unable to save module %q: a database operation failed", m.GetName())) } - resp := perseusapi.CreateModuleResponse{ + return &perseusapi.CreateModuleResponse{ Module: req.GetModule(), - } - return &resp, nil + }, nil } func (s *grpcServer) ListModules(ctx context.Context, req *perseusapi.ListModulesRequest) (*perseusapi.ListModulesResponse, error) { - debugLog("ListModules() called", "args", req.String()) + log.Debug("ListModules() called", "args", req.String()) mods, pageToken, err := s.store.QueryModules(ctx, req.Filter, req.PageToken, int(req.PageSize)) if err != nil { - debugLog("unable to query modules", "filter", req.Filter, "err", err) + log.Error(err, "error querying the database", "filter", req.Filter, "pageToken", req.PageToken, "pageSize", req.PageSize) return nil, status.Errorf(codes.Internal, "Unable to query the database") } - resp := perseusapi.ListModulesResponse{ + resp := &perseusapi.ListModulesResponse{ NextPageToken: pageToken, } for _, m := range mods { @@ -90,11 +88,11 @@ func (s *grpcServer) ListModules(ctx context.Context, req *perseusapi.ListModule } resp.Modules = append(resp.Modules, mod) } - return &resp, nil + return resp, nil } func (s *grpcServer) ListModuleVersions(ctx context.Context, req *perseusapi.ListModuleVersionsRequest) (*perseusapi.ListModuleVersionsResponse, error) { - debugLog("ListModuleVersions() called", "req", req) + log.Debug("ListModuleVersions() called", "req", req) mod, vfilter, vopt, pageToken := req.GetModuleName(), req.GetVersionFilter(), req.GetVersionOption(), req.GetPageToken() if mod == "" { @@ -108,7 +106,7 @@ func (s *grpcServer) ListModuleVersions(ctx context.Context, req *perseusapi.Lis return nil, status.Errorf(codes.InvalidArgument, "The version option cannot be 'none'") case perseusapi.ModuleVersionOption_latest: if pageToken != "" { - return nil, status.Errorf(codes.InvalidArgument, "Paging is only support when the version option is 'all'") + return nil, status.Errorf(codes.InvalidArgument, "Paging is only supported when the version option is 'all'") } default: // all good @@ -127,7 +125,15 @@ func (s *grpcServer) ListModuleVersions(ctx context.Context, req *perseusapi.Lis Count: int(req.GetPageSize()), }) if err != nil { - debugLog("unable to query module versions", "moduleFilter", mod, "versionFilter", vfilter, "err", err) + kvs := []any{ + "moduleFilter", mod, + "versionFilter", vfilter, + "includePrerelease", req.IncludePrerelease, + "latestOnly", req.VersionOption == perseusapi.ModuleVersionOption_latest, + "pageToken", req.GetPageToken(), + "pageSize", req.GetPageSize(), + } + log.Error(err, "unable to query module versions", kvs...) return nil, status.Errorf(codes.Internal, "Unable to retrieve version list for module %s: a database operation failed", req.GetModuleName()) } @@ -151,7 +157,7 @@ func (s *grpcServer) ListModuleVersions(ctx context.Context, req *perseusapi.Lis } func (s *grpcServer) QueryDependencies(ctx context.Context, req *perseusapi.QueryDependenciesRequest) (*perseusapi.QueryDependenciesResponse, error) { - debugLog("QueryDependencies() called", "request", req.String()) + log.Debug("QueryDependencies() called", "request", req.String()) modName, modVer := req.GetModuleName(), req.GetVersion() if err := module.Check(modName, modVer); err != nil { @@ -169,7 +175,14 @@ func (s *grpcServer) QueryDependencies(ctx context.Context, req *perseusapi.Quer deps, pageToken, err = s.store.GetDependents(ctx, modName, strings.TrimPrefix(modVer, "v"), req.GetPageToken(), int(req.GetPageSize())) } if err != nil { - debugLog("unable to query module dependencies", "module", modName, "version", modVer, "direction", req.GetDirection().String(), "err", err) + kvs := []any{ + "module", modName, + "version", modVer, + "direction", req.GetDirection().String(), + "pageToken", req.GetPageToken(), + "pageSize", req.GetPageSize(), + } + log.Error(err, "unable to query module dependencies", kvs...) return nil, status.Errorf(codes.Internal, "Unable to query the graph: a database operation failed") } resp := perseusapi.QueryDependenciesResponse{ @@ -185,7 +198,7 @@ func (s *grpcServer) QueryDependencies(ctx context.Context, req *perseusapi.Quer } func (s *grpcServer) UpdateDependencies(ctx context.Context, req *perseusapi.UpdateDependenciesRequest) (*perseusapi.UpdateDependenciesResponse, error) { - debugLog("UpdateDependencies() called", "args", req) + log.Debug("UpdateDependencies() called", "args", req) modName, modVer := req.GetModuleName(), req.GetVersion() if err := module.Check(modName, modVer); err != nil { @@ -211,7 +224,7 @@ func (s *grpcServer) UpdateDependencies(ctx context.Context, req *perseusapi.Upd } if err := s.store.SaveModuleDependencies(ctx, mod, deps...); err != nil { - debugLog("unable to save module dependencies", "module", mod, "err", err) + log.Error(err, "unable to save module dependencies", "module", mod, "dependencies", deps) return nil, status.Errorf(codes.Internal, "Unable to update the graph: database operation failed") } diff --git a/internal/server/server.go b/internal/server/server.go index b1d6b84..b4853df 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -23,17 +23,35 @@ import ( "github.com/CrowdStrike/perseus/perseusapi" ) -// LogFunc defines a callback function for logging. This type is defined here so that the server -// implementation is not tied to any specified logging library -type LogFunc func(string, ...any) +// Logger defines the required behavior for the service's logger. This type is defined here so that the server +// implementation is not tied to any specified logging library. +type Logger interface { + // Info generates a log entry at INFO level with the specified message and key/value attributes + Info(msg string, kvs ...any) + // Debug generates a log entry at DEBUG level with the specified message and key/value attributes + Debug(msg string, kvs ...any) + // Error generates a log entry at ERROR level with the specified error, message, and key/value attributes + Error(err error, msg string, kvs ...any) +} + +// nopLogger is a [Logger] that does nothing. This is used as a fallback/default if [CreateServerCommand] +// is passed a nil. +type nopLogger struct{} + +func (nopLogger) Info(string, ...any) { /* no-op */ } + +func (nopLogger) Debug(string, ...any) { /* no-op */ } + +func (nopLogger) Error(error, string, ...any) { /* no-op */ } -// debugLog is the logging function for the server. -var debugLog LogFunc = func(string, ...any) { /* no-op by default */ } +// log is the logging implementation for the server. The default is a no-op logger, potentially +// overridden by [CreateServerCommand] +var log Logger = nopLogger{} // CreateServerCommand initializes and returns a *cobra.Command that implements the 'server' CLI sub-command -func CreateServerCommand(logFn LogFunc) *cobra.Command { - if logFn != nil { - debugLog = logFn +func CreateServerCommand(logger Logger) *cobra.Command { + if logger != nil { + log = logger } cmd := cobra.Command{ @@ -79,7 +97,7 @@ func runServer(opts ...serverOption) error { conf.healthzTimeout = 300 * time.Millisecond } - debugLog("starting the server") + log.Debug("starting the server") // create the root listener for cmux lis, err := net.Listen("tcp", conf.listenAddr) if err != nil { @@ -87,7 +105,7 @@ func runServer(opts ...serverOption) error { } defer func() { if err := lis.Close(); err != nil { - debugLog("unexpected error closing TCP listener", "err", err) + log.Error(err, "unexpected error closing TCP listener") } }() @@ -96,13 +114,13 @@ func runServer(opts ...serverOption) error { grpcLis := mux.MatchWithWriters(cmux.HTTP2MatchHeaderFieldSendSettings("content-type", "application/grpc")) defer func() { if err := grpcLis.Close(); err != nil { - debugLog("unexpected error closing gRPC mux listener", "err", err) + log.Error(err, "unexpected error closing gRPC mux listener") } }() httpLis := mux.Match(cmux.HTTP1Fast(http.MethodPatch)) defer func() { if err := httpLis.Close(); err != nil { - debugLog("unexpected error closing HTTP mux listener", "err", err) + log.Error(err, "unexpected error closing HTTP mux listener") } }() @@ -111,11 +129,11 @@ func runServer(opts ...serverOption) error { // connect to the database connStr := fmt.Sprintf("postgres://%s:%s@%s/%s", url.PathEscape(conf.dbUser), url.PathEscape(conf.dbPwd), url.PathEscape(conf.dbAddr), url.PathEscape(conf.dbName)) - db, err := store.NewPostgresClient(ctx, connStr, store.WithLog(debugLog)) + db, err := store.NewPostgresClient(ctx, connStr, store.WithLog(log)) if err != nil { return fmt.Errorf("could not connect to the database: %w", err) } - debugLog("connected to the database", "addr", conf.dbAddr, "database", conf.dbName, "user", conf.dbUser) + log.Debug("connected to the database", "addr", conf.dbAddr, "database", conf.dbName, "user", conf.dbUser) // spin up gRPC server grpcOpts := []grpc.ServerOption{ @@ -133,13 +151,13 @@ func runServer(opts ...serverOption) error { // . use x/sync/errgroup so we can stop everything at once via the context eg, ctx := errgroup.WithContext(ctx) eg.Go(func() error { - debugLog("serving gRPC") - defer debugLog("gRPC server closed") + log.Debug("serving gRPC") + defer log.Debug("gRPC server closed") return grpcSrv.Serve(grpcLis) }) eg.Go(func() error { - debugLog("serving HTTP/REST") - defer debugLog("HTTP/REST server closed") + log.Debug("serving HTTP/REST") + defer log.Debug("HTTP/REST server closed") return httpSrv.Serve(httpLis) }) @@ -158,9 +176,9 @@ func runServer(opts ...serverOption) error { case sig := <-sigs: switch sig { case syscall.SIGHUP: - debugLog("Got SIGNUP signal, TODO - reload config") + log.Debug("Got SIGHUP signal, TODO - reload config") default: - debugLog("Got stop signal, shutting down", "signal", sig.String()) + log.Debug("Got stop signal, shutting down", "signal", sig.String()) return nil } case <-ctx.Done(): @@ -171,8 +189,8 @@ func runServer(opts ...serverOption) error { // spin up the cmux go func() { _ = mux.Serve() }() - debugLog("Server listening", "addr", conf.listenAddr) - defer debugLog("Server exited") + log.Info("Server listening", "addr", conf.listenAddr) + defer log.Info("Server exited") // wait for shutdown if err := eg.Wait(); err != nil && err != context.Canceled { return err diff --git a/internal/store/options.go b/internal/store/options.go index 716d2f6..5df5ed3 100644 --- a/internal/store/options.go +++ b/internal/store/options.go @@ -3,13 +3,23 @@ package store // PGOption defines a configuration option to be used when constructing the database connection. type PGOption func(*PostgresClient) error +type Logger interface { + Debug(string, ...any) + Error(error, string, ...any) +} + +type nopLogger struct{} + +func (nopLogger) Debug(string, ...any) { /*no-op*/ } +func (nopLogger) Error(error, string, ...any) { /*no-op*/ } + // WithLog returns a PGOption that attaches the provided debug logging callback function -func WithLog(fn func(string, ...any)) PGOption { +func WithLog(l Logger) PGOption { return func(c *PostgresClient) error { - if fn == nil { - fn = func(string, ...any) { /* write to /dev/null as a fallback */ } + if l == nil { + l = nopLogger{} } - c.log = fn + c.log = l return nil } } diff --git a/internal/store/pg.go b/internal/store/pg.go index 720409f..571331f 100644 --- a/internal/store/pg.go +++ b/internal/store/pg.go @@ -22,8 +22,6 @@ const ( var ( columnsModules = []string{"id", "name", "description"} - //columnsModuleVersions = []string{"id", "module_id", "version"} - //colummsModuleDependencies = []string{"dependent_id", "dependee_id"} psql = sq.StatementBuilder.PlaceholderFormat(sq.Dollar) ) @@ -32,7 +30,7 @@ var ( // database. type PostgresClient struct { db *sqlx.DB - log func(string, ...any) + log Logger } // ensure the PG client satisfies the Store interface @@ -59,7 +57,7 @@ func NewPostgresClient(ctx context.Context, url string, opts ...PGOption) (*Post } } if p.log == nil { - p.log = func(string, ...any) {} + p.log = nopLogger{} } return p, nil } @@ -117,12 +115,12 @@ func (p *PostgresClient) SaveModuleDependencies(ctx context.Context, mod Version err = txn.Commit() } else { if e2 := txn.Rollback(); e2 != nil { - p.log("error rolling back transaction after error", "error", err, "rollbackError", e2) + p.log.Error(e2, "error rolling back transaction after error") } } }() - p.log("saving module", "moduleName", mod.ModuleID, "version", mod.SemVer) + p.log.Debug("saving module", "moduleName", mod.ModuleID, "version", mod.SemVer) pkey, err := writeModule(ctx, txn, mod.ModuleID, "") if err != nil { return err @@ -142,7 +140,7 @@ func (p *PostgresClient) SaveModuleDependencies(ctx context.Context, mod Version // database multiple times in a single command uniqueDeps := map[string]struct{}{} for _, d := range deps { - p.log("saving dependency", "moduleName", d.ModuleID, "version", d.SemVer) + p.log.Debug("saving dependency", "moduleName", d.ModuleID, "version", d.SemVer) pkey, err := writeModule(ctx, txn, d.ModuleID, "") if err != nil { return err @@ -153,17 +151,17 @@ func (p *PostgresClient) SaveModuleDependencies(ctx context.Context, mod Version } k := fmt.Sprintf("%d-%d", versionIDs[0], vids[0]) if _, found := uniqueDeps[k]; found { - p.log("skipping duplicate dependency", "dependency", d.ModuleID+"@"+d.SemVer) + p.log.Debug("skipping duplicate dependency", "dependency", d.ModuleID+"@"+d.SemVer) continue } cmd = cmd.Values(versionIDs[0], vids[0]) uniqueDeps[k] = struct{}{} } sql, args, err := cmd.Suffix("ON CONFLICT (dependent_id, dependee_id) DO UPDATE SET dependent_id = EXCLUDED.dependent_id").ToSql() - p.log("upsert module dependencies", "sql", sql, "args", args, "err", err) if err != nil { return fmt.Errorf("error constructing SQL query: %w", err) } + p.log.Debug("upsert module dependencies", "sql", sql, "args", args) if _, err = txn.ExecContext(ctx, sql, args...); err != nil { return fmt.Errorf("database error saving new module dependency: %w", err) } @@ -282,7 +280,7 @@ func (p *PostgresClient) QueryModuleVersions(ctx context.Context, query ModuleVe SemVer string `db:"version"` } var rows []queryResult - p.log("QueryModuleVersions", "sql", sql, "args", args) + p.log.Debug("QueryModuleVersions", "sql", sql, "args", args) err = p.db.SelectContext(ctx, &rows, sql, args...) if err != nil { return nil, "", err @@ -444,7 +442,7 @@ func writeModuleVersions(ctx context.Context, db database, moduleID int32, versi // getDependx is a shared query for dependency gathering in either direction, // dependent on the joinType. -func getDependx(ctx context.Context, db *sqlx.DB, module, version, joinType string, pageToken string, count int, log func(string, ...any)) ([]Version, string, error) { +func getDependx(ctx context.Context, db *sqlx.DB, module, version, joinType string, pageToken string, count int, log Logger) ([]Version, string, error) { pageTokenKey := "moduleversions:" + module + version + ":" + joinType offset := 0 if pageToken != "" { @@ -488,7 +486,7 @@ func getDependx(ctx context.Context, db *sqlx.DB, module, version, joinType stri if err != nil { return nil, "", err } - log("getDependx()", "sql", sql, "args", args) + log.Debug("getDependx()", "sql", sql, "args", args) var dependents []Version err = db.SelectContext(ctx, &dependents, sql, args...) if err != nil { diff --git a/log.go b/log.go new file mode 100644 index 0000000..82a7323 --- /dev/null +++ b/log.go @@ -0,0 +1,29 @@ +package main + +import ( + "log/slog" + + "github.com/CrowdStrike/perseus/internal/log" +) + +var ( + // stores the process-level verbosity + logLevel logLevelVar + // the logger + logger = log.New(&logLevel) +) + +// logLevelVar wraps a boolean value that controls logging verbosity and satisfies the [slog.Leveler] +// interface to translate that boolean to the equivalent [slog.Level], either [slog.LevelDebug] or [slog.LevelInfo]. +type logLevelVar struct { + debugMode bool +} + +// Level satisfies the [slog.Leveler] interface and returns either [slog.LevelDebug] or [slog.LevelInfo] +// depending on whether or not debug verbosity was enabled. +func (v *logLevelVar) Level() slog.Level { + if v.debugMode { + return slog.LevelDebug + } + return slog.LevelInfo +} diff --git a/main.go b/main.go index 72abfcd..138f01f 100644 --- a/main.go +++ b/main.go @@ -11,9 +11,11 @@ import ( ) func main() { - rootCommand.PersistentFlags().BoolVarP(&debugMode, "debug", "x", os.Getenv("LOG_VERBOSITY") == "debug", "enable verbose logging") + // we pass the debugMode field on the package-level logLevel variable here to simplify the CLI + // argument management. + rootCommand.PersistentFlags().BoolVarP(&(logLevel.debugMode), "debug", "x", os.Getenv("LOG_VERBOSITY") == "debug", "enable verbose logging") - rootCommand.AddCommand(server.CreateServerCommand(debugLog)) + rootCommand.AddCommand(server.CreateServerCommand(logger)) rootCommand.AddCommand(createUpdateCommand()) rootCommand.AddCommand(createQueryCommand()) rootCommand.AddCommand(createFindPathsCommand()) diff --git a/pathfinder.go b/pathfinder.go index 306277b..f95cb74 100644 --- a/pathfinder.go +++ b/pathfinder.go @@ -92,7 +92,7 @@ func (pf *pathFinder) recursiveSearch(ctx context.Context, chain []module.Versio return default: if d.Module.Path == to.Path && (to.Version == "" || d.Module.Version == to.Version) { - debugLog("found path", "chain", chain, "to", d.Module) + logger.Debug("found path", "chain", chain, "to", d.Module) // data sharing == bad cc := make([]module.Version, len(chain)) copy(cc, chain) diff --git a/perseusapi/buf.lock b/perseusapi/buf.lock index 2472884..34d161a 100644 --- a/perseusapi/buf.lock +++ b/perseusapi/buf.lock @@ -4,8 +4,10 @@ deps: - remote: buf.build owner: googleapis repository: googleapis - commit: 6fa2578641f0414ba54e648d502b34cc + commit: 28151c0d0a1641bf938a7672c500e01d + digest: shake256:49215edf8ef57f7863004539deff8834cfb2195113f0b890dd1f67815d9353e28e668019165b9d872395871eeafcbab3ccfdb2b5f11734d3cca95be9e8d139de - remote: buf.build owner: grpc-ecosystem repository: grpc-gateway - commit: 00116f302b12478b85deb33b734e026c + commit: 3f42134f4c564983838425bc43c7a65f + digest: shake256:3d11d4c0fe5e05fda0131afefbce233940e27f0c31c5d4e385686aea58ccd30f72053f61af432fa83f1fc11cda57f5f18ca3da26a29064f73c5a0d076bba8d92 diff --git a/query.go b/query.go index 148841b..6bc58f3 100644 --- a/query.go +++ b/query.go @@ -178,15 +178,15 @@ func runListModuleVersionsCmd(cmd *cobra.Command, args []string) error { versionFilter, err := cmd.Flags().GetString("versions") if err != nil { - debugLog("error reading 'version' CLI flag", "err", err) + logger.Error(err, "unable to read 'version' CLI flag") } latest, err := cmd.Flags().GetBool("latest") if err != nil { - debugLog("error reading 'latest' CLI flag", "err", err) + logger.Error(err, "unable to read 'latest' CLI flag") } includePrerelease, err := cmd.Flags().GetBool("include-prerelease") if err != nil { - debugLog("error reading 'include-prerelease' CLI flag", "err", err) + logger.Error(err, "unable to read 'include-prerelease' CLI flag") } results, err := listModuleVersions(ctx, ps, listModuleVersionsRequest{ modulePattern: args[0], @@ -200,7 +200,7 @@ func runListModuleVersionsCmd(cmd *cobra.Command, args []string) error { return err } if len(results) == 0 { - debugLog("found no matching versions for matching modules", "filter", versionFilter, "module", args[0]) + logger.Debug("found no matching versions for matching modules", "filter", versionFilter, "module", args[0]) return nil } diff --git a/update.go b/update.go index 85a9d35..52b2e4b 100644 --- a/update.go +++ b/update.go @@ -144,7 +144,7 @@ func getModuleInfoFromDir(dir string) (moduleInfo, error) { if err != nil { return moduleInfo{}, err } - if debugMode { + if logLevel.debugMode { fmt.Printf("Processing Go module %s@%s (path=%q)...\nDirect Dependencies:\n", info.Name, moduleVersion, moduleDir) for _, d := range info.Deps { fmt.Printf("\t%s\n", d) @@ -179,7 +179,7 @@ func getModuleInfoFromProxy(modulePath string) (moduleInfo, error) { if err != nil { return moduleInfo{}, err } - if debugMode { + if logLevel.debugMode { fmt.Printf("Processing Go module %s@%s...\nDirect Dependencies:\n", info.Name, info.Version) for _, d := range info.Deps { fmt.Printf("\t%s\n", d) From 1b9cbe582d42918ef6799cc923a81a727f047173 Mon Sep 17 00:00:00 2001 From: Dylan Bourque Date: Tue, 28 Nov 2023 13:39:04 -0600 Subject: [PATCH 2/3] chore: added support for generating CHANGELOG.md from commits --- .markdownlint.json | 5 +++ Makefile | 14 ++++++++ README.md | 2 ++ cliff.toml | 89 ++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 110 insertions(+) create mode 100644 .markdownlint.json create mode 100644 cliff.toml diff --git a/.markdownlint.json b/.markdownlint.json new file mode 100644 index 0000000..779b818 --- /dev/null +++ b/.markdownlint.json @@ -0,0 +1,5 @@ +{ + "default": true, + "MD013": false, + "MD024": false +} \ No newline at end of file diff --git a/Makefile b/Makefile index a151d21..de0c8bd 100644 --- a/Makefile +++ b/Makefile @@ -66,6 +66,14 @@ check-goreleaser-config: snapshot: check-goreleaser-install @goreleaser release --snapshot --rm-dist --skip-publish +.PHONY: update-changelog +update-changelog: check-git-cliff-install +ifeq ("${NEXT_VERSION}", "") + $(error Must specify the next version via $$NEXT_VERSION) +else + git cliff --unreleased --tag ${NEXT_VERSION} --prepend CHANGELOG.md +endif + .PHONY: check-buf-install check-buf-install: ifeq ("$(shell command -v buf)", "") @@ -83,3 +91,9 @@ check-goreleaser-install: ifeq ("$(shell command -v goreleaser)", "") $(error goreleaser was not found. Please install it using the method of your choice. (https://goreleaser.com/install)) endif + +.PHONY: check-git-cliff-install +check-git-cliff-install: +ifeq ("$(shell command -v git-cliff)", "") + $(error git-cliff was not found. Please install it using the method of your choice. (https://git-cliff.org/docs/installation/)) +endif diff --git a/README.md b/README.md index 1c70d6a..19e0f02 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,8 @@ view that shows what other packages depend on your code. The `go` tool won't sh on you, though. The `pkg.go.dev` site can only show things that it knows about, so it won't help for private modules, and it doesn't show you which versions of those other packages depend on your code. +See [CHANGELOG.md](./CHANGELOG.md) for a detailed history of changes. + ## Existing Tooling Unfortunately, the `go` CLI commands, the `pkg.go.dev` site, and OSS tools like [`goda`](https://github.com/loov/goda) diff --git a/cliff.toml b/cliff.toml new file mode 100644 index 0000000..2ff8dde --- /dev/null +++ b/cliff.toml @@ -0,0 +1,89 @@ +# git-cliff ~ default configuration file +# https://git-cliff.org/docs/configuration +# +# Lines starting with "#" are comments. +# Configuration options are organized into tables and keys. +# See documentation for more information on available options. + +[changelog] +# changelog header +header = """ +# Changelog\n +All notable changes to this project will be documented in this file. + +*Note: Releases that only include dependency updates generated by Dependabot will not appear here.*\n +""" +# template for the changelog body +# https://tera.netlify.app/docs +body = """ +{% if version %}\ +## [{{ version }}] - {{ timestamp | date(format="%Y-%m-%d") }} +{% else %}\ +## [unreleased] +{% endif %}\ +{% for group, commits in commits | group_by(attribute="group") %} + ### {{ group | upper_first }} + {% for commit in commits %} + - {% if commit.scope %}*({{ commit.scope }})* {% endif %}{% if commit.breaking %} **[BREAKING]** {% endif %}{{ commit.message | upper_first }}{% if commit.id %} ([{{ commit.id | truncate(length=7, end="") }}](https://github.com/CrowdStrike/perseus/commits/{{ commit.id }})){% endif %}\ + {% endfor %} +{% endfor %} +Contributors: \ +{% for group, commits in commits | group_by(attribute="author.name") -%} + {{ group }}{% if not loop.last %}, {% endif %} +{%- endfor %}\n + +""" +# remove the leading and trailing whitespace from the template +trim = true +# changelog footer +footer = """ + +""" +# postprocessors +postprocessors = [ + # { pattern = '', replace = "https://github.com/orhun/git-cliff" }, # replace repository URL +] +[git] +# parse the commits based on https://www.conventionalcommits.org +conventional_commits = true +# filter out the commits that are not conventional +filter_unconventional = true +# process each line of a commit as an individual commit +split_commits = false +# regex for preprocessing the commit messages +commit_preprocessors = [ + # { pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](/issues/${2}))"}, # replace issue numbers +] +# regex for parsing and grouping commits +commit_parsers = [ + { message = "^feat", group = "โ›ฐ๏ธ Features" }, + { message = "^fix", group = "๐Ÿ› Bug Fixes" }, + { message = "^doc", group = "๐Ÿ“š Documentation" }, + { message = "^perf", group = "๐ŸŽ Performance" }, + { message = "^refactor", group = "๐Ÿšœ Refactor" }, + { message = "^style", group = "๐Ÿ˜Ž Styling" }, + { message = "^test", group = "๐Ÿงช Testing" }, + { message = "^chore\\(release\\): prepare for", skip = true }, + { message = "^chore\\(deps\\)", skip = true }, + { message = "^chore\\(pr\\)", skip = true }, + { message = "^chore\\(pull\\)", skip = true }, + { message = "^chore|ci", group = "โš™๏ธ Miscellaneous Tasks" }, + { body = ".*security", group = "๐Ÿ‘ฎโ€โ™‚๏ธ Security" }, + { message = "^revert", group = "โŽŒ Revert" }, +] +# protect breaking changes from being skipped due to matching a skipping commit_parser +protect_breaking_commits = false +# filter out the commits that are not matched by commit parsers +filter_commits = false +# glob pattern for matching git tags +tag_pattern = "v[0-9]*" +# regex for skipping tags +skip_tags = "v0.1.0-beta.1" +# regex for ignoring tags +ignore_tags = "" +# sort the tags topologically +topo_order = false +# sort the commits inside sections by oldest/newest order +sort_commits = "oldest" +# limit the number of commits included in the changelog. +# limit_commits = 42 From 2930c9353ceb6160c9e3215867949308058d4050 Mon Sep 17 00:00:00 2001 From: Dylan Bourque Date: Tue, 28 Nov 2023 13:50:13 -0600 Subject: [PATCH 3/3] updated CHANGELOG.md --- .markdownlint.json | 3 +- CHANGELOG.md | 294 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 296 insertions(+), 1 deletion(-) create mode 100644 CHANGELOG.md diff --git a/.markdownlint.json b/.markdownlint.json index 779b818..f535880 100644 --- a/.markdownlint.json +++ b/.markdownlint.json @@ -1,5 +1,6 @@ { "default": true, "MD013": false, - "MD024": false + "MD024": false, + "MD033": false } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..dc7a61e --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,294 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +*Note: Releases that only include dependency updates generated by Dependabot will not appear here.* + +## [v0.14.0] - 2023-11-28 + +### โš™๏ธ Miscellaneous Tasks + +- Added support for generating CHANGELOG.md from commits ([9673490](https://github.com/CrowdStrike/perseus/commits/96734904ac9e947a57c1424b5de903f08366bb07)) + +### โ›ฐ๏ธ Features + +- *(logging)* Update server to always log errors ([4d97e62](https://github.com/CrowdStrike/perseus/commits/4d97e62aa9ea2664bb05b174fce0a0608719a4a0)) + +Contributors: Dylan Bourque + +## [v0.12.2] - 2023-06-02 + +### ๐Ÿ› Bug Fixes + +- Address database error when running 'perseus update' against untidy modules ([780a6d3](https://github.com/CrowdStrike/perseus/commits/780a6d3cfb01fd075bd99ab0c88126d4cbdb31ab)) + +Contributors: Dylan Bourque + +## [v0.12.0] - 2023-04-20 + +### โš™๏ธ Miscellaneous Tasks + +- Code cleanup ([3c1e9f6](https://github.com/CrowdStrike/perseus/commits/3c1e9f6c0ee78c9ec41c066683b77ba93111f945)) + +### โ›ฐ๏ธ Features + +- Implemented new find-paths sub-command ([84b1dc8](https://github.com/CrowdStrike/perseus/commits/84b1dc8c35ebdd11e53e8d68dc0df2d3b04e9217)) +- Add find path command ([24c06bb](https://github.com/CrowdStrike/perseus/commits/24c06bb5aa9d63c39ecca26729d2479489157288)) + +### ๐Ÿ› Bug Fixes + +- *(store)* Partially written module cannot be corrected ([fbafd47](https://github.com/CrowdStrike/perseus/commits/fbafd47b7d9df1751598ff06280c11f5034d0b52)) + +Contributors: Dylan Bourque + +## [v0.11.0] - 2023-04-04 + +### ๐Ÿ› Bug Fixes + +- Updating a module with no dependencies is a no-op ([f338a4d](https://github.com/CrowdStrike/perseus/commits/f338a4d35994f41a1f0ea2901708ed819edbb093)) + +Contributors: Dylan Bourque + +## [v0.10.0] - 2023-03-28 + +### ๐Ÿ› Bug Fixes + +- Eliminate panic when generating debug logs ([81fb085](https://github.com/CrowdStrike/perseus/commits/81fb0851679d879a0c6ffc3d46203208c10615a5)) +- Eliminate panic when generating debug logs ([0559cc1](https://github.com/CrowdStrike/perseus/commits/0559cc1f5675d4de9ef8d8f852ef0717cc76f2d3)) +- *(logs)* File/line info is incorrect in debug logs ([86e1dc9](https://github.com/CrowdStrike/perseus/commits/86e1dc914bf7cdda733aa946de8190588f2bb064)) +- *(logging)* Remove database connection password from debug logs ([8a3f74c](https://github.com/CrowdStrike/perseus/commits/8a3f74c446b528477fbfff3f03baf769c80b59aa)) +- *(ui)* Module detail page would not load when on pre-release versions in database ([daa1902](https://github.com/CrowdStrike/perseus/commits/daa19025b93ef9c743d9053984a2d295804690c1)) +- *(ui)* Module details page does not load when the module has only pre-release versions ([e57d754](https://github.com/CrowdStrike/perseus/commits/e57d75459ca7fbb2765278c75578f5805bdde481)) + +Contributors: Dylan Bourque + +## [v0.11.0] - 2023-04-04 + +### ๐Ÿ› Bug Fixes + +- Updating a module with no dependencies is a no-op ([f531cf0](https://github.com/CrowdStrike/perseus/commits/f531cf0941f0e62290bf58b049cea4c4c508be1c)) +- Updating a module with no dependencies is a no-op ([f338a4d](https://github.com/CrowdStrike/perseus/commits/f338a4d35994f41a1f0ea2901708ed819edbb093)) + +Contributors: Dylan Bourque + +## [v0.10.0] - 2023-03-28 + +### ๐Ÿ› Bug Fixes + +- Eliminate panic when generating debug logs ([81fb085](https://github.com/CrowdStrike/perseus/commits/81fb0851679d879a0c6ffc3d46203208c10615a5)) +- Eliminate panic when generating debug logs ([0559cc1](https://github.com/CrowdStrike/perseus/commits/0559cc1f5675d4de9ef8d8f852ef0717cc76f2d3)) +- *(logs)* File/line info is incorrect in debug logs ([86e1dc9](https://github.com/CrowdStrike/perseus/commits/86e1dc914bf7cdda733aa946de8190588f2bb064)) +- *(logging)* Remove database connection password from debug logs ([8a3f74c](https://github.com/CrowdStrike/perseus/commits/8a3f74c446b528477fbfff3f03baf769c80b59aa)) +- *(ui)* Module detail page would not load when on pre-release versions in database ([daa1902](https://github.com/CrowdStrike/perseus/commits/daa19025b93ef9c743d9053984a2d295804690c1)) +- *(ui)* Module details page does not load when the module has only pre-release versions ([e57d754](https://github.com/CrowdStrike/perseus/commits/e57d75459ca7fbb2765278c75578f5805bdde481)) + +Contributors: Dylan Bourque + +## [v0.11.0] - 2023-04-04 + +### ๐Ÿ› Bug Fixes + +- Updating a module with no dependencies is a no-op ([f531cf0](https://github.com/CrowdStrike/perseus/commits/f531cf0941f0e62290bf58b049cea4c4c508be1c)) +- Updating a module with no dependencies is a no-op ([f338a4d](https://github.com/CrowdStrike/perseus/commits/f338a4d35994f41a1f0ea2901708ed819edbb093)) + +Contributors: Dylan Bourque + +## [v0.10.0] - 2023-03-28 + +### ๐Ÿ› Bug Fixes + +- Eliminate panic when generating debug logs ([81fb085](https://github.com/CrowdStrike/perseus/commits/81fb0851679d879a0c6ffc3d46203208c10615a5)) +- Eliminate panic when generating debug logs ([0559cc1](https://github.com/CrowdStrike/perseus/commits/0559cc1f5675d4de9ef8d8f852ef0717cc76f2d3)) +- *(logs)* File/line info is incorrect in debug logs ([86e1dc9](https://github.com/CrowdStrike/perseus/commits/86e1dc914bf7cdda733aa946de8190588f2bb064)) +- *(logging)* Remove database connection password from debug logs ([8a3f74c](https://github.com/CrowdStrike/perseus/commits/8a3f74c446b528477fbfff3f03baf769c80b59aa)) +- *(ui)* Module detail page would not load when on pre-release versions in database ([daa1902](https://github.com/CrowdStrike/perseus/commits/daa19025b93ef9c743d9053984a2d295804690c1)) +- *(ui)* Module details page does not load when the module has only pre-release versions ([e57d754](https://github.com/CrowdStrike/perseus/commits/e57d75459ca7fbb2765278c75578f5805bdde481)) + +Contributors: Dylan Bourque + +## [v0.12.0] - 2023-04-20 + +### โš™๏ธ Miscellaneous Tasks + +- Code cleanup ([3c1e9f6](https://github.com/CrowdStrike/perseus/commits/3c1e9f6c0ee78c9ec41c066683b77ba93111f945)) + +### โ›ฐ๏ธ Features + +- Implemented new find-paths sub-command ([84b1dc8](https://github.com/CrowdStrike/perseus/commits/84b1dc8c35ebdd11e53e8d68dc0df2d3b04e9217)) +- Add find path command ([24c06bb](https://github.com/CrowdStrike/perseus/commits/24c06bb5aa9d63c39ecca26729d2479489157288)) + +### ๐Ÿ› Bug Fixes + +- *(store)* Partially written module cannot be corrected ([fbafd47](https://github.com/CrowdStrike/perseus/commits/fbafd47b7d9df1751598ff06280c11f5034d0b52)) + +Contributors: Dylan Bourque + +## [v0.11.0] - 2023-04-04 + +### ๐Ÿ› Bug Fixes + +- Updating a module with no dependencies is a no-op ([f531cf0](https://github.com/CrowdStrike/perseus/commits/f531cf0941f0e62290bf58b049cea4c4c508be1c)) +- Updating a module with no dependencies is a no-op ([f338a4d](https://github.com/CrowdStrike/perseus/commits/f338a4d35994f41a1f0ea2901708ed819edbb093)) + +Contributors: Dylan Bourque + +## [v0.10.0] - 2023-03-28 + +### ๐Ÿ› Bug Fixes + +- Eliminate panic when generating debug logs ([81fb085](https://github.com/CrowdStrike/perseus/commits/81fb0851679d879a0c6ffc3d46203208c10615a5)) +- Eliminate panic when generating debug logs ([0559cc1](https://github.com/CrowdStrike/perseus/commits/0559cc1f5675d4de9ef8d8f852ef0717cc76f2d3)) +- *(logs)* File/line info is incorrect in debug logs ([86e1dc9](https://github.com/CrowdStrike/perseus/commits/86e1dc914bf7cdda733aa946de8190588f2bb064)) +- *(logging)* Remove database connection password from debug logs ([8a3f74c](https://github.com/CrowdStrike/perseus/commits/8a3f74c446b528477fbfff3f03baf769c80b59aa)) +- *(ui)* Module detail page would not load when on pre-release versions in database ([daa1902](https://github.com/CrowdStrike/perseus/commits/daa19025b93ef9c743d9053984a2d295804690c1)) +- *(ui)* Module details page does not load when the module has only pre-release versions ([e57d754](https://github.com/CrowdStrike/perseus/commits/e57d75459ca7fbb2765278c75578f5805bdde481)) + +Contributors: Dylan Bourque + +## [v0.9.0] - 2023-03-16 + +### ๐Ÿ› Bug Fixes + +- *(ui)* Module details page does not render ([0b8f5cb](https://github.com/CrowdStrike/perseus/commits/0b8f5cbccb039dd2b057c7f10abf8223e43476a4)) +- *(debug)* Key/value pairs are not correctly output in debug logs ([28c9536](https://github.com/CrowdStrike/perseus/commits/28c9536cf47b466dd1ba945c73e52f8253d354d4)) +- *(tls)* Updates to `query` and `update` sub-commands to work with TLS ([f5a3627](https://github.com/CrowdStrike/perseus/commits/f5a3627e0dcaadde368641b7353a8c6fd8a25641)) +- *(list-modules)* `list-modules` sub-command fails if a module has no stable versions ([de32f31](https://github.com/CrowdStrike/perseus/commits/de32f31680b0a4d2a9a8e6bb8ecfd0f90773aee9)) + +Contributors: Dylan Bourque + +## [v0.8.0] - 2023-03-08 + +### โš™๏ธ Miscellaneous Tasks + +- Improve README.md ([e320392](https://github.com/CrowdStrike/perseus/commits/e3203927eae5f7fcc94cfb6b89a20235dafc3d7a)) + +### โ›ฐ๏ธ Features + +- Export gRPC metrics for Prometheus ([0334446](https://github.com/CrowdStrike/perseus/commits/03344461ee3094b8fffbfb2556e9ef4ded274f7c)) +- Expose pprof endpoints for runtime profiles ([4d24a6f](https://github.com/CrowdStrike/perseus/commits/4d24a6fc35f1336a83ef7c8e6d34054232e3bc39)) +- Made healthz timeout configurable via env var ([4df48b4](https://github.com/CrowdStrike/perseus/commits/4df48b47a9f87a6f2cf317a8ab05fc2c6743a13d)) + +### ๐Ÿ› Bug Fixes + +- Add 'v' prefix to returned versions for ListModuleVersions response ([26a390e](https://github.com/CrowdStrike/perseus/commits/26a390e403f31eec6e79de510d1b6278e4667cdd)) +- Change 'SERVER_ADDR' env var to 'PERSEUS_SERVER_ADDR' for disambiguation ([eb549c7](https://github.com/CrowdStrike/perseus/commits/eb549c7aef05bdefe6b30314a3fdd322bc878cb4)) + +Contributors: Dylan Bourque + +## [v0.7.0] - 2023-02-23 + +### โ›ฐ๏ธ Features + +- Expose healthz/ endpoint ([1396590](https://github.com/CrowdStrike/perseus/commits/1396590fb58cf882c7ab9a8f460cc60c4680fa46)) + +Contributors: Dylan Bourque + +## [v0.6.0] - 2023-02-14 + +### โš™๏ธ Miscellaneous Tasks + +- Bump Go version in GH Actions to 1.19 ([0eeb62c](https://github.com/CrowdStrike/perseus/commits/0eeb62c41abd671b27a1413a7cd2b19465825efa)) + +### โ›ฐ๏ธ Features + +- Update Goreleaser config to publish to GHCR ([9ca81be](https://github.com/CrowdStrike/perseus/commits/9ca81be7226a19257279488781264a54b0d89080)) + +Contributors: Dylan Bourque + +## [v0.5.0] - 2023-02-08 + +### โ›ฐ๏ธ Features + +- *(scripts)* Added support for command line args in processmod.sh ([a5cacdd](https://github.com/CrowdStrike/perseus/commits/a5cacdd49dcb8285e0afb8bd48b370fd1e29ed74)) +- *(api)* Refactored ListModuleVersions RPC to support wildcards for modules and/or versions ([4b534bc](https://github.com/CrowdStrike/perseus/commits/4b534bcdf36df442a2b4b0e19f6bf00e7207870f)) + +### ๐Ÿ› Bug Fixes + +- *(cli)* Error when skipping prerelease versions ([bc49064](https://github.com/CrowdStrike/perseus/commits/bc49064b93c3e5360eb8283e3aa03e093c17e12d)) + +Contributors: Dylan Bourque + +## [v0.4.2] - 2022-12-06 + +### ๐Ÿ› Bug Fixes + +- *(deps)* Update github.com/pjbgf/sha1cd to v0.2.3 ([800b867](https://github.com/CrowdStrike/perseus/commits/800b8677d502d6908dd8cab57a7ce992ae7ecf62)) + +Contributors: Dylan Bourque + +## [v0.4.1] - 2022-12-06 + +### ๐Ÿ› Bug Fixes + +- Goreleaser GH action failing ([4b35f5e](https://github.com/CrowdStrike/perseus/commits/4b35f5e7da8211fae226b6e0ee6310d520633369)) + +Contributors: Dylan Bourque + +## [v0.4.0] - 2022-12-06 + +### โ›ฐ๏ธ Features + +- Publish a runnable Docker image for Perseus server ([453d5c7](https://github.com/CrowdStrike/perseus/commits/453d5c745bb4470b9ca8f1d775871ab4ffd0e5fd)) + +Contributors: Dylan Bourque + +## [v0.3.0] - 2022-11-17 + +### โ›ฐ๏ธ Features + +- *(update)* Extend 'update' command to support OSS deps ([abbbeaf](https://github.com/CrowdStrike/perseus/commits/abbbeaffde6c954abdcb2535ea97fbc71ad94c98)) + +### ๐Ÿ› Bug Fixes + +- The db-name flag now parses correctly ([3e874a9](https://github.com/CrowdStrike/perseus/commits/3e874a9d47f83d69e6eaab53bfc1144c92c51494)) +- The db-name flag now parses correctly ([c5ec243](https://github.com/CrowdStrike/perseus/commits/c5ec243ad6e2b88165b674d4706bb79002a6acde)) + +Contributors: Anthony Lee, Dylan Bourque + +## [v0.2.0] - 2022-09-28 + +### โ›ฐ๏ธ Features + +- *(query)* UX improvements ([b6f3257](https://github.com/CrowdStrike/perseus/commits/b6f325726acb412a29f8fb193dc26bf11bcb495b)) +- *(server)* Make server more configurable ([d1114ca](https://github.com/CrowdStrike/perseus/commits/d1114ca7c7219d81314d2d893d02649058b385aa)) +- *(query)* Implement new 'list-modules' sub-command ([6fb0048](https://github.com/CrowdStrike/perseus/commits/6fb004801e648a3c1fbffabca0cb47d52b397b5a)) +- *(ui)* Polish web UI/UX for the module view ([23c97b8](https://github.com/CrowdStrike/perseus/commits/23c97b817536c5d2bf1221d1faac8f57d9ca7f9c)) + +### ๐Ÿ› Bug Fixes + +- *(ui)* Update graph nav to preserve the selected direction ([c183e19](https://github.com/CrowdStrike/perseus/commits/c183e19abc88c364053970c174c600af8d44fcf7)) + +### ๐Ÿšœ Refactor + +- Code cleanup ([02bbd1d](https://github.com/CrowdStrike/perseus/commits/02bbd1d4557690166203f5c537bf7675d35dcc0a)) +- Move default database name to a shared constant ([ac58b8e](https://github.com/CrowdStrike/perseus/commits/ac58b8e1fce89e01254d7df1826d0d6e9461c46f)) + +Contributors: Dylan Bourque + +## [v0.1.0] - 2022-09-07 + +### โš™๏ธ Miscellaneous Tasks + +- Enable CodeQL analysis ([ad6230c](https://github.com/CrowdStrike/perseus/commits/ad6230caa449da611ba4dcbc63ce68718737b323)) + +### โ›ฐ๏ธ Features + +- MVP ([a9d7425](https://github.com/CrowdStrike/perseus/commits/a9d74252ac52350a04cb2adc8d8f139dbcfc915f)) +- *(ui)* Basic graph explorer UI ([f6fd044](https://github.com/CrowdStrike/perseus/commits/f6fd044c0edc764b8cc68b8a776aaa248428e5da)) +- Implement 'query' CLI sub-command ([0d19011](https://github.com/CrowdStrike/perseus/commits/0d190111d756304703810da2db5b36447450b396)) + +### ๐Ÿ› Bug Fixes + +- *(proto)* Move path parameters to query ([06a73db](https://github.com/CrowdStrike/perseus/commits/06a73db24382828f041106e8e61a0ee7d5a7eda4)) + +Contributors: Ben Woodward, Dylan Bourque + +## [v0.0.0] - 2022-08-04 + +### โš™๏ธ Miscellaneous Tasks + +- Scaffold out initial implementation ([3773d37](https://github.com/CrowdStrike/perseus/commits/3773d376cd48fac78b84736a2012c146c00ed494)) + +Contributors: Dylan Bourque + +