diff --git a/doc/10-Channels.md b/doc/10-Channels.md index 62a74d87f..f6a7bbe55 100644 --- a/doc/10-Channels.md +++ b/doc/10-Channels.md @@ -1,88 +1,312 @@ # Channels After Icinga Notifications decides to send a notification of any kind, it will be passed to a channel plugin. -Such a channel plugin submits the notification event to a channel, e.g., email or a chat client. +Such a plugin submits the notification event to a domain-specific channel, e.g., email or a chat client. -Icinga Notifications comes packed with channel plugins, but also enables you to develop your own plugins. +Icinga Notifications comes packed with channels, but also enables you to develop your own channels. -To make those plugins available to Icinga Notifications, they must be placed in the +To make those channels available to Icinga Notifications, they must be placed in the [channels directory](03-Configuration.md#channels-directory), which is being done automatically for package installations. -Afterwards they can be configured through Icinga Notifications Web. +At startup, Icinga Notifications scans this directory, starts each channel once to query its configuration options +and stores these options in the database. +Using this information, Icinga Notifications Web allows channels to be configured, +which are then started, configured, and lastly used to send notification events from Icinga Notifications. ## Technical Channel Description -Channel plugins are processes that run continuously and independently of each other. They receive many requests over -their lifetime. They receive JSON-formatted requests on stdin and reply with JSON-formatted responses on stdout. The -request and response structure is inspired by JSON-RPC. +!!! warning + + As this is still an early preview version, things might change. + There may still be severe bugs and incompatible changes may happen without any notice. + +Channel plugins are independent processes that run continuously, supervised by Icinga Notifications. +They receive JSON-formatted requests on `stdin` and reply with JSON-formatted responses on `stdout`. +For logging or debugging, channels can write to `stderr`, which will be logged by Icinga Notifications. + +The request and response structure is inspired by JSON-RPC. +The anatomy of requests and responses is described below. +Note that fields with a type marked as optional must be omitted in the JSON object if they do not have a value. + +This documentation uses beautified JSON for ease of reading. ### Request -The request must be in JSON format and should contain following keys: +A channel receives a request as a JSON object with the following fields: -- `method`: The request method to call. -- `params`: The params for request method. -- `id`: Unsigned int value. Required to assign the response to its request as responses can be sent out of order. +| Field | Description | Type | +|----------|-------------------------------|------------------------------------------------------------------------| +| `method` | Request method to call. | String | +| `params` | Params for the request method | Optional JSON object, mapping parameter keys (string) to values (any). | +| `id` | Unique identifier. | Unsigned integer | Examples: +- Simple request without any `params`: + ```json + { + "method": "Simple", + "id" : 1000 + } + ``` +- Request with `params` of different types: + ```json + { + "method": "WithParams", + "params": { + "foo": 23, + "bar": "hello" + }, + "id": 1000 + } + ``` + +### Response + +Each request must be answered by the channel with a response JSON object of the following fields: + +| Field | Description | Type | +|----------|-------------------------------------|---------------------------| +| `result` | Output of a successful method call. | Optional JSON value (any) | +| `error` | Error message. | Optional string | +| `id` | Request id. | Unsigned integer | + +The `result` field may be omitted if the method does not return a value, i.e., for setter calls. + +In case of a present `error` value, the `result` field must be omitted. +Thus, a successful responses without a `result` contains just an `id` field. + +The `id` field in a response must match those of its request, allowing Icinga Notifications to link those two. +This is necessary as responses may be created out of order. + +Examples: + +- Successful response without a `result` message: + ```json + { + "id": 1000 + } + ``` +- Successful response with a `result`: + ```json + { + "result": "hello world", + "id": 1000 + } + ``` +- Response with an error: + ```json + { + "error": "unknown method: 'Foo'", + "id": 1000 + } + ``` + +### Methods + +The following methods must be implemented by a channel. + +#### GetInfo + +The parameterless `GetInfo` method returns information about the channel. + +Its `result` is expected to be a JSON object with the `json` fields defined in the +[`Info` type](https://pkg.go.dev/github.com/icinga/icinga-notifications/pkg/plugin#Info). +The `config_attrs` field must be an array of JSON objects according to the +[`ConfigOption` type](https://pkg.go.dev/github.com/icinga/icinga-notifications/pkg/plugin#ConfigOption). +Those attributes define configuration options for the channel to be set via the `SetConfig` method. +Furthermore, they are used for channel configuration in Icinga Notifications Web. + +##### Example GetInfo Request + ```json { - "method": "Add", - "params": { - "num1": 5, - "num2": 3 + "method": "GetInfo", + "id" : 1 +} +``` + +##### Example GetInfo Response + +```json +{ + "result": { + "name": "Minified Webhook", + "version": "0.0.0-gf369a11-dirty", + "author": "Icinga GmbH", + "config_attrs": [ + { + "name": "url_template", + "type": "string", + "label": { + "de_DE": "URL-Template", + "en_US": "URL Template" + }, + "help": { + "de_DE": "URL, optional als Go-Template über das zu verarbeitende plugin.NotificationRequest.", + "en_US": "URL, optionally as a Go template over the current plugin.NotificationRequest." + }, + "required": true, + "min": null, + "max": null + }, + { + "name": "response_status_codes", + "type": "string", + "label": { + "de_DE": "Antwort-Status-Codes", + "en_US": "Response Status Codes" + }, + "help": { + "de_DE": "Kommaseparierte Liste erwarteter Status-Code der HTTP-Antwort, z.B.: 200,201,202,208,418", + "en_US": "Comma separated list of expected HTTP response status code, e.g., 200,201,202,208,418" + }, + "default": "200", + "min": null, + "max": null + } + ] }, - "id": 2020 + "id": 1 +} +``` + +#### SetConfig + +The `SetConfig` method configures the channel. + +The Icinga Notifications daemon is going to call this method at least once before sending the first notifications +to initialize the channel plugin. + +The passed JSON object in the request's `param` field reflects the objects from `GetInfo`'s `config_attrs`. +Each object within the `config_attrs` array must be configurable by using its `name` attribute as the key together with +the desired configuration value, being of the specified type in the `type` field. + +To illustrate, the URL template from the above output is configurable with the following JSON object passed in `params`: + +```json +{ + "url_template": "http://localhost:8000/update/{{.Incident.Id}}" } ``` +If the channel plugin successfully configured itself, a response without an `result` must be returned. +Otherwise, an `error` must be returned. + +##### Example SetConfig Request + ```json { - "method": "Foo", + "method": "SetConfig", "params": { - "a": "value1", - "b": "value2" + "url_template": "http://localhost:8000/update/{{.Incident.Id}}", + "response_status_codes": "200" }, - "id": 3030 + "id": 2 } ``` -### Response +##### Example GetInfo Response + +```json +{ + "id": 2 +} +``` -The response is in JSON format and contains following keys: +#### SendNotification -- `result`: The result as JSON format. Omitted when the method does not return a value (e.g. setter calls) or an error - has occurred. -- `error`: The error message. Omitted when no error has occurred. -- `id`: The request id. When result value is empty and no error is occurred, the response will only contain the request - id. +The `SendNotification` method requests the channel to dispatch notifications. -Examples: +Within the request's `params`, a JSON object representing a +[`NotificationRequest`](https://pkg.go.dev/github.com/icinga/icinga-notifications/pkg/plugin#NotificationRequest) +is passed. + +If the channel cannot dispatch a notification, an `error` must be returned. +This may be due to channel-specific reasons, such as an email channel where the SMTP server is unavailable. + +#### Example SendNotification Request ```json { - "result": 8, - "id": 2020 + "method": "SendNotification", + "params": { + "contact": { + "full_name": "icingaadmin", + "addresses": [ + { + "type": "email", + "address": "icingaaadmin@example.com" + } + ] + }, + "object": { + "name": "dummy-816!random fortune", + "url": "http://localhost/icingaweb2/icingadb/service?name=random%20fortune&host.name=dummy-816", + "tags": { + "host": "dummy-816", + "service": "random fortune" + }, + "extra_tags": { + "hostgroup/app-mobile": "", + "hostgroup/department-dev": "", + "hostgroup/env-prod": "", + "hostgroup/location-rome": "", + "servicegroup/app-storage": "", + "servicegroup/department-ps": "", + "servicegroup/env-prod": "", + "servicegroup/location-rome": "" + } + }, + "incident": { + "id": 1437, + "url": "http://localhost/icingaweb2/notifications/incident?id=1437", + "severity": "crit" + }, + "event": { + "time": "2024-07-12T10:47:30.445439055Z", + "type": "state", + "username": "", + "message": "Q:\tWhat looks like a cat, flies like a bat, brays like a donkey, and\n\tplays like a monkey?\nA:\tNothing." + } + }, + "id": 3 } ``` +#### Example SendNotification Response + ```json { - "error": "unknown method: 'Foo'", - "id": 3030 + "id": 3 } ``` -### Methods +## Writing Channel Plugins in Go + +!!! warning + + As this is still an early preview version, things might change. + There may still be severe bugs and incompatible changes may happen without any notice. + +!!! tip + + Icinga Notifications comes with a Webhook channel plugin. + Consider using this channel if your transport uses HTTP instead of writing a custom channel. + +!!! tip + + When developing custom channels, consider naming them with a unique prefix, + as additional channels will get added to Icinga Notifications in the future. + For example, name your channel `x_irc` or `my_irc` instead of `irc`. -Currently, the channel plugin include following three methods: +Since Icinga Notifications and all of its channels are written in the Go programming language, +libraries already used internally can be reused. +In particular, the [`Plugin`](https://pkg.go.dev/github.com/icinga/icinga-notifications/pkg/plugin#Plugin) +interface must be implemented, requesting methods for all the RPC methods described above. +The channel plugin's `main` function should call +[`RunPlugin`](https://pkg.go.dev/github.com/icinga/icinga-notifications/pkg/plugin#RunPlugin), +taking care about calling the RPC method implementations. -- `SetConfig`: Initialize the channel plugin with specified config as `params` key. The config is plugin specific - therefore each plugin defines what is expected as config. - [(example)](../internal/channel/examples/set-config.json) -- `GetInfo`: Get the information about the channel e.g. Name. The `params` key has no effect and can be omitted. - [(example)](../internal/channel/examples/get-info.json) -- `SendNotification`: Send the notifications. The `params` key should contain the information about the contact to be - notified, corresponding object, the incident and the triggered event. - [(example)](../internal/channel/examples/send-notification.json) +For concrete examples, there are the implemented channels in the Icinga Notifications repository under +[`./cmd/channels`](https://github.com/Icinga/icinga-notifications/tree/main/cmd/channels). \ No newline at end of file diff --git a/doc/20-HTTP-API.md b/doc/20-HTTP-API.md index f2b122638..d2bfa3a01 100644 --- a/doc/20-HTTP-API.md +++ b/doc/20-HTTP-API.md @@ -35,7 +35,7 @@ curl -v -u 'source-2:insecureinsecure' -d '@-' 'http://localhost:5680/process-ev "type": "state", "severity": "crit", "username": "", - "message": "Something went somehwere very wrong." + "message": "Something went somewhere very wrong." } EOF ``` diff --git a/internal/channel/examples/get-info.json b/internal/channel/examples/get-info.json deleted file mode 100644 index cecd4202e..000000000 --- a/internal/channel/examples/get-info.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "method": "GetInfo", - "id" : 2020 -} diff --git a/internal/channel/examples/send-notification.json b/internal/channel/examples/send-notification.json deleted file mode 100644 index 75d754f1a..000000000 --- a/internal/channel/examples/send-notification.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "method" : "SendNotification", - "params": { - "contact": { - "full_name": "John Doe", - "addresses": [ - { - "type": "email", - "address": "johnd@example.com" - },{ - "type": "rocketchat", - "address": "@johndoe" - } - ] - }, - "object": { - "name": "httpd", - "url": "https://example.com/icingaweb2/icingadb/service?name=httpd&host.name=www1", - "tags": { - "host": "www1", - "service": "httpd" - }, - "extra_tags": { - "hostgroup/server": null - } - }, - "incident": { - "id": 22, - "url": "https://example.com/icingaweb2/notifications/incident?id=22" - }, - "event": { - "type": "state", - "severity": "crit", - "message": "Service is down" - } - }, - "id": 2020 -} diff --git a/internal/channel/examples/set-config.json b/internal/channel/examples/set-config.json deleted file mode 100644 index 182905145..000000000 --- a/internal/channel/examples/set-config.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "method": "SetConfig", - "params": { - "host": "example.com", - "port": "25", - "from": "notifications-daemon@example.com" - }, - "id" : 2020 -} diff --git a/pkg/plugin/plugin.go b/pkg/plugin/plugin.go index 0db441559..84f40b057 100644 --- a/pkg/plugin/plugin.go +++ b/pkg/plugin/plugin.go @@ -21,7 +21,7 @@ const ( MethodSendNotification = "SendNotification" ) -// ConfigOption describes a config element of the channel form +// ConfigOption describes a config element. type ConfigOption struct { // Element name Name string `json:"name"` @@ -69,13 +69,20 @@ type ConfigOption struct { Max types.Int `json:"max,omitempty"` } -// Info contains plugin information. +// Info contains channel plugin information. type Info struct { - Type string `db:"type" json:"-"` - Name string `db:"name" json:"name"` - Version string `db:"version" json:"version"` - Author string `db:"author" json:"author"` - ConfigAttributes json.RawMessage `db:"config_attrs" json:"config_attrs"` // ConfigOption(s) as json-encoded list + // Type of the channel plugin. + // + // Not part of the JSON object. Will be set to the channel plugin file name before database insertion. + Type string `db:"type" json:"-"` + // Name of this channel plugin in a human-readable value. + Name string `db:"name" json:"name"` + // Version of this channel plugin. + Version string `db:"version" json:"version"` + // Author of this channel plugin. + Author string `db:"author" json:"author"` + // ConfigAttributes contains multiple ConfigOption(s) as JSON-encoded list. + ConfigAttributes json.RawMessage `db:"config_attrs" json:"config_attrs"` } // TableName implements the contracts.TableNamer interface. @@ -83,16 +90,19 @@ func (i *Info) TableName() string { return "available_channel_type" } +// Contact to receive notifications for the NotificationRequest. type Contact struct { FullName string `json:"full_name"` Addresses []*Address `json:"addresses"` } +// Address to receive this notification. Each Contact might have multiple addresses. type Address struct { Type string `json:"type"` Address string `json:"address"` } +// Object which this NotificationRequest is all about, e.g., an Icinga 2 Host or Service object. type Object struct { Name string `json:"name"` Url string `json:"url"` @@ -100,12 +110,14 @@ type Object struct { ExtraTags map[string]string `json:"extra_tags"` } +// Incident of this NotificationRequest, grouping Events for this Object. type Incident struct { Id int64 `json:"id"` Url string `json:"url"` Severity string `json:"severity"` } +// Event indicating this NotificationRequest. type Event struct { Time time.Time `json:"time"` Type string `json:"type"` @@ -113,6 +125,7 @@ type Event struct { Message string `json:"message"` } +// NotificationRequest is being sent to a channel plugin via Plugin.SendNotification to request notification dispatching. type NotificationRequest struct { Contact *Contact `json:"contact"` Object *Object `json:"object"` @@ -120,6 +133,10 @@ type NotificationRequest struct { Event *Event `json:"event"` } +// Plugin defines necessary methods for a channel plugin. +// +// Those methods are being called via the internal JSON-RPC and allow channel interaction. Within the channel's main +// function, the channel should be launched via RunPlugin. type Plugin interface { // GetInfo returns the corresponding plugin *Info GetInfo() *Info