mirror of
https://github.com/yusing/godoxy.git
synced 2025-05-19 20:32:35 +02:00
refactor: notifications
This commit is contained in:
parent
28d9a72908
commit
69ee8495d8
11 changed files with 202 additions and 104 deletions
|
@ -10,9 +10,10 @@ import (
|
|||
)
|
||||
|
||||
type ProviderBase struct {
|
||||
Name string `json:"name" validate:"required"`
|
||||
URL string `json:"url" validate:"url"`
|
||||
Token string `json:"token"`
|
||||
Name string `json:"name" validate:"required"`
|
||||
URL string `json:"url" validate:"url"`
|
||||
Token string `json:"token"`
|
||||
Format *LogFormat `json:"format"`
|
||||
}
|
||||
|
||||
var (
|
||||
|
@ -22,8 +23,8 @@ var (
|
|||
|
||||
// Validate implements the utils.CustomValidator interface.
|
||||
func (base *ProviderBase) Validate() gperr.Error {
|
||||
if base.Token == "" {
|
||||
return ErrMissingToken
|
||||
if base.Format == nil {
|
||||
base.Format = LogFormatMarkdown
|
||||
}
|
||||
if !strings.HasPrefix(base.URL, "http://") && !strings.HasPrefix(base.URL, "https://") {
|
||||
return ErrURLMissingScheme
|
||||
|
|
111
internal/notif/body.go
Normal file
111
internal/notif/body.go
Normal file
|
@ -0,0 +1,111 @@
|
|||
package notif
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/yusing/go-proxy/internal/gperr"
|
||||
)
|
||||
|
||||
type (
|
||||
LogField struct {
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
LogFormat struct {
|
||||
string
|
||||
}
|
||||
LogBody interface {
|
||||
Format(format *LogFormat) ([]byte, error)
|
||||
}
|
||||
)
|
||||
|
||||
type (
|
||||
FieldsBody []LogField
|
||||
ListBody []string
|
||||
MessageBody string
|
||||
)
|
||||
|
||||
var (
|
||||
LogFormatMarkdown = &LogFormat{"markdown"}
|
||||
LogFormatPlain = &LogFormat{"plain"}
|
||||
LogFormatRawJSON = &LogFormat{"json"} // internal use only
|
||||
)
|
||||
|
||||
func MakeLogFields(fields ...LogField) LogBody {
|
||||
return FieldsBody(fields)
|
||||
}
|
||||
|
||||
func (f *LogFormat) Parse(format string) error {
|
||||
switch format {
|
||||
case "":
|
||||
f.string = LogFormatMarkdown.string
|
||||
case LogFormatPlain.string, LogFormatMarkdown.string:
|
||||
f.string = format
|
||||
default:
|
||||
return gperr.Multiline().
|
||||
Addf("invalid log format %s, supported formats:", format).
|
||||
AddLines(
|
||||
LogFormatPlain,
|
||||
LogFormatMarkdown,
|
||||
)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f FieldsBody) Format(format *LogFormat) ([]byte, error) {
|
||||
switch format {
|
||||
case LogFormatMarkdown:
|
||||
var msg bytes.Buffer
|
||||
for _, field := range f {
|
||||
msg.WriteString("#### ")
|
||||
msg.WriteString(field.Name)
|
||||
msg.WriteRune('\n')
|
||||
msg.WriteString(field.Value)
|
||||
msg.WriteRune('\n')
|
||||
}
|
||||
return msg.Bytes(), nil
|
||||
case LogFormatPlain:
|
||||
var msg bytes.Buffer
|
||||
for _, field := range f {
|
||||
msg.WriteString(field.Name)
|
||||
msg.WriteString(": ")
|
||||
msg.WriteString(field.Value)
|
||||
msg.WriteRune('\n')
|
||||
}
|
||||
return msg.Bytes(), nil
|
||||
case LogFormatRawJSON:
|
||||
return json.Marshal(f)
|
||||
}
|
||||
return nil, fmt.Errorf("unknown format: %v", format)
|
||||
}
|
||||
|
||||
func (l ListBody) Format(format *LogFormat) ([]byte, error) {
|
||||
switch format {
|
||||
case LogFormatPlain:
|
||||
return []byte(strings.Join(l, "\n")), nil
|
||||
case LogFormatMarkdown:
|
||||
var msg bytes.Buffer
|
||||
for _, item := range l {
|
||||
msg.WriteString("* ")
|
||||
msg.WriteString(item)
|
||||
msg.WriteRune('\n')
|
||||
}
|
||||
return msg.Bytes(), nil
|
||||
case LogFormatRawJSON:
|
||||
return json.Marshal(l)
|
||||
}
|
||||
return nil, fmt.Errorf("unknown format: %v", format)
|
||||
}
|
||||
|
||||
func (m MessageBody) Format(format *LogFormat) ([]byte, error) {
|
||||
switch format {
|
||||
case LogFormatPlain, LogFormatMarkdown:
|
||||
return []byte(m), nil
|
||||
case LogFormatRawJSON:
|
||||
return json.Marshal(m)
|
||||
}
|
||||
return nil, fmt.Errorf("unknown format: %v", format)
|
||||
}
|
|
@ -46,11 +46,5 @@ func (cfg *NotificationConfig) UnmarshalMap(m map[string]any) (err gperr.Error)
|
|||
Withf("expect %s or %s", ProviderWebhook, ProviderGotify)
|
||||
}
|
||||
|
||||
// unmarshal provider config
|
||||
if err := utils.MapUnmarshalValidate(m, cfg.Provider); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// validate provider
|
||||
return cfg.Provider.Validate()
|
||||
return utils.MapUnmarshalValidate(m, cfg.Provider)
|
||||
}
|
||||
|
|
|
@ -25,8 +25,9 @@ func TestNotificationConfig(t *testing.T) {
|
|||
},
|
||||
expected: &Webhook{
|
||||
ProviderBase: ProviderBase{
|
||||
Name: "test",
|
||||
URL: "https://example.com",
|
||||
Name: "test",
|
||||
URL: "https://example.com",
|
||||
Format: LogFormatMarkdown,
|
||||
},
|
||||
Template: "discord",
|
||||
Method: http.MethodPost,
|
||||
|
@ -43,12 +44,32 @@ func TestNotificationConfig(t *testing.T) {
|
|||
"provider": "gotify",
|
||||
"url": "https://example.com",
|
||||
"token": "token",
|
||||
"format": "plain",
|
||||
},
|
||||
expected: &GotifyClient{
|
||||
ProviderBase: ProviderBase{
|
||||
Name: "test",
|
||||
URL: "https://example.com",
|
||||
Token: "token",
|
||||
Name: "test",
|
||||
URL: "https://example.com",
|
||||
Token: "token",
|
||||
Format: LogFormatPlain,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "default_format",
|
||||
cfg: map[string]any{
|
||||
"name": "test",
|
||||
"provider": "gotify",
|
||||
"token": "token",
|
||||
"url": "https://example.com",
|
||||
},
|
||||
expected: &GotifyClient{
|
||||
ProviderBase: ProviderBase{
|
||||
Name: "test",
|
||||
URL: "https://example.com",
|
||||
Token: "token",
|
||||
Format: LogFormatMarkdown,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
|
@ -62,6 +83,16 @@ func TestNotificationConfig(t *testing.T) {
|
|||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid_format",
|
||||
cfg: map[string]any{
|
||||
"name": "test",
|
||||
"provider": "webhook",
|
||||
"url": "https://example.com",
|
||||
"format": "invalid",
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "missing_url",
|
||||
cfg: map[string]any{
|
||||
|
|
|
@ -14,16 +14,11 @@ type (
|
|||
logCh chan *LogMessage
|
||||
providers F.Set[Provider]
|
||||
}
|
||||
LogField struct {
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
LogFields []LogField
|
||||
LogMessage struct {
|
||||
Level zerolog.Level
|
||||
Title string
|
||||
Extras LogFields
|
||||
Color Color
|
||||
Level zerolog.Level
|
||||
Title string
|
||||
Body LogBody
|
||||
Color Color
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -53,7 +48,7 @@ func Notify(msg *LogMessage) {
|
|||
}
|
||||
}
|
||||
|
||||
func (f *LogFields) Add(name, value string) {
|
||||
func (f *FieldsBody) Add(name, value string) {
|
||||
*f = append(*f, LogField{Name: name, Value: value})
|
||||
}
|
||||
|
||||
|
|
|
@ -1,26 +0,0 @@
|
|||
package notif
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
func formatMarkdown(extras LogFields) string {
|
||||
msg := bytes.NewBufferString("")
|
||||
for _, field := range extras {
|
||||
msg.WriteString("#### ")
|
||||
msg.WriteString(field.Name)
|
||||
msg.WriteRune('\n')
|
||||
msg.WriteString(field.Value)
|
||||
msg.WriteRune('\n')
|
||||
}
|
||||
return msg.String()
|
||||
}
|
||||
|
||||
func formatDiscord(extras LogFields) (string, error) {
|
||||
fields, err := json.Marshal(extras)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(fields), nil
|
||||
}
|
|
@ -1,10 +1,8 @@
|
|||
package notif
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/gotify/server/v2/model"
|
||||
|
@ -24,8 +22,8 @@ func (client *GotifyClient) GetURL() string {
|
|||
return client.URL + gotifyMsgEndpoint
|
||||
}
|
||||
|
||||
// MakeBody implements Provider.
|
||||
func (client *GotifyClient) MakeBody(logMsg *LogMessage) (io.Reader, error) {
|
||||
// MarshalMessage implements Provider.
|
||||
func (client *GotifyClient) MarshalMessage(logMsg *LogMessage) ([]byte, error) {
|
||||
var priority int
|
||||
|
||||
switch logMsg.Level {
|
||||
|
@ -37,15 +35,23 @@ func (client *GotifyClient) MakeBody(logMsg *LogMessage) (io.Reader, error) {
|
|||
priority = 8
|
||||
}
|
||||
|
||||
body, err := logMsg.Body.Format(client.Format)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
msg := &GotifyMessage{
|
||||
Title: logMsg.Title,
|
||||
Message: formatMarkdown(logMsg.Extras),
|
||||
Message: string(body),
|
||||
Priority: &priority,
|
||||
Extras: map[string]interface{}{
|
||||
}
|
||||
|
||||
if client.Format == LogFormatMarkdown {
|
||||
msg.Extras = map[string]interface{}{
|
||||
"client::display": map[string]string{
|
||||
"contentType": "text/markdown",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
data, err := json.Marshal(msg)
|
||||
|
@ -53,7 +59,7 @@ func (client *GotifyClient) MakeBody(logMsg *LogMessage) (io.Reader, error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
return bytes.NewReader(data), nil
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// makeRespError implements Provider.
|
||||
|
|
|
@ -1,10 +1,7 @@
|
|||
package notif
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/yusing/go-proxy/internal/gperr"
|
||||
|
@ -13,18 +10,14 @@ import (
|
|||
// See https://docs.ntfy.sh/publish
|
||||
type Ntfy struct {
|
||||
ProviderBase
|
||||
Topic string `json:"topic"`
|
||||
Style NtfyStyle `json:"style"`
|
||||
Topic string `json:"topic"`
|
||||
}
|
||||
|
||||
type NtfyStyle string
|
||||
|
||||
const (
|
||||
NtfyStyleMarkdown NtfyStyle = "markdown"
|
||||
NtfyStylePlain NtfyStyle = "plain"
|
||||
)
|
||||
|
||||
// Validate implements the utils.CustomValidator interface.
|
||||
func (n *Ntfy) Validate() gperr.Error {
|
||||
if err := n.ProviderBase.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
if n.URL == "" {
|
||||
return gperr.New("url is required")
|
||||
}
|
||||
|
@ -34,16 +27,10 @@ func (n *Ntfy) Validate() gperr.Error {
|
|||
if n.Topic[0] == '/' {
|
||||
return gperr.New("topic should not start with a slash")
|
||||
}
|
||||
switch n.Style {
|
||||
case "":
|
||||
n.Style = NtfyStyleMarkdown
|
||||
case NtfyStyleMarkdown, NtfyStylePlain:
|
||||
default:
|
||||
return gperr.Errorf("invalid style, expecting %q or %q, got %q", NtfyStyleMarkdown, NtfyStylePlain, n.Style)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetURL implements Provider.
|
||||
func (n *Ntfy) GetURL() string {
|
||||
if n.URL[len(n.URL)-1] == '/' {
|
||||
return n.URL + n.Topic
|
||||
|
@ -51,23 +38,22 @@ func (n *Ntfy) GetURL() string {
|
|||
return n.URL + "/" + n.Topic
|
||||
}
|
||||
|
||||
// GetMIMEType implements Provider.
|
||||
func (n *Ntfy) GetMIMEType() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// GetToken implements Provider.
|
||||
func (n *Ntfy) GetToken() string {
|
||||
return n.Token
|
||||
}
|
||||
|
||||
func (n *Ntfy) MakeBody(logMsg *LogMessage) (io.Reader, error) {
|
||||
switch n.Style {
|
||||
case NtfyStyleMarkdown:
|
||||
return strings.NewReader(formatMarkdown(logMsg.Extras)), nil
|
||||
default:
|
||||
return &bytes.Buffer{}, nil
|
||||
}
|
||||
// MarshalMessage implements Provider.
|
||||
func (n *Ntfy) MarshalMessage(logMsg *LogMessage) ([]byte, error) {
|
||||
return logMsg.Body.Format(n.Format)
|
||||
}
|
||||
|
||||
// SetHeaders implements Provider.
|
||||
func (n *Ntfy) SetHeaders(logMsg *LogMessage, headers http.Header) {
|
||||
headers.Set("Title", logMsg.Title)
|
||||
|
||||
|
@ -83,7 +69,7 @@ func (n *Ntfy) SetHeaders(logMsg *LogMessage, headers http.Header) {
|
|||
headers.Set("Priority", "min")
|
||||
}
|
||||
|
||||
if n.Style == NtfyStyleMarkdown {
|
||||
if n.Format == LogFormatMarkdown {
|
||||
headers.Set("Markdown", "yes")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
package notif
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
|
@ -21,7 +21,7 @@ type (
|
|||
GetMethod() string
|
||||
GetMIMEType() string
|
||||
|
||||
MakeBody(logMsg *LogMessage) (io.Reader, error)
|
||||
MarshalMessage(logMsg *LogMessage) ([]byte, error)
|
||||
SetHeaders(logMsg *LogMessage, headers http.Header)
|
||||
|
||||
makeRespError(resp *http.Response) error
|
||||
|
@ -37,7 +37,7 @@ const (
|
|||
)
|
||||
|
||||
func notifyProvider(ctx context.Context, provider Provider, msg *LogMessage) error {
|
||||
body, err := provider.MakeBody(msg)
|
||||
body, err := provider.MarshalMessage(msg)
|
||||
if err != nil {
|
||||
return gperr.PrependSubject(provider.GetName(), err)
|
||||
}
|
||||
|
@ -49,7 +49,7 @@ func notifyProvider(ctx context.Context, provider Provider, msg *LogMessage) err
|
|||
ctx,
|
||||
http.MethodPost,
|
||||
provider.GetURL(),
|
||||
body,
|
||||
bytes.NewReader(body),
|
||||
)
|
||||
if err != nil {
|
||||
return gperr.PrependSubject(provider.GetName(), err)
|
||||
|
|
|
@ -100,12 +100,12 @@ func (webhook *Webhook) makeRespError(resp *http.Response) error {
|
|||
return fmt.Errorf("%s status %d", webhook.Name, resp.StatusCode)
|
||||
}
|
||||
|
||||
func (webhook *Webhook) MakeBody(logMsg *LogMessage) (io.Reader, error) {
|
||||
func (webhook *Webhook) MarshalMessage(logMsg *LogMessage) ([]byte, error) {
|
||||
title, err := json.Marshal(logMsg.Title)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fields, err := formatDiscord(logMsg.Extras)
|
||||
fields, err := logMsg.Body.Format(LogFormatRawJSON)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -115,14 +115,14 @@ func (webhook *Webhook) MakeBody(logMsg *LogMessage) (io.Reader, error) {
|
|||
} else {
|
||||
color = logMsg.Color.DecString()
|
||||
}
|
||||
message, err := json.Marshal(formatMarkdown(logMsg.Extras))
|
||||
message, err := logMsg.Body.Format(LogFormatMarkdown)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
plTempl := strings.NewReplacer(
|
||||
"$title", string(title),
|
||||
"$message", string(message),
|
||||
"$fields", fields,
|
||||
"$fields", string(fields),
|
||||
"$color", color,
|
||||
)
|
||||
var pl string
|
||||
|
@ -132,5 +132,5 @@ func (webhook *Webhook) MakeBody(logMsg *LogMessage) (io.Reader, error) {
|
|||
pl = webhook.Payload
|
||||
}
|
||||
pl = plTempl.Replace(pl)
|
||||
return strings.NewReader(pl), nil
|
||||
return []byte(pl), nil
|
||||
}
|
||||
|
|
|
@ -222,7 +222,7 @@ func (mon *monitor) checkUpdateHealth() error {
|
|||
status = health.StatusUnhealthy
|
||||
}
|
||||
if result.Healthy != (mon.status.Swap(status) == health.StatusHealthy) {
|
||||
extras := notif.LogFields{
|
||||
extras := notif.FieldsBody{
|
||||
{Name: "Service Name", Value: mon.service},
|
||||
{Name: "Time", Value: strutils.FormatTime(time.Now())},
|
||||
}
|
||||
|
@ -239,16 +239,16 @@ func (mon *monitor) checkUpdateHealth() error {
|
|||
logger.Info().Msg("service is up")
|
||||
extras.Add("Ping", fmt.Sprintf("%d ms", result.Latency.Milliseconds()))
|
||||
notif.Notify(¬if.LogMessage{
|
||||
Title: "✅ Service is up ✅",
|
||||
Extras: extras,
|
||||
Color: notif.ColorSuccess,
|
||||
Title: "✅ Service is up ✅",
|
||||
Body: extras,
|
||||
Color: notif.ColorSuccess,
|
||||
})
|
||||
} else {
|
||||
logger.Warn().Msg("service went down")
|
||||
notif.Notify(¬if.LogMessage{
|
||||
Title: "❌ Service went down ❌",
|
||||
Extras: extras,
|
||||
Color: notif.ColorError,
|
||||
Title: "❌ Service went down ❌",
|
||||
Body: extras,
|
||||
Color: notif.ColorError,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue