Skip to content

Commit

Permalink
feat(outputs): add hostname, tags, and custom fields to TimesacleDB o…
Browse files Browse the repository at this point in the history
…utput

Signed-off-by: alika <alika.larsen@gmail.com>
  • Loading branch information
alika committed Mar 28, 2023
1 parent a7d896a commit ec4241e
Show file tree
Hide file tree
Showing 3 changed files with 132 additions and 7 deletions.
28 changes: 25 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1236,14 +1236,36 @@ permissions to access the resources you selected to use, like `SQS`, `Lambda`,
To use TimescaleDB you should create the Hypertable first, following this example

```sql
CREATE TABLE falco_events (
CREATE TABLE falcosidekick_events (
time TIMESTAMPTZ NOT NULL,
rule TEXT,
priority VARCHAR(20),
source VARCHAR(20),
output TEXT
output TEXT,
tags TEXT,
hostname TEXT,
);
SELECT create_hypertable('falco_events', 'time');
SELECT create_hypertable('falcosidekick_events', 'time');
```

To support [`customfields`](#yaml-file) ensure you add the custom field to the Hypertable, for example:

```yaml
customfields: "custom_field_1:custom-value-1"
```
```sql
CREATE TABLE falcosidekick_events (
time TIMESTAMPTZ NOT NULL,
rule TEXT,
priority VARCHAR(20),
source VARCHAR(20),
output TEXT,
tags TEXT,
hostname TEXT,
custom_field_1 TEXT
);
SELECT create_hypertable('falcosidekick_events', 'time');
```

The name from the table should match with the `hypertable` output configuration.
Expand Down
57 changes: 53 additions & 4 deletions outputs/timescaledb.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,18 @@ import (
"context"
"fmt"
"log"
"strings"

"github.com/DataDog/datadog-go/statsd"
"github.com/falcosecurity/falcosidekick/types"
"github.com/jackc/pgx/v5/pgxpool"
)

type timescaledbPayload struct {
SQL string `json:"sql"`
Values []any `json:"values"`
}

func NewTimescaleDBClient(config *types.Configuration, stats *types.Statistics, promStats *types.PromStatistics,
statsdClient, dogstatsdClient *statsd.Client) (*Client, error) {

Expand Down Expand Up @@ -39,14 +45,57 @@ func NewTimescaleDBClient(config *types.Configuration, stats *types.Statistics,
}, nil
}

func newTimescaleDBPayload(falcopayload types.FalcoPayload, config *types.Configuration) timescaledbPayload {
vals := make(map[string]any, 7+len(config.Customfields))
vals[Time] = falcopayload.Time
vals[Rule] = falcopayload.Rule
vals[Priority] = falcopayload.Priority.String()
vals[Source] = falcopayload.Source
vals["output"] = falcopayload.Output

if len(falcopayload.Tags) != 0 {
vals[Tags] = strings.Join(falcopayload.Tags, ",")
}

if falcopayload.Hostname != "" {
vals[Hostname] = falcopayload.Hostname
}

for k, v := range config.Customfields {
if _, exist := vals[k]; !exist {
vals[k] = v
}
}

i := 0
retVals := make([]any, len(vals))
var cols strings.Builder
var args strings.Builder
for k, v := range vals {
cols.WriteString(k)
fmt.Fprintf(&args, "$%d", i+1)
if i < (len(vals) - 1) {
cols.WriteString(",")
args.WriteString(",")
}
retVals[i] = v
i++
}

sql := fmt.Sprintf("INSERT INTO %s (%s) VALUES (%s)",
config.TimescaleDB.HypertableName,
cols.String(),
args.String())

return timescaledbPayload{SQL: sql, Values: retVals}
}

func (c *Client) TimescaleDBPost(falcopayload types.FalcoPayload) {
c.Stats.TimescaleDB.Add(Total, 1)

hypertable := c.Config.TimescaleDB.HypertableName
queryInsertData := fmt.Sprintf("INSERT INTO %s (time, rule, priority, source, output) VALUES ($1, $2, $3, $4, $5)", hypertable)

var ctx = context.Background()
_, err := c.TimescaleDBClient.Exec(ctx, queryInsertData, falcopayload.Time, falcopayload.Rule, falcopayload.Priority.String(), falcopayload.Source, falcopayload.Output)
tsdbPayload := newTimescaleDBPayload(falcopayload, c.Config)
_, err := c.TimescaleDBClient.Exec(ctx, tsdbPayload.SQL, tsdbPayload.Values...)
if err != nil {
go c.CountMetric(Outputs, 1, []string{"output:timescaledb", "status:error"})
c.Stats.TimescaleDB.Add(Error, 1)
Expand Down
54 changes: 54 additions & 0 deletions outputs/timescaledb_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package outputs

import (
"encoding/json"
"regexp"
"strings"
"testing"
"time"

"github.com/stretchr/testify/require"

"github.com/falcosecurity/falcosidekick/types"
)

func TestNewTimescaleDBPayload(t *testing.T) {
expectedTableName := "test_hypertable"
expectedTime, _ := time.Parse(time.RFC3339, "2001-01-01T01:10:00Z")
expectedValues := map[string]any{
"time": expectedTime,
"rule": "Test rule",
"priority": "Debug",
"source": "syscalls",
"output": "This is a test from falcosidekick",
"tags": "test,example",
"hostname": "test-host",
"custom_field_1": "test-custom-value-1",
}

var f types.FalcoPayload
require.Nil(t, json.Unmarshal([]byte(falcoTestInput), &f))
config := &types.Configuration{
Customfields: map[string]string{"custom_field_1": "test-custom-value-1"},
TimescaleDB: types.TimescaleDBConfig{
HypertableName: "test_hypertable",
},
}
output := newTimescaleDBPayload(f, config)

re := regexp.MustCompile(`INSERT\s+INTO\s+(test_hypertable)\s+\((.*)\)\s+VALUES\s+\((.*)\)`)
submatches := re.FindStringSubmatch(output.SQL)
tablename := submatches[1]
cols := strings.Split(submatches[2], ",")

require.Equal(t, expectedTableName, tablename)
require.Equal(t, 8, len(cols))
for i, v := range cols {
require.True(t, strings.Contains("time,rule,priority,source,output,tags,hostname,custom_field_1", v))
if val, exist := expectedValues[v]; exist {
require.Equal(t, val, output.Values[i])
} else {
require.Fail(t, "Missing expected column: %s", v)
}
}
}

0 comments on commit ec4241e

Please sign in to comment.