From 43318e1a3fda8f1f0756878cf56834383f6e5f86 Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Sat, 1 Jun 2024 08:01:52 +0700 Subject: [PATCH] chore: pipe slog into sentry breadcrumbs --- Dockerfile | 2 +- cmd/brassite/main.go | 38 ++++++++----- cmd/brassite/slog_breadcrumbs.go | 93 ++++++++++++++++++++++++++++++++ go.mod | 5 ++ go.sum | 10 ++++ 5 files changed, 135 insertions(+), 13 deletions(-) create mode 100644 cmd/brassite/slog_breadcrumbs.go diff --git a/Dockerfile b/Dockerfile index 364b477..596c7b4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,7 +4,7 @@ WORKDIR /build COPY . . -RUN go build -o brassite ./cmd/brassite/main.go +RUN go build -o brassite -ldflags="-X main.version=$(git rev-parse HEAD)" ./cmd/brassite/ FROM alpine:3.20 AS runtime diff --git a/cmd/brassite/main.go b/cmd/brassite/main.go index 87c3a80..2565447 100644 --- a/cmd/brassite/main.go +++ b/cmd/brassite/main.go @@ -26,9 +26,13 @@ import ( "github.com/getsentry/sentry-go" "github.com/mmcdole/gofeed" + slogmulti "github.com/samber/slog-multi" "github.com/teknologi-umum/brassite" ) +var version string +var environment = os.Getenv("ENVIRONMENT") + func main() { // This is a very simple program, you can extend this to any extend you'd like. // 1. Read configuration file @@ -57,9 +61,15 @@ func main() { slogLevel = slog.LevelError } if logPretty { - slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slogLevel}))) + slog.SetDefault(slog.New(slogmulti.Fanout( + slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slogLevel}), + NewSlogSentryBreadcrumbsHandler(), + ))) } else { - slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{Level: slogLevel}))) + slog.SetDefault(slog.New(slogmulti.Fanout( + slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{Level: slogLevel}), + NewSlogSentryBreadcrumbsHandler(), + ))) } if err := sentry.Init(sentry.ClientOptions{ @@ -67,6 +77,8 @@ func main() { SampleRate: 1.0, EnableTracing: true, TracesSampleRate: 0.2, + Environment: environment, + Release: version, }); err != nil { slog.Error("Failed to initialize Sentry", slog.Any("error", err)) os.Exit(70) @@ -106,8 +118,6 @@ func main() { func runWorker(feed brassite.Feed) { for { - slog.Debug("Starting worker", slog.String("feed_name", feed.Name), slog.String("url", feed.URL), slog.Duration("interval", feed.Interval)) - ctx, cancel := context.WithTimeout(context.Background(), time.Minute*5) hub := sentry.CurrentHub().Clone() hub.Scope().SetTag("feed_name", feed.Name) @@ -119,10 +129,12 @@ func runWorker(feed brassite.Feed) { }) ctx = sentry.SetHubOnContext(ctx, hub) + slog.DebugContext(ctx, "Starting worker", slog.String("feed_name", feed.Name), slog.String("url", feed.URL), slog.Duration("interval", feed.Interval)) + // Call the feed parser request, err := http.NewRequestWithContext(ctx, http.MethodGet, feed.URL, nil) if err != nil { - slog.Error("Failed to create request", slog.Any("error", err), slog.String("feed_name", feed.Name)) + slog.ErrorContext(ctx, "Failed to create request", slog.Any("error", err), slog.String("feed_name", feed.Name)) cancel() sentry.GetHubFromContext(ctx).CaptureException(err) time.Sleep(feed.Interval) @@ -142,17 +154,19 @@ func runWorker(feed brassite.Feed) { response, err := http.DefaultClient.Do(request) if err != nil { - slog.Error("Failed to send request", slog.Any("error", err), slog.String("feed_name", feed.Name)) + slog.ErrorContext(ctx, "Failed to send request", slog.Any("error", err), slog.String("feed_name", feed.Name)) cancel() sentry.GetHubFromContext(ctx).CaptureException(err) time.Sleep(feed.Interval) continue } + slog.DebugContext(ctx, "Received response", slog.String("feed_name", feed.Name), slog.Int("status_code", response.StatusCode), slog.String("content_type", response.Header.Get("Content-Type"))) + parser := gofeed.NewParser() remoteFeed, err := parser.Parse(response.Body) if err != nil { - slog.Error("Failed to parse feed", slog.Any("error", err), slog.String("feed_name", feed.Name)) + slog.ErrorContext(ctx, "Failed to parse feed", slog.Any("error", err), slog.String("feed_name", feed.Name)) _ = response.Body.Close() cancel() sentry.GetHubFromContext(ctx).CaptureException(err) @@ -166,10 +180,10 @@ func runWorker(feed brassite.Feed) { // Only select the new items by using now - interval var newItems []*gofeed.Item for _, item := range remoteFeed.Items { - slog.Debug("Parsing item", slog.String("feed_name", feed.Name), slog.String("item_title", item.Title), slog.String("item_link", item.Link)) + slog.DebugContext(ctx, "Parsing item", slog.String("feed_name", feed.Name), slog.String("item_title", item.Title), slog.String("item_link", item.Link)) if item.PublishedParsed != nil { - slog.Debug("Published parsed value", slog.String("feed_name", feed.Name), slog.Time("published_parsed", *item.PublishedParsed), slog.Time("now", time.Now().UTC())) + slog.DebugContext(ctx, "Published parsed value", slog.String("feed_name", feed.Name), slog.Time("published_parsed", *item.PublishedParsed), slog.Time("now", time.Now().UTC())) if item.PublishedParsed.After(time.Now().UTC().Add(-feed.Interval)) { newItems = append(newItems, item) continue @@ -177,7 +191,7 @@ func runWorker(feed brassite.Feed) { } if item.UpdatedParsed != nil { - slog.Debug("Updated parsed value", slog.String("feed_name", feed.Name), slog.Time("updated_parsed", *item.UpdatedParsed), slog.Time("now", time.Now().UTC())) + slog.DebugContext(ctx, "Updated parsed value", slog.String("feed_name", feed.Name), slog.Time("updated_parsed", *item.UpdatedParsed), slog.Time("now", time.Now().UTC())) if item.UpdatedParsed.After(time.Now().UTC().Add(-feed.Interval)) { newItems = append(newItems, item) continue @@ -185,7 +199,7 @@ func runWorker(feed brassite.Feed) { } } - slog.Debug("Found new items", slog.String("feed_name", feed.Name), slog.Int("new_items", len(newItems))) + slog.DebugContext(ctx, "Found new items", slog.String("feed_name", feed.Name), slog.Int("new_items", len(newItems))) // Deliver it for _, item := range newItems { @@ -213,7 +227,7 @@ func runWorker(feed brassite.Feed) { for _, url := range feed.Delivery.DiscordWebhookUrl.Values { err := brassite.DeliverToDiscord(ctx, url, feedItem, feed.Logo) if err != nil { - slog.Error("Failed to deliver to Discord", slog.String("feed_name", feed.Name), slog.Any("error", err)) + slog.ErrorContext(ctx, "Failed to deliver to Discord", slog.String("feed_name", feed.Name), slog.Any("error", err)) sentry.GetHubFromContext(ctx).CaptureException(err) } diff --git a/cmd/brassite/slog_breadcrumbs.go b/cmd/brassite/slog_breadcrumbs.go new file mode 100644 index 0000000..6912874 --- /dev/null +++ b/cmd/brassite/slog_breadcrumbs.go @@ -0,0 +1,93 @@ +// Copyright 2024 Teknologi Umum +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "context" + "log/slog" + "strings" + "time" + + "github.com/getsentry/sentry-go" +) + +type slogSentryBreadcrumbs struct { + attr []slog.Attr + group string +} + +// Enabled implements slog.Handler. +func (s *slogSentryBreadcrumbs) Enabled(context.Context, slog.Level) bool { + return true +} + +// Handle implements slog.Handler. +func (s *slogSentryBreadcrumbs) Handle(ctx context.Context, r slog.Record) error { + if ctx == nil { + return nil + } + + hub := sentry.GetHubFromContext(ctx) + if hub == nil { + return nil + } + + var timestamp = r.Time + if timestamp.IsZero() { + timestamp = time.Now() + } + + var data = make(map[string]any) + r.Attrs(func(a slog.Attr) bool { + data[a.Key] = a.Value + return true + }) + + var group = s.group + if group == "" { + group = "console" + } + + var additionalData = make(sentry.BreadcrumbHint) + for _, a := range s.attr { + additionalData[a.Key] = a.Value + } + + hub.AddBreadcrumb(&sentry.Breadcrumb{ + Type: "default", + Category: group, + Message: r.Message, + Data: data, + Level: sentry.Level(strings.ToLower(r.Level.String())), + Timestamp: timestamp, + }, &additionalData) + + return nil +} + +// WithAttrs implements slog.Handler. +func (s *slogSentryBreadcrumbs) WithAttrs(attrs []slog.Attr) slog.Handler { + return &slogSentryBreadcrumbs{attr: append(s.attr, attrs...), group: s.group} +} + +// WithGroup implements slog.Handler. +func (s *slogSentryBreadcrumbs) WithGroup(name string) slog.Handler { + return &slogSentryBreadcrumbs{group: name, attr: s.attr} +} + +// NewSlogSentryBreadcrumbsHandler creates a new slog handler that sends breadcrumbs to Sentry. +func NewSlogSentryBreadcrumbsHandler() slog.Handler { + return &slogSentryBreadcrumbs{} +} diff --git a/go.mod b/go.mod index b6b0153..2e36744 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/JohannesKaufmann/html-to-markdown v1.6.0 github.com/getsentry/sentry-go v0.28.0 github.com/mmcdole/gofeed v1.3.0 + github.com/samber/slog-multi v1.0.3 github.com/titanous/json5 v1.0.0 gopkg.in/yaml.v3 v3.0.1 ) @@ -14,10 +15,14 @@ require ( require ( github.com/PuerkitoBio/goquery v1.9.2 // indirect github.com/andybalholm/cascadia v1.3.2 // indirect + github.com/hashicorp/errwrap v1.0.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/mmcdole/goxpp v1.1.1-0.20240225020742-a0c311522b23 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/samber/lo v1.38.1 // indirect + golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect golang.org/x/net v0.25.0 // indirect golang.org/x/sys v0.20.0 // indirect golang.org/x/text v0.15.0 // indirect diff --git a/go.sum b/go.sum index efe52ba..752059f 100644 --- a/go.sum +++ b/go.sum @@ -16,6 +16,10 @@ github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3Bop github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= @@ -43,6 +47,10 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/robertkrimen/otto v0.2.1 h1:FVP0PJ0AHIjC+N4pKCG9yCDz6LHNPCwi/GKID5pGGF0= github.com/robertkrimen/otto v0.2.1/go.mod h1:UPwtJ1Xu7JrLcZjNWN8orJaM5n5YEtqL//farB5FlRY= +github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM= +github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA= +github.com/samber/slog-multi v1.0.3 h1:8wlX8ioZE38h91DwoJBVnC7JfhgwERwlekY+NHsVsv0= +github.com/samber/slog-multi v1.0.3/go.mod h1:TvwgIK4XPBb8Dn18as5uiTHf7in8gN/AtUXsT57UYuo= github.com/sebdah/goldie/v2 v2.5.3 h1:9ES/mNN+HNUbNWpVAlrzuZ7jE+Nrczbj8uFRjM7624Y= github.com/sebdah/goldie/v2 v2.5.3/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= @@ -63,6 +71,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 h1:3MTrJm4PyNL9NBqvYDSj3DHl46qQakyfqfWo4jgfaEM= +golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=