1
0
Fork 0

Adding upstream version 1.34.4.

Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
Daniel Baumann 2025-05-24 07:26:29 +02:00
parent e393c3af3f
commit 4978089aab
Signed by: daniel
GPG key ID: FBB4F0E80A80222F
4963 changed files with 677545 additions and 0 deletions

View file

@ -0,0 +1,49 @@
# papertrail webhooks
Enables Telegraf to act as a [Papertrail Webhook](http://help.papertrailapp.com/kb/how-it-works/web-hooks/).
## Events
[Full documentation](http://help.papertrailapp.com/kb/how-it-works/web-hooks/#callback).
Events from Papertrail come in two forms:
* The [event-based callback](http://help.papertrailapp.com/kb/how-it-works/web-hooks/#callback):
* A point is created per event, with the timestamp as `received_at`
* Each point has a field counter (`count`), which is set to `1` (signifying
the event occurred)
* Each event "hostname" object is converted to a `host` tag
* The "saved_search" name in the payload is added as an `event` tag
* The "saved_search" id in the payload is added as a `search_id` field
* The papertrail url to view the event is built and added as a `url` field
* The rest of the event data is converted directly to fields on the point:
* `id`
* `source_ip`
* `source_name`
* `source_id`
* `program`
* `severity`
* `facility`
* `message`
When a callback is received, an event-based point will look similar to:
```shell
papertrail,host=myserver.example.com,event=saved_search_name count=1i,source_name="abc",program="CROND",severity="Info",source_id=2i,message="message body",source_ip="208.75.57.121",id=7711561783320576i,facility="Cron",url="https://papertrailapp.com/searches/42?centered_on_id=7711561783320576",search_id=42i 1453248892000000000
```
* The [count-based callback](http://help.papertrailapp.com/kb/how-it-works/web-hooks/#count-only-webhooks)
* A point is created per timeseries object per count, with the timestamp as
the "timeseries" key (the unix epoch of the event)
* Each point has a field counter (`count`), which is set to the value of each
"timeseries" object
* Each count "source_name" object is converted to a `host` tag
* The "saved_search" name in the payload is added as an `event` tag
When a callback is received, a count-based point will look similar to:
```shell
papertrail,host=myserver.example.com,event=saved_search_name count=3i 1453248892000000000
```

View file

@ -0,0 +1,207 @@
package papertrail
import (
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"github.com/stretchr/testify/require"
"github.com/influxdata/telegraf/testutil"
)
const (
contentType = "application/x-www-form-urlencoded"
)
func post(t *testing.T, pt *Webhook, contentType, body string) *httptest.ResponseRecorder {
req, err := http.NewRequest("POST", "/", strings.NewReader(body))
require.NoError(t, err)
req.Header.Set("Content-Type", contentType)
w := httptest.NewRecorder()
pt.eventHandler(w, req)
return w
}
func TestWrongContentType(t *testing.T) {
var acc testutil.Accumulator
pt := &Webhook{Path: "/papertrail", acc: &acc}
form := url.Values{}
form.Set("payload", sampleEventPayload)
data := form.Encode()
resp := post(t, pt, "", data)
require.Equal(t, http.StatusUnsupportedMediaType, resp.Code)
}
func TestMissingPayload(t *testing.T) {
var acc testutil.Accumulator
pt := &Webhook{Path: "/papertrail", acc: &acc}
resp := post(t, pt, contentType, "")
require.Equal(t, http.StatusBadRequest, resp.Code)
}
func TestPayloadNotJSON(t *testing.T) {
var acc testutil.Accumulator
pt := &Webhook{Path: "/papertrail", acc: &acc}
resp := post(t, pt, contentType, "payload={asdf]")
require.Equal(t, http.StatusBadRequest, resp.Code)
}
func TestPayloadInvalidJSON(t *testing.T) {
var acc testutil.Accumulator
pt := &Webhook{Path: "/papertrail", acc: &acc}
resp := post(t, pt, contentType, `payload={"value": 42}`)
require.Equal(t, http.StatusBadRequest, resp.Code)
}
func TestEventPayload(t *testing.T) {
var acc testutil.Accumulator
pt := &Webhook{Path: "/papertrail", acc: &acc}
form := url.Values{}
form.Set("payload", sampleEventPayload)
resp := post(t, pt, contentType, form.Encode())
require.Equal(t, http.StatusOK, resp.Code)
fields1 := map[string]interface{}{
"count": uint64(1),
"id": int64(7711561783320576),
"source_ip": "208.75.57.121",
"source_name": "abc",
"source_id": int64(2),
"program": "CROND",
"severity": "Info",
"facility": "Cron",
"message": "message body",
"url": "https://papertrailapp.com/searches/42?centered_on_id=7711561783320576",
"search_id": int64(42),
}
fields2 := map[string]interface{}{
"count": uint64(1),
"id": int64(7711562567655424),
"source_ip": "208.75.57.120",
"source_name": "server1",
"source_id": int64(19),
"program": "CROND",
"severity": "Info",
"facility": "Cron",
"message": "A short event",
"url": "https://papertrailapp.com/searches/42?centered_on_id=7711562567655424",
"search_id": int64(42),
}
tags1 := map[string]string{
"event": "Important stuff",
"host": "abc",
}
tags2 := map[string]string{
"event": "Important stuff",
"host": "def",
}
acc.AssertContainsTaggedFields(t, "papertrail", fields1, tags1)
acc.AssertContainsTaggedFields(t, "papertrail", fields2, tags2)
}
func TestCountPayload(t *testing.T) {
var acc testutil.Accumulator
pt := &Webhook{Path: "/papertrail", acc: &acc}
form := url.Values{}
form.Set("payload", sampleCountPayload)
resp := post(t, pt, contentType, form.Encode())
require.Equal(t, http.StatusOK, resp.Code)
fields1 := map[string]interface{}{
"count": uint64(5),
}
fields2 := map[string]interface{}{
"count": uint64(3),
}
tags1 := map[string]string{
"event": "Important stuff",
"host": "arthur",
}
tags2 := map[string]string{
"event": "Important stuff",
"host": "ford",
}
acc.AssertContainsTaggedFields(t, "papertrail", fields1, tags1)
acc.AssertContainsTaggedFields(t, "papertrail", fields2, tags2)
}
const sampleEventPayload = `{
"events": [
{
"id": 7711561783320576,
"received_at": "2011-05-18T20:30:02-07:00",
"display_received_at": "May 18 20:30:02",
"source_ip": "208.75.57.121",
"source_name": "abc",
"source_id": 2,
"hostname": "abc",
"program": "CROND",
"severity": "Info",
"facility": "Cron",
"message": "message body"
},
{
"id": 7711562567655424,
"received_at": "2011-05-18T20:30:02-07:00",
"display_received_at": "May 18 20:30:02",
"source_ip": "208.75.57.120",
"source_name": "server1",
"source_id": 19,
"hostname": "def",
"program": "CROND",
"severity": "Info",
"facility": "Cron",
"message": "A short event"
}
],
"saved_search": {
"id": 42,
"name": "Important stuff",
"query": "cron OR server1",
"html_edit_url": "https://papertrailapp.com/searches/42/edit",
"html_search_url": "https://papertrailapp.com/searches/42"
},
"max_id": "7711582041804800",
"min_id": "7711561783320576"
}`
const sampleCountPayload = `{
"counts": [
{
"source_name": "arthur",
"source_id": 4,
"timeseries": {
"1453248895": 5
}
},
{
"source_name": "ford",
"source_id": 3,
"timeseries": {
"1453248927": 3
}
}
],
"saved_search": {
"id": 42,
"name": "Important stuff",
"query": "cron OR server1",
"html_edit_url": "https://papertrailapp.com/searches/42/edit",
"html_search_url": "https://papertrailapp.com/searches/42"
},
"max_id": "7711582041804800",
"min_id": "7711561783320576"
}`

View file

@ -0,0 +1,97 @@
package papertrail
import (
"encoding/json"
"fmt"
"net/http"
"time"
"github.com/gorilla/mux"
"github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/plugins/common/auth"
)
type Webhook struct {
Path string
acc telegraf.Accumulator
log telegraf.Logger
auth.BasicAuth
}
// Register registers the webhook with the provided router
func (pt *Webhook) Register(router *mux.Router, acc telegraf.Accumulator, log telegraf.Logger) {
router.HandleFunc(pt.Path, pt.eventHandler).Methods("POST")
pt.log = log
pt.log.Infof("Started the papertrail_webhook on %s", pt.Path)
pt.acc = acc
}
func (pt *Webhook) eventHandler(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("Content-Type") != "application/x-www-form-urlencoded" {
http.Error(w, "Unsupported Media Type", http.StatusUnsupportedMediaType)
return
}
if !pt.Verify(r) {
w.WriteHeader(http.StatusUnauthorized)
return
}
data := r.PostFormValue("payload")
if data == "" {
http.Error(w, "Bad Request", http.StatusBadRequest)
return
}
var payload payload
err := json.Unmarshal([]byte(data), &payload)
if err != nil {
http.Error(w, "Bad Request", http.StatusBadRequest)
return
}
if payload.Events != nil {
// Handle event-based payload
for _, e := range payload.Events {
// Warning: Duplicate event timestamps will overwrite each other
tags := map[string]string{
"host": e.Hostname,
"event": payload.SavedSearch.Name,
}
fields := map[string]interface{}{
"count": uint64(1),
"id": e.ID,
"source_ip": e.SourceIP,
"source_name": e.SourceName,
"source_id": int64(e.SourceID),
"program": e.Program,
"severity": e.Severity,
"facility": e.Facility,
"message": e.Message,
"url": fmt.Sprintf("%s?centered_on_id=%d", payload.SavedSearch.SearchURL, e.ID),
"search_id": payload.SavedSearch.ID,
}
pt.acc.AddFields("papertrail", fields, tags, e.ReceivedAt)
}
} else if payload.Counts != nil {
// Handle count-based payload
for _, c := range payload.Counts {
for ts, count := range *c.TimeSeries {
tags := map[string]string{
"host": c.SourceName,
"event": payload.SavedSearch.Name,
}
fields := map[string]interface{}{
"count": count,
}
pt.acc.AddFields("papertrail", fields, tags, time.Unix(ts, 0))
}
}
} else {
http.Error(w, "Bad Request", http.StatusBadRequest)
return
}
w.WriteHeader(http.StatusOK)
}

View file

@ -0,0 +1,41 @@
package papertrail
import (
"time"
)
type event struct {
ID int64 `json:"id"`
ReceivedAt time.Time `json:"received_at"`
DisplayReceivedAt string `json:"display_received_at"`
SourceIP string `json:"source_ip"`
SourceName string `json:"source_name"`
SourceID int `json:"source_id"`
Hostname string `json:"hostname"`
Program string `json:"program"`
Severity string `json:"severity"`
Facility string `json:"facility"`
Message string `json:"message"`
}
type count struct {
SourceName string `json:"source_name"`
SourceID int64 `json:"source_id"`
TimeSeries *map[int64]uint64 `json:"timeseries"`
}
type savedSearch struct {
ID int64 `json:"id"`
Name string `json:"name"`
Query string `json:"query"`
EditURL string `json:"html_edit_url"`
SearchURL string `json:"html_search_url"`
}
type payload struct {
Events []*event `json:"events"`
Counts []*count `json:"counts"`
SavedSearch *savedSearch `json:"saved_search"`
MaxID string `json:"max_id"`
MinID string `json:"min_id"`
}