From ba7cf8cc2ed2d741e058f75eca937fc143e3059a Mon Sep 17 00:00:00 2001 From: "Dr. Gernot Starke" Date: Fri, 2 Feb 2024 09:04:20 +0100 Subject: [PATCH] slack messaging for important events --- go-app/cmd/slack/trySlackMessage.go | 10 +++ go-app/go.mod | 2 + go-app/go.sum | 7 ++ go-app/internal/api/apiGateway.go | 6 ++ go-app/internal/api/modular-stats.gohtml | 72 +++++++++++++++ go-app/internal/slack/slack.go | 110 +++++++++++++++++++++++ go-app/main.go | 3 +- 7 files changed, 209 insertions(+), 1 deletion(-) create mode 100644 go-app/cmd/slack/trySlackMessage.go create mode 100644 go-app/internal/api/modular-stats.gohtml create mode 100644 go-app/internal/slack/slack.go diff --git a/go-app/cmd/slack/trySlackMessage.go b/go-app/cmd/slack/trySlackMessage.go new file mode 100644 index 0000000..5bd2421 --- /dev/null +++ b/go-app/cmd/slack/trySlackMessage.go @@ -0,0 +1,10 @@ +package main + +import ( + "arc42-status/internal/slack" +) + +func main() { + + slack.SendSlackMessage("trying out Slack from Golang@Hotel_Du_Train in Munich") +} diff --git a/go-app/go.mod b/go-app/go.mod index 72860ba..b9312a0 100644 --- a/go-app/go.mod +++ b/go-app/go.mod @@ -16,6 +16,7 @@ require ( github.com/andybalholm/brotli v1.0.5 // indirect github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230512164433-5d1fd1a340c9 // indirect github.com/golang/protobuf v1.5.3 // indirect + github.com/gorilla/websocket v1.4.2 // indirect github.com/klauspost/compress v1.16.3 // indirect github.com/libsql/sqlite-antlr4-parser v0.0.0-20230802215326-5cb5bb604475 // indirect github.com/mattn/go-colorable v0.1.13 // indirect @@ -23,6 +24,7 @@ require ( github.com/mattn/go-sqlite3 v1.14.19 // indirect github.com/patrickmn/go-cache v2.1.0+incompatible // indirect github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466 // indirect + github.com/slack-go/slack v0.12.3 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.50.0 // indirect golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e // indirect diff --git a/go-app/go.sum b/go-app/go.sum index cd71b61..a6bc5b6 100644 --- a/go-app/go.sum +++ b/go-app/go.sum @@ -20,6 +20,7 @@ github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD87 github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY= github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= +github.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0= github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8= @@ -36,11 +37,14 @@ github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiu github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= 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/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= @@ -78,7 +82,10 @@ github.com/shurcooL/githubv4 v0.0.0-20230704064427-599ae7bbf278 h1:kdEGVAV4sO46D github.com/shurcooL/githubv4 v0.0.0-20230704064427-599ae7bbf278/go.mod h1:zqMwyHmnN/eDOZOdiTohqIUKUrTFX62PNlu7IJdu0q8= github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466 h1:17JxqqJY66GmZVHkmAsGEkcIu0oCe3AM420QDgGwZx0= github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466/go.mod h1:9dIRpgIY7hVhoqfe0/FcYp0bpInZaT7dc3BYOprrIUE= +github.com/slack-go/slack v0.12.3 h1:92/dfFU8Q5XP6Wp5rr5/T5JHLM5c5Smtn53fhToAP88= +github.com/slack-go/slack v0.12.3/go.mod h1:hlGi5oXA+Gt+yWTPP0plCdRKmjsDxecdHxYQdlMQKOw= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +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/tursodatabase/libsql-client-go v0.0.0-20231216154754-8383a53d618f h1:teZ0Pj1Wp3Wk0JObKBiKZqgxhYwLeJhVAyj6DRgmQtY= diff --git a/go-app/internal/api/apiGateway.go b/go-app/internal/api/apiGateway.go index b9714f0..9cb72b3 100644 --- a/go-app/internal/api/apiGateway.go +++ b/go-app/internal/api/apiGateway.go @@ -4,7 +4,9 @@ import ( "arc42-status/internal/database" "arc42-status/internal/domain" "arc42-status/internal/fly" + "arc42-status/internal/slack" "embed" + "fmt" "github.com/rs/zerolog/log" "html/template" "net/http" @@ -72,6 +74,10 @@ func statsHTMLTableHandler(w http.ResponseWriter, r *http.Request) { // TODO: include real IP address database.SaveInvocationParams(r.Host, r.RequestURI) + // 4b: inform owner via Slack + msg := fmt.Sprintf("Loaded arc42 statistics in %sms on %s", domain.ArcStats.HowLongDidItTake, time.Now) + slack.SendSlackMessage(msg) + // 5. finally, render the template executeTemplate(w, filepath.Join(TemplatesDir, HtmlTableTmpl), domain.ArcStats) } diff --git a/go-app/internal/api/modular-stats.gohtml b/go-app/internal/api/modular-stats.gohtml new file mode 100644 index 0000000..0f502a3 --- /dev/null +++ b/go-app/internal/api/modular-stats.gohtml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + {{ range .Stats4Site }} + + + + + + + + + + + + {{ end }} + + + + + + + + + + + + + + +
Gopher logo7 Days30 Days12 MonthOpen Tasks
VisitorsPageViewsVisitorsPageViewsVisitorsPageViewsIssuesBugs
{{.Site}}{{ .Visitors7d}}{{ .PageViews7d}}{{ .Visitors30d}}{{ .PageViews30d}}{{ .Visitors12m}}{{ .PageViews12m}} + {{.NrOfOpenIssues}} + + {{if (gt .NrOfOpenBugs 0)}} + {{.NrOfOpenBugs}} + {{end}} +
Totals:{{ .Totals.SumOfVisitors7d}}{{ .Totals.SumOfPageViews7d}}{{ .Totals.SumOfVisitors30d}}{{ .Totals.SumOfPageViews30d}}{{ .Totals.SumOfVisitors12m}}{{ .Totals.SumOfPageViews12m}}{{ .Totals.TotalNrOfIssues}}{{ .Totals.TotalNrOfBugs}}
+ + +
+
+ Data collected in {{ .HowLongDidItTake }} msecs by arc42 statistics service + (v. {{.AppVersion}}) at {{.LastUpdatedString }} + running {{if .FlyRegion}} + on fly.io + in {{.WhereDoesItRun}} (region {{.FlyRegion}}) + {{ else }} + {{.WhereDoesItRun}} + {{ end}} +
diff --git a/go-app/internal/slack/slack.go b/go-app/internal/slack/slack.go new file mode 100644 index 0000000..49e2ce4 --- /dev/null +++ b/go-app/internal/slack/slack.go @@ -0,0 +1,110 @@ +package slack + +import ( + "arc42-status/internal/env" + "fmt" + "github.com/rs/zerolog/log" + "github.com/slack-go/slack" + "os" + "sync" +) + +const SlackTokenName = "SLACK_AUTH_TOKEN" + +// SlackChannelID could be different for PROD and DEV envirionments, +// but is currently identical. +const SlackChannelID = "C06FY002V1D" + +var ( + once sync.Once + slackAPI *slack.Client +) + +// getSlackAuthToken should not be called directly, it is only used by the Singleton GetSlack() +func getSlackAuthToken() string { + slackAuthToken := os.Getenv(SlackTokenName) + if slackAuthToken == "" { + // no value, no slack messages + // we exit here, as we have no chance of recovery + log.Error().Msgf("CRITICAL ERROR: required Slack Auth token not set.\n" + + "You need to set the 'SLACK_AUTH_TOKEN' environment variable prior to launching this application.\n") + os.Exit(14) + } + return slackAuthToken +} + +// checkIfTokenValid checks if the slackAuthToken used to create slackClient +// is valid and has not been revoked +func checkIfTokenValid(api *slack.Client) error { + + _, err := api.AuthTest() + if err != nil { + log.Info().Msgf("Token validation failed: %v\n", err) + return err + } + return nil +} + +// getSlackAPI() returns a Slack API connection if there is a valid auth token available. +// In case of errors, it returns nil +func getSlackAPI() *slack.Client { + once.Do(func() { + var ( + slackClient *slack.Client + slackError error + ) + + switch env.GetEnv() { + case "PROD": + { + slackClient = slack.New(getSlackAuthToken()) + break + } + case "DEV", "TEST": + { + slackClient = slack.New(getSlackAuthToken(), slack.OptionDebug(true)) + + break + } + default: + { + // this should never happen, as env.GetEnv() needs to care for valid environments + log.Error().Msgf("Invalid environment %s specified for slack messages", env.GetEnv()) + os.Exit(14) + } + } + + slackError = checkIfTokenValid(slackClient) + + if slackError != nil { + log.Error().Msgf("Failed to init communication with Slack: %s", slackError) + slackClient = nil + } + slackAPI = slackClient + }) + + return slackAPI + +} + +// SendSlackMessage sends the given message to the configured slack channel +// Different environments (PROD, DEV, TEST) might have different channels configured +func SendSlackMessage(msg string) { + api := getSlackAPI() + + if api != nil { + channelID, timestamp, err := api.PostMessage( + SlackChannelID, + slack.MsgOptionText(msg, false), + ) + if err != nil { + log.Error().Msgf("Error sending message to Slack: %s", err) + } else { + + fmt.Printf("Message successfully sent to channel %s at %s", channelID, timestamp) + } + } else { // api == nil + log.Error().Msgf("Could not sent message %s to slack, invalid api token", msg) + } + +} diff --git a/go-app/main.go b/go-app/main.go index d3b4aeb..a30917d 100644 --- a/go-app/main.go +++ b/go-app/main.go @@ -11,7 +11,7 @@ import ( "time" ) -const appVersion = "0.5.5" +const appVersion = "0.5.6" // version history // 0.5.x rate limit: limit amount of queries to external APIs @@ -19,6 +19,7 @@ const appVersion = "0.5.5" // 0.5.3: BUG and BUGS are both recognized // 0.5.4: start with empty table on homepage // 0.5.5: caching with zcache +// 0.5.6: send slack message for important system events, starting with data acquisition // 0.4.7 replace most inline styles by css // 0.4.6 sortable table (a: initial, b...e: fix layout issues), f: fix #94 // 0.4.5 fix missing separators in large numbers