mirror of
https://github.com/yusing/godoxy.git
synced 2025-05-20 04:42:33 +02:00
Feat/ntfy (#57)
* implement ntfy notification * fix notification fields order * fix schema for ntfy --------- Co-authored-by: yusing <yusing@6uo.me>
This commit is contained in:
parent
c16a0444ca
commit
78900772bb
17 changed files with 171 additions and 44 deletions
|
@ -1,6 +1,7 @@
|
|||
package notif
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
@ -54,3 +55,15 @@ func (base *ProviderBase) GetMethod() string {
|
|||
func (base *ProviderBase) GetMIMEType() string {
|
||||
return "application/json"
|
||||
}
|
||||
|
||||
func (base *ProviderBase) SetHeaders(logMsg *LogMessage, headers http.Header) {
|
||||
// no-op by default
|
||||
}
|
||||
|
||||
func (base *ProviderBase) makeRespError(resp *http.Response) error {
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err == nil {
|
||||
return E.Errorf("%s status %d: %s", base.Name, resp.StatusCode, body)
|
||||
}
|
||||
return E.Errorf("%s status %d", base.Name, resp.StatusCode)
|
||||
}
|
||||
|
|
|
@ -5,9 +5,9 @@ import "fmt"
|
|||
type Color uint
|
||||
|
||||
const (
|
||||
Red Color = 0xff0000
|
||||
Green Color = 0x00ff00
|
||||
Blue Color = 0x0000ff
|
||||
ColorError Color = 0xff0000
|
||||
ColorSuccess Color = 0x00ff00
|
||||
ColorInfo Color = 0x0000ff
|
||||
)
|
||||
|
||||
func (c Color) HexString() string {
|
||||
|
|
|
@ -38,6 +38,8 @@ func (cfg *NotificationConfig) UnmarshalMap(m map[string]any) (err E.Error) {
|
|||
cfg.Provider = &Webhook{}
|
||||
case ProviderGotify:
|
||||
cfg.Provider = &GotifyClient{}
|
||||
case ProviderNtfy:
|
||||
cfg.Provider = &Ntfy{}
|
||||
default:
|
||||
return ErrUnknownNotifProvider.
|
||||
Subject(cfg.ProviderName).
|
||||
|
|
|
@ -14,10 +14,15 @@ 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 map[string]any
|
||||
Extras LogFields
|
||||
Color Color
|
||||
}
|
||||
)
|
||||
|
@ -48,6 +53,10 @@ func Notify(msg *LogMessage) {
|
|||
}
|
||||
}
|
||||
|
||||
func (f *LogFields) Add(name, value string) {
|
||||
*f = append(*f, LogField{Name: name, Value: value})
|
||||
}
|
||||
|
||||
func (disp *Dispatcher) RegisterProvider(cfg *NotificationConfig) {
|
||||
disp.providers.Add(cfg.Provider)
|
||||
}
|
||||
|
|
|
@ -3,32 +3,22 @@ package notif
|
|||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func formatMarkdown(extras map[string]interface{}) string {
|
||||
func formatMarkdown(extras LogFields) string {
|
||||
msg := bytes.NewBufferString("")
|
||||
for k, v := range extras {
|
||||
for _, field := range extras {
|
||||
msg.WriteString("#### ")
|
||||
msg.WriteString(k)
|
||||
msg.WriteString(field.Name)
|
||||
msg.WriteRune('\n')
|
||||
msg.WriteString(fmt.Sprintf("%v", v))
|
||||
msg.WriteString(field.Value)
|
||||
msg.WriteRune('\n')
|
||||
}
|
||||
return msg.String()
|
||||
}
|
||||
|
||||
func formatDiscord(extras map[string]interface{}) (string, error) {
|
||||
fieldsMap := make([]map[string]any, len(extras))
|
||||
i := 0
|
||||
for k, extra := range extras {
|
||||
fieldsMap[i] = map[string]any{
|
||||
"name": k,
|
||||
"value": extra,
|
||||
}
|
||||
i++
|
||||
}
|
||||
fields, err := json.Marshal(fieldsMap)
|
||||
func formatDiscord(extras LogFields) (string, error) {
|
||||
fields, err := json.Marshal(extras)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
|
|
@ -61,7 +61,7 @@ func (client *GotifyClient) makeRespError(resp *http.Response) error {
|
|||
var errm model.Error
|
||||
err := json.NewDecoder(resp.Body).Decode(&errm)
|
||||
if err != nil {
|
||||
return fmt.Errorf(ProviderGotify+" status %d, but failed to decode err response: %w", resp.StatusCode, err)
|
||||
return fmt.Errorf("%s status %d, but failed to decode err response: %w", client.Name, resp.StatusCode, err)
|
||||
}
|
||||
return fmt.Errorf(ProviderGotify+" status %d %s: %s", resp.StatusCode, errm.Error, errm.ErrorDescription)
|
||||
return fmt.Errorf("%s status %d %s: %s", client.Name, resp.StatusCode, errm.Error, errm.ErrorDescription)
|
||||
}
|
||||
|
|
89
internal/notif/ntfy.go
Normal file
89
internal/notif/ntfy.go
Normal file
|
@ -0,0 +1,89 @@
|
|||
package notif
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
)
|
||||
|
||||
// See https://docs.ntfy.sh/publish
|
||||
type Ntfy struct {
|
||||
ProviderBase
|
||||
Topic string `json:"topic"`
|
||||
Style NtfyStyle `json:"style"`
|
||||
}
|
||||
|
||||
type NtfyStyle string
|
||||
|
||||
const (
|
||||
NtfyStyleMarkdown NtfyStyle = "markdown"
|
||||
NtfyStylePlain NtfyStyle = "plain"
|
||||
)
|
||||
|
||||
func (n *Ntfy) Validate() E.Error {
|
||||
if n.URL == "" {
|
||||
return E.New("url is required")
|
||||
}
|
||||
if n.Topic == "" {
|
||||
return E.New("topic is required")
|
||||
}
|
||||
if n.Topic[0] == '/' {
|
||||
return E.New("topic should not start with a slash")
|
||||
}
|
||||
switch n.Style {
|
||||
case "":
|
||||
n.Style = NtfyStyleMarkdown
|
||||
case NtfyStyleMarkdown, NtfyStylePlain:
|
||||
default:
|
||||
return E.Errorf("invalid style, expecting %q or %q, got %q", NtfyStyleMarkdown, NtfyStylePlain, n.Style)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *Ntfy) GetURL() string {
|
||||
if n.URL[len(n.URL)-1] == '/' {
|
||||
return n.URL + n.Topic
|
||||
}
|
||||
return n.URL + "/" + n.Topic
|
||||
}
|
||||
|
||||
func (n *Ntfy) GetMIMEType() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
func (n *Ntfy) SetHeaders(logMsg *LogMessage, headers http.Header) {
|
||||
headers.Set("Title", logMsg.Title)
|
||||
|
||||
switch logMsg.Level {
|
||||
// warning (or other unspecified) uses default priority
|
||||
case zerolog.FatalLevel:
|
||||
headers.Set("Priority", "urgent")
|
||||
case zerolog.ErrorLevel:
|
||||
headers.Set("Priority", "high")
|
||||
case zerolog.InfoLevel:
|
||||
headers.Set("Priority", "low")
|
||||
case zerolog.DebugLevel:
|
||||
headers.Set("Priority", "min")
|
||||
}
|
||||
|
||||
if n.Style == NtfyStyleMarkdown {
|
||||
headers.Set("Markdown", "yes")
|
||||
}
|
||||
}
|
|
@ -21,6 +21,7 @@ type (
|
|||
GetMIMEType() string
|
||||
|
||||
MakeBody(logMsg *LogMessage) (io.Reader, error)
|
||||
SetHeaders(logMsg *LogMessage, headers http.Header)
|
||||
|
||||
makeRespError(resp *http.Response) error
|
||||
}
|
||||
|
@ -30,6 +31,7 @@ type (
|
|||
|
||||
const (
|
||||
ProviderGotify = "gotify"
|
||||
ProviderNtfy = "ntfy"
|
||||
ProviderWebhook = "webhook"
|
||||
)
|
||||
|
||||
|
@ -52,6 +54,7 @@ func notifyProvider(ctx context.Context, provider Provider, msg *LogMessage) err
|
|||
if provider.GetToken() != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+provider.GetToken())
|
||||
}
|
||||
provider.SetHeaders(msg, req.Header)
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
|
|
|
@ -92,12 +92,12 @@ func (webhook *Webhook) GetMIMEType() string {
|
|||
func (webhook *Webhook) makeRespError(resp *http.Response) error {
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("webhook status %d, failed to read body: %w", resp.StatusCode, err)
|
||||
return fmt.Errorf("%s status %d, failed to read body: %w", webhook.Name, resp.StatusCode, err)
|
||||
}
|
||||
if len(body) > 0 {
|
||||
return fmt.Errorf("webhook status %d: %s", resp.StatusCode, body)
|
||||
return fmt.Errorf("%s status %d: %s", webhook.Name, resp.StatusCode, body)
|
||||
}
|
||||
return fmt.Errorf("webhook status %d", resp.StatusCode)
|
||||
return fmt.Errorf("%s status %d", webhook.Name, resp.StatusCode)
|
||||
}
|
||||
|
||||
func (webhook *Webhook) MakeBody(logMsg *LogMessage) (io.Reader, error) {
|
||||
|
|
|
@ -198,33 +198,33 @@ func (mon *monitor) checkUpdateHealth() error {
|
|||
status = health.StatusUnhealthy
|
||||
}
|
||||
if result.Healthy != (mon.status.Swap(status) == health.StatusHealthy) {
|
||||
extras := map[string]any{
|
||||
"Service Name": mon.service,
|
||||
"Time": strutils.FormatTime(time.Now()),
|
||||
extras := notif.LogFields{
|
||||
{Name: "Service Name", Value: mon.service},
|
||||
{Name: "Time", Value: strutils.FormatTime(time.Now())},
|
||||
}
|
||||
if !result.Healthy {
|
||||
extras["Last Seen"] = strutils.FormatLastSeen(GetLastSeen(mon.service))
|
||||
extras.Add("Last Seen", strutils.FormatLastSeen(GetLastSeen(mon.service)))
|
||||
}
|
||||
if !mon.url.Load().Nil() {
|
||||
extras["Service URL"] = mon.url.Load().String()
|
||||
extras.Add("Service URL", mon.url.Load().String())
|
||||
}
|
||||
if result.Detail != "" {
|
||||
extras["Detail"] = result.Detail
|
||||
extras.Add("Detail", result.Detail)
|
||||
}
|
||||
if result.Healthy {
|
||||
logger.Info().Msg("service is up")
|
||||
extras["Ping"] = fmt.Sprintf("%d ms", result.Latency.Milliseconds())
|
||||
extras.Add("Ping", fmt.Sprintf("%d ms", result.Latency.Milliseconds()))
|
||||
notif.Notify(¬if.LogMessage{
|
||||
Title: "✅ Service is up ✅",
|
||||
Extras: extras,
|
||||
Color: notif.Green,
|
||||
Color: notif.ColorSuccess,
|
||||
})
|
||||
} else {
|
||||
logger.Warn().Msg("service went down")
|
||||
notif.Notify(¬if.LogMessage{
|
||||
Title: "❌ Service went down ❌",
|
||||
Extras: extras,
|
||||
Color: notif.Red,
|
||||
Color: notif.ColorError,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "godoxy-schemas",
|
||||
"version": "0.9.0-22",
|
||||
"version": "0.9.1-1",
|
||||
"description": "JSON Schema and typescript types for GoDoxy configuration",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
|
|
File diff suppressed because one or more lines are too long
12
schemas/config/notification.d.ts
vendored
12
schemas/config/notification.d.ts
vendored
|
@ -1,5 +1,5 @@
|
|||
import { URL } from "../types";
|
||||
export declare const NOTIFICATION_PROVIDERS: readonly ["webhook", "gotify"];
|
||||
export declare const NOTIFICATION_PROVIDERS: readonly ["webhook", "gotify", "ntfy"];
|
||||
export type NotificationProvider = (typeof NOTIFICATION_PROVIDERS)[number];
|
||||
export type NotificationConfig = {
|
||||
name: string;
|
||||
|
@ -9,9 +9,17 @@ export interface GotifyConfig extends NotificationConfig {
|
|||
provider: "gotify";
|
||||
token: string;
|
||||
}
|
||||
export declare const NTFY_MSG_STYLES: string[];
|
||||
export type NtfyStyle = (typeof NTFY_MSG_STYLES)[number];
|
||||
export interface NtfyConfig extends NotificationConfig {
|
||||
provider: "ntfy";
|
||||
topic: string;
|
||||
token?: string;
|
||||
style?: NtfyStyle;
|
||||
}
|
||||
export declare const WEBHOOK_TEMPLATES: readonly ["", "discord"];
|
||||
export declare const WEBHOOK_METHODS: readonly ["POST", "GET", "PUT"];
|
||||
export declare const WEBHOOK_MIME_TYPES: readonly ["application/json", "application/x-www-form-urlencoded", "text/plain"];
|
||||
export declare const WEBHOOK_MIME_TYPES: readonly ["application/json", "application/x-www-form-urlencoded", "text/plain", "text/markdown"];
|
||||
export declare const WEBHOOK_COLOR_MODES: readonly ["hex", "dec"];
|
||||
export type WebhookTemplate = (typeof WEBHOOK_TEMPLATES)[number];
|
||||
export type WebhookMethod = (typeof WEBHOOK_METHODS)[number];
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
export const NOTIFICATION_PROVIDERS = ["webhook", "gotify"];
|
||||
export const NOTIFICATION_PROVIDERS = ["webhook", "gotify", "ntfy"];
|
||||
export const NTFY_MSG_STYLES = ["markdown", "plain"];
|
||||
export const WEBHOOK_TEMPLATES = ["", "discord"];
|
||||
export const WEBHOOK_METHODS = ["POST", "GET", "PUT"];
|
||||
export const WEBHOOK_MIME_TYPES = [
|
||||
"application/json",
|
||||
"application/x-www-form-urlencoded",
|
||||
"text/plain",
|
||||
"text/markdown",
|
||||
];
|
||||
export const WEBHOOK_COLOR_MODES = ["hex", "dec"];
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { URL } from "../types";
|
||||
|
||||
export const NOTIFICATION_PROVIDERS = ["webhook", "gotify"] as const;
|
||||
export const NOTIFICATION_PROVIDERS = ["webhook", "gotify", "ntfy"] as const;
|
||||
|
||||
export type NotificationProvider = (typeof NOTIFICATION_PROVIDERS)[number];
|
||||
|
||||
|
@ -17,12 +17,23 @@ export interface GotifyConfig extends NotificationConfig {
|
|||
token: string;
|
||||
}
|
||||
|
||||
export const NTFY_MSG_STYLES = ["markdown", "plain"];
|
||||
export type NtfyStyle = (typeof NTFY_MSG_STYLES)[number];
|
||||
|
||||
export interface NtfyConfig extends NotificationConfig {
|
||||
provider: "ntfy";
|
||||
topic: string;
|
||||
token?: string;
|
||||
style?: NtfyStyle;
|
||||
}
|
||||
|
||||
export const WEBHOOK_TEMPLATES = ["", "discord"] as const;
|
||||
export const WEBHOOK_METHODS = ["POST", "GET", "PUT"] as const;
|
||||
export const WEBHOOK_MIME_TYPES = [
|
||||
"application/json",
|
||||
"application/x-www-form-urlencoded",
|
||||
"text/plain",
|
||||
"text/markdown",
|
||||
] as const;
|
||||
export const WEBHOOK_COLOR_MODES = ["hex", "dec"] as const;
|
||||
|
||||
|
|
4
schemas/config/providers.d.ts
vendored
4
schemas/config/providers.d.ts
vendored
|
@ -1,5 +1,5 @@
|
|||
import { URI, URL } from "../types";
|
||||
import { GotifyConfig, WebhookConfig } from "./notification";
|
||||
import { GotifyConfig, NtfyConfig, WebhookConfig } from "./notification";
|
||||
export type Providers = {
|
||||
/** List of route definition files to include
|
||||
*
|
||||
|
@ -21,7 +21,7 @@ export type Providers = {
|
|||
* @minItems 1
|
||||
* @examples require(".").notificationExamples
|
||||
*/
|
||||
notification?: (WebhookConfig | GotifyConfig)[];
|
||||
notification?: (WebhookConfig | GotifyConfig | NtfyConfig)[];
|
||||
};
|
||||
export declare const includeExamples: readonly ["file1.yml", "file2.yml"];
|
||||
export declare const dockerExamples: readonly [{
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { URI, URL } from "../types";
|
||||
import { GotifyConfig, WebhookConfig } from "./notification";
|
||||
import { GotifyConfig, NtfyConfig, WebhookConfig } from "./notification";
|
||||
|
||||
export type Providers = {
|
||||
/** List of route definition files to include
|
||||
|
@ -20,7 +20,7 @@ export type Providers = {
|
|||
* @minItems 1
|
||||
* @examples require(".").notificationExamples
|
||||
*/
|
||||
notification?: (WebhookConfig | GotifyConfig)[];
|
||||
notification?: (WebhookConfig | GotifyConfig | NtfyConfig)[];
|
||||
};
|
||||
|
||||
export const includeExamples = ["file1.yml", "file2.yml"] as const;
|
||||
|
|
Loading…
Add table
Reference in a new issue