242 lines
		
	
	
		
			6.7 KiB
		
	
	
	
		
			Go
		
	
	
	
			
		
		
	
	
			242 lines
		
	
	
		
			6.7 KiB
		
	
	
	
		
			Go
		
	
	
	
| package cratedb
 | |
| 
 | |
| import (
 | |
| 	"context"
 | |
| 	"crypto/sha512"
 | |
| 	"database/sql"
 | |
| 	"encoding/binary"
 | |
| 	"fmt"
 | |
| 	"sort"
 | |
| 	"strconv"
 | |
| 	"strings"
 | |
| 	"time"
 | |
| 
 | |
| 	"github.com/influxdata/telegraf"
 | |
| 	"github.com/influxdata/telegraf/internal"
 | |
| 	"github.com/influxdata/telegraf/plugins/outputs"
 | |
| 	_ "github.com/jackc/pgx/stdlib"
 | |
| )
 | |
| 
 | |
| const MaxInt64 = int64(^uint64(0) >> 1)
 | |
| 
 | |
| type CrateDB struct {
 | |
| 	URL         string
 | |
| 	Timeout     internal.Duration
 | |
| 	Table       string
 | |
| 	TableCreate bool `toml:"table_create"`
 | |
| 	DB          *sql.DB
 | |
| }
 | |
| 
 | |
| var sampleConfig = `
 | |
|   # A github.com/jackc/pgx connection string.
 | |
|   # See https://godoc.org/github.com/jackc/pgx#ParseDSN
 | |
|   url = "postgres://user:password@localhost/schema?sslmode=disable"
 | |
|   # Timeout for all CrateDB queries.
 | |
|   timeout = "5s"
 | |
|   # Name of the table to store metrics in.
 | |
|   table = "metrics"
 | |
|   # If true, and the metrics table does not exist, create it automatically.
 | |
|   table_create = true
 | |
| `
 | |
| 
 | |
| func (c *CrateDB) Connect() error {
 | |
| 	db, err := sql.Open("pgx", c.URL)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	} else if c.TableCreate {
 | |
| 		sql := `
 | |
| CREATE TABLE IF NOT EXISTS ` + c.Table + ` (
 | |
| 	"hash_id" LONG INDEX OFF,
 | |
| 	"timestamp" TIMESTAMP,
 | |
| 	"name" STRING,
 | |
| 	"tags" OBJECT(DYNAMIC),
 | |
| 	"fields" OBJECT(DYNAMIC),
 | |
| 	"day" TIMESTAMP GENERATED ALWAYS AS date_trunc('day', "timestamp"),
 | |
| 	PRIMARY KEY ("timestamp", "hash_id","day")
 | |
| ) PARTITIONED BY("day");
 | |
| `
 | |
| 		ctx, cancel := context.WithTimeout(context.Background(), c.Timeout.Duration)
 | |
| 		defer cancel()
 | |
| 		if _, err := db.ExecContext(ctx, sql); err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 	}
 | |
| 	c.DB = db
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func (c *CrateDB) Write(metrics []telegraf.Metric) error {
 | |
| 	ctx, cancel := context.WithTimeout(context.Background(), c.Timeout.Duration)
 | |
| 	defer cancel()
 | |
| 	if sql, err := insertSQL(c.Table, metrics); err != nil {
 | |
| 		return err
 | |
| 	} else if _, err := c.DB.ExecContext(ctx, sql); err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func insertSQL(table string, metrics []telegraf.Metric) (string, error) {
 | |
| 	rows := make([]string, len(metrics))
 | |
| 	for i, m := range metrics {
 | |
| 
 | |
| 		cols := []interface{}{
 | |
| 			hashID(m),
 | |
| 			m.Time().UTC(),
 | |
| 			m.Name(),
 | |
| 			m.Tags(),
 | |
| 			m.Fields(),
 | |
| 		}
 | |
| 
 | |
| 		escapedCols := make([]string, len(cols))
 | |
| 		for i, col := range cols {
 | |
| 			escaped, err := escapeValue(col)
 | |
| 			if err != nil {
 | |
| 				return "", err
 | |
| 			}
 | |
| 			escapedCols[i] = escaped
 | |
| 		}
 | |
| 		rows[i] = `(` + strings.Join(escapedCols, ", ") + `)`
 | |
| 	}
 | |
| 	sql := `INSERT INTO ` + table + ` ("hash_id", "timestamp", "name", "tags", "fields")
 | |
| VALUES
 | |
| ` + strings.Join(rows, " ,\n") + `;`
 | |
| 	return sql, nil
 | |
| }
 | |
| 
 | |
| // escapeValue returns a string version of val that is suitable for being used
 | |
| // inside of a VALUES expression or similar. Unsupported types return an error.
 | |
| //
 | |
| // Warning: This is not ideal from a security perspective, but unfortunately
 | |
| // CrateDB does not support enough of the PostgreSQL wire protocol to allow
 | |
| // using pgx with $1, $2 placeholders [1]. Security conscious users of this
 | |
| // plugin should probably refrain from using it in combination with untrusted
 | |
| // inputs.
 | |
| //
 | |
| // [1] https://github.com/influxdata/telegraf/pull/3210#issuecomment-339273371
 | |
| func escapeValue(val interface{}) (string, error) {
 | |
| 	switch t := val.(type) {
 | |
| 	case string:
 | |
| 		return escapeString(t, `'`), nil
 | |
| 	case int64, float64:
 | |
| 		return fmt.Sprint(t), nil
 | |
| 	case uint64:
 | |
| 		// The long type is the largest integer type in CrateDB and is the
 | |
| 		// size of a signed int64.  If our value is too large send the largest
 | |
| 		// possible value.
 | |
| 		if t <= uint64(MaxInt64) {
 | |
| 			return strconv.FormatInt(int64(t), 10), nil
 | |
| 		} else {
 | |
| 			return strconv.FormatInt(MaxInt64, 10), nil
 | |
| 		}
 | |
| 	case bool:
 | |
| 		return strconv.FormatBool(t), nil
 | |
| 	case time.Time:
 | |
| 		// see https://crate.io/docs/crate/reference/sql/data_types.html#timestamp
 | |
| 		return escapeValue(t.Format("2006-01-02T15:04:05.999-0700"))
 | |
| 	case map[string]string:
 | |
| 		return escapeObject(convertMap(t))
 | |
| 	case map[string]interface{}:
 | |
| 		return escapeObject(t)
 | |
| 	default:
 | |
| 		// This might be panic worthy under normal circumstances, but it's probably
 | |
| 		// better to not shut down the entire telegraf process because of one
 | |
| 		// misbehaving plugin.
 | |
| 		return "", fmt.Errorf("unexpected type: %T: %#v", t, t)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // convertMap converts m from map[string]string to map[string]interface{} by
 | |
| // copying it. Generics, oh generics where art thou?
 | |
| func convertMap(m map[string]string) map[string]interface{} {
 | |
| 	c := make(map[string]interface{}, len(m))
 | |
| 	for k, v := range m {
 | |
| 		c[k] = v
 | |
| 	}
 | |
| 	return c
 | |
| }
 | |
| 
 | |
| func escapeObject(m map[string]interface{}) (string, error) {
 | |
| 	// There is a decent chance that the implementation below doesn't catch all
 | |
| 	// edge cases, but it's hard to tell since the format seems to be a bit
 | |
| 	// underspecified.
 | |
| 	// See https://crate.io/docs/crate/reference/sql/data_types.html#object
 | |
| 
 | |
| 	// We find all keys and sort them first because iterating a map in go is
 | |
| 	// randomized and we need consistent output for our unit tests.
 | |
| 	keys := make([]string, 0, len(m))
 | |
| 	for k, _ := range m {
 | |
| 		keys = append(keys, k)
 | |
| 	}
 | |
| 	sort.Strings(keys)
 | |
| 
 | |
| 	// Now we build our key = val pairs
 | |
| 	pairs := make([]string, 0, len(m))
 | |
| 	for _, k := range keys {
 | |
| 		// escape the value of our key k (potentially recursive)
 | |
| 		val, err := escapeValue(m[k])
 | |
| 		if err != nil {
 | |
| 			return "", err
 | |
| 		}
 | |
| 		pairs = append(pairs, escapeString(k, `"`)+" = "+val)
 | |
| 	}
 | |
| 	return `{` + strings.Join(pairs, ", ") + `}`, nil
 | |
| }
 | |
| 
 | |
| // escapeString wraps s in the given quote string and replaces all occurences
 | |
| // of it inside of s with a double quote.
 | |
| func escapeString(s string, quote string) string {
 | |
| 	return quote + strings.Replace(s, quote, quote+quote, -1) + quote
 | |
| }
 | |
| 
 | |
| // hashID returns a cryptographic hash int64 hash that includes the metric name
 | |
| // and tags. It's used instead of m.HashID() because it's not considered stable
 | |
| // and because a cryptogtaphic hash makes more sense for the use case of
 | |
| // deduplication.
 | |
| // [1] https://github.com/influxdata/telegraf/pull/3210#discussion_r148411201
 | |
| func hashID(m telegraf.Metric) int64 {
 | |
| 	h := sha512.New()
 | |
| 	h.Write([]byte(m.Name()))
 | |
| 	tags := m.Tags()
 | |
| 	tmp := make([]string, len(tags))
 | |
| 	i := 0
 | |
| 	for k, v := range tags {
 | |
| 		tmp[i] = k + v
 | |
| 		i++
 | |
| 	}
 | |
| 	sort.Strings(tmp)
 | |
| 
 | |
| 	for _, s := range tmp {
 | |
| 		h.Write([]byte(s))
 | |
| 	}
 | |
| 	sum := h.Sum(nil)
 | |
| 
 | |
| 	// Note: We have to convert from uint64 to int64 below because CrateDB only
 | |
| 	// supports a signed 64 bit LONG type:
 | |
| 	//
 | |
| 	// CREATE TABLE my_long (val LONG);
 | |
| 	// INSERT INTO my_long(val) VALUES (14305102049502225714);
 | |
| 	// -> ERROR:  SQLParseException: For input string: "14305102049502225714"
 | |
| 	return int64(binary.LittleEndian.Uint64(sum))
 | |
| }
 | |
| 
 | |
| func (c *CrateDB) SampleConfig() string {
 | |
| 	return sampleConfig
 | |
| }
 | |
| 
 | |
| func (c *CrateDB) Description() string {
 | |
| 	return "Configuration for CrateDB to send metrics to."
 | |
| }
 | |
| 
 | |
| func (c *CrateDB) Close() error {
 | |
| 	return c.DB.Close()
 | |
| }
 | |
| 
 | |
| func init() {
 | |
| 	outputs.Add("cratedb", func() telegraf.Output {
 | |
| 		return &CrateDB{
 | |
| 			Timeout: internal.Duration{Duration: time.Second * 5},
 | |
| 		}
 | |
| 	})
 | |
| }
 |