Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added docs/assets/notif/elasticsearch.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions docs/config/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,7 @@ Can be transposed to:
* [amqp](../notif/amqp.md)
* [apprise](../notif/apprise.md)
* [discord](../notif/discord.md)
* [elasticsearch](../notif/elasticsearch.md)
* [gotify](../notif/gotify.md)
* [mail](../notif/mail.md)
* [matrix](../notif/matrix.md)
Expand Down
1 change: 1 addition & 0 deletions docs/config/notif.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
* [`amqp`](../notif/amqp.md)
* [`apprise`](../notif/apprise.md)
* [`discord`](../notif/discord.md)
* [`elasticsearch`](../notif/elasticsearch.md)
* [`gotify`](../notif/gotify.md)
* [`mail`](../notif/mail.md)
* [`matrix`](../notif/matrix.md)
Expand Down
76 changes: 76 additions & 0 deletions docs/notif/elasticsearch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# Elasticsearch notifications

Send notifications to your Elasticsearch cluster as structured documents.

## Configuration

!!! example "File"
```yaml
notif:
elasticsearch:
address: http://localhost:9200
username: elastic
password: password
client: diun
index: diun-notifications
timeout: 10s
insecureSkipVerify: false
```

| Name | Default | Description |
|----------------------|-------------------------|---------------------------------------------------------------------|
| `address`[^1] | `http://localhost:9200` | Elasticsearch base URL |
| `username` | | Elasticsearch username for authentication |
| `usernameFile` | | Use content of secret file as username if `username` is not defined |
| `password` | | Elasticsearch password for authentication |
| `passwordFile` | | Use content of secret file as password if `password` is not defined |
| `client`[^1] | `diun` | Client name to identify the source of notifications |
| `index`[^1] | `diun-notifications` | Elasticsearch index name where notifications will be stored |
| `timeout`[^1] | `10s` | Timeout specifies a time limit for the request to be made |
| `insecureSkipVerify` | `false` | Skip TLS certificate verification |

!!! abstract "Environment variables"
* `DIUN_NOTIF_ELASTICSEARCH_ADDRESS`
* `DIUN_NOTIF_ELASTICSEARCH_USERNAME`
* `DIUN_NOTIF_ELASTICSEARCH_USERNAMEFILE`
* `DIUN_NOTIF_ELASTICSEARCH_PASSWORD`
* `DIUN_NOTIF_ELASTICSEARCH_PASSWORDFILE`
* `DIUN_NOTIF_ELASTICSEARCH_CLIENT`
* `DIUN_NOTIF_ELASTICSEARCH_INDEX`
* `DIUN_NOTIF_ELASTICSEARCH_TIMEOUT`
* `DIUN_NOTIF_ELASTICSEARCH_INSECURESKIPVERIFY`

## Document Structure

Each notification is stored as a JSON document with the following structure:

```json
{
"diun_version": "4.24.0",
"hostname": "myserver",
"status": "new",
"provider": "file",
"image": "docker.io/crazymax/diun:latest",
"hub_link": "https://hub.docker.com/r/crazymax/diun",
"mime_type": "application/vnd.docker.distribution.manifest.list.v2+json",
"digest": "sha256:216e3ae7de4ca8b553eb11ef7abda00651e79e537e85c46108284e5e91673e01",
"created": "2020-03-26T12:23:56Z",
"platform": "linux/amd64",
"client": "diun",
"metadata": {
"ctn_command": "diun serve",
"ctn_createdat": "2022-12-29 10:22:15 +0100 CET",
"ctn_id": "0dbd10e15b31add2c48856fd34451adabf50d276efa466fe19a8ef5fbd87ad7c",
"ctn_names": "diun",
"ctn_size": "0B",
"ctn_state": "running",
"ctn_status": "Up Less than a second (health: starting)"
}
}
```

## Sample

![](../assets/notif/elasticsearch.png)

[^1]: Value required
9 changes: 9 additions & 0 deletions internal/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,15 @@ func TestLoadFile(t *testing.T) {
Timeout: utl.NewDuration(10 * time.Second),
TemplateBody: model.NotifDefaultTemplateBody,
},
Elasticsearch: &model.NotifElasticsearch{
Address: "https://elastic.foo.com",
Username: "elastic",
Password: "password",
Client: "diun",
Index: "diun-notifications",
Timeout: utl.NewDuration(10 * time.Second),
InsecureSkipVerify: false,
},
Gotify: &model.NotifGotify{
Endpoint: "http://gotify.foo.com",
Token: "Token123456",
Expand Down
8 changes: 8 additions & 0 deletions internal/config/fixtures/config.test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,14 @@ notif:
- "<@&200>"
renderFields: true
timeout: 10s
elasticsearch:
address: https://elastic.foo.com
username: elastic
password: password
client: diun
index: diun-notifications
timeout: 10s
insecureSkipVerify: false
gotify:
endpoint: http://gotify.foo.com
token: Token123456
Expand Down
8 changes: 8 additions & 0 deletions internal/config/fixtures/config.validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@ notif:
- "<@&200>"
renderFields: true
timeout: 10s
elasticsearch:
address: https://elastic.foo.com
username: elastic
password: password
client: diun
index: diun-notifications
timeout: 10s
insecureSkipVerify: false
gotify:
endpoint: http://gotify.foo.com
token: Token123456
Expand Down
33 changes: 17 additions & 16 deletions internal/model/notif.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,22 +32,23 @@ type NotifEntry struct {

// Notif holds data necessary for notification configuration
type Notif struct {
Amqp *NotifAmqp `yaml:"amqp,omitempty" json:"amqp,omitempty"`
Apprise *NotifApprise `yaml:"apprise,omitempty" json:"apprise,omitempty"`
Discord *NotifDiscord `yaml:"discord,omitempty" json:"discord,omitempty"`
Gotify *NotifGotify `yaml:"gotify,omitempty" json:"gotify,omitempty"`
Mail *NotifMail `yaml:"mail,omitempty" json:"mail,omitempty"`
Matrix *NotifMatrix `yaml:"matrix,omitempty" json:"matrix,omitempty"`
Mqtt *NotifMqtt `yaml:"mqtt,omitempty" json:"mqtt,omitempty"`
Ntfy *NotifNtfy `yaml:"ntfy,omitempty" json:"ntfy,omitempty"`
Pushover *NotifPushover `yaml:"pushover,omitempty" json:"pushover,omitempty"`
RocketChat *NotifRocketChat `yaml:"rocketchat,omitempty" json:"rocketchat,omitempty"`
Script *NotifScript `yaml:"script,omitempty" json:"script,omitempty"`
SignalRest *NotifSignalRest `yaml:"signalrest,omitempty" json:"signalrest,omitempty"`
Slack *NotifSlack `yaml:"slack,omitempty" json:"slack,omitempty"`
Teams *NotifTeams `yaml:"teams,omitempty" json:"teams,omitempty"`
Telegram *NotifTelegram `yaml:"telegram,omitempty" json:"telegram,omitempty"`
Webhook *NotifWebhook `yaml:"webhook,omitempty" json:"webhook,omitempty"`
Amqp *NotifAmqp `yaml:"amqp,omitempty" json:"amqp,omitempty"`
Apprise *NotifApprise `yaml:"apprise,omitempty" json:"apprise,omitempty"`
Discord *NotifDiscord `yaml:"discord,omitempty" json:"discord,omitempty"`
Elasticsearch *NotifElasticsearch `yaml:"elasticsearch,omitempty" json:"elasticsearch,omitempty"`
Gotify *NotifGotify `yaml:"gotify,omitempty" json:"gotify,omitempty"`
Mail *NotifMail `yaml:"mail,omitempty" json:"mail,omitempty"`
Matrix *NotifMatrix `yaml:"matrix,omitempty" json:"matrix,omitempty"`
Mqtt *NotifMqtt `yaml:"mqtt,omitempty" json:"mqtt,omitempty"`
Ntfy *NotifNtfy `yaml:"ntfy,omitempty" json:"ntfy,omitempty"`
Pushover *NotifPushover `yaml:"pushover,omitempty" json:"pushover,omitempty"`
RocketChat *NotifRocketChat `yaml:"rocketchat,omitempty" json:"rocketchat,omitempty"`
Script *NotifScript `yaml:"script,omitempty" json:"script,omitempty"`
SignalRest *NotifSignalRest `yaml:"signalrest,omitempty" json:"signalrest,omitempty"`
Slack *NotifSlack `yaml:"slack,omitempty" json:"slack,omitempty"`
Teams *NotifTeams `yaml:"teams,omitempty" json:"teams,omitempty"`
Telegram *NotifTelegram `yaml:"telegram,omitempty" json:"telegram,omitempty"`
Webhook *NotifWebhook `yaml:"webhook,omitempty" json:"webhook,omitempty"`
}

// GetDefaults gets the default values
Expand Down
35 changes: 35 additions & 0 deletions internal/model/notif_elasticsearch.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package model

import (
"time"

"github.com/crazy-max/diun/v4/pkg/utl"
)

type NotifElasticsearch struct {
Address string `yaml:"address,omitempty" json:"address,omitempty" validate:"required"`
Username string `yaml:"username,omitempty" json:"username,omitempty" validate:"omitempty"`
UsernameFile string `yaml:"usernameFile,omitempty" json:"usernameFile,omitempty" validate:"omitempty,file"`
Password string `yaml:"password,omitempty" json:"password,omitempty" validate:"omitempty"`
PasswordFile string `yaml:"passwordFile,omitempty" json:"passwordFile,omitempty" validate:"omitempty,file"`
Client string `yaml:"client,omitempty" json:"client,omitempty" validate:"required"`
Index string `yaml:"index,omitempty" json:"index,omitempty" validate:"required"`
Timeout *time.Duration `yaml:"timeout,omitempty" json:"timeout,omitempty" validate:"required"`
InsecureSkipVerify bool `yaml:"insecureSkipVerify,omitempty" json:"insecureSkipVerify,omitempty" validate:"omitempty"`
}

// GetDefaults gets the default values
func (s *NotifElasticsearch) GetDefaults() *NotifElasticsearch {
n := &NotifElasticsearch{}
n.SetDefaults()
return n
}

// SetDefaults sets the default values
func (s *NotifElasticsearch) SetDefaults() {
s.Address = "http://localhost:9200"
s.Client = "diun"
s.Index = "diun-notifications"
s.Timeout = utl.NewDuration(10 * time.Second)
s.InsecureSkipVerify = false
}
4 changes: 4 additions & 0 deletions internal/notif/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"github.com/crazy-max/diun/v4/internal/notif/amqp"
"github.com/crazy-max/diun/v4/internal/notif/apprise"
"github.com/crazy-max/diun/v4/internal/notif/discord"
"github.com/crazy-max/diun/v4/internal/notif/elasticsearch"
"github.com/crazy-max/diun/v4/internal/notif/gotify"
"github.com/crazy-max/diun/v4/internal/notif/mail"
"github.com/crazy-max/diun/v4/internal/notif/matrix"
Expand Down Expand Up @@ -54,6 +55,9 @@ func New(config *model.Notif, meta model.Meta) (*Client, error) {
if config.Discord != nil {
c.notifiers = append(c.notifiers, discord.New(config.Discord, meta))
}
if config.Elasticsearch != nil {
c.notifiers = append(c.notifiers, elasticsearch.New(config.Elasticsearch, meta))
}
if config.Gotify != nil {
c.notifiers = append(c.notifiers, gotify.New(config.Gotify, meta))
}
Expand Down
142 changes: 142 additions & 0 deletions internal/notif/elasticsearch/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
package elasticsearch

import (
"bytes"
"context"
"crypto/tls"
"encoding/json"
"net/http"
"net/url"
"path"
"time"

"github.com/crazy-max/diun/v4/internal/model"
"github.com/crazy-max/diun/v4/internal/msg"
"github.com/crazy-max/diun/v4/internal/notif/notifier"
"github.com/crazy-max/diun/v4/pkg/utl"
"github.com/pkg/errors"
)

// Client represents an active elasticsearch notification object
type Client struct {
*notifier.Notifier
cfg *model.NotifElasticsearch
meta model.Meta
}

// New creates a new elasticsearch notification instance
func New(config *model.NotifElasticsearch, meta model.Meta) notifier.Notifier {
return notifier.Notifier{
Handler: &Client{
cfg: config,
meta: meta,
},
}
}

// Name returns notifier's name
func (c *Client) Name() string {
return "elasticsearch"
}

// Send creates and sends an elasticsearch notification with an entry
func (c *Client) Send(entry model.NotifEntry) error {
username, err := utl.GetSecret(c.cfg.Username, c.cfg.UsernameFile)
if err != nil {
return err
}

password, err := utl.GetSecret(c.cfg.Password, c.cfg.PasswordFile)
if err != nil {
return err
}

// Use the same JSON structure as webhook notifier
message, err := msg.New(msg.Options{
Meta: c.meta,
Entry: entry,
})
if err != nil {
return err
}

body, err := message.RenderJSON()
if err != nil {
return err
}

// Parse the JSON to add the client field
var doc map[string]any
if err := json.Unmarshal(body, &doc); err != nil {
return err
}

// Add the current time
doc["@timestamp"] = time.Now().Format(time.RFC3339Nano)

// Add the client field from the configuration
doc["client"] = c.cfg.Client

// Re-marshal the JSON with the client field
body, err = json.Marshal(doc)
if err != nil {
return err
}

// Build the Elasticsearch indexing URL
// This uses the Index API (POST /{index}/_doc) to create a document with an auto-generated _id:
// https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-create
u, err := url.Parse(c.cfg.Address)
if err != nil {
return err
}
u.Path = path.Join(u.Path, c.cfg.Index, "_doc")

cancelCtx, cancel := context.WithCancelCause(context.Background())
timeoutCtx, _ := context.WithTimeoutCause(cancelCtx, *c.cfg.Timeout, errors.WithStack(context.DeadlineExceeded)) //nolint:govet // no need to manually cancel this context as we already rely on parent
defer func() { cancel(errors.WithStack(context.Canceled)) }()

hc := http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: c.cfg.InsecureSkipVerify,
},
},
}

req, err := http.NewRequestWithContext(timeoutCtx, "POST", u.String(), bytes.NewBuffer(body))
if err != nil {
return err
}

req.Header.Set("Content-Type", "application/json")
req.Header.Set("User-Agent", c.meta.UserAgent)

// Add authentication if provided
if username != "" && password != "" {
req.SetBasicAuth(username, password)
}

resp, err := hc.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusCreated {
var errBody struct {
Status int `json:"status"`
Error struct {
Type string `json:"type"`
Reason string `json:"reason"`
} `json:"error"`
}
if err := json.NewDecoder(resp.Body).Decode(&errBody); err != nil {
return errors.Wrapf(err, "cannot decode JSON error response for HTTP %d %s status",
resp.StatusCode, http.StatusText(resp.StatusCode))
}
return errors.Errorf("%d %s: %s", errBody.Status, errBody.Error.Type, errBody.Error.Reason)
}

return nil
}
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ nav:
- Amqp: notif/amqp.md
- Apprise: notif/apprise.md
- Discord: notif/discord.md
- Elasticsearch: notif/elasticsearch.md
- Gotify: notif/gotify.md
- Mail: notif/mail.md
- Matrix: notif/matrix.md
Expand Down