diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..1121932 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,14 @@ +version: 2 + +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + reviewers: [ "SkYNewZ" ] + + - package-ecosystem: "gomod" + directory: "/" + schedule: + interval: "weekly" + reviewers: [ "SkYNewZ" ] diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml new file mode 100644 index 0000000..aaabf85 --- /dev/null +++ b/.github/workflows/lint.yaml @@ -0,0 +1,25 @@ +name: Lint + +on: + push: + pull_request: + +permissions: + contents: read + +jobs: + golangci: + name: lint + runs-on: ubuntu-latest + steps: + - name: Setup Go + uses: actions/setup-go@v4 + with: + go-version: 1.21 + check-latest: true + + - name: Checkout code + uses: actions/checkout@v4 + + - name: golangci-lint + uses: golangci/golangci-lint-action@v3 \ No newline at end of file diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..0879946 --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,40 @@ +name: Release + +permissions: + contents: write + +on: + workflow_dispatch: + inputs: + semver: + type: string + description: 'Semver (eg: v1.2.3)' + required: true + +jobs: + release: + if: github.triggering_actor == 'SkYNewZ' # Prevent release from forks + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: rickstaa/action-create-tag@v1 + with: + tag: ${{ inputs.semver }} + force_push_tag: true + tag_exists_error: false + message: ${{ inputs.semver }} + + - name: Build changelog + uses: mikepenz/release-changelog-builder-action@v4 + id: changelog + + - name: Release + uses: softprops/action-gh-release@v1 + with: + name: ${{ inputs.semver }} + tag_name: ${{ inputs.semver }} + body: ${{ steps.changelog.outputs.changelog }} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3b735ec --- /dev/null +++ b/.gitignore @@ -0,0 +1,21 @@ +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e2f7d18 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Quentin Lemaire + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..f82711a --- /dev/null +++ b/README.md @@ -0,0 +1,83 @@ +# slog: Pushover handler + +[![tag](https://img.shields.io/github/tag/skynewz/slog-pushover.svg)](https://github.com/skynewz/slog-pushover/releases) +![Go Version](https://img.shields.io/badge/Go-%3E%3D%201.21-%23007d9c) +[![GoDoc](https://pkg.go.dev/badge/github.com/skynewz/slog-pushover)](https://pkg.go.dev/github.com/skynewz/slog-pushover) +[![Go report](https://goreportcard.com/badge/github.com/skynewz/slog-pushover)](https://goreportcard.com/report/github.com/skynewz/slog-pushover) +[![License](https://img.shields.io/github/license/skynewz/slog-pushover)](./LICENSE) + +A [Pushover](https://pushover.net/) Handler for [slog](https://pkg.go.dev/log/slog) Go library. Inspired +from [samber](https://github.com/samber)' +s [slog repositories](https://github.com/samber?tab=repositories&q=slog&type=source&language=go&sort=name) + +## 🚀 Install + +```sh +go get github.com/SkYNewZ/slog-pushover +``` + +**Compatibility**: go >= 1.21 + +## 💡 Usage + +GoDoc: [https://pkg.go.dev/github.com/skynewz/slog-pushover](https://pkg.go.dev/github.com/skynewz/slog-pushover) + +### Handler options + +```go +type Options struct { + Level slog.Leveler // minimum level of messages to log (default: slog.LevelDebug) + + Token string // Pushover application token + Recipient string // Pushover user/group key + + Message *pushover.Message // optional: customize Pushover message. 'Message' will be replaced by the log message + Converter Converter // optional: customize Pushover message builder + + // optional: see slog.HandlerOptions + AddSource bool + ReplaceAttr func(groups []string, a slog.Attr) slog.Attr +} +``` + +### Example + +```go +package main + +func main() { + handler := slogpushover.NewHandler(&slogpushover.Options{ + Level: slog.LevelDebug, + Token: os.Getenv("PUSHOVER_TOKEN"), + Recipient: os.Getenv("PUSHOVER_RECIPIENT"), + Message: nil, // You can customize the message details + Converter: nil, + AddSource: true, + ReplaceAttr: nil, + }) + + logger := slog.New(handler) + logger = logger.With("release", "v1.0.0") + + logger. + With( + slog.Group("user", + slog.String("id", "user-123"), + slog.Time("created_at", time.Now().AddDate(0, 0, -1)), + ), + ). + With("environment", "dev"). + With("error", fmt.Errorf("an error")). + Error("A message") +} +``` + +## 👤 Contributors + +![Contributors](https://contrib.rocks/image?repo=skynewz/slog-pushover) + +## 📝 License + +Copyright © 2023 [Quentin Lemaire](https://github.com/SkYNewZ). + +This project is [MIT](./LICENSE) licensed. \ No newline at end of file diff --git a/converter.go b/converter.go new file mode 100644 index 0000000..d714d55 --- /dev/null +++ b/converter.go @@ -0,0 +1,41 @@ +package slogpushover + +import ( + "fmt" + "log/slog" + + slogcommon "github.com/samber/slog-common" +) + +// Converter is a function that converts a slog.Record to a string. +type Converter func(addSource bool, replaceAttr func(groups []string, a slog.Attr) slog.Attr, loggerAttr []slog.Attr, groups []string, record *slog.Record) string + +// DefaultConverter is the default Converter used by Handler. +func DefaultConverter(addSource bool, replaceAttr func(groups []string, a slog.Attr) slog.Attr, loggerAttr []slog.Attr, groups []string, record *slog.Record) string { + attrs := slogcommon.AppendRecordAttrsToAttrs(loggerAttr, groups, record) // aggregate all attributes + attrs = slogcommon.ReplaceAttrs(replaceAttr, []string{}, attrs...) // developer formatters + + // handler formatter + message := fmt.Sprintf("%s\n------------\n\n", record.Message) + message += attrToPushoverMessage("", attrs) + return message +} + +func attrToPushoverMessage(base string, attrs []slog.Attr) string { + message := "" + + for i := range attrs { + attr := attrs[i] + k := base + attr.Key + v := attr.Value + kind := attr.Value.Kind() + + if kind == slog.KindGroup { + message += attrToPushoverMessage(k+".", v.Group()) + } else { + message += fmt.Sprintf("%s: %s\n", k, slogcommon.ValueToString(v)) + } + } + + return message +} diff --git a/example_test.go b/example_test.go new file mode 100644 index 0000000..dd75ba5 --- /dev/null +++ b/example_test.go @@ -0,0 +1,69 @@ +package slogpushover_test + +import ( + "fmt" + "log/slog" + "os" + "time" + + "github.com/gregdel/pushover" + + slogpushover "github.com/SkYNewZ/slog-pushover" +) + +func Example() { + handler := slogpushover.NewHandler(&slogpushover.Options{ + Level: slog.LevelDebug, + Token: os.Getenv("PUSHOVER_TOKEN"), + Recipient: os.Getenv("PUSHOVER_RECIPIENT"), + Message: nil, + Converter: nil, + AddSource: true, + ReplaceAttr: nil, + }) + + logger := slog.New(handler) + logger = logger.With("release", "v1.0.0") + + logger. + With( + slog.Group("user", + slog.String("id", "user-123"), + slog.Time("created_at", time.Now().AddDate(0, 0, -1)), + ), + ). + With("environment", "dev"). + With("error", fmt.Errorf("an error")). + Error("A message") + + // Output: +} + +// nolint:govet +func ExampleTitle() { + handler := slogpushover.NewHandler(&slogpushover.Options{ + Level: slog.LevelDebug, + Token: os.Getenv("PUSHOVER_TOKEN"), + Recipient: os.Getenv("PUSHOVER_RECIPIENT"), + Message: &pushover.Message{Title: "My App"}, + Converter: nil, + AddSource: true, + ReplaceAttr: nil, + }) + + logger := slog.New(handler) + logger = logger.With("release", "v1.0.0") + + logger. + With( + slog.Group("user", + slog.String("id", "user-123"), + slog.Time("created_at", time.Now().AddDate(0, 0, -1)), + ), + ). + With("environment", "dev"). + With("error", fmt.Errorf("an error")). + Error("A message") + + // Output: +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..ab2cf09 --- /dev/null +++ b/go.mod @@ -0,0 +1,13 @@ +module github.com/SkYNewZ/slog-pushover + +go 1.21.4 + +require ( + github.com/gregdel/pushover v1.3.0 + github.com/samber/slog-common v0.13.0 +) + +require ( + github.com/samber/lo v1.38.1 // indirect + golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..3eb00d1 --- /dev/null +++ b/go.sum @@ -0,0 +1,8 @@ +github.com/gregdel/pushover v1.3.0 h1:CewbxqsThoN/1imgwkDKFkRkltaQMoyBV0K9IquQLtw= +github.com/gregdel/pushover v1.3.0/go.mod h1:EcaO66Nn1StkpEm1iKtBTV3d2A16SoMsVER1PthX7to= +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-common v0.13.0 h1:WfUdlqs5l6juZ8y0cJkRXfY4VIXcvwunbmkA/Z2Qv0w= +github.com/samber/slog-common v0.13.0/go.mod h1:Qjrfhwk79XiCIhBj8+jTq1Cr0u9rlWbjawh3dWXzaHk= +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= diff --git a/handler.go b/handler.go new file mode 100644 index 0000000..edba08e --- /dev/null +++ b/handler.go @@ -0,0 +1,115 @@ +package slogpushover + +import ( + "context" + "log/slog" + + "github.com/gregdel/pushover" + slogcommon "github.com/samber/slog-common" +) + +type Options struct { + Level slog.Leveler // minimum level of messages to log (default: slog.LevelDebug) + + Token string // Pushover application token + Recipient string // Pushover user/group key + + Message *pushover.Message // optional: customize Pushover message. 'Message' will be replaced by the log message + Converter Converter // optional: customize Pushover message builder + + // optional: see slog.HandlerOptions + AddSource bool + ReplaceAttr func(groups []string, a slog.Attr) slog.Attr +} + +// NewHandler creates a new slog.Handler that sends messages to Pushover. +func NewHandler(opts *Options) slog.Handler { + if opts == nil { + opts = &Options{} + } + + if opts.Level == nil { + opts.Level = slog.LevelDebug + } + + if opts.Token == "" { + panic("missing Pushover token") + } + + if opts.Recipient == "" { + panic("missing Pushover recipient") + } + + if opts.Message == nil { + opts.Message = &pushover.Message{} + } + + if opts.Converter == nil { + opts.Converter = DefaultConverter + } + + return &handler{ + options: opts, + client: pushover.New(opts.Token), + recipient: pushover.NewRecipient(opts.Recipient), + message: opts.Message, + attrs: make([]slog.Attr, 0), + groups: make([]string, 0), + } +} + +type handler struct { + options *Options + client *pushover.Pushover + recipient *pushover.Recipient + message *pushover.Message + attrs []slog.Attr + groups []string +} + +func (h *handler) Enabled(_ context.Context, level slog.Level) bool { + return level >= h.options.Level.Level() +} + +func (h *handler) Handle(_ context.Context, record slog.Record) error { + message := h.options.Converter(h.options.AddSource, h.options.ReplaceAttr, h.attrs, h.groups, &record) + msg := &pushover.Message{ + Message: message, + Title: h.message.Title, + Priority: h.message.Priority, + URL: h.message.URL, + URLTitle: h.message.URLTitle, + Timestamp: h.message.Timestamp, + Retry: h.message.Retry, + Expire: h.message.Expire, + CallbackURL: h.message.CallbackURL, + DeviceName: h.message.DeviceName, + Sound: h.message.Sound, + HTML: h.message.HTML, + Monospace: h.message.Monospace, + TTL: h.message.TTL, + } + + _, err := h.client.SendMessage(msg, h.recipient) + return err +} + +func (h *handler) WithAttrs(attrs []slog.Attr) slog.Handler { + return &handler{ + options: h.options, + client: h.client, + recipient: h.recipient, + message: h.message, + attrs: slogcommon.AppendAttrsToGroup(h.groups, h.attrs, attrs...), + groups: h.groups, + } +} + +func (h *handler) WithGroup(name string) slog.Handler { + return &handler{ + options: h.options, + client: h.client, + attrs: h.attrs, + groups: append(h.groups, name), + } +}