MF-1584 - Upgrade InfluxDB from 1.x to 2.x (#1709)

* Upgrade InfluxDB from 1.x to 2.x

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* Influx DB configuration updated

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* Connection to InfluxDBv2

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* Token cannot be created

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* Connected to InfluxDB2

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* Connected to InfluxDB2

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* InfluxDB v2 Consumer Implementation

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* quickfix

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* Influxdb-Writer Unit Tests Update

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* Consumer Update

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* InfluxDB Writer Tests Implemented

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* fix

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* InfluxDB Connection Check

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* Resolving Remarks

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* Resolved consumer-test remark

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* consumer-test slow working version

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* reader changes

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* Consumer tests time issue fixed

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* Eof warning fixed

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* Resolved Reviews

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* Makefile Fixed

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* Resolved Reviews

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* Resolved Reviews

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* Resolved Reviews

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* Resolved Reviews

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* Resolved Reviews

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* Readers Initial Setup

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* consumer json fix

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* readers simple version

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* readers simple version fix

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* readers simple version fix

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* readers simple version fix

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* readers simple version fix

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* readers simple version fix

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* readers simple version fix

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* Influxdb Reader Parsers

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* Influxdb Reader Parsers

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* Influxdb Reader Parsers

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* Bugfix and resolves comments.

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* one test fails

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* solved last page read

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* writers future time problem fixed

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* weird

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* weird

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* Tests Passes

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* Resolve Semaphore Issues

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* UUID comment on consumer tests resolved

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* Inclusive from and Exclusive to

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* Inclusive from and Exclusive to

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* 1 second limits

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* 1 second limits

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* resolved review

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* resolved review

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* resolved review

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* resolved review

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* fixed json time

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* fixed CI error

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* resolved request

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* resolved request

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* resolved requests

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* removed blank line

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* resolved comment

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* deleted unnecessary string builder

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* initial commit

Signed-off-by: aryan <aryangodara03@gmail.com>

* change influxdb docker image version.

Signed-off-by: aryan <aryangodara03@gmail.com>

* go mod and vendor fixing

Signed-off-by: aryan <aryangodara03@gmail.com>

* Upgrade InfluxDB from 1.x to 2.x

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* Influx DB configuration updated

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* Connection to InfluxDBv2

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* Token cannot be created

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* Connected to InfluxDB2

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* Connected to InfluxDB2

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* InfluxDB v2 Consumer Implementation

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* quickfix

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* Influxdb-Writer Unit Tests Update

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* Consumer Update

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* InfluxDB Writer Tests Implemented

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* fix

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* InfluxDB Connection Check

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* Resolving Remarks

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* Resolved consumer-test remark

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* consumer-test slow working version

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* reader changes

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* Consumer tests time issue fixed

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* Resolved Reviews

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* Resolved Reviews

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* Resolved Reviews

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* Resolved Reviews

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* Resolved Reviews

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* Resolved Reviews

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* Readers Initial Setup

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* consumer json fix

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* readers simple version

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* readers simple version fix

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* readers simple version fix

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* readers simple version fix

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* readers simple version fix

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* readers simple version fix

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* readers simple version fix

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* Influxdb Reader Parsers

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* Influxdb Reader Parsers

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* Influxdb Reader Parsers

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* Bugfix and resolves comments.

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* one test fails

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* solved last page read

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* writers future time problem fixed

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* weird

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* weird

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* Tests Passes

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* Resolve Semaphore Issues

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* UUID comment on consumer tests resolved

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* Inclusive from and Exclusive to

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* Inclusive from and Exclusive to

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* 1 second limits

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* 1 second limits

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* resolved review

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* resolved review

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* resolved review

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* resolved review

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* fixed json time

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* fixed CI error

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* resolved request

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* resolved request

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* resolved requests

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* removed blank line

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* resolved comment

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* deleted unnecessary string builder

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>

* initial commit

Signed-off-by: aryan <aryangodara03@gmail.com>

* change influxdb docker image version.

Signed-off-by: aryan <aryangodara03@gmail.com>

* go mod and vendor fixing

Signed-off-by: aryan <aryangodara03@gmail.com>

* go mod fixing

Signed-off-by: aryan <aryangodara03@gmail.com>

* make tests pass locally, fix errors

Signed-off-by: aryan <aryangodara03@gmail.com>

* rem unsused variables/consts

Signed-off-by: aryan <aryangodara03@gmail.com>

* go mod tidy vendor

Signed-off-by: aryan <aryangodara03@gmail.com>

* add env var and data types

Signed-off-by: aryan <aryangodara03@gmail.com>

* update influxdb version to latest (2.3 to 2.12)

Signed-off-by: aryan <aryangodara03@gmail.com>

* change time precision for message tests

Signed-off-by: aryan <aryangodara03@gmail.com>

* renamed influxdb2 to influxdata

Signed-off-by: aryan <aryangodara03@gmail.com>

* address remarks on PR

Signed-off-by: aryan <aryangodara03@gmail.com>

* update influxbd according to latest mf commit

Signed-off-by: aryan <aryangodara03@gmail.com>

* temp commit, rem before pushing

Signed-off-by: aryan <aryangodara03@gmail.com>

* update main files and cassandra-reader messages_tests

Signed-off-by: aryan <aryangodara03@gmail.com>

* fix name of logger while importing

Signed-off-by: aryan <aryangodara03@gmail.com>

* remove unnecessary print lines

Signed-off-by: aryan <aryangodara03@gmail.com>

* correct env var name

Signed-off-by: aryan <aryangodara03@gmail.com>

* change to async consume

Signed-off-by: aryan <aryangodara03@gmail.com>

* add option to switch bw sync and async

Signed-off-by: aryan <aryangodara03@gmail.com>

* test for both async and sync

Signed-off-by: aryan <aryangodara03@gmail.com>

* update consumer and add writeAPIs to config

Signed-off-by: aryan <aryangodara03@gmail.com>

* revert back to sync consuming

Signed-off-by: aryan <aryangodara03@gmail.com>

* temp fix for default timeout value

Signed-off-by: aryan <aryangodara03@gmail.com>

* set default timeout in config.

Signed-off-by: aryan <aryangodara03@gmail.com>

* remove unwanted env vars, add required ones.

Signed-off-by: aryan <aryangodara03@gmail.com>

* rem unused username password from config

Signed-off-by: aryan <aryangodara03@gmail.com>

* update readme, env vars, and remove grafana

Signed-off-by: aryan <aryangodara03@gmail.com>

* update readme

Signed-off-by: aryan <aryangodara03@gmail.com>

* fix typo

Signed-off-by: aryan <aryangodara03@gmail.com>

* update readme description.

Signed-off-by: aryan <aryangodara03@gmail.com>

* fix more typos.

Signed-off-by: aryan <aryangodara03@gmail.com>

* add link to official docs to readme.

Signed-off-by: aryan <aryangodara03@gmail.com>

---------

Signed-off-by: fatih <fatihdurmaz@sabanciuniv.edu>
Signed-off-by: aryan <aryangodara03@gmail.com>
Co-authored-by: fatih <fatihdurmaz@sabanciuniv.edu>
Co-authored-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>
This commit is contained in:
Aryan Godara
2023-03-15 18:36:14 -07:00
committed by GitHub
parent cea1def6a8
commit 986edacfc7
100 changed files with 97750 additions and 5514 deletions
+1 -1
View File
@@ -23,7 +23,7 @@ For more details, check out the [official documentation][docs].
- Mutual TLS Authentication (mTLS) using X.509 Certificates
- Fine-grained access control (policies, ABAC/RBAC)
- Message persistence (Cassandra, InfluxDB, MongoDB and PostgresSQL)
- Platform logging and instrumentation support (Grafana, Prometheus and OpenTracing)
- Platform logging and instrumentation support (Prometheus and OpenTracing)
- Event sourcing
- Container-based deployment using [Docker][docker] and [Kubernetes][kubernetes]
- [LoRaWAN][lora] network integration
+11 -5
View File
@@ -6,7 +6,7 @@ import (
"log"
"os"
influxdata "github.com/influxdata/influxdb/client/v2"
influxdb2 "github.com/influxdata/influxdb-client-go/v2"
"github.com/mainflux/mainflux/internal"
authClient "github.com/mainflux/mainflux/internal/clients/grpc/auth"
thingsClient "github.com/mainflux/mainflux/internal/clients/grpc/things"
@@ -66,13 +66,19 @@ func main() {
if err := env.Parse(&influxDBConfig, env.Options{Prefix: envPrefixInfluxdb}); err != nil {
logger.Fatal(fmt.Sprintf("failed to load InfluxDB client configuration from environment variable : %s", err))
}
client, err := influxDBClient.Connect(influxDBConfig)
influxDBConfig.DBUrl = fmt.Sprintf("%s://%s:%s", influxDBConfig.Protocol, influxDBConfig.Host, influxDBConfig.Port)
repocfg := influxdb.RepoConfig{
Bucket: influxDBConfig.Bucket,
Org: influxDBConfig.Org,
}
client, err := influxDBClient.Connect(influxDBConfig, ctx)
if err != nil {
logger.Fatal(fmt.Sprintf("failed to connect to InfluxDB : %s", err))
}
defer client.Close()
repo := newService(client, influxDBConfig.DbName, logger)
repo := newService(client, repocfg, logger)
httpServerConfig := server.Config{Port: defSvcHttpPort}
if err := env.Parse(&httpServerConfig, env.Options{Prefix: envPrefixHttp, AltPrefix: envPrefix}); err != nil {
@@ -93,8 +99,8 @@ func main() {
}
}
func newService(client influxdata.Client, dbName string, logger mflog.Logger) readers.MessageRepository {
repo := influxdb.New(client, dbName)
func newService(client influxdb2.Client, repocfg influxdb.RepoConfig, logger mflog.Logger) readers.MessageRepository {
repo := influxdb.New(client, repocfg)
repo = api.LoggingMiddleware(repo, logger)
counter, latency := internal.MakeMetrics("influxdb", "message_reader")
repo = api.MetricsMiddleware(repo, counter, latency)
+11 -5
View File
@@ -9,7 +9,7 @@ import (
"log"
"os"
influxdata "github.com/influxdata/influxdb/client/v2"
influxdb2 "github.com/influxdata/influxdb-client-go/v2"
"github.com/mainflux/mainflux/consumers"
"github.com/mainflux/mainflux/consumers/writers/api"
"github.com/mainflux/mainflux/consumers/writers/influxdb"
@@ -61,13 +61,19 @@ func main() {
if err := env.Parse(&influxDBConfig, env.Options{Prefix: envPrefixInfluxdb}); err != nil {
logger.Fatal(fmt.Sprintf("failed to load InfluxDB client configuration from environment variable : %s", err))
}
client, err := influxDBClient.Connect(influxDBConfig)
influxDBConfig.DBUrl = fmt.Sprintf("%s://%s:%s", influxDBConfig.Protocol, influxDBConfig.Host, influxDBConfig.Port)
repocfg := influxdb.RepoConfig{
Bucket: influxDBConfig.Bucket,
Org: influxDBConfig.Org,
}
client, err := influxDBClient.Connect(influxDBConfig, ctx)
if err != nil {
logger.Fatal(fmt.Sprintf("failed to connect to InfluxDB : %s", err))
}
defer client.Close()
repo := newService(client, influxDBConfig.DbName, logger)
repo := newService(client, repocfg, logger)
if err := consumers.Start(svcName, pubSub, repo, cfg.ConfigPath, logger); err != nil {
logger.Fatal(fmt.Sprintf("failed to start InfluxDB writer: %s", err))
@@ -92,8 +98,8 @@ func main() {
}
}
func newService(client influxdata.Client, dbName string, logger mflog.Logger) consumers.Consumer {
repo := influxdb.New(client, dbName)
func newService(client influxdb2.Client, repocfg influxdb.RepoConfig, logger mflog.Logger) consumers.Consumer {
repo := influxdb.New(client, repocfg, true)
repo = api.LoggingMiddleware(repo, logger)
counter, latency := internal.MakeMetrics("influxdb", "message_writer")
repo = api.MetricsMiddleware(repo, counter, latency)
+24 -3
View File
@@ -12,12 +12,20 @@ default values.
| ----------------------------- | --------------------------------------------------------------------------------- | ---------------------- |
| MF_BROKER_URL | Message broker instance URL | nats://localhost:4222 |
| MF_INFLUX_WRITER_LOG_LEVEL | Log level for InfluxDB writer (debug, info, warn, error) | info |
| MF_INFLUX_WRITER_PORT | Service HTTP port | 8180 |
| MF_INFLUX_WRITER_PORT | Service HTTP port | 8900 |
| MF_INFLUX_WRITER_DB_HOST | InfluxDB host | localhost |
| MF_INFLUXDB_PORT | Default port of InfluxDB database | 8086 |
| MF_INFLUXDB_ADMIN_USER | Default user of InfluxDB database | mainflux |
| MF_INFLUXDB_ADMIN_PASSWORD | Default password of InfluxDB user | mainflux |
| MF_INFLUXDB_DB | InfluxDB database name | mainflux |
| MF_INFLUXDB_HOST | InfluxDB host name | mainflux-influxdb |
| MF_INFLUXDB_PROTOCOL | InfluxDB protocol | http |
| MF_INFLUXDB_TIMEOUT | InfluxDB client connection readiness timeout | 1s |
| MF_INFLUXDB_ORG | InfluxDB organization name | mainflux |
| MF_INFLUXDB_BUCKET | InfluxDB bucket name | mainflux-bucket |
| MF_INFLUXDB_TOKEN | InfluxDB API token | mainflux-token |
| MF_INFLUXDB_HTTP_ENABLED | InfluxDB http enabled status | true |
| MF_INFLUXDB_INIT_MODE | InfluxDB initialization mode | setup |
| MF_INFLUX_WRITER_CONFIG_PATH | Config file path with message broker subjects list, payload type and content-type | /configs.toml |
## Deployment
@@ -47,6 +55,13 @@ MF_INFLUXDB_HOST=[InfluxDB database host] \
MF_INFLUXDB_PORT=[InfluxDB database port] \
MF_INFLUXDB_ADMIN_USER=[InfluxDB admin user] \
MF_INFLUXDB_ADMIN_PASSWORD=[InfluxDB admin password] \
MF_INFLUXDB_PROTOCOL=[InfluxDB protocol] \
MF_INFLUXDB_TIMEOUT=[InfluxDB timeout] \
MF_INFLUXDB_ORG=[InfluxDB org] \
MF_INFLUXDB_BUCKET=[InfluxDB bucket] \
MF_INFLUXDB_TOKEN=[InfluxDB token] \
MF_INFLUXDB_HTTP_ENABLED=[InfluxDB http enabled] \
MF_INFLUXDB_INIT_MODE=[InfluxDB init mode] \
MF_INFLUX_WRITER_CONFIG_PATH=[Config file path with Message broker subjects list, payload type and content-type] \
$GOBIN/mainflux-influxdb
```
@@ -55,17 +70,23 @@ $GOBIN/mainflux-influxdb
This service can be deployed using docker containers.
Docker compose file is available in `<project_root>/docker/addons/influxdb-writer/docker-compose.yml`. Besides database
and writer service, it contains [Grafana platform](https://grafana.com/) which can be used for database
and writer service, it contains InfluxData Web Admin Interface which can be used for database
exploration and data visualization and analytics. In order to run Mainflux InfluxDB writer, execute the following command:
```bash
docker-compose -f docker/addons/influxdb-writer/docker-compose.yml up -d
```
And, to use the default .env file, execute the following command:
```bash
docker-compose -f docker/addons/influxdb-writer/docker-compose.yml up --env-file docker/.env -d
```
_Please note that you need to start core services before the additional ones._
## Usage
Starting service will start consuming normalized messages in SenML format.
[doc]: https://docs.mainflux.io
Official docs can be found [here](https://docs.mainflux.io).
+29 -31
View File
@@ -4,6 +4,7 @@
package influxdb
import (
"context"
"math"
"time"
@@ -12,7 +13,9 @@ import (
"github.com/mainflux/mainflux/pkg/transformers/json"
"github.com/mainflux/mainflux/pkg/transformers/senml"
influxdata "github.com/influxdata/influxdb/client/v2"
influxdb2 "github.com/influxdata/influxdb-client-go/v2"
"github.com/influxdata/influxdb-client-go/v2/api"
"github.com/influxdata/influxdb-client-go/v2/api/write"
)
const senmlPoints = "messages"
@@ -21,65 +24,63 @@ var errSaveMessage = errors.New("failed to save message to influxdb database")
var _ consumers.Consumer = (*influxRepo)(nil)
type RepoConfig struct {
Bucket string
Org string
}
type influxRepo struct {
client influxdata.Client
cfg influxdata.BatchPointsConfig
client influxdb2.Client
cfg RepoConfig
writeAPIBlocking api.WriteAPIBlocking
}
// New returns new InfluxDB writer.
func New(client influxdata.Client, database string) consumers.Consumer {
func New(client influxdb2.Client, config RepoConfig, async bool) consumers.Consumer {
return &influxRepo{
client: client,
cfg: influxdata.BatchPointsConfig{
Database: database,
},
client: client,
cfg: config,
writeAPIBlocking: client.WriteAPIBlocking(config.Org, config.Bucket),
}
}
func (repo *influxRepo) Consume(message interface{}) error {
pts, err := influxdata.NewBatchPoints(repo.cfg)
if err != nil {
return errors.Wrap(errSaveMessage, err)
}
var err error
var pts []*write.Point
switch m := message.(type) {
case json.Messages:
pts, err = repo.jsonPoints(pts, m)
pts, err = repo.jsonPoints(m)
default:
pts, err = repo.senmlPoints(pts, m)
pts, err = repo.senmlPoints(m)
}
if err != nil {
return err
}
if err := repo.client.Write(pts); err != nil {
return errors.Wrap(errSaveMessage, err)
}
return nil
return repo.writeAPIBlocking.WritePoint(context.Background(), pts...)
}
func (repo *influxRepo) senmlPoints(pts influxdata.BatchPoints, messages interface{}) (influxdata.BatchPoints, error) {
func (repo *influxRepo) senmlPoints(messages interface{}) ([]*write.Point, error) {
msgs, ok := messages.([]senml.Message)
if !ok {
return nil, errSaveMessage
}
var pts []*write.Point
for _, msg := range msgs {
tgs, flds := senmlTags(msg), senmlFields(msg)
sec, dec := math.Modf(msg.Time)
t := time.Unix(int64(sec), int64(dec*(1e9)))
pt, err := influxdata.NewPoint(senmlPoints, tgs, flds, t)
if err != nil {
return nil, errors.Wrap(errSaveMessage, err)
}
pts.AddPoint(pt)
pt := influxdb2.NewPoint(senmlPoints, tgs, flds, t)
pts = append(pts, pt)
}
return pts, nil
}
func (repo *influxRepo) jsonPoints(pts influxdata.BatchPoints, msgs json.Messages) (influxdata.BatchPoints, error) {
func (repo *influxRepo) jsonPoints(msgs json.Messages) ([]*write.Point, error) {
var pts []*write.Point
for i, m := range msgs.Data {
t := time.Unix(0, m.Created+int64(i))
@@ -96,11 +97,8 @@ func (repo *influxRepo) jsonPoints(pts influxdata.BatchPoints, msgs json.Message
}
// At least one known field need to exist so that COUNT can be performed.
fields["protocol"] = m.Protocol
pt, err := influxdata.NewPoint(msgs.Format, jsonTags(m), fields, t)
if err != nil {
return nil, errors.Wrap(errSaveMessage, err)
}
pts.AddPoint(pt)
pt := influxdb2.NewPoint(msgs.Format, jsonTags(m), fields, t)
pts = append(pts, pt)
}
return pts, nil
+234 -174
View File
@@ -4,6 +4,7 @@
package influxdb_test
import (
"context"
"fmt"
"os"
"testing"
@@ -11,145 +12,213 @@ import (
"github.com/mainflux/mainflux/pkg/errors"
"github.com/gofrs/uuid"
influxdata "github.com/influxdata/influxdb/client/v2"
influxdata "github.com/influxdata/influxdb-client-go/v2"
writer "github.com/mainflux/mainflux/consumers/writers/influxdb"
log "github.com/mainflux/mainflux/logger"
"github.com/mainflux/mainflux/pkg/transformers/json"
"github.com/mainflux/mainflux/pkg/transformers/senml"
"github.com/mainflux/mainflux/pkg/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const valueFields = 5
var (
port string
testLog, _ = log.New(os.Stdout, log.Info.String())
testDB = "test"
streamsSize = 250
selectMsgs = "SELECT * FROM test..messages"
dropMsgs = "DROP SERIES FROM messages"
client influxdata.Client
clientCfg = influxdata.HTTPConfig{
Username: "test",
Password: "test",
}
subtopic = "topic"
)
testLog, _ = log.New(os.Stdout, log.Info.String())
streamsSize = 250
rowCountSenml = fmt.Sprintf(`from(bucket: "%s")
|> range(start: -1h, stop: 1h)
|> filter(fn: (r) => r["_measurement"] == "messages")
|> filter(fn: (r) => r["_field"] == "dataValue" or r["_field"] == "stringValue" or r["_field"] == "value" or r["_field"] == "boolValue" or r["_field"] == "sum" )
|> group(columns: ["_measurement"])
|> count()
|> yield(name: "count")`, repoCfg.Bucket)
var (
rowCountJson = fmt.Sprintf(`from(bucket: "%s")
|> range(start: -1h, stop: 1h)
|> filter(fn: (r) => r["_measurement"] == "some_json")
|> filter(fn: (r) => r["_field"] == "field_1" or r["_field"] == "field_2" or r["_field"] == "field_3" or r["_field"] == "field_4" or r["_field"] == "field_5/field_1" or r["_field"] == "field_5/field_2")
|> count()
|> yield(name: "count")`, repoCfg.Bucket)
subtopic = "topic"
client influxdata.Client
v float64 = 5
stringV = "value"
boolV = true
dataV = "base64"
sum float64 = 42
repoCfg = writer.RepoConfig{
Bucket: dbBucket,
Org: dbOrg,
}
errUnexpectedType = errors.New("Unexpected response type")
idProvider = uuid.New()
)
// This is utility function to query the database.
func queryDB(cmd string) ([][]interface{}, error) {
q := influxdata.Query{
Command: cmd,
Database: testDB,
}
response, err := client.Query(q)
func deleteBucket() error {
bucketsAPI := client.BucketsAPI()
bucket, err := bucketsAPI.FindBucketByName(context.Background(), repoCfg.Bucket)
if err != nil {
return nil, err
return err
}
if response.Error() != nil {
return nil, response.Error()
if err = bucketsAPI.DeleteBucket(context.Background(), bucket); err != nil {
return err
}
if len(response.Results[0].Series) == 0 {
return nil, nil
return nil
}
func createBucket() error {
orgAPI := client.OrganizationsAPI()
org, err := orgAPI.FindOrganizationByName(context.Background(), repoCfg.Org)
if err != nil {
return err
}
// There is only one query, so only one result and
// all data are stored in the same series.
return response.Results[0].Series[0].Values, nil
bucketsAPI := client.BucketsAPI()
if _, err = bucketsAPI.CreateBucketWithName(context.Background(), org, repoCfg.Bucket); err != nil {
return err
}
return nil
}
func resetBucket() error {
if err := deleteBucket(); err != nil {
return err
}
if err := createBucket(); err != nil {
return err
}
return nil
}
func queryDB(fluxQuery string) (int, error) {
rowCount := 0
queryAPI := client.QueryAPI(repoCfg.Org)
// get QueryTableResult
result, err := queryAPI.Query(context.Background(), fluxQuery)
if err != nil {
return rowCount, err
}
if result.Next() {
value, ok := result.Record().Value().(int64)
if !ok {
return rowCount, errUnexpectedType
}
rowCount = int(value)
}
if result.Err() != nil {
return rowCount, result.Err()
}
return rowCount, nil
}
func TestSaveSenml(t *testing.T) {
repo := writer.New(client, testDB)
for i := 0; i < 2; i++ {
// Testing both async and sync
repo := writer.New(client, repoCfg, i == 0)
cases := []struct {
desc string
msgsNum int
expectedSize int
}{
{
desc: "save a single message",
msgsNum: 1,
expectedSize: 1,
},
{
desc: "save a batch of messages",
msgsNum: streamsSize,
expectedSize: streamsSize,
},
}
for _, tc := range cases {
// Clean previously saved messages.
_, err := queryDB(dropMsgs)
require.Nil(t, err, fmt.Sprintf("Cleaning data from InfluxDB expected to succeed: %s.\n", err))
now := time.Now().UnixNano()
msg := senml.Message{
Channel: "45",
Publisher: "2580",
Protocol: "http",
Name: "test name",
Unit: "km",
UpdateTime: 5456565466,
cases := []struct {
desc string
msgsNum int
expectedSize int
}{
{
desc: "save a single message",
msgsNum: 1,
expectedSize: 1,
},
{
desc: "save a batch of messages",
msgsNum: streamsSize,
expectedSize: streamsSize,
},
}
var msgs []senml.Message
for i := 0; i < tc.msgsNum; i++ {
// Mix possible values as well as value sum.
count := i % valueFields
switch count {
case 0:
msg.Subtopic = subtopic
msg.Value = &v
case 1:
msg.BoolValue = &boolV
case 2:
msg.StringValue = &stringV
case 3:
msg.DataValue = &dataV
case 4:
msg.Sum = &sum
for _, tc := range cases {
err := resetBucket()
assert.Nil(t, err, fmt.Sprintf("Cleaning data from InfluxDB expected to succeed: %s.\n", err))
now := time.Now().UnixNano()
var msgs []senml.Message
chanID, err := idProvider.ID()
assert.Nil(t, err, fmt.Sprintf("got unexpected error: %s\n", err))
pubID, err := idProvider.ID()
assert.Nil(t, err, fmt.Sprintf("got unexpected error: %s\n", err))
for i := 0; i < tc.msgsNum; i++ {
msg := senml.Message{
Channel: chanID,
Publisher: pubID,
Protocol: "http",
Name: "test name",
Unit: "km",
UpdateTime: 5456565466,
}
// Mix possible values as well as value sum.
count := i % valueFields
switch count {
case 0:
msg.Subtopic = subtopic
msg.Value = &v
case 1:
msg.BoolValue = &boolV
case 2:
msg.StringValue = &stringV
case 3:
msg.DataValue = &dataV
case 4:
msg.Sum = &sum
}
msg.Time = float64(now)/float64(1e9) - float64(i)
msgs = append(msgs, msg)
}
msg.Time = float64(now)/float64(1e9) + float64(i)
msgs = append(msgs, msg)
err = repo.Consume(msgs)
assert.Nil(t, err, fmt.Sprintf("Save operation expected to succeed: %s.\n", err))
count, err := queryDB(rowCountSenml)
assert.Nil(t, err, fmt.Sprintf("Querying InfluxDB to retrieve data expected to succeed: %s.\n", err))
assert.Equal(t, tc.expectedSize, count, fmt.Sprintf("Expected to have %d messages saved, found %d instead.\n", tc.expectedSize, count))
}
err = repo.Consume(msgs)
assert.Nil(t, err, fmt.Sprintf("Save operation expected to succeed: %s.\n", err))
row, err := queryDB(selectMsgs)
assert.Nil(t, err, fmt.Sprintf("Querying InfluxDB to retrieve data expected to succeed: %s.\n", err))
count := len(row)
assert.Equal(t, tc.expectedSize, count, fmt.Sprintf("Expected to have %d messages saved, found %d instead.\n", tc.expectedSize, count))
}
}
func TestSaveJSON(t *testing.T) {
repo := writer.New(client, testDB)
// Testing both async and sync
for i := 0; i < 2; i++ {
// Testing both async and sync
repo := writer.New(client, repoCfg, i == 0)
chid, err := uuid.NewV4()
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
pubid, err := uuid.NewV4()
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
chanID, err := idProvider.ID()
assert.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
pubID, err := idProvider.ID()
assert.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
msg := json.Message{
Channel: chid.String(),
Publisher: pubid.String(),
Created: time.Now().Unix(),
Subtopic: "subtopic/format/some_json",
Protocol: "mqtt",
Payload: map[string]interface{}{
msg := json.Message{
Channel: chanID,
Publisher: pubID,
Created: time.Now().UnixNano(),
Subtopic: "subtopic/format/some_json",
Protocol: "mqtt",
Payload: map[string]interface{}{
"field_1": 123,
"field_2": "value",
"field_3": false,
"field_4": 12.344,
"field_5": map[string]interface{}{
"field_1": "value",
"field_2": 42,
},
},
}
invalidKeySepMsg := msg
invalidKeySepMsg.Payload = map[string]interface{}{
"field_1": 123,
"field_2": "value",
"field_3": false,
@@ -158,81 +227,72 @@ func TestSaveJSON(t *testing.T) {
"field_1": "value",
"field_2": 42,
},
},
}
"field_6/field_7": "value",
}
invalidKeyNameMsg := msg
invalidKeyNameMsg.Payload = map[string]interface{}{
"field_1": 123,
"field_2": "value",
"field_3": false,
"field_4": 12.344,
"field_5": map[string]interface{}{
"field_1": "value",
"field_2": 42,
},
"publisher": "value",
}
invalidKeySepMsg := msg
invalidKeySepMsg.Payload = map[string]interface{}{
"field_1": 123,
"field_2": "value",
"field_3": false,
"field_4": 12.344,
"field_5": map[string]interface{}{
"field_1": "value",
"field_2": 42,
},
"field_6/field_7": "value",
}
invalidKeyNameMsg := msg
invalidKeyNameMsg.Payload = map[string]interface{}{
"field_1": 123,
"field_2": "value",
"field_3": false,
"field_4": 12.344,
"field_5": map[string]interface{}{
"field_1": "value",
"field_2": 42,
},
"publisher": "value",
}
now := time.Now().UnixNano()
msgs := json.Messages{
Format: "some_json",
}
invalidKeySepMsgs := json.Messages{
Format: "some_json",
}
invalidKeyNameMsgs := json.Messages{
Format: "some_json",
}
now := time.Now().Unix()
msgs := json.Messages{
Format: "some_json",
}
invalidKeySepMsgs := json.Messages{
Format: "some_json",
}
invalidKeyNameMsgs := json.Messages{
Format: "some_json",
}
for i := 0; i < streamsSize; i++ {
msg.Created = now
msgs.Data = append(msgs.Data, msg)
invalidKeySepMsgs.Data = append(invalidKeySepMsgs.Data, invalidKeySepMsg)
invalidKeyNameMsgs.Data = append(invalidKeyNameMsgs.Data, invalidKeyNameMsg)
}
for i := 0; i < streamsSize; i++ {
msg.Created = now + int64(i)
msgs.Data = append(msgs.Data, msg)
invalidKeySepMsgs.Data = append(invalidKeySepMsgs.Data, invalidKeySepMsg)
invalidKeyNameMsgs.Data = append(invalidKeyNameMsgs.Data, invalidKeyNameMsg)
}
cases := []struct {
desc string
msgs json.Messages
err error
}{
{
desc: "consume valid json messages",
msgs: msgs,
err: nil,
},
{
desc: "consume invalid json messages containing invalid key separator",
msgs: invalidKeySepMsgs,
err: json.ErrInvalidKey,
},
{
desc: "consume invalid json messages containing invalid key name",
msgs: invalidKeySepMsgs,
err: json.ErrInvalidKey,
},
}
for _, tc := range cases {
err := resetBucket()
assert.Nil(t, err, fmt.Sprintf("Cleaning data from InfluxDB expected to succeed: %s.\n", err))
cases := []struct {
desc string
msgs json.Messages
err error
}{
{
desc: "consume valid json messages",
msgs: msgs,
err: nil,
},
{
desc: "consume invalid json messages containing invalid key separator",
msgs: invalidKeySepMsgs,
err: json.ErrInvalidKey,
},
{
desc: "consume invalid json messages containing invalid key name",
msgs: invalidKeySepMsgs,
err: json.ErrInvalidKey,
},
}
for _, tc := range cases {
err = repo.Consume(tc.msgs)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s expected %s, got %s", tc.desc, tc.err, err))
row, err := queryDB(selectMsgs)
assert.Nil(t, err, fmt.Sprintf("Querying InfluxDB to retrieve data expected to succeed: %s.\n", err))
count := len(row)
assert.Equal(t, streamsSize, count, fmt.Sprintf("Expected to have %d messages saved, found %d instead.\n", streamsSize, count))
switch err = repo.Consume(tc.msgs); err {
case nil:
count, err := queryDB(rowCountJson)
assert.Nil(t, err, fmt.Sprintf("Querying InfluxDB to retrieve data expected to succeed: %s.\n", err))
assert.Equal(t, streamsSize, count, fmt.Sprintf("Expected to have %d messages saved, found %d instead.\n", streamsSize, count))
default:
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s expected %s, got %s", tc.desc, tc.err, err))
}
}
}
}
+49 -10
View File
@@ -4,15 +4,36 @@
package influxdb_test
import (
"context"
"fmt"
"log"
"os"
"os/signal"
"syscall"
"testing"
"time"
influxdb "github.com/influxdata/influxdb/client/v2"
influxdata "github.com/influxdata/influxdb-client-go/v2"
dockertest "github.com/ory/dockertest/v3"
)
const (
dbToken = "test-token"
dbOrg = "test-org"
dbAdmin = "test-admin"
dbPass = "test-password"
dbBucket = "test-bucket"
dbInitMode = "setup"
dbFluxEnabled = "true"
dbBindAddress = ":8088"
port = "8086/tcp"
broker = "influxdb"
brokerVersion = "2.2-alpine"
poolMaxWait = 120 * time.Second
)
var address string
func TestMain(m *testing.M) {
pool, err := dockertest.NewPool("")
if err != nil {
@@ -20,31 +41,49 @@ func TestMain(m *testing.M) {
}
cfg := []string{
"INFLUXDB_USER=test",
"INFLUXDB_USER_PASSWORD=test",
"INFLUXDB_DB=test",
fmt.Sprintf("DOCKER_INFLUXDB_INIT_MODE=%s", dbInitMode),
fmt.Sprintf("DOCKER_INFLUXDB_INIT_USERNAME=%s", dbAdmin),
fmt.Sprintf("DOCKER_INFLUXDB_INIT_PASSWORD=%s", dbPass),
fmt.Sprintf("DOCKER_INFLUXDB_INIT_ORG=%s", dbOrg),
fmt.Sprintf("DOCKER_INFLUXDB_INIT_BUCKET=%s", dbBucket),
fmt.Sprintf("DOCKER_INFLUXDB_INIT_ADMIN_TOKEN=%s", dbToken),
fmt.Sprintf("INFLUXDB_HTTP_FLUX_ENABLED=%s", dbFluxEnabled),
fmt.Sprintf("INFLUXDB_BIND_ADDRESS=%s", dbBindAddress),
}
container, err := pool.Run("influxdb", "1.8.5", cfg)
container, err := pool.Run(broker, brokerVersion, cfg)
if err != nil {
testLog.Error(fmt.Sprintf("Could not start container: %s", err))
}
port = container.GetPort("8086/tcp")
clientCfg.Addr = fmt.Sprintf("http://localhost:%s", port)
handleInterrupt(m, pool, container)
address = fmt.Sprintf("%s:%s", "http://localhost", container.GetPort(port))
pool.MaxWait = poolMaxWait
if err := pool.Retry(func() error {
client, err = influxdb.NewHTTPClient(clientCfg)
_, _, err = client.Ping(5 * time.Millisecond)
client = influxdata.NewClientWithOptions(address, dbToken, influxdata.DefaultOptions())
_, err = client.Ready(context.Background())
return err
}); err != nil {
testLog.Error(fmt.Sprintf("Could not connect to docker: %s", err))
}
code := m.Run()
if err := pool.Purge(container); err != nil {
testLog.Error(fmt.Sprintf("Could not purge container: %s", err))
}
os.Exit(code)
}
func handleInterrupt(m *testing.M, pool *dockertest.Pool, container *dockertest.Resource) {
c := make(chan os.Signal, 2)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
go func() {
<-c
if err := pool.Purge(container); err != nil {
log.Fatalf("Could not purge container: %s", err)
}
os.Exit(0)
}()
}
+9 -6
View File
@@ -26,9 +26,6 @@ MF_BROKER_URL=${MF_NATS_URL}
## Redis
MF_REDIS_TCP_PORT=6379
## Grafana
MF_GRAFANA_PORT=3000
## Jaeger
MF_JAEGER_PORT=6831
MF_JAEGER_FRONTEND=16686
@@ -240,23 +237,29 @@ MF_CASSANDRA_READER_SERVER_KEY=
### InfluxDB
MF_INFLUXDB_PORT=8086
MF_INFLUXDB_DB=mainflux
MF_INFLUXDB_HOST=mainflux-influxdb
MF_INFLUXDB_ADMIN_USER=mainflux
MF_INFLUXDB_ADMIN_PASSWORD=mainflux
MF_INFLUXDB_HTTP_AUTH_ENABLED=true
MF_INFLUXDB_PROTOCOL=http
MF_INFLUXDB_TIMEOUT=1s
MF_INFLUXDB_ORG=mainflux
MF_INFLUXDB_BUCKET=mainflux-bucket
MF_INFLUXDB_TOKEN=mainflux-token
MF_INFLUXDB_HTTP_ENABLED=true
MF_INFLUXDB_INIT_MODE=setup
### InfluxDB Writer
MF_INFLUX_WRITER_LOG_LEVEL=debug
MF_INFLUX_WRITER_PORT=8900
MF_INFLUX_WRITER_BATCH_SIZE=5000
MF_INFLUX_WRITER_BATCH_TIMEOUT=5
MF_INFLUX_WRITER_GRAFANA_PORT=3001
### InfluxDB Reader
MF_INFLUX_READER_LOG_LEVEL=debug
MF_INFLUX_READER_PORT=8905
MF_INFLUX_READER_SERVER_KEY=
MF_INFLUX_READER_SERVER_CERT=
MF_INFLUXDB_DB=mainflux
### MongoDB Writer
MF_MONGO_WRITER_LOG_LEVEL=debug
@@ -25,6 +25,8 @@ services:
MF_INFLUXDB_DB: ${MF_INFLUXDB_DB}
MF_INFLUXDB_HOST: mainflux-influxdb
MF_INFLUXDB_PORT: ${MF_INFLUXDB_PORT}
MF_INFLUXDB_TIMEOUT: ${MF_INFLUXDB_TIMEOUT}
MF_INFLUXDB_PROTOCOL: ${MF_INFLUXDB_PROTOCOL}
MF_INFLUXDB_ADMIN_USER: ${MF_INFLUXDB_ADMIN_USER}
MF_INFLUXDB_ADMIN_PASSWORD: ${MF_INFLUXDB_ADMIN_PASSWORD}
MF_INFLUX_READER_SERVER_CERT: ${MF_INFLUX_READER_SERVER_CERT}
@@ -1,7 +1,7 @@
# Copyright (c) Mainflux
# SPDX-License-Identifier: Apache-2.0
# This docker-compose file contains optional InfluxDB, InfluxDB-writer and Grafana services
# This docker-compose file contains optional InfluxDB and InfluxDB-writer services
# for the Mainflux platform. Since this services are optional, this file is dependent on the
# docker-compose.yml file from <project_root>/docker/. In order to run these services,
# core services, as well as the network from the core composition, should be already running.
@@ -14,18 +14,20 @@ networks:
volumes:
mainflux-influxdb-volume:
mainflux-grafana-volume:
services:
influxdb:
image: influxdb:1.8.5
image: influxdb:2.5
container_name: mainflux-influxdb
restart: on-failure
environment:
INFLUXDB_DB: ${MF_INFLUXDB_DB}
INFLUXDB_ADMIN_USER: ${MF_INFLUXDB_ADMIN_USER}
INFLUXDB_ADMIN_PASSWORD: ${MF_INFLUXDB_ADMIN_PASSWORD}
INFLUXDB_HTTP_AUTH_ENABLED: ${MF_INFLUXDB_HTTP_AUTH_ENABLED}
DOCKER_INFLUXDB_INIT_MODE: ${MF_INFLUXDB_INIT_MODE}
DOCKER_INFLUXDB_INIT_USERNAME: ${MF_INFLUXDB_ADMIN_USER}
DOCKER_INFLUXDB_INIT_PASSWORD: ${MF_INFLUXDB_ADMIN_PASSWORD}
DOCKER_INFLUXDB_INIT_ORG: ${MF_INFLUXDB_ORG}
DOCKER_INFLUXDB_INIT_BUCKET: ${MF_INFLUXDB_BUCKET}
DOCKER_INFLUXDB_INIT_ADMIN_TOKEN: ${MF_INFLUXDB_TOKEN}
INFLUXDB_HTTP_FLUX_ENABLED: ${MF_INFLUXDB_HTTP_ENABLED}
networks:
- docker_mainflux-base-net
ports:
@@ -45,9 +47,10 @@ services:
MF_INFLUX_WRITER_PORT: ${MF_INFLUX_WRITER_PORT}
MF_INFLUX_WRITER_BATCH_SIZE: ${MF_INFLUX_WRITER_BATCH_SIZE}
MF_INFLUX_WRITER_BATCH_TIMEOUT: ${MF_INFLUX_WRITER_BATCH_TIMEOUT}
MF_INFLUXDB_DB: ${MF_INFLUXDB_DB}
MF_INFLUXDB_HOST: mainflux-influxdb
MF_INFLUXDB_HOST: ${MF_INFLUXDB_HOST}
MF_INFLUXDB_PORT: ${MF_INFLUXDB_PORT}
MF_INFLUXDB_TIMEOUT: ${MF_INFLUXDB_TIMEOUT}
MF_INFLUXDB_PROTOCOL: ${MF_INFLUXDB_PROTOCOL}
MF_INFLUXDB_ADMIN_USER: ${MF_INFLUXDB_ADMIN_USER}
MF_INFLUXDB_ADMIN_PASSWORD: ${MF_INFLUXDB_ADMIN_PASSWORD}
ports:
@@ -56,16 +59,3 @@ services:
- docker_mainflux-base-net
volumes:
- ./config.toml:/config.toml
grafana:
image: grafana/grafana:7.3.7
container_name: mainflux-grafana
depends_on:
- influxdb
restart: on-failure
ports:
- ${MF_INFLUX_WRITER_GRAFANA_PORT}:${MF_GRAFANA_PORT}
networks:
- docker_mainflux-base-net
volumes:
- mainflux-grafana-volume:/var/lib/grafana
@@ -1,7 +1,7 @@
# Copyright (c) Mainflux
# SPDX-License-Identifier: Apache-2.0
# This docker-compose file contains optional InfluxDB, InfluxDB-writer and Grafana services
# This docker-compose file contains optional InfluxDB and InfluxDB-writer services
# for the Mainflux platform. Since this services are optional, this file is dependent on the
# docker-compose.yml file from <project_root>/docker/. In order to run these services,
# core services, as well as the network from the core composition, should be already running.
@@ -1,7 +1,7 @@
# Copyright (c) Mainflux
# SPDX-License-Identifier: Apache-2.0
# This docker-compose file contains optional InfluxDB, InfluxDB-writer and Grafana services
# This docker-compose file contains optional InfluxDB and InfluxDB-writer services
# for the Mainflux platform. Since this services are optional, this file is dependent on the
# docker-compose.yml file from <project_root>/docker/. In order to run these services,
# core services, as well as the network from the core composition, should be already running.
+3 -1
View File
@@ -21,7 +21,7 @@ require (
github.com/gorilla/websocket v1.5.0
github.com/hashicorp/vault/api v1.8.1
github.com/hokaccha/go-prettyjson v0.0.0-20211117102719-0474bc63780f
github.com/influxdata/influxdb v1.10.0
github.com/influxdata/influxdb-client-go/v2 v2.12.2
github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa
github.com/jackc/pgtype v1.13.0
github.com/jackc/pgx/v5 v5.2.0
@@ -65,6 +65,7 @@ require (
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/containerd/continuity v0.3.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/deepmap/oapi-codegen v1.8.2 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/docker/cli v20.10.21+incompatible // indirect
github.com/docker/go-connections v0.4.0 // indirect
@@ -98,6 +99,7 @@ require (
github.com/hashicorp/yamux v0.1.1 // indirect
github.com/imdario/mergo v0.3.13 // indirect
github.com/inconshreveable/mousetrap v1.0.1 // indirect
github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839 // indirect
github.com/jackc/pgio v1.0.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect
+32 -2
View File
@@ -113,10 +113,14 @@ github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw=
github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/cyberdelia/templates v0.0.0-20141128023046-ca7fffd4298c/go.mod h1:GyV+0YP4qX0UQ7r2MoYZ+AvYDp12OF5yg4q8rGnyNh4=
github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/deepmap/oapi-codegen v1.6.0/go.mod h1:ryDa9AgbELGeB+YEXE1dR53yAjHwFvE9iAUlWl9Al3M=
github.com/deepmap/oapi-codegen v1.8.2 h1:SegyeYGcdi0jLLrpbCMoJxnUUn8GBXHsvr4rbzjuhfU=
github.com/deepmap/oapi-codegen v1.8.2/go.mod h1:YLgSKSDv/bZQB7N4ws6luhozi3cEdRktEqrX88CvjIw=
github.com/denisenkom/go-mssqldb v0.9.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
@@ -157,8 +161,11 @@ github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbS
github.com/fxamacker/cbor/v2 v2.2.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo=
github.com/fxamacker/cbor/v2 v2.4.0 h1:ri0ArlOR+5XunOP8CRUowT0pSJOwhW098ZCUyskZD88=
github.com/fxamacker/cbor/v2 v2.4.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo=
github.com/getkin/kin-openapi v0.53.0/go.mod h1:7Yn5whZr5kJi6t+kShccXS8ae1APpYTW6yheSwk8Yi4=
github.com/getkin/kin-openapi v0.61.0/go.mod h1:7Yn5whZr5kJi6t+kShccXS8ae1APpYTW6yheSwk8Yi4=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-acme/lego v2.7.2+incompatible/go.mod h1:yzMNe9CasVUhkquNvti5nAtPmG94USbYxYrZfTkIn0M=
github.com/go-chi/chi/v5 v5.0.0/go.mod h1:BBug9lr0cqtdAhsu6R4AAdvufI0/XBzAQSsUqJpoZOs=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
@@ -180,6 +187,8 @@ github.com/go-logfmt/logfmt v0.5.1 h1:otpy5pqBCBZ1ng9RQ0dPu4PN7ba75Y/aA+UpowDyNV
github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
github.com/go-ocf/go-coap/v2 v2.0.4-0.20200728125043-f38b86f047a7/go.mod h1:X9wVKcaOSx7wBxKcvrWgMQq1R2DNeA7NBLW2osIb8TM=
github.com/go-ocf/kit v0.0.0-20200728130040-4aebdb6982bc/go.mod h1:TIsoMT/iB7t9P6ahkcOnsmvS83SIJsv9qXRfz/yLf6M=
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
@@ -247,6 +256,7 @@ github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEW
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golangci/lint-1 v0.0.0-20181222135242-d2cdd8c08219/go.mod h1:/X8TswGSh1pIozq4ZwCfxS0WA5JGXguxk94ar/4c87Y=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
@@ -289,6 +299,7 @@ github.com/gopcua/opcua v0.1.6 h1:B9SVRKQGzcWcwP2QPYN93Uku32+3wL+v5cgzBxE6V5I=
github.com/gopcua/opcua v0.1.6/go.mod h1:INwnDoRxmNWAt7+tzqxuGqQkSF2c1C69VAL0c2q6AcY=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
@@ -369,8 +380,12 @@ github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc=
github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/influxdata/influxdb v1.10.0 h1:8xDpt8KO3lzrzf/ss+l8r42AGUZvoITu5824berK7SE=
github.com/influxdata/influxdb v1.10.0/go.mod h1:IVPuoA2pOOxau/NguX7ipW0Jp9Bn+dMWlo0+VOscevU=
github.com/influxdata/influxdb-client-go/v2 v2.3.1-0.20210518120617-5d1fff431040 h1:MBLCfcSsUyFPDJp6T7EoHp/Ph3Jkrm4EuUKLD2rUWHg=
github.com/influxdata/influxdb-client-go/v2 v2.3.1-0.20210518120617-5d1fff431040/go.mod h1:vLNHdxTJkIf2mSLvGrpj8TCcISApPoXkaxP8g9uRlW8=
github.com/influxdata/influxdb-client-go/v2 v2.12.2 h1:uYABKdrEKlYm+++qfKdbgaHKBPmoWR5wpbmj6MBB/2g=
github.com/influxdata/influxdb-client-go/v2 v2.12.2/go.mod h1:YteV91FiQxRdccyJ2cHvj2f/5sq4y4Njqu1fQzsQCOU=
github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839 h1:W9WBk7wlPfJLvMCdtV4zPulc4uCPrlywQOmbFOhgQNU=
github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo=
github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0=
github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=
github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
@@ -458,6 +473,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/labstack/echo/v4 v4.2.1/go.mod h1:AA49e0DZ8kk5jTOOCKNuPR6oTnBS0dYiM4FW1e6jwpg=
github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=
github.com/lestrrat-go/iter v0.0.0-20200422075355-fc1769541911/go.mod h1:zIdgO1mRKhn8l9vrZJZz9TUMMFbQbLeTsbqPDrJ/OJc=
github.com/lestrrat-go/jwx v1.0.2/go.mod h1:TPF17WiSFegZo+c20fdpw49QD+/7n4/IsGvEmCSWwT0=
github.com/lestrrat-go/pdebug v0.0.0-20200204225717-4d6bd78da58d/go.mod h1:B06CSso/AWxiPejj+fheUINGeBKeeEZNt8w+EoU7+L8=
@@ -470,6 +487,8 @@ github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw=
github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo=
github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mainflux/mproxy v0.2.3 h1:tKc3i+/fNLUb2qwLVDinAJmtW7SMHAE9cgoueJmYsIU=
github.com/mainflux/mproxy v0.2.3/go.mod h1:qJGrcNHt2e63IEIXowgHyRqgjIqsgOoU0CjAUTOUvII=
github.com/mainflux/senml v1.5.0 h1:GAd1y1eMohfa6sVYcr2iQfVfkkh9l/q7B1TWF5L68xs=
@@ -480,9 +499,13 @@ github.com/markbates/oncer v1.0.0 h1:E83IaVAHygyndzPimgUYJjbshhDTALZyXxvk9FOlQRY
github.com/markbates/oncer v1.0.0/go.mod h1:Z59JA581E9GP6w96jai+TGqafHPW+cPfRxz2aSZ0mcI=
github.com/markbates/safe v1.0.1 h1:yjZkbvRM6IzKj9tlu/zMJLS0n/V351OZWRnF3QfaUxI=
github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0=
github.com/matryer/moq v0.0.0-20190312154309-6cfb0558e1bd/go.mod h1:9ELz6aaclSIGnZBoaSLZ3NAl1VTufbOrXBPvtcy6WiQ=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
@@ -490,6 +513,8 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
@@ -731,6 +756,8 @@ github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtX
github.com/urfave/cli v1.22.5/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.12.0/go.mod h1:229t1eWu9UXTPmoUkbpN/fctKPBY4IJoFXQnxHGXy6E=
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio=
github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE=
github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU=
@@ -808,6 +835,7 @@ golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20210314154223-e6e6c4f2bb5b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
@@ -981,6 +1009,7 @@ golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200826173525-f9321e4c35a6/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -1042,6 +1071,7 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.1.0 h1:xYY+Bajn2a7VBmTM5GikTmnK8ZuX8YgnQCqZpbBNtmA=
golang.org/x/time v0.1.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+1 -2
View File
@@ -18,7 +18,6 @@ func sendMessageEndpoint(svc http.Service) endpoint.Endpoint {
return nil, err
}
err := svc.Publish(ctx, req.token, req.msg)
return nil, err
return nil, svc.Publish(ctx, req.token, req.msg)
}
}
+17 -24
View File
@@ -1,10 +1,10 @@
package influxdb
import (
"fmt"
"context"
"time"
"github.com/influxdata/influxdb/client/v2"
influxdb2 "github.com/influxdata/influxdb-client-go/v2"
"github.com/mainflux/mainflux/internal/env"
"github.com/mainflux/mainflux/pkg/errors"
)
@@ -15,38 +15,31 @@ var (
)
type Config struct {
Protocol string `env:"PROTOCOL" envDefault:"http"`
Host string `env:"HOST" envDefault:"localhost"`
Port string `env:"PORT" envDefault:"8086"`
Username string `env:"ADMIN_USER" envDefault:"mainflux"`
Password string `env:"ADMIN_PASSWORD" envDefault:"mainflux"`
DbName string `env:"DB" envDefault:"mainflux"`
UserAgent string `env:"USER_AGENT" envDefault:"InfluxDBClient"`
Timeout time.Duration `env:"TIMEOUT"` // Influxdb client configuration by default there is no timeout duration , this field will not have fallback default timeout duration Reference: https://pkg.go.dev/github.com/influxdata/influxdb@v1.10.0/client/v2#HTTPConfig
InsecureSkipVerify bool `env:"INSECURE_SKIP_VERIFY" envDefault:"false"`
Protocol string `env:"PROTOCOL" envDefault:"http"`
Host string `env:"HOST" envDefault:"localhost"`
Port string `env:"PORT" envDefault:"8086"`
Bucket string `env:"BUCKET" envDefault:"mainflux-bucket"`
Org string `env:"ORG" envDefault:"mainflux"`
Token string `env:"TOKEN" envDefault:"mainflux-token"`
DBUrl string `env:"DBURL" envDefault:""`
Timeout time.Duration `env:"TIMEOUT" envDefault:"1s"`
}
// Setup load configuration from environment variable, create InfluxDB client and connect to InfluxDB server
func Setup(envPrefix string) (client.HTTPClient, error) {
func Setup(envPrefix string, ctx context.Context) (influxdb2.Client, error) {
config := Config{}
if err := env.Parse(&config, env.Options{Prefix: envPrefix}); err != nil {
return nil, errors.Wrap(errConfig, err)
}
return Connect(config)
return Connect(config, ctx)
}
// Connect create InfluxDB client and connect to InfluxDB server
func Connect(config Config) (client.HTTPClient, error) {
address := fmt.Sprintf("%s://%s:%s", config.Protocol, config.Host, config.Port)
clientConfig := client.HTTPConfig{
Addr: address,
Username: config.Username,
Password: config.Password,
UserAgent: config.UserAgent,
Timeout: config.Timeout,
}
client, err := client.NewHTTPClient(clientConfig)
if err != nil {
func Connect(config Config, ctx context.Context) (influxdb2.Client, error) {
client := influxdb2.NewClient(config.DBUrl, config.Token)
ctx, cancel := context.WithTimeout(ctx, config.Timeout)
defer cancel()
if _, err := client.Ready(ctx); err != nil {
return nil, errors.Wrap(errConnect, err)
}
return client, nil
-771
View File
@@ -1,771 +0,0 @@
{
"__inputs": [
{
"name": "DS_MAINFLUX",
"label": "mainflux",
"description": "",
"type": "datasource",
"pluginId": "prometheus",
"pluginName": "Prometheus"
}
],
"__requires": [
{
"type": "grafana",
"id": "grafana",
"name": "Grafana",
"version": "5.1.3"
},
{
"type": "panel",
"id": "graph",
"name": "Graph",
"version": "5.0.0"
},
{
"type": "datasource",
"id": "prometheus",
"name": "Prometheus",
"version": "5.0.0"
}
],
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": "-- Grafana --",
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"type": "dashboard"
}
]
},
"editable": true,
"gnetId": null,
"graphTooltip": 0,
"id": null,
"links": [],
"panels": [
{
"aliasColors": {},
"bars": false,
"dashLength": 10,
"dashes": false,
"datasource": "${DS_MAINFLUX}",
"fill": 1,
"gridPos": {
"h": 9,
"w": 12,
"x": 0,
"y": 0
},
"id": 10,
"legend": {
"avg": false,
"current": false,
"max": false,
"min": false,
"show": true,
"total": false,
"values": false
},
"lines": true,
"linewidth": 1,
"links": [],
"nullPointMode": "null",
"percentage": false,
"pointradius": 5,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"spaceLength": 10,
"stack": false,
"steppedLine": false,
"targets": [
{
"expr": "rate(ws_adapter_api_request_count{job=\"mainflux\"}[30s])",
"format": "time_series",
"intervalFactor": 1,
"legendFormat": "ws_count_{{method}}",
"refId": "A"
}
],
"thresholds": [],
"timeFrom": null,
"timeShift": null,
"title": "WebSocket adapter request count",
"tooltip": {
"shared": true,
"sort": 0,
"value_type": "individual"
},
"type": "graph",
"xaxis": {
"buckets": null,
"mode": "time",
"name": null,
"show": true,
"values": []
},
"yaxes": [
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
},
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
}
],
"yaxis": {
"align": false,
"alignLevel": null
}
},
{
"aliasColors": {},
"bars": false,
"dashLength": 10,
"dashes": false,
"datasource": "${DS_MAINFLUX}",
"fill": 1,
"gridPos": {
"h": 9,
"w": 12,
"x": 12,
"y": 0
},
"id": 18,
"legend": {
"avg": false,
"current": false,
"max": false,
"min": false,
"show": true,
"total": false,
"values": false
},
"lines": true,
"linewidth": 1,
"links": [],
"nullPointMode": "null",
"percentage": false,
"pointradius": 5,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"spaceLength": 10,
"stack": false,
"steppedLine": false,
"targets": [
{
"expr": "avg(avg_over_time(ws_adapter_api_request_latency_microseconds{job=\"mainflux\"}[30s])) by (method)",
"format": "time_series",
"intervalFactor": 1,
"legendFormat": "ws_latency_{{method}}",
"refId": "A"
}
],
"thresholds": [],
"timeFrom": null,
"timeShift": null,
"title": "WebSocket adapter request latency",
"tooltip": {
"shared": true,
"sort": 0,
"value_type": "individual"
},
"type": "graph",
"xaxis": {
"buckets": null,
"mode": "time",
"name": null,
"show": true,
"values": []
},
"yaxes": [
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
},
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
}
],
"yaxis": {
"align": false,
"alignLevel": null
}
},
{
"aliasColors": {},
"bars": false,
"dashLength": 10,
"dashes": false,
"datasource": "${DS_MAINFLUX}",
"fill": 1,
"gridPos": {
"h": 9,
"w": 12,
"x": 0,
"y": 9
},
"id": 8,
"legend": {
"avg": false,
"current": false,
"max": false,
"min": false,
"show": true,
"total": false,
"values": false
},
"lines": true,
"linewidth": 1,
"links": [],
"nullPointMode": "null",
"percentage": false,
"pointradius": 5,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"spaceLength": 10,
"stack": false,
"steppedLine": false,
"targets": [
{
"expr": "rate(http_adapter_api_request_count{job=\"mainflux\"}[30s])",
"format": "time_series",
"intervalFactor": 1,
"legendFormat": "http_count_{{method}}",
"refId": "A"
}
],
"thresholds": [],
"timeFrom": null,
"timeShift": null,
"title": "HTTP adapter request count",
"tooltip": {
"shared": true,
"sort": 0,
"value_type": "individual"
},
"type": "graph",
"xaxis": {
"buckets": null,
"mode": "time",
"name": null,
"show": true,
"values": []
},
"yaxes": [
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
},
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
}
],
"yaxis": {
"align": false,
"alignLevel": null
}
},
{
"aliasColors": {},
"bars": false,
"dashLength": 10,
"dashes": false,
"datasource": "${DS_MAINFLUX}",
"fill": 1,
"gridPos": {
"h": 9,
"w": 12,
"x": 12,
"y": 9
},
"id": 16,
"legend": {
"avg": false,
"current": false,
"max": false,
"min": false,
"show": true,
"total": false,
"values": false
},
"lines": true,
"linewidth": 1,
"links": [],
"nullPointMode": "null",
"percentage": false,
"pointradius": 5,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"spaceLength": 10,
"stack": false,
"steppedLine": false,
"targets": [
{
"expr": "avg(avg_over_time(http_adapter_api_request_latency_microseconds{job=\"mainflux\"}[30s])) by (method)",
"format": "time_series",
"intervalFactor": 1,
"legendFormat": "http_latency_{{method}}",
"refId": "A"
}
],
"thresholds": [],
"timeFrom": null,
"timeShift": null,
"title": "HTTP adapter request latency",
"tooltip": {
"shared": true,
"sort": 0,
"value_type": "individual"
},
"type": "graph",
"xaxis": {
"buckets": null,
"mode": "time",
"name": null,
"show": true,
"values": []
},
"yaxes": [
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
},
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
}
],
"yaxis": {
"align": false,
"alignLevel": null
}
},
{
"aliasColors": {},
"bars": false,
"dashLength": 10,
"dashes": false,
"datasource": "${DS_MAINFLUX}",
"fill": 1,
"gridPos": {
"h": 9,
"w": 12,
"x": 0,
"y": 18
},
"id": 4,
"legend": {
"avg": false,
"current": false,
"max": false,
"min": false,
"show": true,
"total": false,
"values": false
},
"lines": true,
"linewidth": 1,
"links": [],
"nullPointMode": "null",
"percentage": false,
"pointradius": 5,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"spaceLength": 10,
"stack": false,
"steppedLine": false,
"targets": [
{
"expr": "rate(things_api_request_count{job=\"mainflux\"}[30s])",
"format": "time_series",
"intervalFactor": 1,
"legendFormat": "things_count_{{method}}",
"refId": "A"
}
],
"thresholds": [],
"timeFrom": null,
"timeShift": null,
"title": "Things request count",
"tooltip": {
"shared": true,
"sort": 0,
"value_type": "individual"
},
"type": "graph",
"xaxis": {
"buckets": null,
"mode": "time",
"name": null,
"show": true,
"values": []
},
"yaxes": [
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
},
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
}
],
"yaxis": {
"align": false,
"alignLevel": null
}
},
{
"aliasColors": {},
"bars": false,
"dashLength": 10,
"dashes": false,
"datasource": "${DS_MAINFLUX}",
"fill": 1,
"gridPos": {
"h": 9,
"w": 12,
"x": 12,
"y": 18
},
"id": 14,
"legend": {
"avg": false,
"current": false,
"max": false,
"min": false,
"show": true,
"total": false,
"values": false
},
"lines": true,
"linewidth": 1,
"links": [],
"nullPointMode": "null",
"percentage": false,
"pointradius": 5,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"spaceLength": 10,
"stack": false,
"steppedLine": false,
"targets": [
{
"expr": "avg(avg_over_time(things_api_request_latency_microseconds{job=\"mainflux\"}[30s])) by (method)",
"format": "time_series",
"intervalFactor": 1,
"legendFormat": "things_latency_{{method}}",
"refId": "A"
}
],
"thresholds": [],
"timeFrom": null,
"timeShift": null,
"title": "Things request latency",
"tooltip": {
"shared": true,
"sort": 0,
"value_type": "individual"
},
"type": "graph",
"xaxis": {
"buckets": null,
"mode": "time",
"name": null,
"show": true,
"values": []
},
"yaxes": [
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
},
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
}
],
"yaxis": {
"align": false,
"alignLevel": null
}
},
{
"aliasColors": {
"users_count": "#7eb26d"
},
"bars": false,
"dashLength": 10,
"dashes": false,
"datasource": "${DS_MAINFLUX}",
"fill": 1,
"gridPos": {
"h": 9,
"w": 12,
"x": 0,
"y": 27
},
"id": 2,
"legend": {
"avg": false,
"current": false,
"max": false,
"min": false,
"show": true,
"total": false,
"values": false
},
"lines": true,
"linewidth": 1,
"links": [],
"nullPointMode": "null",
"percentage": false,
"pointradius": 5,
"points": false,
"renderer": "flot",
"seriesOverrides": [
{
"alias": "users_count",
"yaxis": 1
}
],
"spaceLength": 10,
"stack": false,
"steppedLine": false,
"targets": [
{
"expr": "rate(users_api_request_count{job=\"mainflux\"}[30s])",
"format": "time_series",
"intervalFactor": 1,
"legendFormat": "users_count_{{method}}",
"refId": "A"
}
],
"thresholds": [],
"timeFrom": null,
"timeShift": null,
"title": "Users request count",
"tooltip": {
"shared": true,
"sort": 0,
"value_type": "individual"
},
"type": "graph",
"xaxis": {
"buckets": null,
"mode": "time",
"name": null,
"show": true,
"values": []
},
"yaxes": [
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
},
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
}
],
"yaxis": {
"align": false,
"alignLevel": null
}
},
{
"aliasColors": {},
"bars": false,
"dashLength": 10,
"dashes": false,
"datasource": "${DS_MAINFLUX}",
"fill": 1,
"gridPos": {
"h": 9,
"w": 12,
"x": 12,
"y": 27
},
"id": 12,
"legend": {
"avg": false,
"current": false,
"max": false,
"min": false,
"show": true,
"total": false,
"values": false
},
"lines": true,
"linewidth": 1,
"links": [],
"nullPointMode": "null",
"percentage": false,
"pointradius": 5,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"spaceLength": 10,
"stack": false,
"steppedLine": false,
"targets": [
{
"expr": "avg(avg_over_time(users_api_request_latency_microseconds{job=\"mainflux\"}[30s])) by (method)",
"format": "time_series",
"intervalFactor": 1,
"legendFormat": "users_latency_{{method}}",
"refId": "A"
}
],
"thresholds": [],
"timeFrom": null,
"timeShift": null,
"title": "Users request latency",
"tooltip": {
"shared": true,
"sort": 0,
"value_type": "individual"
},
"type": "graph",
"xaxis": {
"buckets": null,
"mode": "time",
"name": null,
"show": true,
"values": []
},
"yaxes": [
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
},
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
}
],
"yaxis": {
"align": false,
"alignLevel": null
}
}
],
"refresh": "5s",
"schemaVersion": 16,
"style": "dark",
"tags": [],
"templating": {
"list": []
},
"time": {
"from": "now-5m",
"to": "now"
},
"timepicker": {
"refresh_intervals": [
"5s",
"10s",
"30s",
"1m",
"5m",
"15m",
"30m",
"1h",
"2h",
"1d"
],
"time_options": [
"5m",
"15m",
"1h",
"6h",
"12h",
"24h",
"2d",
"7d",
"30d"
]
},
"timezone": "",
"title": "MainfluxDashboard",
"uid": "NOIbtxImk",
"version": 29
}
-4
View File
@@ -30,10 +30,6 @@ func (req listMessagesReq) validate() error {
return apiutil.ErrLimitSize
}
if req.pageMeta.Offset < 0 {
return apiutil.ErrOffsetSize
}
if req.pageMeta.Comparator != "" &&
req.pageMeta.Comparator != readers.EqualKey &&
req.pageMeta.Comparator != readers.LowerThanKey &&
+73 -44
View File
@@ -50,20 +50,22 @@ func TestReadSenml(t *testing.T) {
Hosts: []string{addr},
Keyspace: keyspace,
})
require.Nil(t, err, fmt.Sprintf("failed to connect to Cassandra: %s", err))
assert.Nil(t, err, fmt.Sprintf("failed to connect to Cassandra: %s", err))
defer session.Close()
err = casClient.InitDB(session, cwriter.Table)
assert.Nil(t, err, fmt.Sprintf("failed to initialize to Cassandra: %s", err))
err = casClient.InitDB(session, cwriter.Table)
require.Nil(t, err, fmt.Sprintf("failed to initialize to Cassandra: %s", err))
writer := cwriter.New(session)
chanID, err := idProvider.ID()
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
assert.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
pubID, err := idProvider.ID()
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
assert.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
pubID2, err := idProvider.ID()
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
assert.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
wrongID, err := idProvider.ID()
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
assert.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
m := senml.Message{
Channel: chanID,
@@ -111,19 +113,21 @@ func TestReadSenml(t *testing.T) {
}
err = writer.Consume(messages)
require.Nil(t, err, fmt.Sprintf("failed to store message to Cassandra: %s", err))
assert.Nil(t, err, fmt.Sprintf("failed to store message to Cassandra: %s", err))
reader := creader.New(session)
// Since messages are not saved in natural order,
// cases that return subset of messages are only
// checking data result set size, but not content.
cases := map[string]struct {
cases := []struct {
desc string
chanID string
pageMeta readers.PageMetadata
page readers.MessagesPage
}{
"read message page for existing channel": {
{
desc: "read message page for existing channel",
chanID: chanID,
pageMeta: readers.PageMetadata{
Offset: 0,
@@ -134,7 +138,8 @@ func TestReadSenml(t *testing.T) {
Messages: fromSenml(messages),
},
},
"read message page for non-existent channel": {
{
desc: "read message page for non-existent channel",
chanID: wrongID,
pageMeta: readers.PageMetadata{
Offset: 0,
@@ -144,7 +149,8 @@ func TestReadSenml(t *testing.T) {
Messages: []readers.Message{},
},
},
"read message last page": {
{
desc: "read message last page",
chanID: chanID,
pageMeta: readers.PageMetadata{
Offset: msgsNum - 20,
@@ -155,7 +161,8 @@ func TestReadSenml(t *testing.T) {
Messages: fromSenml(messages[msgsNum-20 : msgsNum]),
},
},
"read message with non-existent subtopic": {
{
desc: "read message with non-existent subtopic",
chanID: chanID,
pageMeta: readers.PageMetadata{
Offset: 0,
@@ -166,7 +173,8 @@ func TestReadSenml(t *testing.T) {
Messages: []readers.Message{},
},
},
"read message with subtopic": {
{
desc: "read message with subtopic",
chanID: chanID,
pageMeta: readers.PageMetadata{
Offset: 0,
@@ -178,7 +186,8 @@ func TestReadSenml(t *testing.T) {
Messages: fromSenml(queryMsgs),
},
},
"read message with publisher": {
{
desc: "read message with publisher",
chanID: chanID,
pageMeta: readers.PageMetadata{
Offset: 0,
@@ -190,7 +199,8 @@ func TestReadSenml(t *testing.T) {
Messages: fromSenml(queryMsgs),
},
},
"read message with wrong format": {
{
desc: "read message with wrong format",
chanID: chanID,
pageMeta: readers.PageMetadata{
Format: "messagess",
@@ -203,7 +213,8 @@ func TestReadSenml(t *testing.T) {
Messages: []readers.Message{},
},
},
"read message with protocol": {
{
desc: "read message with protocol",
chanID: chanID,
pageMeta: readers.PageMetadata{
Offset: 0,
@@ -215,7 +226,8 @@ func TestReadSenml(t *testing.T) {
Messages: fromSenml(queryMsgs),
},
},
"read message with name": {
{
desc: "read message with name",
chanID: chanID,
pageMeta: readers.PageMetadata{
Offset: 0,
@@ -227,7 +239,8 @@ func TestReadSenml(t *testing.T) {
Messages: fromSenml(queryMsgs[0:limit]),
},
},
"read message with value": {
{
desc: "read message with value",
chanID: chanID,
pageMeta: readers.PageMetadata{
Offset: 0,
@@ -239,7 +252,8 @@ func TestReadSenml(t *testing.T) {
Messages: fromSenml(valueMsgs[0:limit]),
},
},
"read message with value and equal comparator": {
{
desc: "read message with value and equal comparator",
chanID: chanID,
pageMeta: readers.PageMetadata{
Offset: 0,
@@ -252,7 +266,8 @@ func TestReadSenml(t *testing.T) {
Messages: fromSenml(valueMsgs[0:limit]),
},
},
"read message with value and lower-than comparator": {
{
desc: "read message with value and lower-than comparator",
chanID: chanID,
pageMeta: readers.PageMetadata{
Offset: 0,
@@ -265,7 +280,8 @@ func TestReadSenml(t *testing.T) {
Messages: fromSenml(valueMsgs[0:limit]),
},
},
"read message with value and lower-than-or-equal comparator": {
{
desc: "read message with value and lower-than-or-equal comparator",
chanID: chanID,
pageMeta: readers.PageMetadata{
Offset: 0,
@@ -278,7 +294,8 @@ func TestReadSenml(t *testing.T) {
Messages: fromSenml(valueMsgs[0:limit]),
},
},
"read message with value and greater-than comparator": {
{
desc: "read message with value and greater-than comparator",
chanID: chanID,
pageMeta: readers.PageMetadata{
Offset: 0,
@@ -291,7 +308,8 @@ func TestReadSenml(t *testing.T) {
Messages: fromSenml(valueMsgs[0:limit]),
},
},
"read message with value and greater-than-or-equal comparator": {
{
desc: "read message with value and greater-than-or-equal comparator",
chanID: chanID,
pageMeta: readers.PageMetadata{
Offset: 0,
@@ -304,7 +322,8 @@ func TestReadSenml(t *testing.T) {
Messages: fromSenml(valueMsgs[0:limit]),
},
},
"read message with boolean value": {
{
desc: "read message with boolean value",
chanID: chanID,
pageMeta: readers.PageMetadata{
Offset: 0,
@@ -316,7 +335,8 @@ func TestReadSenml(t *testing.T) {
Messages: fromSenml(boolMsgs[0:limit]),
},
},
"read message with string value": {
{
desc: "read message with string value",
chanID: chanID,
pageMeta: readers.PageMetadata{
Offset: 0,
@@ -328,7 +348,8 @@ func TestReadSenml(t *testing.T) {
Messages: fromSenml(stringMsgs[0:limit]),
},
},
"read message with data value": {
{
desc: "read message with data value",
chanID: chanID,
pageMeta: readers.PageMetadata{
Offset: 0,
@@ -340,7 +361,8 @@ func TestReadSenml(t *testing.T) {
Messages: fromSenml(dataMsgs[0:limit]),
},
},
"read message with from": {
{
desc: "read message with from",
chanID: chanID,
pageMeta: readers.PageMetadata{
Offset: 0,
@@ -352,7 +374,8 @@ func TestReadSenml(t *testing.T) {
Messages: fromSenml(messages[0:21]),
},
},
"read message with to": {
{
desc: "read message with to",
chanID: chanID,
pageMeta: readers.PageMetadata{
Offset: 0,
@@ -364,7 +387,8 @@ func TestReadSenml(t *testing.T) {
Messages: fromSenml(messages[21:]),
},
},
"read message with from/to": {
{
desc: "read message with from/to",
chanID: chanID,
pageMeta: readers.PageMetadata{
Offset: 0,
@@ -379,11 +403,11 @@ func TestReadSenml(t *testing.T) {
},
}
for desc, tc := range cases {
for _, tc := range cases {
result, err := reader.ReadAll(tc.chanID, tc.pageMeta)
assert.Nil(t, err, fmt.Sprintf("%s: expected no error got %s", desc, err))
assert.ElementsMatch(t, tc.page.Messages, result.Messages, fmt.Sprintf("%s: expected %v got %v", desc, tc.page.Messages, result.Messages))
assert.Equal(t, tc.page.Total, result.Total, fmt.Sprintf("%s: expected %v got %v", desc, tc.page.Total, result.Total))
assert.Nil(t, err, fmt.Sprintf("%s: got unexpected error: %s", tc.desc, err))
assert.ElementsMatch(t, tc.page.Messages, result.Messages, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.page.Messages, result.Messages))
assert.Equal(t, tc.page.Total, result.Total, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.page.Total, result.Total))
}
}
@@ -392,12 +416,12 @@ func TestReadJSON(t *testing.T) {
Hosts: []string{addr},
Keyspace: keyspace,
})
require.Nil(t, err, fmt.Sprintf("failed to connect to Cassandra: %s", err))
assert.Nil(t, err, fmt.Sprintf("failed to connect to Cassandra: %s", err))
defer session.Close()
writer := cwriter.New(session)
id1, err := idProvider.ID()
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
assert.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
m := json.Message{
Channel: id1,
Publisher: id1,
@@ -428,7 +452,7 @@ func TestReadJSON(t *testing.T) {
assert.Nil(t, err, fmt.Sprintf("expected no error got %s\n", err))
id2, err := idProvider.ID()
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
assert.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
m = json.Message{
Channel: id2,
Publisher: id2,
@@ -467,12 +491,14 @@ func TestReadJSON(t *testing.T) {
reader := creader.New(session)
cases := map[string]struct {
cases := []struct {
desc string
chanID string
pageMeta readers.PageMetadata
page readers.MessagesPage
}{
"read message page for existing channel": {
{
desc: "read message page for existing channel",
chanID: id1,
pageMeta: readers.PageMetadata{
Format: messages1.Format,
@@ -484,7 +510,8 @@ func TestReadJSON(t *testing.T) {
Messages: fromJSON(msgs1[:10]),
},
},
"read message page for non-existent channel": {
{
desc: "read message page for non-existent channel",
chanID: wrongID,
pageMeta: readers.PageMetadata{
Format: messages1.Format,
@@ -495,7 +522,8 @@ func TestReadJSON(t *testing.T) {
Messages: []readers.Message{},
},
},
"read message last page": {
{
desc: "read message last page",
chanID: id2,
pageMeta: readers.PageMetadata{
Format: messages2.Format,
@@ -507,7 +535,8 @@ func TestReadJSON(t *testing.T) {
Messages: fromJSON(msgs2[msgsNum-20 : msgsNum]),
},
},
"read message with protocol": {
{
desc: "read message with protocol",
chanID: id2,
pageMeta: readers.PageMetadata{
Format: messages2.Format,
@@ -522,7 +551,7 @@ func TestReadJSON(t *testing.T) {
},
}
for desc, tc := range cases {
for _, tc := range cases {
result, err := reader.ReadAll(tc.chanID, tc.pageMeta)
for i := 0; i < len(result.Messages); i++ {
m := result.Messages[i]
@@ -530,9 +559,9 @@ func TestReadJSON(t *testing.T) {
delete(m.(map[string]interface{}), "id")
result.Messages[i] = m
}
assert.Nil(t, err, fmt.Sprintf("%s: expected no error got %s", desc, err))
assert.ElementsMatch(t, tc.page.Messages, result.Messages, fmt.Sprintf("%s: expected %v got %v", desc, tc.page.Messages, result.Messages))
assert.Equal(t, tc.page.Total, result.Total, fmt.Sprintf("%s: expected %v got %v", desc, tc.page.Total, result.Total))
assert.Nil(t, err, fmt.Sprintf("%s: got unexpected error: %s", tc.desc, err))
assert.ElementsMatch(t, tc.page.Messages, result.Messages, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.page.Messages, result.Messages))
assert.Equal(t, tc.page.Total, result.Total, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.page.Total, result.Total))
}
}
+40 -19
View File
@@ -8,24 +8,32 @@ The service is configured using the environment variables presented in the
following table. Note that any unset variables will be replaced with their
default values.
| Variable | Description | Default |
|------------------------------|-----------------------------------------------------|----------------|
| MF_INFLUX_READER_LOG_LEVEL | Service log level | info |
| MF_INFLUX_READER_PORT | Service HTTP port | 8180 |
| MF_INFLUXDB_HOST | InfluxDB host | localhost |
| MF_INFLUXDB_PORT | Default port of InfluxDB database | 8086 |
| MF_INFLUXDB_ADMIN_USER | Default user of InfluxDB database | mainflux |
| MF_INFLUXDB_ADMIN_PASSWORD | Default password of InfluxDB user | mainflux |
| MF_INFLUXDB_DB | InfluxDB database name | mainflux |
| MF_INFLUX_READER_CLIENT_TLS | Flag that indicates if TLS should be turned on | false |
| MF_INFLUX_READER_CA_CERTS | Path to trusted CAs in PEM format | |
| MF_INFLUX_READER_SERVER_CERT | Path to server certificate in pem format | |
| MF_INFLUX_READER_SERVER_KEY | Path to server key in pem format | |
| MF_JAEGER_URL | Jaeger server URL | localhost:6831 |
| MF_THINGS_AUTH_GRPC_URL | Things service Auth gRPC URL | localhost:8183 |
| MF_THINGS_AUTH_GRPC_TIMEOUT | Things service Auth gRPC request timeout in seconds | 1s |
| MF_AUTH_GRPC_URL | Auth service gRPC URL | localhost:8181 |
| MF_AUTH_GRPC_TIMEOUT | Auth service gRPC request timeout in seconds | 1s |
| Variable | Description | Default |
|------------------------------|-----------------------------------------------------|-------------------|
| MF_INFLUX_READER_LOG_LEVEL | Service log level | info |
| MF_INFLUX_READER_PORT | Service HTTP port | 8905 |
| MF_INFLUXDB_HOST | InfluxDB host | localhost |
| MF_INFLUXDB_PORT | Default port of InfluxDB database | 8086 |
| MF_INFLUXDB_ADMIN_USER | Default user of InfluxDB database | mainflux |
| MF_INFLUXDB_ADMIN_PASSWORD | Default password of InfluxDB user | mainflux |
| MF_INFLUXDB_DB | InfluxDB database name | mainflux |
| MF_INFLUXDB_HOST | InfluxDB host name | mainflux-influxdb |
| MF_INFLUXDB_PROTOCOL | InfluxDB protocol | http |
| MF_INFLUXDB_TIMEOUT | InfluxDB client connection readiness timeout | 1s |
| MF_INFLUXDB_ORG | InfluxDB organization name | mainflux |
| MF_INFLUXDB_BUCKET | InfluxDB bucket name | mainflux-bucket |
| MF_INFLUXDB_TOKEN | InfluxDB API token | mainflux-token |
| MF_INFLUXDB_HTTP_ENABLED | InfluxDB http enabled status | true |
| MF_INFLUXDB_INIT_MODE | InfluxDB initialization mode | setup |
| MF_INFLUX_READER_CLIENT_TLS | Flag that indicates if TLS should be turned on | false |
| MF_INFLUX_READER_CA_CERTS | Path to trusted CAs in PEM format | |
| MF_INFLUX_READER_SERVER_CERT | Path to server certificate in pem format | |
| MF_INFLUX_READER_SERVER_KEY | Path to server key in pem format | |
| MF_JAEGER_URL | Jaeger server URL | localhost:6831 |
| MF_THINGS_AUTH_GRPC_URL | Things service Auth gRPC URL | localhost:8183 |
| MF_THINGS_AUTH_GRPC_TIMEOUT | Things service Auth gRPC request timeout in seconds | 1s |
| MF_AUTH_GRPC_URL | Auth service gRPC URL | localhost:8181 |
| MF_AUTH_GRPC_TIMEOUT | Auth service gRPC request timeout in seconds | 1s |
## Deployment
@@ -53,6 +61,13 @@ MF_INFLUXDB_HOST=[InfluxDB database host] \
MF_INFLUXDB_ADMIN_USER=[InfluxDB database port] \
MF_INFLUXDB_ADMIN_USER=[InfluxDB admin user] \
MF_INFLUXDB_ADMIN_PASSWORD=[InfluxDB admin password] \
MF_INFLUXDB_PROTOCOL=[InfluxDB protocol] \
MF_INFLUXDB_TIMEOUT=[InfluxDB timeout] \
MF_INFLUXDB_ORG=[InfluxDB org] \
MF_INFLUXDB_BUCKET=[InfluxDB bucket] \
MF_INFLUXDB_TOKEN=[InfluxDB token] \
MF_INFLUXDB_HTTP_ENABLED=[InfluxDB http enabled] \
MF_INFLUXDB_INIT_MODE=[InfluxDB init mode] \
MF_INFLUX_READER_CLIENT_TLS=[Flag that indicates if TLS should be turned on] \
MF_INFLUX_READER_CA_CERTS=[Path to trusted CAs in PEM format] \
MF_INFLUX_READER_SERVER_CERT=[Path to server pem certificate file] \
@@ -76,8 +91,14 @@ docker-compose -f docker/docker-compose.yml up -d
docker-compose -f docker/addons/influxdb-reader/docker-compose.yml up -d
```
And, to use the default .env file, execute the following command:
```bash
docker-compose -f docker/addons/influxdb-reader/docker-compose.yml up --env-file docker/.env -d
```
## Usage
Service exposes [HTTP API](https://api.mainflux.io/?urls.primaryName=readers-openapi.yml) for fetching messages.
[doc]: https://docs.mainflux.io
Official docs can be found [here](https://docs.mainflux.io).
+148 -114
View File
@@ -1,22 +1,22 @@
package influxdb
import (
"context"
"encoding/json"
"fmt"
"strconv"
"strings"
"time"
"unicode"
"github.com/mainflux/mainflux/pkg/errors"
"github.com/mainflux/mainflux/readers"
influxdata "github.com/influxdata/influxdb/client/v2"
influxdb2 "github.com/influxdata/influxdb-client-go/v2"
jsont "github.com/mainflux/mainflux/pkg/transformers/json"
"github.com/mainflux/mainflux/pkg/transformers/senml"
)
const (
countCol = "count_protocol"
// Measurement for SenML messages
defMeasurement = "messages"
)
@@ -24,19 +24,22 @@ const (
var _ readers.MessageRepository = (*influxRepository)(nil)
var (
errResultSet = errors.New("invalid result set")
errResultTime = errors.New("invalid result time")
)
type RepoConfig struct {
Bucket string
Org string
}
type influxRepository struct {
database string
client influxdata.Client
cfg RepoConfig
client influxdb2.Client
}
// New returns new InfluxDB reader.
func New(client influxdata.Client, database string) readers.MessageRepository {
func New(client influxdb2.Client, repoCfg RepoConfig) readers.MessageRepository {
return &influxRepository{
database,
repoCfg,
client,
}
}
@@ -47,37 +50,45 @@ func (repo *influxRepository) ReadAll(chanID string, rpm readers.PageMetadata) (
format = rpm.Format
}
condition := fmtCondition(chanID, rpm)
queryAPI := repo.client.QueryAPI(repo.cfg.Org)
condition, timeRange := fmtCondition(chanID, rpm)
query := fmt.Sprintf(`
import "influxdata/influxdb/v1" from(bucket: "%s")
%s
|> v1.fieldsAsCols()
|> group()
|> filter(fn: (r) => r._measurement == "%s")
%s
|> sort(columns: ["_time"], desc: true)
|> limit(n:%d,offset:%d)
|> yield(name: "sort")`,
repo.cfg.Bucket,
timeRange,
format,
condition,
rpm.Limit, rpm.Offset,
)
cmd := fmt.Sprintf(`SELECT * FROM %s WHERE %s ORDER BY time DESC LIMIT %d OFFSET %d`, format, condition, rpm.Limit, rpm.Offset)
q := influxdata.Query{
Command: cmd,
Database: repo.database,
}
resp, err := repo.client.Query(q)
resp, err := queryAPI.Query(context.Background(), query)
if err != nil {
return readers.MessagesPage{}, errors.Wrap(readers.ErrReadMessages, err)
}
if resp.Error() != nil {
return readers.MessagesPage{}, errors.Wrap(readers.ErrReadMessages, resp.Error())
}
if len(resp.Results) == 0 || len(resp.Results[0].Series) == 0 {
return readers.MessagesPage{}, nil
}
var messages []readers.Message
result := resp.Results[0].Series[0]
for _, v := range result.Values {
msg, err := parseMessage(format, result.Columns, v)
var valueMap map[string]interface{}
for resp.Next() {
valueMap = resp.Record().Values()
msg, err := parseMessage(format, valueMap)
if err != nil {
return readers.MessagesPage{}, err
}
messages = append(messages, msg)
}
if resp.Err() != nil {
return readers.MessagesPage{}, errors.Wrap(readers.ErrReadMessages, resp.Err())
}
total, err := repo.count(format, condition)
total, err := repo.count(format, condition, timeRange)
if err != nil {
return readers.MessagesPage{}, errors.Wrap(readers.ErrReadMessages, err)
}
@@ -89,62 +100,79 @@ func (repo *influxRepository) ReadAll(chanID string, rpm readers.PageMetadata) (
}
return page, nil
}
func (repo *influxRepository) count(measurement, condition string) (uint64, error) {
cmd := fmt.Sprintf(`SELECT COUNT(*) FROM %s WHERE %s`, measurement, condition)
q := influxdata.Query{
Command: cmd,
Database: repo.database,
}
func (repo *influxRepository) count(measurement, condition string, timeRange string) (uint64, error) {
cmd := fmt.Sprintf(`
import "influxdata/influxdb/v1" from(bucket: "%s")
%s
|> v1.fieldsAsCols()
|> filter(fn: (r) => r._measurement == "%s")
%s
|> group()
|> count(column:"_measurement")
|> yield(name: "count")
`,
repo.cfg.Bucket,
timeRange,
measurement,
condition)
queryAPI := repo.client.QueryAPI(repo.cfg.Org)
resp, err := queryAPI.Query(context.Background(), cmd)
resp, err := repo.client.Query(q)
if err != nil {
return 0, err
}
if resp.Error() != nil {
return 0, resp.Error()
}
if len(resp.Results) == 0 ||
len(resp.Results[0].Series) == 0 ||
len(resp.Results[0].Series[0].Values) == 0 {
return 0, nil
}
switch resp.Next() {
case true:
valueMap := resp.Record().Values()
countIndex := 0
for i, col := range resp.Results[0].Series[0].Columns {
if col == countCol {
countIndex = i
break
val, ok := valueMap["_measurement"].(int64)
if !ok {
return 0, nil
}
}
return uint64(val), nil
result := resp.Results[0].Series[0].Values[0]
if len(result) < countIndex+1 {
default:
// same as no rows.
return 0, nil
}
count, ok := result[countIndex].(json.Number)
if !ok {
return 0, nil
}
return strconv.ParseUint(count.String(), 10, 64)
}
func fmtCondition(chanID string, rpm readers.PageMetadata) string {
condition := fmt.Sprintf(`channel='%s'`, chanID)
func fmtCondition(chanID string, rpm readers.PageMetadata) (string, string) {
var timeRange string
var sb strings.Builder
sb.WriteString(fmt.Sprintf(`|> filter(fn: (r) => r["channel"] == "%s" )`, chanID))
var query map[string]interface{}
meta, err := json.Marshal(rpm)
if err != nil {
return condition
return sb.String(), timeRange
}
if err := json.Unmarshal(meta, &query); err != nil {
return condition
return sb.String(), timeRange
}
//range(start:...) is a must for FluxQL syntax
from := `start: time(v:0)`
if value, ok := query["from"]; ok {
fromValue := int64(value.(float64)*1e9) - 1
from = fmt.Sprintf(`start: time(v: %d )`, fromValue)
}
//range(...,stop:) is an option for FluxQL syntax
to := ""
if value, ok := query["to"]; ok {
toValue := int64(value.(float64) * 1e9)
to = fmt.Sprintf(`, stop: time(v: %d )`, toValue)
}
// timeRange returned seperately because
// in FluxQL time range must be at the
// beginning of the query.
timeRange = fmt.Sprintf(`|> range(%s %s)`, from, to)
for name, value := range query {
switch name {
case
@@ -153,95 +181,101 @@ func fmtCondition(chanID string, rpm readers.PageMetadata) string {
"publisher",
"name",
"protocol":
condition = fmt.Sprintf(`%s AND "%s"='%s'`, condition, name, value)
sb.WriteString(fmt.Sprintf(`|> filter(fn: (r) => r.%s == "%s" )`, name, value))
case "v":
comparator := readers.ParseValueComparator(query)
condition = fmt.Sprintf(`%s AND value %s %f`, condition, comparator, value)
// flux eq comparator is different
if comparator == "=" {
comparator = "=="
}
sb.WriteString(`|> filter(fn: (r) => exists r.value)`)
sb.WriteString(fmt.Sprintf(`|> filter(fn: (r) => r.value %s %v)`, comparator, value))
case "vb":
condition = fmt.Sprintf(`%s AND boolValue = %t`, condition, value)
sb.WriteString(`|> filter(fn: (r) => exists r.boolValue)`)
sb.WriteString(fmt.Sprintf(`|> filter(fn: (r) => r.boolValue == %v)`, value))
case "vs":
condition = fmt.Sprintf(`%s AND stringValue = '%s'`, condition, value)
sb.WriteString(`|> filter(fn: (r) => exists r.stringValue)`)
sb.WriteString(fmt.Sprintf(`|> filter(fn: (r) => r.stringValue == "%s")`, value))
case "vd":
condition = fmt.Sprintf(`%s AND dataValue = '%s'`, condition, value)
case "from":
iVal := int64(value.(float64) * 1e9)
condition = fmt.Sprintf(`%s AND time >= %d`, condition, iVal)
case "to":
iVal := int64(value.(float64) * 1e9)
condition = fmt.Sprintf(`%s AND time < %d`, condition, iVal)
sb.WriteString(`|> filter(fn: (r) => exists r.dataValue)`)
sb.WriteString(fmt.Sprintf(`|> filter(fn: (r) => r.dataValue == "%s")`, value))
}
}
return condition
return sb.String(), timeRange
}
func parseMessage(measurement string, names []string, fields []interface{}) (interface{}, error) {
func parseMessage(measurement string, valueMap map[string]interface{}) (interface{}, error) {
switch measurement {
case defMeasurement:
return parseSenml(names, fields)
return parseSenml(valueMap)
default:
return parseJSON(names, fields)
return parseJSON(valueMap)
}
}
func underscore(names []string) {
for i, name := range names {
var buff []rune
idx := 0
for i, c := range name {
if unicode.IsUpper(c) {
buff = append(buff, []rune(name[idx:i])...)
buff = append(buff, []rune{'_', unicode.ToLower(c)}...)
idx = i + 1
continue
}
func underscore(name string) string {
var buff []rune
idx := 0
for i, c := range name {
if unicode.IsUpper(c) {
buff = append(buff, []rune(name[idx:i])...)
buff = append(buff, []rune{'_', unicode.ToLower(c)}...)
idx = i + 1
continue
}
buff = append(buff, []rune(name[idx:])...)
names[i] = string(buff)
}
buff = append(buff, []rune(name[idx:])...)
return string(buff)
}
func parseSenml(names []string, fields []interface{}) (interface{}, error) {
m := make(map[string]interface{})
if len(names) > len(fields) {
return nil, errResultSet
}
underscore(names)
for i, name := range names {
if name == "time" {
val, ok := fields[i].(string)
func parseSenml(valueMap map[string]interface{}) (interface{}, error) {
msg := make(map[string]interface{})
for k, v := range valueMap {
k = underscore(k)
if k == "_time" {
k = "time"
t, ok := v.(time.Time)
if !ok {
return nil, errResultTime
}
t, err := time.Parse(time.RFC3339Nano, val)
if err != nil {
return nil, err
}
v := float64(t.UnixNano()) / 1e9
m[name] = v
msg[k] = v
continue
}
m[name] = fields[i]
msg[k] = v
}
data, err := json.Marshal(m)
data, err := json.Marshal(msg)
if err != nil {
return nil, err
}
msg := senml.Message{}
if err := json.Unmarshal(data, &msg); err != nil {
senmlMsg := senml.Message{}
if err := json.Unmarshal(data, &senmlMsg); err != nil {
return nil, err
}
return msg, nil
return senmlMsg, nil
}
func parseJSON(names []string, fields []interface{}) (interface{}, error) {
func parseJSON(valueMap map[string]interface{}) (interface{}, error) {
ret := make(map[string]interface{})
pld := make(map[string]interface{})
for i, n := range names {
switch n {
case "channel", "created", "subtopic", "publisher", "protocol", "time":
ret[n] = fields[i]
for name, field := range valueMap {
switch name {
case "channel", "created", "subtopic", "publisher", "protocol":
ret[name] = field
case "_time":
name = "time"
t, ok := field.(time.Time)
if !ok {
return nil, errResultTime
}
v := float64(t.UnixNano()) / 1e9
ret[name] = v
continue
case "table", "_start", "_stop", "result", "_measurement":
default:
v := fields[i]
v := field
if val, ok := v.(json.Number); ok {
var err error
v, err = val.Float64()
@@ -249,7 +283,7 @@ func parseJSON(names []string, fields []interface{}) (interface{}, error) {
return nil, err
}
}
pld[n] = v
pld[name] = v
}
}
ret["payload"] = jsont.ParseFlat(pld)
+100 -73
View File
@@ -2,11 +2,10 @@ package influxdb_test
import (
"fmt"
"math/rand"
"testing"
"time"
influxdata "github.com/influxdata/influxdb/client/v2"
influxdata "github.com/influxdata/influxdb-client-go/v2"
iwriter "github.com/mainflux/mainflux/consumers/writers/influxdb"
"github.com/mainflux/mainflux/pkg/transformers/json"
"github.com/mainflux/mainflux/pkg/transformers/senml"
@@ -15,11 +14,9 @@ import (
ireader "github.com/mainflux/mainflux/readers/influxdb"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const (
testDB = "test"
subtopic = "topic"
msgsNum = 100
limit = 10
@@ -28,34 +25,38 @@ const (
httpProt = "http"
msgName = "temperature"
offset = 21
format1 = "format1"
format2 = "format2"
wrongID = "wrong_id"
format1 = "format1"
format2 = "format2"
wrongID = "wrong_id"
)
var (
v float64 = 5
vs = "a"
vb = true
vd = "dataValue"
vs string = "a"
vb bool = true
vd string = "dataValue"
sum float64 = 42
client influxdata.Client
client influxdata.Client
repoCfg = struct {
Bucket string
Org string
}{
Bucket: dbBucket,
Org: dbOrg,
}
idProvider = uuid.New()
)
func TestReadAll(t *testing.T) {
writer := iwriter.New(client, testDB)
func TestReadSenml(t *testing.T) {
writer := iwriter.New(client, repoCfg, true)
chanID, err := idProvider.ID()
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
assert.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
pubID, err := idProvider.ID()
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
assert.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
pubID2, err := idProvider.ID()
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
wrongID, err := idProvider.ID()
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
assert.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
m := senml.Message{
Channel: chanID,
@@ -72,9 +73,7 @@ func TestReadAll(t *testing.T) {
stringMsgs := []senml.Message{}
dataMsgs := []senml.Message{}
queryMsgs := []senml.Message{}
rand.Seed(time.Now().UnixNano())
to := msgsNum
now := float64(rand.Intn(to) + offset)
now := float64(time.Now().Unix())
for i := 0; i < msgsNum; i++ {
// Mix possible values as well as value sum.
@@ -103,21 +102,22 @@ func TestReadAll(t *testing.T) {
msg.Name = msgName
queryMsgs = append(queryMsgs, msg)
}
messages = append(messages, msg)
}
err = writer.Consume(messages)
require.Nil(t, err, fmt.Sprintf("failed to store message to InfluxDB: %s", err))
assert.Nil(t, err, fmt.Sprintf("failed to store message to InfluxDB: %s", err))
reader := ireader.New(client, testDB)
reader := ireader.New(client, repoCfg)
cases := map[string]struct {
cases := []struct {
desc string
chanID string
pageMeta readers.PageMetadata
page readers.MessagesPage
}{
"read message page for existing channel": {
{
desc: "read message page for existing channel",
chanID: chanID,
pageMeta: readers.PageMetadata{
Offset: 0,
@@ -128,7 +128,8 @@ func TestReadAll(t *testing.T) {
Messages: fromSenml(messages),
},
},
"read message page for non-existent channel": {
{
desc: "read message page for non-existent channel",
chanID: wrongID,
pageMeta: readers.PageMetadata{
Offset: 0,
@@ -138,7 +139,8 @@ func TestReadAll(t *testing.T) {
Messages: []readers.Message{},
},
},
"read message last page": {
{
desc: "read message last page",
chanID: chanID,
pageMeta: readers.PageMetadata{
Offset: msgsNum - 20,
@@ -149,7 +151,8 @@ func TestReadAll(t *testing.T) {
Messages: fromSenml(messages[msgsNum-20 : msgsNum]),
},
},
"read message with non-existent subtopic": {
{
desc: "read message with non-existent subtopic",
chanID: chanID,
pageMeta: readers.PageMetadata{
Offset: 0,
@@ -160,7 +163,8 @@ func TestReadAll(t *testing.T) {
Messages: []readers.Message{},
},
},
"read message with subtopic": {
{
desc: "read message with subtopic",
chanID: chanID,
pageMeta: readers.PageMetadata{
Offset: 0,
@@ -172,7 +176,8 @@ func TestReadAll(t *testing.T) {
Messages: fromSenml(queryMsgs),
},
},
"read message with publisher": {
{
desc: "read message with publisher",
chanID: chanID,
pageMeta: readers.PageMetadata{
Offset: 0,
@@ -184,7 +189,8 @@ func TestReadAll(t *testing.T) {
Messages: fromSenml(queryMsgs),
},
},
"read message with wrong format": {
{
desc: "read message with wrong format",
chanID: chanID,
pageMeta: readers.PageMetadata{
Format: "messagess",
@@ -197,7 +203,8 @@ func TestReadAll(t *testing.T) {
Messages: []readers.Message{},
},
},
"read message with protocol": {
{
desc: "read message with protocol",
chanID: chanID,
pageMeta: readers.PageMetadata{
Offset: 0,
@@ -209,7 +216,8 @@ func TestReadAll(t *testing.T) {
Messages: fromSenml(queryMsgs),
},
},
"read message with name": {
{
desc: "read message with name",
chanID: chanID,
pageMeta: readers.PageMetadata{
Offset: 0,
@@ -221,7 +229,8 @@ func TestReadAll(t *testing.T) {
Messages: fromSenml(queryMsgs[0:limit]),
},
},
"read message with value": {
{
desc: "read message with value",
chanID: chanID,
pageMeta: readers.PageMetadata{
Offset: 0,
@@ -233,7 +242,8 @@ func TestReadAll(t *testing.T) {
Messages: fromSenml(valueMsgs[0:limit]),
},
},
"read message with value and equal comparator": {
{
desc: "read message with value and equal comparator",
chanID: chanID,
pageMeta: readers.PageMetadata{
Offset: 0,
@@ -246,7 +256,8 @@ func TestReadAll(t *testing.T) {
Messages: fromSenml(valueMsgs[0:limit]),
},
},
"read message with value and lower-than comparator": {
{
desc: "read message with value and lower-than comparator",
chanID: chanID,
pageMeta: readers.PageMetadata{
Offset: 0,
@@ -259,7 +270,8 @@ func TestReadAll(t *testing.T) {
Messages: fromSenml(valueMsgs[0:limit]),
},
},
"read message with value and lower-than-or-equal comparator": {
{
desc: "read message with value and lower-than-or-equal comparator",
chanID: chanID,
pageMeta: readers.PageMetadata{
Offset: 0,
@@ -272,7 +284,8 @@ func TestReadAll(t *testing.T) {
Messages: fromSenml(valueMsgs[0:limit]),
},
},
"read message with value and greater-than comparator": {
{
desc: "read message with value and greater-than comparator",
chanID: chanID,
pageMeta: readers.PageMetadata{
Offset: 0,
@@ -285,7 +298,8 @@ func TestReadAll(t *testing.T) {
Messages: fromSenml(valueMsgs[0:limit]),
},
},
"read message with value and greater-than-or-equal comparator": {
{
desc: "read message with value and greater-than-or-equal comparator",
chanID: chanID,
pageMeta: readers.PageMetadata{
Offset: 0,
@@ -298,7 +312,8 @@ func TestReadAll(t *testing.T) {
Messages: fromSenml(valueMsgs[0:limit]),
},
},
"read message with boolean value": {
{
desc: "read message with boolean value",
chanID: chanID,
pageMeta: readers.PageMetadata{
Offset: 0,
@@ -310,7 +325,8 @@ func TestReadAll(t *testing.T) {
Messages: fromSenml(boolMsgs[0:limit]),
},
},
"read message with string value": {
{
desc: "read message with string value",
chanID: chanID,
pageMeta: readers.PageMetadata{
Offset: 0,
@@ -322,7 +338,8 @@ func TestReadAll(t *testing.T) {
Messages: fromSenml(stringMsgs[0:limit]),
},
},
"read message with data value": {
{
desc: "read message with data value",
chanID: chanID,
pageMeta: readers.PageMetadata{
Offset: 0,
@@ -334,23 +351,25 @@ func TestReadAll(t *testing.T) {
Messages: fromSenml(dataMsgs[0:limit]),
},
},
"read message with from": {
{
desc: "failing test case : read message with from",
chanID: chanID,
pageMeta: readers.PageMetadata{
Offset: 0,
Limit: uint64(len(messages[0:offset])),
From: messages[offset-1].Time,
Limit: uint64(len(messages[0 : offset+1])),
From: messages[offset].Time,
},
page: readers.MessagesPage{
Total: uint64(len(messages[0:offset])),
Messages: fromSenml(messages[0:offset]),
Total: uint64(len(messages[0 : offset+1])),
Messages: fromSenml(messages[0 : offset+1]),
},
},
"read message with to": {
{
desc: "failing test case : read message with to",
chanID: chanID,
pageMeta: readers.PageMetadata{
Offset: 0,
Limit: uint64(len(messages[offset:])),
Limit: uint64(len(messages[offset-1:])),
To: messages[offset-1].Time,
},
page: readers.MessagesPage{
@@ -358,7 +377,8 @@ func TestReadAll(t *testing.T) {
Messages: fromSenml(messages[offset:]),
},
},
"read message with from/to": {
{
desc: "read message with from/to",
chanID: chanID,
pageMeta: readers.PageMetadata{
Offset: 0,
@@ -367,29 +387,30 @@ func TestReadAll(t *testing.T) {
To: messages[0].Time,
},
page: readers.MessagesPage{
Total: 5,
Messages: fromSenml(messages[1:6]),
Total: uint64(len(messages[0+1 : 5+1])),
Messages: fromSenml(messages[0+1 : 5+1]),
},
},
}
for desc, tc := range cases {
for _, tc := range cases {
result, err := reader.ReadAll(tc.chanID, tc.pageMeta)
assert.Nil(t, err, fmt.Sprintf("%s: expected no error got %s", desc, err))
assert.ElementsMatch(t, tc.page.Messages, result.Messages, fmt.Sprintf("%s: expected: %v, got: %v", desc, tc.page.Messages, result.Messages))
assert.Equal(t, tc.page.Total, result.Total, fmt.Sprintf("%s: expected %d got %d", desc, tc.page.Total, result.Total))
assert.Nil(t, err, fmt.Sprintf("%s: got unexpected error: %s\n", tc.desc, err))
assert.ElementsMatch(t, tc.page.Messages, result.Messages, fmt.Sprintf("%s: expected: %v, got: %v\n", tc.desc, tc.page.Messages, result.Messages))
assert.Equal(t, tc.page.Total, result.Total, fmt.Sprintf("%s: expected %d got %d\n", tc.desc, tc.page.Total, result.Total))
}
}
func TestReadJSON(t *testing.T) {
writer := iwriter.New(client, testDB)
writer := iwriter.New(client, repoCfg, true)
id1, err := idProvider.ID()
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
assert.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
m := json.Message{
Channel: id1,
Publisher: id1,
Created: time.Now().UnixNano(),
Created: time.Now().Unix() * 1e9,
Subtopic: "subtopic/format/some_json",
Protocol: "coap",
Payload: map[string]interface{}{
@@ -411,11 +432,12 @@ func TestReadJSON(t *testing.T) {
assert.Nil(t, err, fmt.Sprintf("expected no error got %s\n", err))
id2, err := idProvider.ID()
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
assert.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
m = json.Message{
Channel: id2,
Publisher: id2,
Created: time.Now().UnixNano() + msgsNum,
Created: time.Now().Unix()*1e9 + msgsNum,
Subtopic: "subtopic/other_format/some_other_json",
Protocol: "udp",
Payload: map[string]interface{}{
@@ -442,14 +464,16 @@ func TestReadJSON(t *testing.T) {
for i := 0; i < msgsNum; i += 2 {
httpMsgs = append(httpMsgs, msgs2[i])
}
reader := ireader.New(client, testDB)
reader := ireader.New(client, repoCfg)
cases := map[string]struct {
cases := []struct {
desc string
chanID string
pageMeta readers.PageMetadata
page readers.MessagesPage
}{
"read message page for existing channel": {
{
desc: "read message page for existing channel",
chanID: id1,
pageMeta: readers.PageMetadata{
Format: messages1.Format,
@@ -461,7 +485,8 @@ func TestReadJSON(t *testing.T) {
Messages: fromJSON(msgs1[:1]),
},
},
"read message page for non-existent channel": {
{
desc: "read message page for non-existent channel",
chanID: wrongID,
pageMeta: readers.PageMetadata{
Format: messages1.Format,
@@ -472,7 +497,8 @@ func TestReadJSON(t *testing.T) {
Messages: []readers.Message{},
},
},
"read message last page": {
{
desc: "read message last page",
chanID: id2,
pageMeta: readers.PageMetadata{
Format: messages2.Format,
@@ -484,7 +510,8 @@ func TestReadJSON(t *testing.T) {
Messages: fromJSON(msgs2[msgsNum-20 : msgsNum]),
},
},
"read message with protocol": {
{
desc: "read message with protocol",
chanID: id2,
pageMeta: readers.PageMetadata{
Format: messages2.Format,
@@ -499,9 +526,9 @@ func TestReadJSON(t *testing.T) {
},
}
for desc, tc := range cases {
for _, tc := range cases {
result, err := reader.ReadAll(tc.chanID, tc.pageMeta)
assert.Nil(t, err, fmt.Sprintf("%s: expected no error got %s", desc, err))
assert.Nil(t, err, fmt.Sprintf("%s: got unexpected error: %s", tc.desc, err))
for i := 0; i < len(result.Messages); i++ {
m := result.Messages[i]
@@ -510,8 +537,8 @@ func TestReadJSON(t *testing.T) {
result.Messages[i] = m
}
assert.ElementsMatch(t, tc.page.Messages, result.Messages, fmt.Sprintf("%s: expected \n%v got \n%v", desc, tc.page.Messages, result.Messages))
assert.Equal(t, tc.page.Total, result.Total, fmt.Sprintf("%s: expected %v got %v", desc, tc.page.Total, result.Total))
assert.ElementsMatch(t, tc.page.Messages, result.Messages, fmt.Sprintf("%s: expected \n%v got \n%v", tc.desc, tc.page.Messages, result.Messages))
assert.Equal(t, tc.page.Total, result.Total, fmt.Sprintf("%s: expected %v got %v", tc.desc, tc.page.Total, result.Total))
}
}
+49 -17
View File
@@ -1,24 +1,38 @@
package influxdb_test
import (
"context"
"fmt"
"log"
"os"
"os/signal"
"syscall"
"testing"
"time"
influxdata "github.com/influxdata/influxdb/client/v2"
influxdb "github.com/influxdata/influxdb/client/v2"
log "github.com/mainflux/mainflux/logger"
influxdata "github.com/influxdata/influxdb-client-go/v2"
mainflux_log "github.com/mainflux/mainflux/logger"
dockertest "github.com/ory/dockertest/v3"
)
var (
testLog, _ = log.New(os.Stdout, log.Info.String())
testLog, _ = mainflux_log.New(os.Stdout, mainflux_log.Info.String())
address string
)
clientCfg = influxdata.HTTPConfig{
Username: "test",
Password: "test",
}
const (
dbToken = "test-token"
dbOrg = "test-org"
dbAdmin = "test-admin"
dbPass = "test-password"
dbBucket = "test-bucket"
dbInitMode = "setup"
dbFluxEnabled = "true"
dbBindAddress = ":8088"
port = "8086/tcp"
broker = "influxdb"
brokerVersion = "2.2-alpine"
poolMaxWait = 120 * time.Second
)
func TestMain(m *testing.M) {
@@ -28,26 +42,32 @@ func TestMain(m *testing.M) {
}
cfg := []string{
"INFLUXDB_USER=test",
"INFLUXDB_USER_PASSWORD=test",
"INFLUXDB_DB=test",
fmt.Sprintf("DOCKER_INFLUXDB_INIT_MODE=%s", dbInitMode),
fmt.Sprintf("DOCKER_INFLUXDB_INIT_USERNAME=%s", dbAdmin),
fmt.Sprintf("DOCKER_INFLUXDB_INIT_PASSWORD=%s", dbPass),
fmt.Sprintf("DOCKER_INFLUXDB_INIT_ORG=%s", dbOrg),
fmt.Sprintf("DOCKER_INFLUXDB_INIT_BUCKET=%s", dbBucket),
fmt.Sprintf("DOCKER_INFLUXDB_INIT_ADMIN_TOKEN=%s", dbToken),
fmt.Sprintf("INFLUXDB_HTTP_FLUX_ENABLED=%s", dbFluxEnabled),
fmt.Sprintf("INFLUXDB_BIND_ADDRESS=%s", dbBindAddress),
}
container, err := pool.Run("influxdb", "1.8.4", cfg)
container, err := pool.Run(broker, brokerVersion, cfg)
if err != nil {
testLog.Error(fmt.Sprintf("Could not start container: %s", err))
}
port := container.GetPort("8086/tcp")
clientCfg.Addr = fmt.Sprintf("http://localhost:%s", port)
handleInterrupt(m, pool, container)
address = fmt.Sprintf("%s:%s", "http://localhost", container.GetPort(port))
pool.MaxWait = poolMaxWait
if err := pool.Retry(func() error {
client, err = influxdb.NewHTTPClient(clientCfg)
_, _, err = client.Ping(5 * time.Millisecond)
client = influxdata.NewClient(address, dbToken)
_, err = client.Ready(context.Background())
return err
}); err != nil {
testLog.Error(fmt.Sprintf("Could not connect to docker: %s", err))
}
code := m.Run()
if err := pool.Purge(container); err != nil {
@@ -56,3 +76,15 @@ func TestMain(m *testing.M) {
os.Exit(code)
}
func handleInterrupt(m *testing.M, pool *dockertest.Pool, container *dockertest.Resource) {
c := make(chan os.Signal, 2)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
go func() {
<-c
if err := pool.Purge(container); err != nil {
log.Fatalf("Could not purge container: %s", err)
}
os.Exit(0)
}()
}
+201
View File
@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+24
View File
@@ -0,0 +1,24 @@
// Copyright 2021 DeepMap, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package runtime
// Binder is the interface implemented by types that can be bound to a query string or a parameter string
// The input can be assumed to be a valid string. If you define a Bind method you are responsible for all
// data being completely bound to the type.
//
// By convention, to approximate the behavior of Bind functions themselves,
// Binder implements Bind("") as a no-op.
type Binder interface {
Bind(src string) error
}
+502
View File
@@ -0,0 +1,502 @@
// Copyright 2019 DeepMap, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package runtime
import (
"encoding"
"encoding/json"
"fmt"
"net/url"
"reflect"
"strings"
"time"
"github.com/pkg/errors"
"github.com/deepmap/oapi-codegen/pkg/types"
)
// This function binds a parameter as described in the Path Parameters
// section here to a Go object:
// https://swagger.io/docs/specification/serialization/
// It is a backward compatible function to clients generated with codegen
// up to version v1.5.5. v1.5.6+ calls the function below.
func BindStyledParameter(style string, explode bool, paramName string,
value string, dest interface{}) error {
return BindStyledParameterWithLocation(style, explode, paramName, ParamLocationUndefined, value, dest)
}
// This function binds a parameter as described in the Path Parameters
// section here to a Go object:
// https://swagger.io/docs/specification/serialization/
func BindStyledParameterWithLocation(style string, explode bool, paramName string,
paramLocation ParamLocation, value string, dest interface{}) error {
if value == "" {
return fmt.Errorf("parameter '%s' is empty, can't bind its value", paramName)
}
// Based on the location of the parameter, we need to unescape it properly.
var err error
switch paramLocation {
case ParamLocationQuery, ParamLocationUndefined:
// We unescape undefined parameter locations here for older generated code,
// since prior to this refactoring, they always query unescaped.
value, err = url.QueryUnescape(value)
if err != nil {
return fmt.Errorf("error unescaping query parameter '%s': %v", paramName, err)
}
case ParamLocationPath:
value, err = url.PathUnescape(value)
if err != nil {
return fmt.Errorf("error unescaping path parameter '%s': %v", paramName, err)
}
default:
// Headers and cookies aren't escaped.
}
// If the destination implements encoding.TextUnmarshaler we use it for binding
if tu, ok := dest.(encoding.TextUnmarshaler); ok {
if err := tu.UnmarshalText([]byte(value)); err != nil {
return fmt.Errorf("error unmarshaling '%s' text as %T: %s", value, dest, err)
}
return nil
}
// Everything comes in by pointer, dereference it
v := reflect.Indirect(reflect.ValueOf(dest))
// This is the basic type of the destination object.
t := v.Type()
if t.Kind() == reflect.Struct {
// We've got a destination object, we'll create a JSON representation
// of the input value, and let the json library deal with the unmarshaling
parts, err := splitStyledParameter(style, explode, true, paramName, value)
if err != nil {
return err
}
return bindSplitPartsToDestinationStruct(paramName, parts, explode, dest)
}
if t.Kind() == reflect.Slice {
// Chop up the parameter into parts based on its style
parts, err := splitStyledParameter(style, explode, false, paramName, value)
if err != nil {
return fmt.Errorf("error splitting input '%s' into parts: %s", value, err)
}
return bindSplitPartsToDestinationArray(parts, dest)
}
// Try to bind the remaining types as a base type.
return BindStringToObject(value, dest)
}
// This is a complex set of operations, but each given parameter style can be
// packed together in multiple ways, using different styles of separators, and
// different packing strategies based on the explode flag. This function takes
// as input any parameter format, and unpacks it to a simple list of strings
// or key-values which we can then treat generically.
// Why, oh why, great Swagger gods, did you have to make this so complicated?
func splitStyledParameter(style string, explode bool, object bool, paramName string, value string) ([]string, error) {
switch style {
case "simple":
// In the simple case, we always split on comma
parts := strings.Split(value, ",")
return parts, nil
case "label":
// In the label case, it's more tricky. In the no explode case, we have
// /users/.3,4,5 for arrays
// /users/.role,admin,firstName,Alex for objects
// in the explode case, we have:
// /users/.3.4.5
// /users/.role=admin.firstName=Alex
if explode {
// In the exploded case, split everything on periods.
parts := strings.Split(value, ".")
// The first part should be an empty string because we have a
// leading period.
if parts[0] != "" {
return nil, fmt.Errorf("invalid format for label parameter '%s', should start with '.'", paramName)
}
return parts[1:], nil
} else {
// In the unexploded case, we strip off the leading period.
if value[0] != '.' {
return nil, fmt.Errorf("invalid format for label parameter '%s', should start with '.'", paramName)
}
// The rest is comma separated.
return strings.Split(value[1:], ","), nil
}
case "matrix":
if explode {
// In the exploded case, we break everything up on semicolon
parts := strings.Split(value, ";")
// The first part should always be empty string, since we started
// with ;something
if parts[0] != "" {
return nil, fmt.Errorf("invalid format for matrix parameter '%s', should start with ';'", paramName)
}
parts = parts[1:]
// Now, if we have an object, we just have a list of x=y statements.
// for a non-object, like an array, we have id=x, id=y. id=z, etc,
// so we need to strip the prefix from each of them.
if !object {
prefix := paramName + "="
for i := range parts {
parts[i] = strings.TrimPrefix(parts[i], prefix)
}
}
return parts, nil
} else {
// In the unexploded case, parameters will start with ;paramName=
prefix := ";" + paramName + "="
if !strings.HasPrefix(value, prefix) {
return nil, fmt.Errorf("expected parameter '%s' to start with %s", paramName, prefix)
}
str := strings.TrimPrefix(value, prefix)
return strings.Split(str, ","), nil
}
case "form":
var parts []string
if explode {
parts = strings.Split(value, "&")
if !object {
prefix := paramName + "="
for i := range parts {
parts[i] = strings.TrimPrefix(parts[i], prefix)
}
}
return parts, nil
} else {
parts = strings.Split(value, ",")
prefix := paramName + "="
for i := range parts {
parts[i] = strings.TrimPrefix(parts[i], prefix)
}
}
return parts, nil
}
return nil, fmt.Errorf("unhandled parameter style: %s", style)
}
// Given a set of values as a slice, create a slice to hold them all, and
// assign to each one by one.
func bindSplitPartsToDestinationArray(parts []string, dest interface{}) error {
// Everything comes in by pointer, dereference it
v := reflect.Indirect(reflect.ValueOf(dest))
// This is the basic type of the destination object.
t := v.Type()
// We've got a destination array, bind each object one by one.
// This generates a slice of the correct element type and length to
// hold all the parts.
newArray := reflect.MakeSlice(t, len(parts), len(parts))
for i, p := range parts {
err := BindStringToObject(p, newArray.Index(i).Addr().Interface())
if err != nil {
return fmt.Errorf("error setting array element: %s", err)
}
}
v.Set(newArray)
return nil
}
// Given a set of chopped up parameter parts, bind them to a destination
// struct. The exploded parameter controls whether we send key value pairs
// in the exploded case, or a sequence of values which are interpreted as
// tuples.
// Given the struct Id { firstName string, role string }, as in the canonical
// swagger examples, in the exploded case, we would pass
// ["firstName=Alex", "role=admin"], where in the non-exploded case, we would
// pass "firstName", "Alex", "role", "admin"]
//
// We punt the hard work of binding these values to the object to the json
// library. We'll turn those arrays into JSON strings, and unmarshal
// into the struct.
func bindSplitPartsToDestinationStruct(paramName string, parts []string, explode bool, dest interface{}) error {
// We've got a destination object, we'll create a JSON representation
// of the input value, and let the json library deal with the unmarshaling
var fields []string
if explode {
fields = make([]string, len(parts))
for i, property := range parts {
propertyParts := strings.Split(property, "=")
if len(propertyParts) != 2 {
return fmt.Errorf("parameter '%s' has invalid exploded format", paramName)
}
fields[i] = "\"" + propertyParts[0] + "\":\"" + propertyParts[1] + "\""
}
} else {
if len(parts)%2 != 0 {
return fmt.Errorf("parameter '%s' has invalid format, property/values need to be pairs", paramName)
}
fields = make([]string, len(parts)/2)
for i := 0; i < len(parts); i += 2 {
key := parts[i]
value := parts[i+1]
fields[i/2] = "\"" + key + "\":\"" + value + "\""
}
}
jsonParam := "{" + strings.Join(fields, ",") + "}"
err := json.Unmarshal([]byte(jsonParam), dest)
if err != nil {
return fmt.Errorf("error binding parameter %s fields: %s", paramName, err)
}
return nil
}
// This works much like BindStyledParameter, however it takes a query argument
// input array from the url package, since query arguments come through a
// different path than the styled arguments. They're also exceptionally fussy.
// For example, consider the exploded and unexploded form parameter examples:
// (exploded) /users?role=admin&firstName=Alex
// (unexploded) /users?id=role,admin,firstName,Alex
//
// In the first case, we can pull the "id" parameter off the context,
// and unmarshal via json as an intermediate. Easy. In the second case, we
// don't have the id QueryParam present, but must find "role", and "firstName".
// what if there is another parameter similar to "ID" named "role"? We can't
// tell them apart. This code tries to fail, but the moral of the story is that
// you shouldn't pass objects via form styled query arguments, just use
// the Content parameter form.
func BindQueryParameter(style string, explode bool, required bool, paramName string,
queryParams url.Values, dest interface{}) error {
// dv = destination value.
dv := reflect.Indirect(reflect.ValueOf(dest))
// intermediate value form which is either dv or dv dereferenced.
v := dv
// inner code will bind the string's value to this interface.
var output interface{}
if required {
// If the parameter is required, then the generated code will pass us
// a pointer to it: &int, &object, and so forth. We can directly set
// them.
output = dest
} else {
// For optional parameters, we have an extra indirect. An optional
// parameter of type "int" will be *int on the struct. We pass that
// in by pointer, and have **int.
// If the destination, is a nil pointer, we need to allocate it.
if v.IsNil() {
t := v.Type()
newValue := reflect.New(t.Elem())
// for now, hang onto the output buffer separately from destination,
// as we don't want to write anything to destination until we can
// unmarshal successfully, and check whether a field is required.
output = newValue.Interface()
} else {
// If the destination isn't nil, just use that.
output = v.Interface()
}
// Get rid of that extra indirect as compared to the required case,
// so the code below doesn't have to care.
v = reflect.Indirect(reflect.ValueOf(output))
}
// This is the basic type of the destination object.
t := v.Type()
k := t.Kind()
switch style {
case "form":
var parts []string
if explode {
// ok, the explode case in query arguments is very, very annoying,
// because an exploded object, such as /users?role=admin&firstName=Alex
// isn't actually present in the parameter array. We have to do
// different things based on destination type.
values, found := queryParams[paramName]
var err error
switch k {
case reflect.Slice:
// In the slice case, we simply use the arguments provided by
// http library.
if !found {
if required {
return fmt.Errorf("query parameter '%s' is required", paramName)
} else {
return nil
}
}
err = bindSplitPartsToDestinationArray(values, output)
case reflect.Struct:
// This case is really annoying, and error prone, but the
// form style object binding doesn't tell us which arguments
// in the query string correspond to the object's fields. We'll
// try to bind field by field.
err = bindParamsToExplodedObject(paramName, queryParams, output)
default:
// Primitive object case. We expect to have 1 value to
// unmarshal.
if len(values) == 0 {
if required {
return fmt.Errorf("query parameter '%s' is required", paramName)
} else {
return nil
}
}
if len(values) != 1 {
return fmt.Errorf("multiple values for single value parameter '%s'", paramName)
}
err = BindStringToObject(values[0], output)
}
if err != nil {
return err
}
// If the parameter is required, and we've successfully unmarshaled
// it, this assigns the new object to the pointer pointer.
if !required {
dv.Set(reflect.ValueOf(output))
}
return nil
} else {
values, found := queryParams[paramName]
if !found {
if required {
return fmt.Errorf("query parameter '%s' is required", paramName)
} else {
return nil
}
}
if len(values) != 1 {
return fmt.Errorf("parameter '%s' is not exploded, but is specified multiple times", paramName)
}
parts = strings.Split(values[0], ",")
}
var err error
switch k {
case reflect.Slice:
err = bindSplitPartsToDestinationArray(parts, output)
case reflect.Struct:
err = bindSplitPartsToDestinationStruct(paramName, parts, explode, output)
default:
if len(parts) == 0 {
if required {
return fmt.Errorf("query parameter '%s' is required", paramName)
} else {
return nil
}
}
if len(parts) != 1 {
return fmt.Errorf("multiple values for single value parameter '%s'", paramName)
}
err = BindStringToObject(parts[0], output)
}
if err != nil {
return err
}
if !required {
dv.Set(reflect.ValueOf(output))
}
return nil
case "deepObject":
if !explode {
return errors.New("deepObjects must be exploded")
}
return UnmarshalDeepObject(dest, paramName, queryParams)
case "spaceDelimited", "pipeDelimited":
return fmt.Errorf("query arguments of style '%s' aren't yet supported", style)
default:
return fmt.Errorf("style '%s' on parameter '%s' is invalid", style, paramName)
}
}
// This function reflects the destination structure, and pulls the value for
// each settable field from the given parameters map. This is to deal with the
// exploded form styled object which may occupy any number of parameter names.
// We don't try to be smart here, if the field exists as a query argument,
// set its value.
func bindParamsToExplodedObject(paramName string, values url.Values, dest interface{}) error {
// Dereference pointers to their destination values
binder, v, t := indirect(dest)
if binder != nil {
return BindStringToObject(values.Get(paramName), dest)
}
if t.Kind() != reflect.Struct {
return fmt.Errorf("unmarshaling query arg '%s' into wrong type", paramName)
}
for i := 0; i < t.NumField(); i++ {
fieldT := t.Field(i)
// Skip unsettable fields, such as internal ones.
if !v.Field(i).CanSet() {
continue
}
// Find the json annotation on the field, and use the json specified
// name if available, otherwise, just the field name.
tag := fieldT.Tag.Get("json")
fieldName := fieldT.Name
if tag != "" {
tagParts := strings.Split(tag, ",")
name := tagParts[0]
if name != "" {
fieldName = name
}
}
// At this point, we look up field name in the parameter list.
fieldVal, found := values[fieldName]
if found {
if len(fieldVal) != 1 {
return fmt.Errorf("field '%s' specified multiple times for param '%s'", fieldName, paramName)
}
err := BindStringToObject(fieldVal[0], v.Field(i).Addr().Interface())
if err != nil {
return fmt.Errorf("could not bind query arg '%s' to request object: %s'", paramName, err)
}
}
}
return nil
}
// indirect
func indirect(dest interface{}) (interface{}, reflect.Value, reflect.Type) {
v := reflect.ValueOf(dest)
if v.Type().NumMethod() > 0 && v.CanInterface() {
if u, ok := v.Interface().(Binder); ok {
return u, reflect.Value{}, nil
}
}
v = reflect.Indirect(v)
t := v.Type()
// special handling for custom types which might look like an object. We
// don't want to use object binding on them, but rather treat them as
// primitive types. time.Time{} is a unique case since we can't add a Binder
// to it without changing the underlying generated code.
if t.ConvertibleTo(reflect.TypeOf(time.Time{})) {
return dest, reflect.Value{}, nil
}
if t.ConvertibleTo(reflect.TypeOf(types.Date{})) {
return dest, reflect.Value{}, nil
}
return nil, v, t
}
+143
View File
@@ -0,0 +1,143 @@
// Copyright 2019 DeepMap, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package runtime
import (
"errors"
"fmt"
"reflect"
"strconv"
"time"
"github.com/deepmap/oapi-codegen/pkg/types"
)
// This function takes a string, and attempts to assign it to the destination
// interface via whatever type conversion is necessary. We have to do this
// via reflection instead of a much simpler type switch so that we can handle
// type aliases. This function was the easy way out, the better way, since we
// know the destination type each place that we use this, is to generate code
// to read each specific type.
func BindStringToObject(src string, dst interface{}) error {
var err error
v := reflect.ValueOf(dst)
t := reflect.TypeOf(dst)
// We need to dereference pointers
if t.Kind() == reflect.Ptr {
v = reflect.Indirect(v)
t = v.Type()
}
// The resulting type must be settable. reflect will catch issues like
// passing the destination by value.
if !v.CanSet() {
return errors.New("destination is not settable")
}
switch t.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
var val int64
val, err = strconv.ParseInt(src, 10, 64)
if err == nil {
v.SetInt(val)
}
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
var val uint64
val, err = strconv.ParseUint(src, 10, 64)
if err == nil {
v.SetUint(val)
}
case reflect.String:
v.SetString(src)
err = nil
case reflect.Float64, reflect.Float32:
var val float64
val, err = strconv.ParseFloat(src, 64)
if err == nil {
v.SetFloat(val)
}
case reflect.Bool:
var val bool
val, err = strconv.ParseBool(src)
if err == nil {
v.SetBool(val)
}
case reflect.Struct:
// if this is not of type Time or of type Date look to see if this is of type Binder.
if dstType, ok := dst.(Binder); ok {
return dstType.Bind(src)
}
if t.ConvertibleTo(reflect.TypeOf(time.Time{})) {
// Don't fail on empty string.
if src == "" {
return nil
}
// Time is a special case of a struct that we handle
parsedTime, err := time.Parse(time.RFC3339Nano, src)
if err != nil {
parsedTime, err = time.Parse(types.DateFormat, src)
if err != nil {
return fmt.Errorf("error parsing '%s' as RFC3339 or 2006-01-02 time: %s", src, err)
}
}
// So, assigning this gets a little fun. We have a value to the
// dereference destination. We can't do a conversion to
// time.Time because the result isn't assignable, so we need to
// convert pointers.
if t != reflect.TypeOf(time.Time{}) {
vPtr := v.Addr()
vtPtr := vPtr.Convert(reflect.TypeOf(&time.Time{}))
v = reflect.Indirect(vtPtr)
}
v.Set(reflect.ValueOf(parsedTime))
return nil
}
if t.ConvertibleTo(reflect.TypeOf(types.Date{})) {
// Don't fail on empty string.
if src == "" {
return nil
}
parsedTime, err := time.Parse(types.DateFormat, src)
if err != nil {
return fmt.Errorf("error parsing '%s' as date: %s", src, err)
}
parsedDate := types.Date{Time: parsedTime}
// We have to do the same dance here to assign, just like with times
// above.
if t != reflect.TypeOf(types.Date{}) {
vPtr := v.Addr()
vtPtr := vPtr.Convert(reflect.TypeOf(&types.Date{}))
v = reflect.Indirect(vtPtr)
}
v.Set(reflect.ValueOf(parsedDate))
return nil
}
// We fall through to the error case below if we haven't handled the
// destination type above.
fallthrough
default:
// We've got a bunch of types unimplemented, don't fail silently.
err = fmt.Errorf("can not bind to destination of type: %s", t.Kind())
}
if err != nil {
return fmt.Errorf("error binding string parameter: %s", err)
}
return nil
}
+357
View File
@@ -0,0 +1,357 @@
package runtime
import (
"encoding/json"
"fmt"
"github.com/deepmap/oapi-codegen/pkg/types"
"net/url"
"reflect"
"sort"
"strconv"
"strings"
"time"
"github.com/pkg/errors"
)
func marshalDeepObject(in interface{}, path []string) ([]string, error) {
var result []string
switch t := in.(type) {
case []interface{}:
// For the array, we will use numerical subscripts of the form [x],
// in the same order as the array.
for i, iface := range t {
newPath := append(path, strconv.Itoa(i))
fields, err := marshalDeepObject(iface, newPath)
if err != nil {
return nil, errors.Wrap(err, "error traversing array")
}
result = append(result, fields...)
}
case map[string]interface{}:
// For a map, each key (field name) becomes a member of the path, and
// we recurse. First, sort the keys.
keys := make([]string, len(t))
i := 0
for k := range t {
keys[i] = k
i++
}
sort.Strings(keys)
// Now, for each key, we recursively marshal it.
for _, k := range keys {
newPath := append(path, k)
fields, err := marshalDeepObject(t[k], newPath)
if err != nil {
return nil, errors.Wrap(err, "error traversing map")
}
result = append(result, fields...)
}
default:
// Now, for a concrete value, we will turn the path elements
// into a deepObject style set of subscripts. [a, b, c] turns into
// [a][b][c]
prefix := "[" + strings.Join(path, "][") + "]"
result = []string{
prefix + fmt.Sprintf("=%v", t),
}
}
return result, nil
}
func MarshalDeepObject(i interface{}, paramName string) (string, error) {
// We're going to marshal to JSON and unmarshal into an interface{},
// which will use the json pkg to deal with all the field annotations. We
// can then walk the generic object structure to produce a deepObject. This
// isn't efficient and it would be more efficient to reflect on our own,
// but it's complicated, error-prone code.
buf, err := json.Marshal(i)
if err != nil {
return "", errors.Wrap(err, "failed to marshal input to JSON")
}
var i2 interface{}
err = json.Unmarshal(buf, &i2)
if err != nil {
return "", errors.Wrap(err, "failed to unmarshal JSON")
}
fields, err := marshalDeepObject(i2, nil)
if err != nil {
return "", errors.Wrap(err, "error traversing JSON structure")
}
// Prefix the param name to each subscripted field.
for i := range fields {
fields[i] = paramName + fields[i]
}
return strings.Join(fields, "&"), nil
}
type fieldOrValue struct {
fields map[string]fieldOrValue
value string
}
func (f *fieldOrValue) appendPathValue(path []string, value string) {
fieldName := path[0]
if len(path) == 1 {
f.fields[fieldName] = fieldOrValue{value: value}
return
}
pv, found := f.fields[fieldName]
if !found {
pv = fieldOrValue{
fields: make(map[string]fieldOrValue),
}
f.fields[fieldName] = pv
}
pv.appendPathValue(path[1:], value)
}
func makeFieldOrValue(paths [][]string, values []string) fieldOrValue {
f := fieldOrValue{
fields: make(map[string]fieldOrValue),
}
for i := range paths {
path := paths[i]
value := values[i]
f.appendPathValue(path, value)
}
return f
}
func UnmarshalDeepObject(dst interface{}, paramName string, params url.Values) error {
// Params are all the query args, so we need those that look like
// "paramName["...
var fieldNames []string
var fieldValues []string
searchStr := paramName + "["
for pName, pValues := range params {
if strings.HasPrefix(pName, searchStr) {
// trim the parameter name from the full name.
pName = pName[len(paramName):]
fieldNames = append(fieldNames, pName)
if len(pValues) != 1 {
return fmt.Errorf("%s has multiple values", pName)
}
fieldValues = append(fieldValues, pValues[0])
}
}
// Now, for each field, reconstruct its subscript path and value
paths := make([][]string, len(fieldNames))
for i, path := range fieldNames {
path = strings.TrimLeft(path, "[")
path = strings.TrimRight(path, "]")
paths[i] = strings.Split(path, "][")
}
fieldPaths := makeFieldOrValue(paths, fieldValues)
err := assignPathValues(dst, fieldPaths)
if err != nil {
return errors.Wrap(err, "error assigning value to destination")
}
return nil
}
// This returns a field name, either using the variable name, or the json
// annotation if that exists.
func getFieldName(f reflect.StructField) string {
n := f.Name
tag, found := f.Tag.Lookup("json")
if found {
// If we have a json field, and the first part of it before the
// first comma is non-empty, that's our field name.
parts := strings.Split(tag, ",")
if parts[0] != "" {
n = parts[0]
}
}
return n
}
// Create a map of field names that we'll see in the deepObject to reflect
// field indices on the given type.
func fieldIndicesByJsonTag(i interface{}) (map[string]int, error) {
t := reflect.TypeOf(i)
if t.Kind() != reflect.Struct {
return nil, errors.New("expected a struct as input")
}
n := t.NumField()
fieldMap := make(map[string]int)
for i := 0; i < n; i++ {
field := t.Field(i)
fieldName := getFieldName(field)
fieldMap[fieldName] = i
}
return fieldMap, nil
}
func assignPathValues(dst interface{}, pathValues fieldOrValue) error {
//t := reflect.TypeOf(dst)
v := reflect.ValueOf(dst)
iv := reflect.Indirect(v)
it := iv.Type()
switch it.Kind() {
case reflect.Slice:
sliceLength := len(pathValues.fields)
dstSlice := reflect.MakeSlice(it, sliceLength, sliceLength)
err := assignSlice(dstSlice, pathValues)
if err != nil {
return errors.Wrap(err, "error assigning slice")
}
iv.Set(dstSlice)
return nil
case reflect.Struct:
// Some special types we care about are structs. Handle them
// here. They may be redefined, so we need to do some hoop
// jumping. If the types are aliased, we need to type convert
// the pointer, then set the value of the dereference pointer.
// We check to see if the object implements the Binder interface first.
if dst, isBinder := v.Interface().(Binder); isBinder {
return dst.Bind(pathValues.value)
}
// Then check the legacy types
if it.ConvertibleTo(reflect.TypeOf(types.Date{})) {
var date types.Date
var err error
date.Time, err = time.Parse(types.DateFormat, pathValues.value)
if err != nil {
return errors.Wrap(err, "invalid date format")
}
dst := iv
if it != reflect.TypeOf(types.Date{}) {
// Types are aliased, convert the pointers.
ivPtr := iv.Addr()
aPtr := ivPtr.Convert(reflect.TypeOf(&types.Date{}))
dst = reflect.Indirect(aPtr)
}
dst.Set(reflect.ValueOf(date))
}
if it.ConvertibleTo(reflect.TypeOf(time.Time{})) {
var tm time.Time
var err error
tm, err = time.Parse(time.RFC3339Nano, pathValues.value)
if err != nil {
// Fall back to parsing it as a date.
tm, err = time.Parse(types.DateFormat, pathValues.value)
if err != nil {
return fmt.Errorf("error parsing tim as RFC3339 or 2006-01-02 time: %s", err)
}
return errors.Wrap(err, "invalid date format")
}
dst := iv
if it != reflect.TypeOf(time.Time{}) {
// Types are aliased, convert the pointers.
ivPtr := iv.Addr()
aPtr := ivPtr.Convert(reflect.TypeOf(&time.Time{}))
dst = reflect.Indirect(aPtr)
}
dst.Set(reflect.ValueOf(tm))
}
fieldMap, err := fieldIndicesByJsonTag(iv.Interface())
if err != nil {
return errors.Wrap(err, "failed enumerating fields")
}
for _, fieldName := range sortedFieldOrValueKeys(pathValues.fields) {
fieldValue := pathValues.fields[fieldName]
fieldIndex, found := fieldMap[fieldName]
if !found {
return fmt.Errorf("field [%s] is not present in destination object", fieldName)
}
field := iv.Field(fieldIndex)
err = assignPathValues(field.Addr().Interface(), fieldValue)
if err != nil {
return errors.Wrapf(err, "error assigning field [%s]", fieldName)
}
}
return nil
case reflect.Ptr:
// If we have a pointer after redirecting, it means we're dealing with
// an optional field, such as *string, which was passed in as &foo. We
// will allocate it if necessary, and call ourselves with a different
// interface.
dstVal := reflect.New(it.Elem())
dstPtr := dstVal.Interface()
err := assignPathValues(dstPtr, pathValues)
iv.Set(dstVal)
return err
case reflect.Bool:
val, err := strconv.ParseBool(pathValues.value)
if err != nil {
return fmt.Errorf("expected a valid bool, got %s", pathValues.value)
}
iv.SetBool(val)
return nil
case reflect.Float32:
val, err := strconv.ParseFloat(pathValues.value, 32)
if err != nil {
return fmt.Errorf("expected a valid float, got %s", pathValues.value)
}
iv.SetFloat(val)
return nil
case reflect.Float64:
val, err := strconv.ParseFloat(pathValues.value, 64)
if err != nil {
return fmt.Errorf("expected a valid float, got %s", pathValues.value)
}
iv.SetFloat(val)
return nil
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
val, err := strconv.ParseInt(pathValues.value, 10, 64)
if err != nil {
return fmt.Errorf("expected a valid int, got %s", pathValues.value)
}
iv.SetInt(val)
return nil
case reflect.String:
iv.SetString(pathValues.value)
return nil
default:
return errors.New("unhandled type: " + it.String())
}
}
func assignSlice(dst reflect.Value, pathValues fieldOrValue) error {
// Gather up the values
nValues := len(pathValues.fields)
values := make([]string, nValues)
// We expect to have consecutive array indices in the map
for i := 0; i < nValues; i++ {
indexStr := strconv.Itoa(i)
fv, found := pathValues.fields[indexStr]
if !found {
return errors.New("array deepObjects must have consecutive indices")
}
values[i] = fv.value
}
// This could be cleaner, but we can call into assignPathValues to
// avoid recreating this logic.
for i := 0; i < nValues; i++ {
dstElem := dst.Index(i).Addr()
err := assignPathValues(dstElem.Interface(), fieldOrValue{value: values[i]})
if err != nil {
return errors.Wrap(err, "error binding array")
}
}
return nil
}
func sortedFieldOrValueKeys(m map[string]fieldOrValue) []string {
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
return keys
}
+390
View File
@@ -0,0 +1,390 @@
// Copyright 2019 DeepMap, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package runtime
import (
"fmt"
"net/url"
"reflect"
"sort"
"strconv"
"strings"
"time"
"github.com/pkg/errors"
"github.com/deepmap/oapi-codegen/pkg/types"
)
// Parameter escaping works differently based on where a header is found
type ParamLocation int
const (
ParamLocationUndefined ParamLocation = iota
ParamLocationQuery
ParamLocationPath
ParamLocationHeader
ParamLocationCookie
)
// This function is used by older generated code, and must remain compatible
// with that code. It is not to be used in new templates. Please see the
// function below, which can specialize its output based on the location of
// the parameter.
func StyleParam(style string, explode bool, paramName string, value interface{}) (string, error) {
return StyleParamWithLocation(style, explode, paramName, ParamLocationUndefined, value)
}
// Given an input value, such as a primitive type, array or object, turn it
// into a parameter based on style/explode definition, performing whatever
// escaping is necessary based on parameter location
func StyleParamWithLocation(style string, explode bool, paramName string, paramLocation ParamLocation, value interface{}) (string, error) {
t := reflect.TypeOf(value)
v := reflect.ValueOf(value)
// Things may be passed in by pointer, we need to dereference, so return
// error on nil.
if t.Kind() == reflect.Ptr {
if v.IsNil() {
return "", fmt.Errorf("value is a nil pointer")
}
v = reflect.Indirect(v)
t = v.Type()
}
switch t.Kind() {
case reflect.Slice:
n := v.Len()
sliceVal := make([]interface{}, n)
for i := 0; i < n; i++ {
sliceVal[i] = v.Index(i).Interface()
}
return styleSlice(style, explode, paramName, paramLocation, sliceVal)
case reflect.Struct:
return styleStruct(style, explode, paramName, paramLocation, value)
case reflect.Map:
return styleMap(style, explode, paramName, paramLocation, value)
default:
return stylePrimitive(style, explode, paramName, paramLocation, value)
}
}
func styleSlice(style string, explode bool, paramName string, paramLocation ParamLocation, values []interface{}) (string, error) {
if style == "deepObject" {
if !explode {
return "", errors.New("deepObjects must be exploded")
}
return MarshalDeepObject(values, paramName)
}
var prefix string
var separator string
switch style {
case "simple":
separator = ","
case "label":
prefix = "."
if explode {
separator = "."
} else {
separator = ","
}
case "matrix":
prefix = fmt.Sprintf(";%s=", paramName)
if explode {
separator = prefix
} else {
separator = ","
}
case "form":
prefix = fmt.Sprintf("%s=", paramName)
if explode {
separator = "&" + prefix
} else {
separator = ","
}
case "spaceDelimited":
prefix = fmt.Sprintf("%s=", paramName)
if explode {
separator = "&" + prefix
} else {
separator = " "
}
case "pipeDelimited":
prefix = fmt.Sprintf("%s=", paramName)
if explode {
separator = "&" + prefix
} else {
separator = "|"
}
default:
return "", fmt.Errorf("unsupported style '%s'", style)
}
// We're going to assume here that the array is one of simple types.
var err error
var part string
parts := make([]string, len(values))
for i, v := range values {
part, err = primitiveToString(v)
part = escapeParameterString(part, paramLocation)
parts[i] = part
if err != nil {
return "", fmt.Errorf("error formatting '%s': %s", paramName, err)
}
}
return prefix + strings.Join(parts, separator), nil
}
func sortedKeys(strMap map[string]string) []string {
keys := make([]string, len(strMap))
i := 0
for k := range strMap {
keys[i] = k
i++
}
sort.Strings(keys)
return keys
}
// This is a special case. The struct may be a date or time, in
// which case, marshal it in correct format.
func marshalDateTimeValue(value interface{}) (string, bool) {
v := reflect.Indirect(reflect.ValueOf(value))
t := v.Type()
if t.ConvertibleTo(reflect.TypeOf(time.Time{})) {
tt := v.Convert(reflect.TypeOf(time.Time{}))
timeVal := tt.Interface().(time.Time)
return timeVal.Format(time.RFC3339Nano), true
}
if t.ConvertibleTo(reflect.TypeOf(types.Date{})) {
d := v.Convert(reflect.TypeOf(types.Date{}))
dateVal := d.Interface().(types.Date)
return dateVal.Format(types.DateFormat), true
}
return "", false
}
func styleStruct(style string, explode bool, paramName string, paramLocation ParamLocation, value interface{}) (string, error) {
if timeVal, ok := marshalDateTimeValue(value); ok {
styledVal, err := stylePrimitive(style, explode, paramName, paramLocation, timeVal)
if err != nil {
return "", errors.Wrap(err, "failed to style time")
}
return styledVal, nil
}
if style == "deepObject" {
if !explode {
return "", errors.New("deepObjects must be exploded")
}
return MarshalDeepObject(value, paramName)
}
// Otherwise, we need to build a dictionary of the struct's fields. Each
// field may only be a primitive value.
v := reflect.ValueOf(value)
t := reflect.TypeOf(value)
fieldDict := make(map[string]string)
for i := 0; i < t.NumField(); i++ {
fieldT := t.Field(i)
// Find the json annotation on the field, and use the json specified
// name if available, otherwise, just the field name.
tag := fieldT.Tag.Get("json")
fieldName := fieldT.Name
if tag != "" {
tagParts := strings.Split(tag, ",")
name := tagParts[0]
if name != "" {
fieldName = name
}
}
f := v.Field(i)
// Unset optional fields will be nil pointers, skip over those.
if f.Type().Kind() == reflect.Ptr && f.IsNil() {
continue
}
str, err := primitiveToString(f.Interface())
if err != nil {
return "", fmt.Errorf("error formatting '%s': %s", paramName, err)
}
fieldDict[fieldName] = str
}
return processFieldDict(style, explode, paramName, paramLocation, fieldDict)
}
func styleMap(style string, explode bool, paramName string, paramLocation ParamLocation, value interface{}) (string, error) {
if style == "deepObject" {
if !explode {
return "", errors.New("deepObjects must be exploded")
}
return MarshalDeepObject(value, paramName)
}
dict, ok := value.(map[string]interface{})
if !ok {
return "", errors.New("map not of type map[string]interface{}")
}
fieldDict := make(map[string]string)
for fieldName, value := range dict {
str, err := primitiveToString(value)
if err != nil {
return "", fmt.Errorf("error formatting '%s': %s", paramName, err)
}
fieldDict[fieldName] = str
}
return processFieldDict(style, explode, paramName, paramLocation, fieldDict)
}
func processFieldDict(style string, explode bool, paramName string, paramLocation ParamLocation, fieldDict map[string]string) (string, error) {
var parts []string
// This works for everything except deepObject. We'll handle that one
// separately.
if style != "deepObject" {
if explode {
for _, k := range sortedKeys(fieldDict) {
v := escapeParameterString(fieldDict[k], paramLocation)
parts = append(parts, k+"="+v)
}
} else {
for _, k := range sortedKeys(fieldDict) {
v := escapeParameterString(fieldDict[k], paramLocation)
parts = append(parts, k)
parts = append(parts, v)
}
}
}
var prefix string
var separator string
switch style {
case "simple":
separator = ","
case "label":
prefix = "."
if explode {
separator = prefix
} else {
separator = ","
}
case "matrix":
if explode {
separator = ";"
prefix = ";"
} else {
separator = ","
prefix = fmt.Sprintf(";%s=", paramName)
}
case "form":
if explode {
separator = "&"
} else {
prefix = fmt.Sprintf("%s=", paramName)
separator = ","
}
case "deepObject":
{
if !explode {
return "", fmt.Errorf("deepObject parameters must be exploded")
}
for _, k := range sortedKeys(fieldDict) {
v := fieldDict[k]
part := fmt.Sprintf("%s[%s]=%s", paramName, k, v)
parts = append(parts, part)
}
separator = "&"
}
default:
return "", fmt.Errorf("unsupported style '%s'", style)
}
return prefix + strings.Join(parts, separator), nil
}
func stylePrimitive(style string, explode bool, paramName string, paramLocation ParamLocation, value interface{}) (string, error) {
strVal, err := primitiveToString(value)
if err != nil {
return "", err
}
var prefix string
switch style {
case "simple":
case "label":
prefix = "."
case "matrix":
prefix = fmt.Sprintf(";%s=", paramName)
case "form":
prefix = fmt.Sprintf("%s=", paramName)
default:
return "", fmt.Errorf("unsupported style '%s'", style)
}
return prefix + escapeParameterString(strVal, paramLocation), nil
}
// Converts a primitive value to a string. We need to do this based on the
// Kind of an interface, not the Type to work with aliased types.
func primitiveToString(value interface{}) (string, error) {
var output string
// Values may come in by pointer for optionals, so make sure to dereferene.
v := reflect.Indirect(reflect.ValueOf(value))
t := v.Type()
kind := t.Kind()
switch kind {
case reflect.Int8, reflect.Int32, reflect.Int64, reflect.Int:
output = strconv.FormatInt(v.Int(), 10)
case reflect.Float64:
output = strconv.FormatFloat(v.Float(), 'f', -1, 64)
case reflect.Float32:
output = strconv.FormatFloat(v.Float(), 'f', -1, 32)
case reflect.Bool:
if v.Bool() {
output = "true"
} else {
output = "false"
}
case reflect.String:
output = v.String()
default:
return "", fmt.Errorf("unsupported type %s", reflect.TypeOf(value).String())
}
return output, nil
}
// This function escapes a parameter value bas on the location of that parameter.
// Query params and path params need different kinds of escaping, while header
// and cookie params seem not to need escaping.
func escapeParameterString(value string, paramLocation ParamLocation) string {
switch paramLocation {
case ParamLocationQuery:
return url.QueryEscape(value)
case ParamLocationPath:
return url.PathEscape(value)
default:
return value
}
}
+30
View File
@@ -0,0 +1,30 @@
package types
import (
"encoding/json"
"time"
)
const DateFormat = "2006-01-02"
type Date struct {
time.Time
}
func (d Date) MarshalJSON() ([]byte, error) {
return json.Marshal(d.Time.Format(DateFormat))
}
func (d *Date) UnmarshalJSON(data []byte) error {
var dateStr string
err := json.Unmarshal(data, &dateStr)
if err != nil {
return err
}
parsed, err := time.Parse(DateFormat, dateStr)
if err != nil {
return err
}
d.Time = parsed
return nil
}
+27
View File
@@ -0,0 +1,27 @@
package types
import (
"encoding/json"
"errors"
)
type Email string
func (e Email) MarshalJSON() ([]byte, error) {
if !emailRegex.MatchString(string(e)) {
return nil, errors.New("email: failed to pass regex validation")
}
return json.Marshal(string(e))
}
func (e *Email) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); err != nil {
return err
}
if !emailRegex.MatchString(s) {
return errors.New("email: failed to pass regex validation")
}
*e = Email(s)
return nil
}
+11
View File
@@ -0,0 +1,11 @@
package types
import "regexp"
const (
emailRegexString = "^(?:(?:(?:(?:[a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+(?:\\.([a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+)*)|(?:(?:\\x22)(?:(?:(?:(?:\\x20|\\x09)*(?:\\x0d\\x0a))?(?:\\x20|\\x09)+)?(?:(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]|\\x21|[\\x23-\\x5b]|[\\x5d-\\x7e]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[\\x01-\\x09\\x0b\\x0c\\x0d-\\x7f]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}]))))*(?:(?:(?:\\x20|\\x09)*(?:\\x0d\\x0a))?(\\x20|\\x09)+)?(?:\\x22))))@(?:(?:(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])(?:[a-zA-Z]|\\d|-|\\.|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.)+(?:(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])(?:[a-zA-Z]|\\d|-|\\.|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.?$"
)
var (
emailRegex = regexp.MustCompile(emailRegexString)
)
@@ -0,0 +1,20 @@
# Binaries for programs and plugins
*.exe
*.bat
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Dependency directories (remove the comment below to include it)
# vendor/
# IntelliJ IDEA
.IDEA
*.IML
+276
View File
@@ -0,0 +1,276 @@
## 2.12.2 [2023-01-26]
### Bug fixes
- [#368](https://github.com/influxdata/influxdb-client-go/pull/368) Allowing proxy from environment variable
## 2.12.1 [2022-12-01]
### Bug fixes
- [#363](https://github.com/influxdata/influxdb-client-go/pull/363) Generated server stubs return also error message from InfluxDB 1.x forward compatible API.
- [#364](https://github.com/influxdata/influxdb-client-go/pull/364) Fixed panic when retrying over a long period without a server connection.
### Documentation
- [#366](https://github.com/influxdata/influxdb-client-go/pull/366) Readme improvements:
- Added GOPATH installation description
- Added error handling to Basic Example.
## 2.12.0 [2022-10-27]
### Features
- [#358](https://github.com/influxdata/influxdb-client-go/pull/358):
- Added possibility to set an application name, which will be part of the User-Agent HTTP header:
- Set using `Options.SetApplicationName`
- Warning message is written to log if an application name is not set
- This may change to be logged as an error in a future release
- Added example how to fully override `User-Agent` header using `Doer` interface
### Bug fixes
- [#359](https://github.com/influxdata/influxdb-client-go/pull/359) `WriteAPIBlocking.Flush()` correctly returns nil error.
## 2.11.0 [2022-09-29]
### Features
- [#353](https://github.com/influxdata/influxdb-client-go/pull/353) Simplify generated code.
- [#353](https://github.com/influxdata/influxdb-client-go/pull/353) Regenerate code from swagger.
- [#355](https://github.com/influxdata/influxdb-client-go/pull/355) Upgrade of lib gopkg.in/yaml from v2 to v3
### Bug fixes
- [#354](https://github.com/influxdata/influxdb-client-go/pull/354) More efficient synchronization in WriteAPIBlocking.
### Breaking change
- [#353](https://github.com/influxdata/influxdb-client-go/pull/353):
- Interface `Client` has been extended with `APIClient()` function.
- The generated client API changed:
- Function names are simplified (was `PostDBRPWithResponse`, now `PostDBRP`)
- All functions now accept a context and a single wrapper structure with request body and HTTP parameters
- The functions return deserialized response body or an error (it was a response wrapper with a status code that had to be then validated)
## 2.10.0 [2022-08-25]
### Features
- [#348](https://github.com/influxdata/influxdb-client-go/pull/348) Added `write.Options.Consitency` parameter to support InfluxDB Enterprise.
- [#350](https://github.com/influxdata/influxdb-client-go/pull/350) Added support for implicit batching to `WriteAPIBlocking`. It's off by default, enabled by `EnableBatching()`.
### Bug fixes
- [#349](https://github.com/influxdata/influxdb-client-go/pull/349) Skip retrying on specific write errors (mostly partial write error).
### Breaking change
- [#350](https://github.com/influxdata/influxdb-client-go/pull/350) Interface `WriteAPIBlocking` is extend with `EnableBatching()` and `Flush()`.
## 2.9.2 [2022-07-29]
### Bug fixes
- [#341](https://github.com/influxdata/influxdb-client-go/pull/341) Changing logging level of messages about discarding batch to Error.
- [#344](https://github.com/influxdata/influxdb-client-go/pull/344) `WriteAPI.Flush()` writes also batches from the retry queue.
### Test
- [#345](https://github.com/influxdata/influxdb-client-go/pul/345) Added makefile for simplifying testing from command line.
## 2.9.1 [2022-06-24]
### Bug fixes
- [#332](https://github.com/influxdata/influxdb-client-go/pull/332) Retry strategy drops expired batches as soon as they expire.
- [#335](https://github.com/influxdata/influxdb-client-go/pull/335) Retry strategy keeps max retry delay for new batches.
## 2.9.0 [2022-05-20]
### Features
- [#323](https://github.com/influxdata/influxdb-client-go/pull/323) Added `TasksAPI.CreateTaskByFlux` to allow full control of task script.
- [#328](https://github.com/influxdata/influxdb-client-go/pull/328) Added `Client.SetupWithToken` allowing to specify a custom token.
### Bug fixes
- [#324](https://github.com/influxdata/influxdb-client-go/pull/324) Non-empty error channel will not block writes
## 2.8.2 [2022-04-19]
### Bug fixes
- [#319](https://github.com/influxdata/influxdb-client-go/pull/319) Synchronize `WriteAPIImpl.Close` to prevent panic when closing client by multiple go-routines.
## 2.8.1 [2022-03-21]
### Bug fixes
- [#311](https://github.com/influxdata/influxdb-client-go/pull/311) Correctly unwrapping http.Error from Server API calls
- [#315](https://github.com/influxdata/influxdb-client-go/pull/315) Masking authorization token in log
## 2.8.0 [2022-02-18]
### Features
- [#304](https://github.com/influxdata/influxdb-client-go/pull/304) Added public constructor for `QueryTableResult`
- [#307](https://github.com/influxdata/influxdb-client-go/pull/307) Synced generated server API with the latest [oss.yml](https://github.com/influxdata/openapi/blob/master/contracts/oss.yml).
- [#308](https://github.com/influxdata/influxdb-client-go/pull/308) Added Flux query parameters. Supported by InfluxDB Cloud only now.
- [#308](https://github.com/influxdata/influxdb-client-go/pull/308) Go 1.17 is required
## 2.7.0[2022-01-20]
### Features
- [#297](https://github.com/influxdata/influxdb-client-go/pull/297),[#298](https://github.com/influxdata/influxdb-client-go/pull/298) Optimized `WriteRecord` of [WriteAPIBlocking](https://pkg.go.dev/github.com/influxdata/influxdb-client-go/v2/api#WriteAPIBlocking). Custom batch can be written by single argument.
### Bug fixes
- [#294](https://github.com/influxdata/influxdb-client-go/pull/294) `WritePoint` and `WriteRecord` of [WriteAPIBlocking](https://pkg.go.dev/github.com/influxdata/influxdb-client-go/v2/api#WriteAPIBlocking) returns always full error information.
- [300](https://github.com/influxdata/influxdb-client-go/pull/300) Closing the response body after write batch.
- [302](https://github.com/influxdata/influxdb-client-go/pull/302) FluxRecord.Table() returns value of the table column.
## 2.6.0[2021-11-26]
### Features
- [#285](https://github.com/influxdata/influxdb-client-go/pull/285) Added *Client.Ping()* function as the only validation method available in both OSS and Cloud.
- [#286](https://github.com/influxdata/influxdb-client-go/pull/286) Synced generated server API with the latest [oss.yml](https://github.com/influxdata/openapi/blob/master/contracts/oss.yml).
- [#287](https://github.com/influxdata/influxdb-client-go/pull/287) Added *FluxRecord.Result()* function as a convenient way to retrieve the Flux result name of data.
### Bug fixes
- [#285](https://github.com/influxdata/influxdb-client-go/pull/285) Functions *Client.Health()* and *Client.Ready()* correctly report an error when called against InfluxDB Cloud.
### Breaking change
- [#285](https://github.com/influxdata/influxdb-client-go/pull/285) Function *Client.Ready()* now returns `*domain.Ready` with full uptime info.
## 2.5.1[2021-09-17]
### Bug fixes
- [#276](https://github.com/influxdata/influxdb-client-go/pull/276) Synchronized logging methods of _log.Logger_.
## 2.5.0 [2021-08-20]
### Features
- [#264](https://github.com/influxdata/influxdb-client-go/pull/264) Synced generated server API with the latest [oss.yml](https://github.com/influxdata/openapi/blob/master/contracts/oss.yml).
- [#271](https://github.com/influxdata/influxdb-client-go/pull/271) Use exponential _random_ retry strategy
- [#273](https://github.com/influxdata/influxdb-client-go/pull/273) Added `WriteFailedCallback` for `WriteAPI` allowing to be _synchronously_ notified about failed writes and decide on further batch processing.
### Bug fixes
- [#269](https://github.com/influxdata/influxdb-client-go/pull/269) Synchronized setters of _log.Logger_ to allow concurrent usage
- [#270](https://github.com/influxdata/influxdb-client-go/pull/270) Fixed duplicate `Content-Type` header in requests to managemet API
### Documentation
- [#261](https://github.com/influxdata/influxdb-client-go/pull/261) Update Line Protocol document link to v2.0
- [#274](https://github.com/influxdata/influxdb-client-go/pull/274) Documenting proxy configuration and HTTP redirects handling
## 2.4.0 [2021-06-04]
### Features
- [#256](https://github.com/influxdata/influxdb-client-go/pull/256) Allowing 'Doer' interface for HTTP requests
### Bug fixes
- [#259](https://github.com/influxdata/influxdb-client-go/pull/259) Fixed leaking connection in case of not reading whole query result on TLS connection
## 2.3.0 [2021-04-30]
### Breaking change
- [#253](https://github.com/influxdata/influxdb-client-go/pull/253) Interface 'Logger' extended with 'LogLevel() uint' getter.
### Features
- [#241](https://github.com/influxdata/influxdb-client-go/pull/241),[#248](https://github.com/influxdata/influxdb-client-go/pull/248) Synced with InfluxDB 2.0.5 swagger:
- Setup (onboarding) now sends correctly retentionDuration if specified
- `RetentionRule` used in `Bucket` now contains `ShardGroupDurationSeconds` to specify the shard group duration.
### Documentation
1. [#242](https://github.com/influxdata/influxdb-client-go/pull/242) Documentation improvements:
- [Custom server API example](https://pkg.go.dev/github.com/influxdata/influxdb-client-go/v2#example-Client-CustomServerAPICall) now shows how to create DBRP mapping
- Improved documentation about concurrency
1. [#251](https://github.com/influxdata/influxdb-client-go/pull/251) Fixed Readme.md formatting
### Bug fixes
1. [#252](https://github.com/influxdata/influxdb-client-go/pull/252) Fixed panic when getting not present standard Flux columns
1. [#253](https://github.com/influxdata/influxdb-client-go/pull/253) Conditional debug logging of buffers
1. [#254](https://github.com/influxdata/influxdb-client-go/pull/254) Fixed golint pull
## 2.2.3 [2021-04-01]
### Bug fixes
1. [#236](https://github.com/influxdata/influxdb-client-go/pull/236) Setting MaxRetries to zero value disables retry strategy.
1. [#239](https://github.com/influxdata/influxdb-client-go/pull/239) Blocking write client doesn't use retry handling.
## 2.2.2 [2021-01-29]
### Bug fixes
1. [#229](https://github.com/influxdata/influxdb-client-go/pull/229) Connection errors are also subject for retrying.
## 2.2.1 [2020-12-24]
### Bug fixes
1. [#220](https://github.com/influxdata/influxdb-client-go/pull/220) Fixed runtime error occurring when calling v2 API on v1 server.
### Documentation
1. [#218](https://github.com/influxdata/influxdb-client-go/pull/218), [#221](https://github.com/influxdata/influxdb-client-go/pull/221), [#222](https://github.com/influxdata/influxdb-client-go/pull/222), Changed links leading to sources to point to API docs in Readme, fixed broken links to InfluxDB docs.
## 2.2.0 [2020-10-30]
### Features
1. [#206](https://github.com/influxdata/influxdb-client-go/pull/206) Adding TasksAPI for managing tasks and associated logs and runs.
### Bug fixes
1. [#209](https://github.com/influxdata/influxdb-client-go/pull/209) Synchronizing access to the write service in WriteAPIBlocking.
## 2.1.0 [2020-10-02]
### Features
1. [#193](https://github.com/influxdata/influxdb-client-go/pull/193) Added authentication using username and password. See `UsersAPI.SignIn()` and `UsersAPI.SignOut()`
1. [#204](https://github.com/influxdata/influxdb-client-go/pull/204) Synced with InfluxDB 2 RC0 swagger. Added pagination to Organizations API and `After` paging param to Buckets API.
### Bug fixes
1. [#191](https://github.com/influxdata/influxdb-client-go/pull/191) Fixed QueryTableResult.Next() failed to parse boolean datatype.
1. [#192](https://github.com/influxdata/influxdb-client-go/pull/192) Client.Close() closes idle connections of internally created HTTP client
### Documentation
1. [#189](https://github.com/influxdata/influxdb-client-go/pull/189) Added clarification that server URL has to be the InfluxDB server base URL to API docs and all examples.
1. [#196](https://github.com/influxdata/influxdb-client-go/pull/196) Changed default server port 9999 to 8086 in docs and examples
1. [#200](https://github.com/influxdata/influxdb-client-go/pull/200) Fix example code in the Readme
## 2.0.1 [2020-08-14]
### Bug fixes
1. [#187](https://github.com/influxdata/influxdb-client-go/pull/187) Properly updated library for new major version.
## 2.0.0 [2020-08-14]
### Breaking changes
1. [#173](https://github.com/influxdata/influxdb-client-go/pull/173) Removed deprecated API.
1. [#174](https://github.com/influxdata/influxdb-client-go/pull/174) Removed orgs labels API cause [it has been removed from the server API](https://github.com/influxdata/influxdb/pull/19104)
1. [#175](https://github.com/influxdata/influxdb-client-go/pull/175) Removed WriteAPI.Close()
### Features
1. [#165](https://github.com/influxdata/influxdb-client-go/pull/165) Allow overriding the http.Client for the http service.
1. [#179](https://github.com/influxdata/influxdb-client-go/pull/179) Unifying retry strategy among InfluxDB 2 clients: added exponential backoff.
1. [#180](https://github.com/influxdata/influxdb-client-go/pull/180) Provided public logger API to enable overriding logging. It is also possible to disable logging.
1. [#181](https://github.com/influxdata/influxdb-client-go/pull/181) Exposed HTTP service to allow custom server API calls. Added example.
### Bug fixes
1. [#175](https://github.com/influxdata/influxdb-client-go/pull/175) Fixed WriteAPIs management. Keeping single instance for each org and bucket pair.
### Documentation
1. [#185](https://github.com/influxdata/influxdb-client-go/pull/185) DeleteAPI and sample WriteAPIBlocking wrapper for implicit batching
## 1.4.0 [2020-07-17]
### Breaking changes
1. [#156](https://github.com/influxdata/influxdb-client-go/pull/156) Fixing Go naming and code style violations:
- Introducing new *API interfaces with proper name of types, methods and arguments.
- This also affects the `Client` interface and the `Options` type.
- Affected types and methods have been deprecated and they will be removed in the next release.
### Bug fixes
1. [#152](https://github.com/influxdata/influxdb-client-go/pull/152) Allow connecting to server on a URL path
1. [#154](https://github.com/influxdata/influxdb-client-go/pull/154) Use idiomatic go style for write channels (internal)
1. [#155](https://github.com/influxdata/influxdb-client-go/pull/155) Fix panic in FindOrganizationByName in case of no permissions
## 1.3.0 [2020-06-19]
### Features
1. [#131](https://github.com/influxdata/influxdb-client-go/pull/131) Labels API
1. [#136](https://github.com/influxdata/influxdb-client-go/pull/136) Possibility to specify default tags
1. [#138](https://github.com/influxdata/influxdb-client-go/pull/138) Fix errors from InfluxDB 1.8 being empty
### Bug fixes
1. [#132](https://github.com/influxdata/influxdb-client-go/pull/132) Handle unsupported write type as string instead of generating panic
1. [#134](https://github.com/influxdata/influxdb-client-go/pull/134) FluxQueryResult: support reordering of annotations
## 1.2.0 [2020-05-15]
### Breaking Changes
- [#107](https://github.com/influxdata/influxdb-client-go/pull/107) Renamed `InfluxDBClient` interface to `Client`, so the full name `influxdb2.Client` suits better to Go naming conventions
- [#125](https://github.com/influxdata/influxdb-client-go/pull/125) `WriteApi`,`WriteApiBlocking`,`QueryApi` interfaces and related objects like `Point`, `FluxTableMetadata`, `FluxTableColumn`, `FluxRecord`, moved to the `api` ( and `api/write`, `api/query`) packages
to provide consistent interface
### Features
1. [#120](https://github.com/influxdata/influxdb-client-go/pull/120) Health check API
1. [#122](https://github.com/influxdata/influxdb-client-go/pull/122) Delete API
1. [#124](https://github.com/influxdata/influxdb-client-go/pull/124) Buckets API
### Bug fixes
1. [#108](https://github.com/influxdata/influxdb-client-go/pull/108) Fix default retry interval doc
1. [#110](https://github.com/influxdata/influxdb-client-go/pull/110) Allowing empty (nil) values in query result
### Documentation
- [#112](https://github.com/influxdata/influxdb-client-go/pull/112) Clarify how to use client with InfluxDB 1.8+
- [#115](https://github.com/influxdata/influxdb-client-go/pull/115) Doc and examples for reading write api errors
## 1.1.0 [2020-04-24]
### Features
1. [#100](https://github.com/influxdata/influxdb-client-go/pull/100) HTTP request timeout made configurable
1. [#99](https://github.com/influxdata/influxdb-client-go/pull/99) Organizations API and Users API
1. [#96](https://github.com/influxdata/influxdb-client-go/pull/96) Authorization API
### Docs
1. [#101](https://github.com/influxdata/influxdb-client-go/pull/101) Added examples to API docs
## 1.0.0 [2020-04-01]
### Core
- initial release of new client version
### APIs
- initial release of new client version
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020-2021 Influxdata, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+31
View File
@@ -0,0 +1,31 @@
artifacts_path := /tmp/artifacts
help:
@echo 'Targets:'
@echo ' all - runs lint, server, coverage'
@echo ' lint - runs code style checks'
@echo ' shorttest - runs unit and integration tests'
@echo ' test - runs all tests, including e2e tests - requires running influxdb 2 server'
@echo ' coverage - runs all tests, including e2e tests, with coverage report - requires running influxdb 2 server'
@echo ' server - prepares InfluxDB in docker environment'
lint:
go vet ./...
go install honnef.co/go/tools/cmd/staticcheck@latest && staticcheck --checks='all' --tags e2e ./...
go install golang.org/x/lint/golint@latest && golint ./...
shorttest:
go test -race -v -count=1 ./...
test:
go test -race -v -count=1 --tags e2e ./...
coverage:
go install gotest.tools/gotestsum@latest && gotestsum --junitfile /tmp/test-results/unit-tests.xml -- -race -coverprofile=coverage.txt -covermode=atomic -coverpkg '.,./api/...,./internal/.../,./log/...' -tags e2e ./...
if test ! -e $(artifacts_path); then mkdir $(artifacts_path); fi
go tool cover -html=coverage.txt -o $(artifacts_path)/coverage.html
server:
./scripts/influxdb-restart.sh
all: lint server coverage
+711
View File
@@ -0,0 +1,711 @@
# InfluxDB Client Go
[![CircleCI](https://circleci.com/gh/influxdata/influxdb-client-go.svg?style=svg)](https://circleci.com/gh/influxdata/influxdb-client-go)
[![codecov](https://codecov.io/gh/influxdata/influxdb-client-go/branch/master/graph/badge.svg)](https://codecov.io/gh/influxdata/influxdb-client-go)
[![License](https://img.shields.io/github/license/influxdata/influxdb-client-go.svg)](https://github.com/influxdata/influxdb-client-go/blob/master/LICENSE)
[![Slack Status](https://img.shields.io/badge/slack-join_chat-white.svg?logo=slack&style=social)](https://www.influxdata.com/slack)
This repository contains the reference Go client for InfluxDB 2.
#### Note: Use this client library with InfluxDB 2.x and InfluxDB 1.8+ ([see details](#influxdb-18-api-compatibility)). For connecting to InfluxDB 1.7 or earlier instances, use the [influxdb1-go](https://github.com/influxdata/influxdb1-client) client library.
- [Features](#features)
- [Documentation](#documentation)
- [Examples](#examples)
- [How To Use](#how-to-use)
- [Installation](#installation)
- [Basic Example](#basic-example)
- [Writes in Detail](#writes)
- [Queries in Detail](#queries)
- [Parametrized Queries](#parametrized-queries)
- [Concurrency](#concurrency)
- [Proxy and redirects](#proxy-and-redirects)
- [Checking Server State](#checking-server-state)
- [InfluxDB 1.8 API compatibility](#influxdb-18-api-compatibility)
- [Contributing](#contributing)
- [License](#license)
## Features
- InfluxDB 2 client
- Querying data
- using the Flux language
- into raw data, flux table representation
- [How to queries](#queries)
- Writing data using
- [Line Protocol](https://docs.influxdata.com/influxdb/v2.0/reference/syntax/line-protocol/)
- [Data Point](https://pkg.go.dev/github.com/influxdata/influxdb-client-go/v2/api/write#Point)
- Both [asynchronous](https://pkg.go.dev/github.com/influxdata/influxdb-client-go/v2/api#WriteAPI) or [synchronous](https://pkg.go.dev/github.com/influxdata/influxdb-client-go/v2/api#WriteAPIBlocking) ways
- [How to writes](#writes)
- InfluxDB 2 API
- setup, ready, health
- authotizations, users, organizations
- buckets, delete
- ...
## Documentation
This section contains links to the client library documentation.
- [Product documentation](https://docs.influxdata.com/influxdb/v2.0/tools/client-libraries/), [Getting Started](#how-to-use)
- [Examples](#examples)
- [API Reference](https://pkg.go.dev/github.com/influxdata/influxdb-client-go/v2)
- [Changelog](CHANGELOG.md)
### Examples
Examples for basic writing and querying data are shown below in this document
There are also other examples in the API docs:
- [Client usage](https://pkg.go.dev/github.com/influxdata/influxdb-client-go/v2?tab=doc#pkg-examples)
- [Management APIs](https://pkg.go.dev/github.com/influxdata/influxdb-client-go/v2/api?tab=doc#pkg-examples)
## How To Use
### Installation
**Go 1.17** or later is required.
#### Go mod project
1. Add the latest version of the client package to your project dependencies (go.mod).
```sh
go get github.com/influxdata/influxdb-client-go/v2
```
1. Add import `github.com/influxdata/influxdb-client-go/v2` to your source code.
#### GOPATH project
```sh
go get github.com/influxdata/influxdb-client-go
```
Note: To have _go get_ in the GOPATH mode, the environment variable `GO111MODULE` must have the `off` value.
### Basic Example
The following example demonstrates how to write data to InfluxDB 2 and read them back using the Flux language:
```go
package main
import (
"context"
"fmt"
"time"
"github.com/influxdata/influxdb-client-go/v2"
)
func main() {
// Create a new client using an InfluxDB server base URL and an authentication token
client := influxdb2.NewClient("http://localhost:8086", "my-token")
// Use blocking write client for writes to desired bucket
writeAPI := client.WriteAPIBlocking("my-org", "my-bucket")
// Create point using full params constructor
p := influxdb2.NewPoint("stat",
map[string]string{"unit": "temperature"},
map[string]interface{}{"avg": 24.5, "max": 45.0},
time.Now())
// write point immediately
writeAPI.WritePoint(context.Background(), p)
// Create point using fluent style
p = influxdb2.NewPointWithMeasurement("stat").
AddTag("unit", "temperature").
AddField("avg", 23.2).
AddField("max", 45.0).
SetTime(time.Now())
err := writeAPI.WritePoint(context.Background(), p)
if err != nil {
panic(err)
}
// Or write directly line protocol
line := fmt.Sprintf("stat,unit=temperature avg=%f,max=%f", 23.5, 45.0)
err = writeAPI.WriteRecord(context.Background(), line)
if err != nil {
panic(err)
}
// Get query client
queryAPI := client.QueryAPI("my-org")
// Get parser flux query result
result, err := queryAPI.Query(context.Background(), `from(bucket:"my-bucket")|> range(start: -1h) |> filter(fn: (r) => r._measurement == "stat")`)
if err == nil {
// Use Next() to iterate over query result lines
for result.Next() {
// Observe when there is new grouping key producing new table
if result.TableChanged() {
fmt.Printf("table: %s\n", result.TableMetadata().String())
}
// read result
fmt.Printf("row: %s\n", result.Record().String())
}
if result.Err() != nil {
fmt.Printf("Query error: %s\n", result.Err().Error())
}
} else {
panic(err)
}
// Ensures background processes finishes
client.Close()
}
```
### Options
The InfluxDBClient uses set of options to configure behavior. These are available in the [Options](https://pkg.go.dev/github.com/influxdata/influxdb-client-go/v2#Options) object
Creating a client instance using
```go
client := influxdb2.NewClient("http://localhost:8086", "my-token")
```
will use the default options.
To set different configuration values, e.g. to set gzip compression and trust all server certificates, get default options
and change what is needed:
```go
client := influxdb2.NewClientWithOptions("http://localhost:8086", "my-token",
influxdb2.DefaultOptions().
SetUseGZip(true).
SetTLSConfig(&tls.Config{
InsecureSkipVerify: true,
}))
```
### Writes
Client offers two ways of writing, non-blocking and blocking.
### Non-blocking write client
Non-blocking write client uses implicit batching. Data are asynchronously
written to the underlying buffer and they are automatically sent to a server when the size of the write buffer reaches the batch size, default 5000, or the flush interval, default 1s, times out.
Writes are automatically retried on server back pressure.
This write client also offers synchronous blocking method to ensure that write buffer is flushed and all pending writes are finished,
see [Flush()](https://pkg.go.dev/github.com/influxdata/influxdb-client-go/v2/api#WriteAPI.Flush) method.
Always use [Close()](https://pkg.go.dev/github.com/influxdata/influxdb-client-go/v2#Client.Close) method of the client to stop all background processes.
Asynchronous write client is recommended for frequent periodic writes.
```go
package main
import (
"fmt"
"math/rand"
"time"
"github.com/influxdata/influxdb-client-go/v2"
)
func main() {
// Create a new client using an InfluxDB server base URL and an authentication token
// and set batch size to 20
client := influxdb2.NewClientWithOptions("http://localhost:8086", "my-token",
influxdb2.DefaultOptions().SetBatchSize(20))
// Get non-blocking write client
writeAPI := client.WriteAPI("my-org","my-bucket")
// write some points
for i := 0; i <100; i++ {
// create point
p := influxdb2.NewPoint(
"system",
map[string]string{
"id": fmt.Sprintf("rack_%v", i%10),
"vendor": "AWS",
"hostname": fmt.Sprintf("host_%v", i%100),
},
map[string]interface{}{
"temperature": rand.Float64() * 80.0,
"disk_free": rand.Float64() * 1000.0,
"disk_total": (i/10 + 1) * 1000000,
"mem_total": (i/100 + 1) * 10000000,
"mem_free": rand.Uint64(),
},
time.Now())
// write asynchronously
writeAPI.WritePoint(p)
}
// Force all unwritten data to be sent
writeAPI.Flush()
// Ensures background processes finishes
client.Close()
}
```
### Handling of failed async writes
WriteAPI by default continues with retrying of failed writes.
Retried are automatically writes that fail on a connection failure or when server returns response HTTP status code >= 429.
Retrying algorithm uses random exponential strategy to set retry time.
The delay for the next retry attempt is a random value in the interval _retryInterval * exponentialBase^(attempts)_ and _retryInterval * exponentialBase^(attempts+1)_.
If writes of batch repeatedly fails, WriteAPI continues with retrying until _maxRetries_ is reached or the overall retry time of batch exceeds _maxRetryTime_.
The defaults parameters (part of the WriteOptions) are:
- _retryInterval_=5,000ms
- _exponentialBase_=2
- _maxRetryDelay_=125,000ms
- _maxRetries_=5
- _maxRetryTime_=180,000ms
Retry delays are by default randomly distributed within the ranges:
1. 5,000-10,000
1. 10,000-20,000
1. 20,000-40,000
1. 40,000-80,000
1. 80,000-125,000
Setting _retryInterval_ to 0 disables retry strategy and any failed write will discard the batch.
[WriteFailedCallback](https://pkg.go.dev/github.com/influxdata/influxdb-client-go/v2/api#WriteFailedCallback) allows advanced controlling of retrying.
It is synchronously notified in case async write fails.
It controls further batch handling by its return value. If it returns `true`, WriteAPI continues with retrying of writes of this batch. Returned `false` means the batch should be discarded.
### Reading async errors
WriteAPI automatically logs write errors. Use [Errors()](https://pkg.go.dev/github.com/influxdata/influxdb-client-go/v2/api#WriteAPI.Errors) method, which returns the channel for reading errors occuring during async writes, for writing write error to a custom target:
```go
package main
import (
"fmt"
"math/rand"
"time"
"github.com/influxdata/influxdb-client-go/v2"
)
func main() {
// Create a new client using an InfluxDB server base URL and an authentication token
client := influxdb2.NewClient("http://localhost:8086", "my-token")
// Get non-blocking write client
writeAPI := client.WriteAPI("my-org", "my-bucket")
// Get errors channel
errorsCh := writeAPI.Errors()
// Create go proc for reading and logging errors
go func() {
for err := range errorsCh {
fmt.Printf("write error: %s\n", err.Error())
}
}()
// write some points
for i := 0; i < 100; i++ {
// create point
p := influxdb2.NewPointWithMeasurement("stat").
AddTag("id", fmt.Sprintf("rack_%v", i%10)).
AddTag("vendor", "AWS").
AddTag("hostname", fmt.Sprintf("host_%v", i%100)).
AddField("temperature", rand.Float64()*80.0).
AddField("disk_free", rand.Float64()*1000.0).
AddField("disk_total", (i/10+1)*1000000).
AddField("mem_total", (i/100+1)*10000000).
AddField("mem_free", rand.Uint64()).
SetTime(time.Now())
// write asynchronously
writeAPI.WritePoint(p)
}
// Force all unwritten data to be sent
writeAPI.Flush()
// Ensures background processes finishes
client.Close()
}
```
### Blocking write client
Blocking write client writes given point(s) synchronously. It doesn't do implicit batching. Batch is created from given set of points.
Implicit batching can be enabled with `WriteAPIBlocking.EnableBatching()`.
```go
package main
import (
"context"
"fmt"
"math/rand"
"time"
"github.com/influxdata/influxdb-client-go/v2"
)
func main() {
// Create a new client using an InfluxDB server base URL and an authentication token
client := influxdb2.NewClient("http://localhost:8086", "my-token")
// Get blocking write client
writeAPI := client.WriteAPIBlocking("my-org","my-bucket")
// write some points
for i := 0; i <100; i++ {
// create data point
p := influxdb2.NewPoint(
"system",
map[string]string{
"id": fmt.Sprintf("rack_%v", i%10),
"vendor": "AWS",
"hostname": fmt.Sprintf("host_%v", i%100),
},
map[string]interface{}{
"temperature": rand.Float64() * 80.0,
"disk_free": rand.Float64() * 1000.0,
"disk_total": (i/10 + 1) * 1000000,
"mem_total": (i/100 + 1) * 10000000,
"mem_free": rand.Uint64(),
},
time.Now())
// write synchronously
err := writeAPI.WritePoint(context.Background(), p)
if err != nil {
panic(err)
}
}
// Ensures background processes finishes
client.Close()
}
```
### Queries
Query client offers retrieving of query results to a parsed representation in a [QueryTableResult](https://pkg.go.dev/github.com/influxdata/influxdb-client-go/v2/api#QueryTableResult) or to a raw string.
### QueryTableResult
QueryTableResult offers comfortable way how to deal with flux query CSV response. It parses CSV stream into FluxTableMetaData, FluxColumn and FluxRecord objects
for easy reading the result.
```go
package main
import (
"context"
"fmt"
"github.com/influxdata/influxdb-client-go/v2"
)
func main() {
// Create a new client using an InfluxDB server base URL and an authentication token
client := influxdb2.NewClient("http://localhost:8086", "my-token")
// Get query client
queryAPI := client.QueryAPI("my-org")
// get QueryTableResult
result, err := queryAPI.Query(context.Background(), `from(bucket:"my-bucket")|> range(start: -1h) |> filter(fn: (r) => r._measurement == "stat")`)
if err == nil {
// Iterate over query response
for result.Next() {
// Notice when group key has changed
if result.TableChanged() {
fmt.Printf("table: %s\n", result.TableMetadata().String())
}
// Access data
fmt.Printf("value: %v\n", result.Record().Value())
}
// check for an error
if result.Err() != nil {
fmt.Printf("query parsing error: %s\n", result.Err().Error())
}
} else {
panic(err)
}
// Ensures background processes finishes
client.Close()
}
```
### Raw
[QueryRaw()](https://pkg.go.dev/github.com/influxdata/influxdb-client-go/v2/api#QueryAPI.QueryRaw) returns raw, unparsed, query result string and process it on your own. Returned csv format
can be controlled by the third parameter, query dialect.
```go
package main
import (
"context"
"fmt"
"github.com/influxdata/influxdb-client-go/v2"
)
func main() {
// Create a new client using an InfluxDB server base URL and an authentication token
client := influxdb2.NewClient("http://localhost:8086", "my-token")
// Get query client
queryAPI := client.QueryAPI("my-org")
// Query and get complete result as a string
// Use default dialect
result, err := queryAPI.QueryRaw(context.Background(), `from(bucket:"my-bucket")|> range(start: -1h) |> filter(fn: (r) => r._measurement == "stat")`, influxdb2.DefaultDialect())
if err == nil {
fmt.Println("QueryResult:")
fmt.Println(result)
} else {
panic(err)
}
// Ensures background processes finishes
client.Close()
}
```
### Parametrized Queries
InfluxDB Cloud supports [Parameterized Queries](https://docs.influxdata.com/influxdb/cloud/query-data/parameterized-queries/)
that let you dynamically change values in a query using the InfluxDB API. Parameterized queries make Flux queries more
reusable and can also be used to help prevent injection attacks.
InfluxDB Cloud inserts the params object into the Flux query as a Flux record named `params`. Use dot or bracket
notation to access parameters in the `params` record in your Flux query. Parameterized Flux queries support only `int`
, `float`, and `string` data types. To convert the supported data types into
other [Flux basic data types, use Flux type conversion functions](https://docs.influxdata.com/influxdb/cloud/query-data/parameterized-queries/#supported-parameter-data-types).
Query parameters can be passed as a struct or map. Param values can be only simple types or `time.Time`.
The name of the parameter represented by a struct field can be specified by JSON annotation.
Parameterized query example:
> :warning: Parameterized Queries are supported only in InfluxDB Cloud. There is no support in InfluxDB OSS currently.
```go
package main
import (
"context"
"fmt"
"github.com/influxdata/influxdb-client-go/v2"
)
func main() {
// Create a new client using an InfluxDB server base URL and an authentication token
client := influxdb2.NewClient("http://localhost:8086", "my-token")
// Get query client
queryAPI := client.QueryAPI("my-org")
// Define parameters
parameters := struct {
Start string `json:"start"`
Field string `json:"field"`
Value float64 `json:"value"`
}{
"-1h",
"temperature",
25,
}
// Query with parameters
query := `from(bucket:"my-bucket")
|> range(start: duration(params.start))
|> filter(fn: (r) => r._measurement == "stat")
|> filter(fn: (r) => r._field == params.field)
|> filter(fn: (r) => r._value > params.value)`
// Get result
result, err := queryAPI.QueryWithParams(context.Background(), query, parameters)
if err == nil {
// Iterate over query response
for result.Next() {
// Notice when group key has changed
if result.TableChanged() {
fmt.Printf("table: %s\n", result.TableMetadata().String())
}
// Access data
fmt.Printf("value: %v\n", result.Record().Value())
}
// check for an error
if result.Err() != nil {
fmt.Printf("query parsing error: %s\n", result.Err().Error())
}
} else {
panic(err)
}
// Ensures background processes finishes
client.Close()
}
```
### Concurrency
InfluxDB Go Client can be used in a concurrent environment. All its functions are thread-safe.
The best practise is to use a single `Client` instance per server URL. This ensures optimized resources usage,
most importantly reusing HTTP connections.
For efficient reuse of HTTP resources among multiple clients, create an HTTP client and use `Options.SetHTTPClient()` for setting it to all clients:
```go
// Create HTTP client
httpClient := &http.Client{
Timeout: time.Second * time.Duration(60),
Transport: &http.Transport{
DialContext: (&net.Dialer{
Timeout: 5 * time.Second,
}).DialContext,
TLSHandshakeTimeout: 5 * time.Second,
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
MaxIdleConns: 100,
MaxIdleConnsPerHost: 100,
IdleConnTimeout: 90 * time.Second,
},
}
// Client for server 1
client1 := influxdb2.NewClientWithOptions("https://server:8086", "my-token", influxdb2.DefaultOptions().SetHTTPClient(httpClient))
// Client for server 2
client2 := influxdb2.NewClientWithOptions("https://server:9999", "my-token2", influxdb2.DefaultOptions().SetHTTPClient(httpClient))
```
Client ensures that there is a single instance of each server API sub-client for the specific area. E.g. a single `WriteAPI` instance for each org/bucket pair,
a single `QueryAPI` for each org.
Such a single API sub-client instance can be used concurrently:
```go
package main
import (
"math/rand"
"sync"
"time"
influxdb2 "github.com/influxdata/influxdb-client-go"
"github.com/influxdata/influxdb-client-go/v2/api/write"
)
func main() {
// Create client
client := influxdb2.NewClient("http://localhost:8086", "my-token")
// Ensure closing the client
defer client.Close()
// Get write client
writeApi := client.WriteAPI("my-org", "my-bucket")
// Create channel for points feeding
pointsCh := make(chan *write.Point, 200)
threads := 5
var wg sync.WaitGroup
go func(points int) {
for i := 0; i < points; i++ {
p := influxdb2.NewPoint("meas",
map[string]string{"tag": "tagvalue"},
map[string]interface{}{"val1": rand.Int63n(1000), "val2": rand.Float64()*100.0 - 50.0},
time.Now())
pointsCh <- p
}
close(pointsCh)
}(1000000)
// Launch write routines
for t := 0; t < threads; t++ {
wg.Add(1)
go func() {
for p := range pointsCh {
writeApi.WritePoint(p)
}
wg.Done()
}()
}
// Wait for writes complete
wg.Wait()
}
```
### Proxy and redirects
You can configure InfluxDB Go client behind a proxy in two ways:
1. Using environment variable
Set environment variable `HTTP_PROXY` (or `HTTPS_PROXY` based on the scheme of your server url).
e.g. (linux) `export HTTP_PROXY=http://my-proxy:8080` or in Go code `os.Setenv("HTTP_PROXY","http://my-proxy:8080")`
1. Configure `http.Client` to use proxy<br>
Create a custom `http.Client` with a proxy configuration:
```go
proxyUrl, err := url.Parse("http://my-proxy:8080")
httpClient := &http.Client{
Transport: &http.Transport{
Proxy: http.ProxyURL(proxyUrl)
}
}
client := influxdb2.NewClientWithOptions("http://localhost:8086", token, influxdb2.DefaultOptions().SetHTTPClient(httpClient))
```
Client automatically follows HTTP redirects. The default redirect policy is to follow up to 10 consecutive requests.
Due to a security reason _Authorization_ header is not forwarded when redirect leads to a different domain.
To overcome this limitation you have to set a custom redirect handler:
```go
token := "my-token"
httpClient := &http.Client{
CheckRedirect: func(req *http.Request, via []*http.Request) error {
req.Header.Add("Authorization","Token " + token)
return nil
},
}
client := influxdb2.NewClientWithOptions("http://localhost:8086", token, influxdb2.DefaultOptions().SetHTTPClient(httpClient))
```
### Checking Server State
There are three functions for checking whether a server is up and ready for communication:
| Function| Description | Availability |
|:----------|:----------|:----------|
| [Health()](https://pkg.go.dev/github.com/influxdata/influxdb-client-go/v2#Client.Health) | Detailed info about the server status, along with version string | OSS |
| [Ready()](https://pkg.go.dev/github.com/influxdata/influxdb-client-go/v2#Client.Ready) | Server uptime info | OSS |
| [Ping()](https://pkg.go.dev/github.com/influxdata/influxdb-client-go/v2#Client.Ping) | Whether a server is up | OSS, Cloud |
Only the [Ping()](https://pkg.go.dev/github.com/influxdata/influxdb-client-go/v2#Client.Ping) function works in InfluxDB Cloud server.
## InfluxDB 1.8 API compatibility
[InfluxDB 1.8.0 introduced forward compatibility APIs](https://docs.influxdata.com/influxdb/latest/tools/api/#influxdb-2-0-api-compatibility-endpoints) for InfluxDB 2.0. This allow you to easily move from InfluxDB 1.x to InfluxDB 2.0 Cloud or open source.
Client API usage differences summary:
1. Use the form `username:password` for an **authentication token**. Example: `my-user:my-password`. Use an empty string (`""`) if the server doesn't require authentication.
1. The organization parameter is not used. Use an empty string (`""`) where necessary.
1. Use the form `database/retention-policy` where a **bucket** is required. Skip retention policy if the default retention policy should be used. Examples: `telegraf/autogen`, `telegraf`.  
The following forward compatible APIs are available:
| API | Endpoint | Description |
|:----------|:----------|:----------|
| [WriteAPI](https://pkg.go.dev/github.com/influxdata/influxdb-client-go/v2/api#WriteAPI) (also [WriteAPIBlocking](https://pkg.go.dev/github.com/influxdata/influxdb-client-go/v2/api#WriteAPIBlocking))| [/api/v2/write](https://docs.influxdata.com/influxdb/v2.0/write-data/developer-tools/api/) | Write data to InfluxDB 1.8.0+ using the InfluxDB 2.0 API |
| [QueryAPI](https://pkg.go.dev/github.com/influxdata/influxdb-client-go/v2/api#QueryAPI) | [/api/v2/query](https://docs.influxdata.com/influxdb/v2.0/query-data/execute-queries/influx-api/) | Query data in InfluxDB 1.8.0+ using the InfluxDB 2.0 API and [Flux](https://docs.influxdata.com/flux/latest/) endpoint should be enabled by the [`flux-enabled` option](https://docs.influxdata.com/influxdb/v1.8/administration/config/#flux-enabled-false)
| [Health()](https://pkg.go.dev/github.com/influxdata/influxdb-client-go/v2#Client.Health) | [/health](https://docs.influxdata.com/influxdb/v2.0/api/#tag/Health) | Check the health of your InfluxDB instance |
### Example
```go
package main
import (
"context"
"fmt"
"time"
"github.com/influxdata/influxdb-client-go/v2"
)
func main() {
userName := "my-user"
password := "my-password"
// Create a new client using an InfluxDB server base URL and an authentication token
// For authentication token supply a string in the form: "username:password" as a token. Set empty value for an unauthenticated server
client := influxdb2.NewClient("http://localhost:8086", fmt.Sprintf("%s:%s",userName, password))
// Get the blocking write client
// Supply a string in the form database/retention-policy as a bucket. Skip retention policy for the default one, use just a database name (without the slash character)
// Org name is not used
writeAPI := client.WriteAPIBlocking("", "test/autogen")
// create point using full params constructor
p := influxdb2.NewPoint("stat",
map[string]string{"unit": "temperature"},
map[string]interface{}{"avg": 24.5, "max": 45},
time.Now())
// Write data
err := writeAPI.WritePoint(context.Background(), p)
if err != nil {
fmt.Printf("Write error: %s\n", err.Error())
}
// Get query client. Org name is not used
queryAPI := client.QueryAPI("")
// Supply string in a form database/retention-policy as a bucket. Skip retention policy for the default one, use just a database name (without the slash character)
result, err := queryAPI.Query(context.Background(), `from(bucket:"test")|> range(start: -1h) |> filter(fn: (r) => r._measurement == "stat")`)
if err == nil {
for result.Next() {
if result.TableChanged() {
fmt.Printf("table: %s\n", result.TableMetadata().String())
}
fmt.Printf("row: %s\n", result.Record().String())
}
if result.Err() != nil {
fmt.Printf("Query error: %s\n", result.Err().Error())
}
} else {
fmt.Printf("Query error: %s\n", err.Error())
}
// Close client
client.Close()
}
```
## Contributing
If you would like to contribute code you can do through GitHub by forking the repository and sending a pull request into the `master` branch.
## License
The InfluxDB 2 Go Client is released under the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,127 @@
// Copyright 2020-2021 InfluxData, Inc. All rights reserved.
// Use of this source code is governed by MIT
// license that can be found in the LICENSE file.
package api
import (
"context"
"github.com/influxdata/influxdb-client-go/v2/domain"
)
// AuthorizationsAPI provides methods for organizing Authorization in a InfluxDB server
type AuthorizationsAPI interface {
// GetAuthorizations returns all authorizations
GetAuthorizations(ctx context.Context) (*[]domain.Authorization, error)
// FindAuthorizationsByUserName returns all authorizations for given userName
FindAuthorizationsByUserName(ctx context.Context, userName string) (*[]domain.Authorization, error)
// FindAuthorizationsByUserID returns all authorizations for given userID
FindAuthorizationsByUserID(ctx context.Context, userID string) (*[]domain.Authorization, error)
// FindAuthorizationsByOrgName returns all authorizations for given organization name
FindAuthorizationsByOrgName(ctx context.Context, orgName string) (*[]domain.Authorization, error)
// FindAuthorizationsByOrgID returns all authorizations for given organization id
FindAuthorizationsByOrgID(ctx context.Context, orgID string) (*[]domain.Authorization, error)
// CreateAuthorization creates new authorization
CreateAuthorization(ctx context.Context, authorization *domain.Authorization) (*domain.Authorization, error)
// CreateAuthorizationWithOrgID creates new authorization with given permissions scoped to given orgID
CreateAuthorizationWithOrgID(ctx context.Context, orgID string, permissions []domain.Permission) (*domain.Authorization, error)
// UpdateAuthorizationStatus updates status of authorization
UpdateAuthorizationStatus(ctx context.Context, authorization *domain.Authorization, status domain.AuthorizationUpdateRequestStatus) (*domain.Authorization, error)
// UpdateAuthorizationStatusWithID updates status of authorization with authID
UpdateAuthorizationStatusWithID(ctx context.Context, authID string, status domain.AuthorizationUpdateRequestStatus) (*domain.Authorization, error)
// DeleteAuthorization deletes authorization
DeleteAuthorization(ctx context.Context, authorization *domain.Authorization) error
// DeleteAuthorization deletes authorization with authID
DeleteAuthorizationWithID(ctx context.Context, authID string) error
}
// authorizationsAPI implements AuthorizationsAPI
type authorizationsAPI struct {
apiClient *domain.Client
}
// NewAuthorizationsAPI creates new instance of AuthorizationsAPI
func NewAuthorizationsAPI(apiClient *domain.Client) AuthorizationsAPI {
return &authorizationsAPI{
apiClient: apiClient,
}
}
func (a *authorizationsAPI) GetAuthorizations(ctx context.Context) (*[]domain.Authorization, error) {
authQuery := &domain.GetAuthorizationsParams{}
return a.listAuthorizations(ctx, authQuery)
}
func (a *authorizationsAPI) FindAuthorizationsByUserName(ctx context.Context, userName string) (*[]domain.Authorization, error) {
authQuery := &domain.GetAuthorizationsParams{User: &userName}
return a.listAuthorizations(ctx, authQuery)
}
func (a *authorizationsAPI) FindAuthorizationsByUserID(ctx context.Context, userID string) (*[]domain.Authorization, error) {
authQuery := &domain.GetAuthorizationsParams{UserID: &userID}
return a.listAuthorizations(ctx, authQuery)
}
func (a *authorizationsAPI) FindAuthorizationsByOrgName(ctx context.Context, orgName string) (*[]domain.Authorization, error) {
authQuery := &domain.GetAuthorizationsParams{Org: &orgName}
return a.listAuthorizations(ctx, authQuery)
}
func (a *authorizationsAPI) FindAuthorizationsByOrgID(ctx context.Context, orgID string) (*[]domain.Authorization, error) {
authQuery := &domain.GetAuthorizationsParams{OrgID: &orgID}
return a.listAuthorizations(ctx, authQuery)
}
func (a *authorizationsAPI) listAuthorizations(ctx context.Context, query *domain.GetAuthorizationsParams) (*[]domain.Authorization, error) {
response, err := a.apiClient.GetAuthorizations(ctx, query)
if err != nil {
return nil, err
}
return response.Authorizations, nil
}
func (a *authorizationsAPI) CreateAuthorization(ctx context.Context, authorization *domain.Authorization) (*domain.Authorization, error) {
params := &domain.PostAuthorizationsAllParams{
Body: domain.PostAuthorizationsJSONRequestBody{
AuthorizationUpdateRequest: authorization.AuthorizationUpdateRequest,
OrgID: authorization.OrgID,
Permissions: authorization.Permissions,
UserID: authorization.UserID,
},
}
return a.apiClient.PostAuthorizations(ctx, params)
}
func (a *authorizationsAPI) CreateAuthorizationWithOrgID(ctx context.Context, orgID string, permissions []domain.Permission) (*domain.Authorization, error) {
status := domain.AuthorizationUpdateRequestStatusActive
auth := &domain.Authorization{
AuthorizationUpdateRequest: domain.AuthorizationUpdateRequest{Status: &status},
OrgID: &orgID,
Permissions: &permissions,
}
return a.CreateAuthorization(ctx, auth)
}
func (a *authorizationsAPI) UpdateAuthorizationStatusWithID(ctx context.Context, authID string, status domain.AuthorizationUpdateRequestStatus) (*domain.Authorization, error) {
params := &domain.PatchAuthorizationsIDAllParams{
Body: domain.PatchAuthorizationsIDJSONRequestBody{Status: &status},
AuthID: authID,
}
return a.apiClient.PatchAuthorizationsID(ctx, params)
}
func (a *authorizationsAPI) UpdateAuthorizationStatus(ctx context.Context, authorization *domain.Authorization, status domain.AuthorizationUpdateRequestStatus) (*domain.Authorization, error) {
return a.UpdateAuthorizationStatusWithID(ctx, *authorization.Id, status)
}
func (a *authorizationsAPI) DeleteAuthorization(ctx context.Context, authorization *domain.Authorization) error {
return a.DeleteAuthorizationWithID(ctx, *authorization.Id)
}
func (a *authorizationsAPI) DeleteAuthorizationWithID(ctx context.Context, authID string) error {
params := &domain.DeleteAuthorizationsIDAllParams{
AuthID: authID,
}
return a.apiClient.DeleteAuthorizationsID(ctx, params)
}
+275
View File
@@ -0,0 +1,275 @@
// Copyright 2020-2021 InfluxData, Inc. All rights reserved.
// Use of this source code is governed by MIT
// license that can be found in the LICENSE file.
package api
import (
"context"
"fmt"
"github.com/influxdata/influxdb-client-go/v2/domain"
)
// BucketsAPI provides methods for managing Buckets in a InfluxDB server.
type BucketsAPI interface {
// GetBuckets returns all buckets.
// GetBuckets supports PagingOptions: Offset, Limit, After. Empty pagingOptions means the default paging (first 20 results).
GetBuckets(ctx context.Context, pagingOptions ...PagingOption) (*[]domain.Bucket, error)
// FindBucketByName returns a bucket found using bucketName.
FindBucketByName(ctx context.Context, bucketName string) (*domain.Bucket, error)
// FindBucketByID returns a bucket found using bucketID.
FindBucketByID(ctx context.Context, bucketID string) (*domain.Bucket, error)
// FindBucketsByOrgID returns buckets belonging to the organization with ID orgID.
// FindBucketsByOrgID supports PagingOptions: Offset, Limit, After. Empty pagingOptions means the default paging (first 20 results).
FindBucketsByOrgID(ctx context.Context, orgID string, pagingOptions ...PagingOption) (*[]domain.Bucket, error)
// FindBucketsByOrgName returns buckets belonging to the organization with name orgName, with the specified paging. Empty pagingOptions means the default paging (first 20 results).
FindBucketsByOrgName(ctx context.Context, orgName string, pagingOptions ...PagingOption) (*[]domain.Bucket, error)
// CreateBucket creates a new bucket.
CreateBucket(ctx context.Context, bucket *domain.Bucket) (*domain.Bucket, error)
// CreateBucketWithName creates a new bucket with bucketName in organization org, with retention specified in rules. Empty rules means infinite retention.
CreateBucketWithName(ctx context.Context, org *domain.Organization, bucketName string, rules ...domain.RetentionRule) (*domain.Bucket, error)
// CreateBucketWithNameWithID creates a new bucket with bucketName in organization with orgID, with retention specified in rules. Empty rules means infinite retention.
CreateBucketWithNameWithID(ctx context.Context, orgID, bucketName string, rules ...domain.RetentionRule) (*domain.Bucket, error)
// UpdateBucket updates a bucket.
UpdateBucket(ctx context.Context, bucket *domain.Bucket) (*domain.Bucket, error)
// DeleteBucket deletes a bucket.
DeleteBucket(ctx context.Context, bucket *domain.Bucket) error
// DeleteBucketWithID deletes a bucket with bucketID.
DeleteBucketWithID(ctx context.Context, bucketID string) error
// GetMembers returns members of a bucket.
GetMembers(ctx context.Context, bucket *domain.Bucket) (*[]domain.ResourceMember, error)
// GetMembersWithID returns members of a bucket with bucketID.
GetMembersWithID(ctx context.Context, bucketID string) (*[]domain.ResourceMember, error)
// AddMember adds a member to a bucket.
AddMember(ctx context.Context, bucket *domain.Bucket, user *domain.User) (*domain.ResourceMember, error)
// AddMemberWithID adds a member with id memberID to a bucket with bucketID.
AddMemberWithID(ctx context.Context, bucketID, memberID string) (*domain.ResourceMember, error)
// RemoveMember removes a member from a bucket.
RemoveMember(ctx context.Context, bucket *domain.Bucket, user *domain.User) error
// RemoveMemberWithID removes a member with id memberID from a bucket with bucketID.
RemoveMemberWithID(ctx context.Context, bucketID, memberID string) error
// GetOwners returns owners of a bucket.
GetOwners(ctx context.Context, bucket *domain.Bucket) (*[]domain.ResourceOwner, error)
// GetOwnersWithID returns owners of a bucket with bucketID.
GetOwnersWithID(ctx context.Context, bucketID string) (*[]domain.ResourceOwner, error)
// AddOwner adds an owner to a bucket.
AddOwner(ctx context.Context, bucket *domain.Bucket, user *domain.User) (*domain.ResourceOwner, error)
// AddOwnerWithID adds an owner with id memberID to a bucket with bucketID.
AddOwnerWithID(ctx context.Context, bucketID, memberID string) (*domain.ResourceOwner, error)
// RemoveOwner removes an owner from a bucket.
RemoveOwner(ctx context.Context, bucket *domain.Bucket, user *domain.User) error
// RemoveOwnerWithID removes a member with id memberID from a bucket with bucketID.
RemoveOwnerWithID(ctx context.Context, bucketID, memberID string) error
}
// bucketsAPI implements BucketsAPI
type bucketsAPI struct {
apiClient *domain.Client
}
// NewBucketsAPI creates new instance of BucketsAPI
func NewBucketsAPI(apiClient *domain.Client) BucketsAPI {
return &bucketsAPI{
apiClient: apiClient,
}
}
func (b *bucketsAPI) GetBuckets(ctx context.Context, pagingOptions ...PagingOption) (*[]domain.Bucket, error) {
return b.getBuckets(ctx, nil, pagingOptions...)
}
func (b *bucketsAPI) getBuckets(ctx context.Context, params *domain.GetBucketsParams, pagingOptions ...PagingOption) (*[]domain.Bucket, error) {
if params == nil {
params = &domain.GetBucketsParams{}
}
options := defaultPaging()
for _, opt := range pagingOptions {
opt(options)
}
if options.limit > 0 {
params.Limit = &options.limit
}
params.Offset = &options.offset
response, err := b.apiClient.GetBuckets(ctx, params)
if err != nil {
return nil, err
}
return response.Buckets, nil
}
func (b *bucketsAPI) FindBucketByName(ctx context.Context, bucketName string) (*domain.Bucket, error) {
params := &domain.GetBucketsParams{Name: &bucketName}
response, err := b.apiClient.GetBuckets(ctx, params)
if err != nil {
return nil, err
}
if response.Buckets != nil && len(*response.Buckets) > 0 {
return &(*response.Buckets)[0], nil
}
return nil, fmt.Errorf("bucket '%s' not found", bucketName)
}
func (b *bucketsAPI) FindBucketByID(ctx context.Context, bucketID string) (*domain.Bucket, error) {
params := &domain.GetBucketsIDAllParams{
BucketID: bucketID,
}
return b.apiClient.GetBucketsID(ctx, params)
}
func (b *bucketsAPI) FindBucketsByOrgID(ctx context.Context, orgID string, pagingOptions ...PagingOption) (*[]domain.Bucket, error) {
params := &domain.GetBucketsParams{OrgID: &orgID}
return b.getBuckets(ctx, params, pagingOptions...)
}
func (b *bucketsAPI) FindBucketsByOrgName(ctx context.Context, orgName string, pagingOptions ...PagingOption) (*[]domain.Bucket, error) {
params := &domain.GetBucketsParams{Org: &orgName}
return b.getBuckets(ctx, params, pagingOptions...)
}
func (b *bucketsAPI) createBucket(ctx context.Context, bucketReq *domain.PostBucketRequest) (*domain.Bucket, error) {
params := &domain.PostBucketsAllParams{
Body: domain.PostBucketsJSONRequestBody(*bucketReq),
}
return b.apiClient.PostBuckets(ctx, params)
}
func (b *bucketsAPI) CreateBucket(ctx context.Context, bucket *domain.Bucket) (*domain.Bucket, error) {
bucketReq := &domain.PostBucketRequest{
Description: bucket.Description,
Name: bucket.Name,
OrgID: *bucket.OrgID,
RetentionRules: &bucket.RetentionRules,
Rp: bucket.Rp,
}
return b.createBucket(ctx, bucketReq)
}
func (b *bucketsAPI) CreateBucketWithNameWithID(ctx context.Context, orgID, bucketName string, rules ...domain.RetentionRule) (*domain.Bucket, error) {
rs := domain.RetentionRules(rules)
bucket := &domain.PostBucketRequest{Name: bucketName, OrgID: orgID, RetentionRules: &rs}
return b.createBucket(ctx, bucket)
}
func (b *bucketsAPI) CreateBucketWithName(ctx context.Context, org *domain.Organization, bucketName string, rules ...domain.RetentionRule) (*domain.Bucket, error) {
return b.CreateBucketWithNameWithID(ctx, *org.Id, bucketName, rules...)
}
func (b *bucketsAPI) DeleteBucket(ctx context.Context, bucket *domain.Bucket) error {
return b.DeleteBucketWithID(ctx, *bucket.Id)
}
func (b *bucketsAPI) DeleteBucketWithID(ctx context.Context, bucketID string) error {
params := &domain.DeleteBucketsIDAllParams{
BucketID: bucketID,
}
return b.apiClient.DeleteBucketsID(ctx, params)
}
func (b *bucketsAPI) UpdateBucket(ctx context.Context, bucket *domain.Bucket) (*domain.Bucket, error) {
params := &domain.PatchBucketsIDAllParams{
Body: domain.PatchBucketsIDJSONRequestBody{
Description: bucket.Description,
Name: &bucket.Name,
RetentionRules: retentionRulesToPatchRetentionRules(&bucket.RetentionRules),
},
BucketID: *bucket.Id,
}
return b.apiClient.PatchBucketsID(ctx, params)
}
func (b *bucketsAPI) GetMembers(ctx context.Context, bucket *domain.Bucket) (*[]domain.ResourceMember, error) {
return b.GetMembersWithID(ctx, *bucket.Id)
}
func (b *bucketsAPI) GetMembersWithID(ctx context.Context, bucketID string) (*[]domain.ResourceMember, error) {
params := &domain.GetBucketsIDMembersAllParams{
BucketID: bucketID,
}
response, err := b.apiClient.GetBucketsIDMembers(ctx, params)
if err != nil {
return nil, err
}
return response.Users, nil
}
func (b *bucketsAPI) AddMember(ctx context.Context, bucket *domain.Bucket, user *domain.User) (*domain.ResourceMember, error) {
return b.AddMemberWithID(ctx, *bucket.Id, *user.Id)
}
func (b *bucketsAPI) AddMemberWithID(ctx context.Context, bucketID, memberID string) (*domain.ResourceMember, error) {
params := &domain.PostBucketsIDMembersAllParams{
BucketID: bucketID,
Body: domain.PostBucketsIDMembersJSONRequestBody{Id: memberID},
}
return b.apiClient.PostBucketsIDMembers(ctx, params)
}
func (b *bucketsAPI) RemoveMember(ctx context.Context, bucket *domain.Bucket, user *domain.User) error {
return b.RemoveMemberWithID(ctx, *bucket.Id, *user.Id)
}
func (b *bucketsAPI) RemoveMemberWithID(ctx context.Context, bucketID, memberID string) error {
params := &domain.DeleteBucketsIDMembersIDAllParams{
BucketID: bucketID,
UserID: memberID,
}
return b.apiClient.DeleteBucketsIDMembersID(ctx, params)
}
func (b *bucketsAPI) GetOwners(ctx context.Context, bucket *domain.Bucket) (*[]domain.ResourceOwner, error) {
return b.GetOwnersWithID(ctx, *bucket.Id)
}
func (b *bucketsAPI) GetOwnersWithID(ctx context.Context, bucketID string) (*[]domain.ResourceOwner, error) {
params := &domain.GetBucketsIDOwnersAllParams{
BucketID: bucketID,
}
response, err := b.apiClient.GetBucketsIDOwners(ctx, params)
if err != nil {
return nil, err
}
return response.Users, nil
}
func (b *bucketsAPI) AddOwner(ctx context.Context, bucket *domain.Bucket, user *domain.User) (*domain.ResourceOwner, error) {
return b.AddOwnerWithID(ctx, *bucket.Id, *user.Id)
}
func (b *bucketsAPI) AddOwnerWithID(ctx context.Context, bucketID, memberID string) (*domain.ResourceOwner, error) {
params := &domain.PostBucketsIDOwnersAllParams{
BucketID: bucketID,
Body: domain.PostBucketsIDOwnersJSONRequestBody{Id: memberID},
}
return b.apiClient.PostBucketsIDOwners(ctx, params)
}
func (b *bucketsAPI) RemoveOwner(ctx context.Context, bucket *domain.Bucket, user *domain.User) error {
return b.RemoveOwnerWithID(ctx, *bucket.Id, *user.Id)
}
func (b *bucketsAPI) RemoveOwnerWithID(ctx context.Context, bucketID, memberID string) error {
params := &domain.DeleteBucketsIDOwnersIDAllParams{
BucketID: bucketID,
UserID: memberID,
}
return b.apiClient.DeleteBucketsIDOwnersID(ctx, params)
}
func retentionRulesToPatchRetentionRules(rrs *domain.RetentionRules) *domain.PatchRetentionRules {
if rrs == nil {
return nil
}
prrs := make([]domain.PatchRetentionRule, len(*rrs))
for i, rr := range *rrs {
prrs[i] = domain.PatchRetentionRule{
EverySeconds: rr.EverySeconds,
ShardGroupDurationSeconds: rr.ShardGroupDurationSeconds,
}
if rr.Type != nil {
rrt := domain.PatchRetentionRuleType(*rr.Type)
prrs[i].Type = &rrt
}
}
dprrs := domain.PatchRetentionRules(prrs)
return &dprrs
}
+84
View File
@@ -0,0 +1,84 @@
// Copyright 2020-2021 InfluxData, Inc. All rights reserved.
// Use of this source code is governed by MIT
// license that can be found in the LICENSE file.
package api
import (
"context"
"github.com/influxdata/influxdb-client-go/v2/domain"
"time"
)
// DeleteAPI provides methods for deleting time series data from buckets.
// Deleted series are selected by the time range specified by start and stop arguments and optional predicate string which contains condition for selecting data for deletion, such as:
// tag1="value1" and (tag2="value2" and tag3!="value3")
// Empty predicate string means all data from the given time range will be deleted. See https://v2.docs.influxdata.com/v2.0/reference/syntax/delete-predicate/
// for more info about predicate syntax.
type DeleteAPI interface {
// Delete deletes series selected by the time range specified by start and stop arguments and optional predicate string from the bucket bucket belonging to the organization org.
Delete(ctx context.Context, org *domain.Organization, bucket *domain.Bucket, start, stop time.Time, predicate string) error
// DeleteWithID deletes series selected by the time range specified by start and stop arguments and optional predicate string from the bucket with ID bucketID belonging to the organization with ID orgID.
DeleteWithID(ctx context.Context, orgID, bucketID string, start, stop time.Time, predicate string) error
// DeleteWithName deletes series selected by the time range specified by start and stop arguments and optional predicate string from the bucket with name bucketName belonging to the organization with name orgName.
DeleteWithName(ctx context.Context, orgName, bucketName string, start, stop time.Time, predicate string) error
}
// deleteAPI implements DeleteAPI
type deleteAPI struct {
apiClient *domain.Client
}
// NewDeleteAPI creates new instance of DeleteAPI
func NewDeleteAPI(apiClient *domain.Client) DeleteAPI {
return &deleteAPI{
apiClient: apiClient,
}
}
func (d *deleteAPI) delete(ctx context.Context, params *domain.PostDeleteParams, conditions *domain.DeletePredicateRequest) error {
allParams := &domain.PostDeleteAllParams{
PostDeleteParams: *params,
Body: domain.PostDeleteJSONRequestBody(*conditions),
}
return d.apiClient.PostDelete(ctx, allParams)
}
func (d *deleteAPI) Delete(ctx context.Context, org *domain.Organization, bucket *domain.Bucket, start, stop time.Time, predicate string) error {
params := &domain.PostDeleteParams{
OrgID: org.Id,
BucketID: bucket.Id,
}
conditions := &domain.DeletePredicateRequest{
Predicate: &predicate,
Start: start,
Stop: stop,
}
return d.delete(ctx, params, conditions)
}
func (d *deleteAPI) DeleteWithID(ctx context.Context, orgID, bucketID string, start, stop time.Time, predicate string) error {
params := &domain.PostDeleteParams{
OrgID: &orgID,
BucketID: &bucketID,
}
conditions := &domain.DeletePredicateRequest{
Predicate: &predicate,
Start: start,
Stop: stop,
}
return d.delete(ctx, params, conditions)
}
func (d *deleteAPI) DeleteWithName(ctx context.Context, orgName, bucketName string, start, stop time.Time, predicate string) error {
params := &domain.PostDeleteParams{
Org: &orgName,
Bucket: &bucketName,
}
conditions := &domain.DeletePredicateRequest{
Predicate: &predicate,
Start: start,
Stop: stop,
}
return d.delete(ctx, params, conditions)
}
+6
View File
@@ -0,0 +1,6 @@
// Copyright 2020-2021 InfluxData, Inc. All rights reserved.
// Use of this source code is governed by MIT
// license that can be found in the LICENSE file.
// Package api provides clients for InfluxDB server APIs.
package api
+49
View File
@@ -0,0 +1,49 @@
// Copyright 2020-2021 InfluxData, Inc. All rights reserved.
// Use of this source code is governed by MIT
// license that can be found in the LICENSE file.
package http
import (
"fmt"
"strconv"
)
// Error represent error response from InfluxDBServer or http error
type Error struct {
StatusCode int
Code string
Message string
Err error
RetryAfter uint
}
// Error fulfils error interface
func (e *Error) Error() string {
switch {
case e.Err != nil:
return e.Err.Error()
case e.Code != "" && e.Message != "":
return fmt.Sprintf("%s: %s", e.Code, e.Message)
default:
return "Unexpected status code " + strconv.Itoa(e.StatusCode)
}
}
func (e *Error) Unwrap() error {
if e.Err != nil {
return e.Err
}
return nil
}
// NewError returns newly created Error initialised with nested error and default values
func NewError(err error) *Error {
return &Error{
StatusCode: 0,
Code: "",
Message: "",
Err: err,
RetryAfter: 0,
}
}
+139
View File
@@ -0,0 +1,139 @@
// Copyright 2020-2021 InfluxData, Inc. All rights reserved.
// Use of this source code is governed by MIT
// license that can be found in the LICENSE file.
package http
import (
"crypto/tls"
"net"
"net/http"
"time"
)
// Options holds http configuration properties for communicating with InfluxDB server
type Options struct {
// HTTP client. Default is http.DefaultClient.
httpClient *http.Client
// doer is an http Doer - if set it overrides httpClient
doer Doer
// Flag whether http client was created internally
ownClient bool
// TLS configuration for secure connection. Default nil
tlsConfig *tls.Config
// HTTP request timeout in sec. Default 20
httpRequestTimeout uint
// Application name in the User-Agent HTTP header string
appName string
}
// HTTPClient returns the http.Client that is configured to be used
// for HTTP requests. It will return the one that has been set using
// SetHTTPClient or it will construct a default client using the
// other configured options.
// HTTPClient panics if SetHTTPDoer was called.
func (o *Options) HTTPClient() *http.Client {
if o.doer != nil {
panic("HTTPClient called after SetHTTPDoer")
}
if o.httpClient == nil {
o.httpClient = &http.Client{
Timeout: time.Second * time.Duration(o.HTTPRequestTimeout()),
Transport: &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 5 * time.Second,
}).DialContext,
TLSHandshakeTimeout: 5 * time.Second,
TLSClientConfig: o.TLSConfig(),
MaxIdleConns: 100,
MaxIdleConnsPerHost: 100,
IdleConnTimeout: 90 * time.Second,
},
}
o.ownClient = true
}
return o.httpClient
}
// SetHTTPClient will configure the http.Client that is used
// for HTTP requests. If set to nil, an HTTPClient will be
// generated.
//
// Setting the HTTPClient will cause the other HTTP options
// to be ignored.
// In case of UsersAPI.SignIn() is used, HTTPClient.Jar will be used for storing session cookie.
func (o *Options) SetHTTPClient(c *http.Client) *Options {
o.httpClient = c
o.ownClient = false
return o
}
// OwnHTTPClient returns true of HTTP client was created internally. False if it was set externally.
func (o *Options) OwnHTTPClient() bool {
return o.ownClient
}
// Doer allows proving custom Do for HTTP operations
type Doer interface {
Do(*http.Request) (*http.Response, error)
}
// SetHTTPDoer will configure the http.Client that is used
// for HTTP requests. If set to nil, this has no effect.
//
// Setting the HTTPDoer will cause the other HTTP options
// to be ignored.
func (o *Options) SetHTTPDoer(d Doer) *Options {
if d != nil {
o.doer = d
o.ownClient = false
}
return o
}
// HTTPDoer returns actual Doer if set, or http.Client
func (o *Options) HTTPDoer() Doer {
if o.doer != nil {
return o.doer
}
return o.HTTPClient()
}
// TLSConfig returns tls.Config
func (o *Options) TLSConfig() *tls.Config {
return o.tlsConfig
}
// SetTLSConfig sets TLS configuration for secure connection
func (o *Options) SetTLSConfig(tlsConfig *tls.Config) *Options {
o.tlsConfig = tlsConfig
return o
}
// HTTPRequestTimeout returns HTTP request timeout
func (o *Options) HTTPRequestTimeout() uint {
return o.httpRequestTimeout
}
// SetHTTPRequestTimeout sets HTTP request timeout in sec
func (o *Options) SetHTTPRequestTimeout(httpRequestTimeout uint) *Options {
o.httpRequestTimeout = httpRequestTimeout
return o
}
// ApplicationName returns application name used in the User-Agent HTTP header
func (o *Options) ApplicationName() string {
return o.appName
}
// SetApplicationName sets an application name to the User-Agent HTTP header
func (o *Options) SetApplicationName(appName string) *Options {
o.appName = appName
return o
}
// DefaultOptions returns Options object with default values
func DefaultOptions() *Options {
return &Options{httpRequestTimeout: 20}
}
+192
View File
@@ -0,0 +1,192 @@
// Copyright 2020-2021 InfluxData, Inc. All rights reserved.
// Use of this source code is governed by MIT
// license that can be found in the LICENSE file.
// Package http provides HTTP servicing related code.
//
// Important type is Service which handles HTTP operations. It is internally used by library and it is not necessary to use it directly for common operations.
// It can be useful when creating custom InfluxDB2 server API calls using generated code from the domain package, that are not yet exposed by API of this library.
//
// Service can be obtained from client using HTTPService() method.
// It can be also created directly. To instantiate a Service use NewService(). Remember, the authorization param is in form "Token your-auth-token". e.g. "Token DXnd7annkGteV5Wqx9G3YjO9Ezkw87nHk8OabcyHCxF5451kdBV0Ag2cG7OmZZgCUTHroagUPdxbuoyen6TSPw==".
// srv := http.NewService("http://localhost:8086", "Token my-token", http.DefaultOptions())
package http
import (
"context"
"encoding/json"
"io"
"io/ioutil"
"mime"
"net/http"
"net/url"
"strconv"
http2 "github.com/influxdata/influxdb-client-go/v2/internal/http"
"github.com/influxdata/influxdb-client-go/v2/internal/log"
)
// RequestCallback defines function called after a request is created before any call
type RequestCallback func(req *http.Request)
// ResponseCallback defines function called after a successful response was received
type ResponseCallback func(resp *http.Response) error
// Service handles HTTP operations with taking care of mandatory request headers and known errors
type Service interface {
// DoPostRequest sends HTTP POST request to the given url with body
DoPostRequest(ctx context.Context, url string, body io.Reader, requestCallback RequestCallback, responseCallback ResponseCallback) *Error
// DoHTTPRequest sends given HTTP request and handles response
DoHTTPRequest(req *http.Request, requestCallback RequestCallback, responseCallback ResponseCallback) *Error
// DoHTTPRequestWithResponse sends given HTTP request and returns response
DoHTTPRequestWithResponse(req *http.Request, requestCallback RequestCallback) (*http.Response, error)
// SetAuthorization sets the authorization header value
SetAuthorization(authorization string)
// Authorization returns current authorization header value
Authorization() string
// ServerAPIURL returns URL to InfluxDB2 server API space
ServerAPIURL() string
// ServerURL returns URL to InfluxDB2 server
ServerURL() string
}
// service implements Service interface
type service struct {
serverAPIURL string
serverURL string
authorization string
client Doer
userAgent string
}
// NewService creates instance of http Service with given parameters
func NewService(serverURL, authorization string, httpOptions *Options) Service {
apiURL, err := url.Parse(serverURL)
serverAPIURL := serverURL
if err == nil {
apiURL, err = apiURL.Parse("api/v2/")
if err == nil {
serverAPIURL = apiURL.String()
}
}
return &service{
serverAPIURL: serverAPIURL,
serverURL: serverURL,
authorization: authorization,
client: httpOptions.HTTPDoer(),
userAgent: http2.FormatUserAgent(httpOptions.ApplicationName()),
}
}
func (s *service) ServerAPIURL() string {
return s.serverAPIURL
}
func (s *service) ServerURL() string {
return s.serverURL
}
func (s *service) SetAuthorization(authorization string) {
s.authorization = authorization
}
func (s *service) Authorization() string {
return s.authorization
}
func (s *service) DoPostRequest(ctx context.Context, url string, body io.Reader, requestCallback RequestCallback, responseCallback ResponseCallback) *Error {
return s.doHTTPRequestWithURL(ctx, http.MethodPost, url, body, requestCallback, responseCallback)
}
func (s *service) doHTTPRequestWithURL(ctx context.Context, method, url string, body io.Reader, requestCallback RequestCallback, responseCallback ResponseCallback) *Error {
req, err := http.NewRequestWithContext(ctx, method, url, body)
if err != nil {
return NewError(err)
}
return s.DoHTTPRequest(req, requestCallback, responseCallback)
}
func (s *service) DoHTTPRequest(req *http.Request, requestCallback RequestCallback, responseCallback ResponseCallback) *Error {
resp, err := s.DoHTTPRequestWithResponse(req, requestCallback)
if err != nil {
return NewError(err)
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return s.parseHTTPError(resp)
}
if responseCallback != nil {
err := responseCallback(resp)
if err != nil {
return NewError(err)
}
}
return nil
}
func (s *service) DoHTTPRequestWithResponse(req *http.Request, requestCallback RequestCallback) (*http.Response, error) {
log.Infof("HTTP %s req to %s", req.Method, req.URL.String())
if len(s.authorization) > 0 {
req.Header.Set("Authorization", s.authorization)
}
if req.Header.Get("User-Agent") == "" {
req.Header.Set("User-Agent", s.userAgent)
}
if requestCallback != nil {
requestCallback(req)
}
return s.client.Do(req)
}
func (s *service) parseHTTPError(r *http.Response) *Error {
// successful status code range
if r.StatusCode >= 200 && r.StatusCode < 300 {
return nil
}
defer func() {
// discard body so connection can be reused
_, _ = io.Copy(ioutil.Discard, r.Body)
_ = r.Body.Close()
}()
perror := NewError(nil)
perror.StatusCode = r.StatusCode
if v := r.Header.Get("Retry-After"); v != "" {
r, err := strconv.ParseUint(v, 10, 32)
if err == nil {
perror.RetryAfter = uint(r)
}
}
// json encoded error
ctype, _, _ := mime.ParseMediaType(r.Header.Get("Content-Type"))
if ctype == "application/json" {
perror.Err = json.NewDecoder(r.Body).Decode(perror)
} else {
body, err := ioutil.ReadAll(r.Body)
if err != nil {
perror.Err = err
return perror
}
perror.Code = r.Status
perror.Message = string(body)
}
if perror.Code == "" && perror.Message == "" {
switch r.StatusCode {
case http.StatusTooManyRequests:
perror.Code = "too many requests"
perror.Message = "exceeded rate limit"
case http.StatusServiceUnavailable:
perror.Code = "unavailable"
perror.Message = "service temporarily unavailable"
default:
perror.Code = r.Status
perror.Message = r.Header.Get("X-Influxdb-Error")
}
}
return perror
}
+155
View File
@@ -0,0 +1,155 @@
// Copyright 2020-2021 InfluxData, Inc. All rights reserved.
// Use of this source code is governed by MIT
// license that can be found in the LICENSE file.
package api
import (
"context"
"fmt"
"github.com/influxdata/influxdb-client-go/v2/domain"
)
// LabelsAPI provides methods for managing labels in a InfluxDB server.
type LabelsAPI interface {
// GetLabels returns all labels.
GetLabels(ctx context.Context) (*[]domain.Label, error)
// FindLabelsByOrg returns labels belonging to organization org.
FindLabelsByOrg(ctx context.Context, org *domain.Organization) (*[]domain.Label, error)
// FindLabelsByOrgID returns labels belonging to organization with id orgID.
FindLabelsByOrgID(ctx context.Context, orgID string) (*[]domain.Label, error)
// FindLabelByID returns a label with labelID.
FindLabelByID(ctx context.Context, labelID string) (*domain.Label, error)
// FindLabelByName returns a label with name labelName under an organization orgID.
FindLabelByName(ctx context.Context, orgID, labelName string) (*domain.Label, error)
// CreateLabel creates a new label.
CreateLabel(ctx context.Context, label *domain.LabelCreateRequest) (*domain.Label, error)
// CreateLabelWithName creates a new label with label labelName and properties, under the organization org.
// Properties example: {"color": "ffb3b3", "description": "this is a description"}.
CreateLabelWithName(ctx context.Context, org *domain.Organization, labelName string, properties map[string]string) (*domain.Label, error)
// CreateLabelWithNameWithID creates a new label with label labelName and properties, under the organization with id orgID.
// Properties example: {"color": "ffb3b3", "description": "this is a description"}.
CreateLabelWithNameWithID(ctx context.Context, orgID, labelName string, properties map[string]string) (*domain.Label, error)
// UpdateLabel updates the label.
// Properties can be removed by sending an update with an empty value.
UpdateLabel(ctx context.Context, label *domain.Label) (*domain.Label, error)
// DeleteLabelWithID deletes a label with labelID.
DeleteLabelWithID(ctx context.Context, labelID string) error
// DeleteLabel deletes a label.
DeleteLabel(ctx context.Context, label *domain.Label) error
}
// labelsAPI implements LabelsAPI
type labelsAPI struct {
apiClient *domain.Client
}
// NewLabelsAPI creates new instance of LabelsAPI
func NewLabelsAPI(apiClient *domain.Client) LabelsAPI {
return &labelsAPI{
apiClient: apiClient,
}
}
func (u *labelsAPI) GetLabels(ctx context.Context) (*[]domain.Label, error) {
params := &domain.GetLabelsParams{}
return u.getLabels(ctx, params)
}
func (u *labelsAPI) getLabels(ctx context.Context, params *domain.GetLabelsParams) (*[]domain.Label, error) {
response, err := u.apiClient.GetLabels(ctx, params)
if err != nil {
return nil, err
}
return (*[]domain.Label)(response.Labels), nil
}
func (u *labelsAPI) FindLabelsByOrg(ctx context.Context, org *domain.Organization) (*[]domain.Label, error) {
return u.FindLabelsByOrgID(ctx, *org.Id)
}
func (u *labelsAPI) FindLabelsByOrgID(ctx context.Context, orgID string) (*[]domain.Label, error) {
params := &domain.GetLabelsParams{OrgID: &orgID}
return u.getLabels(ctx, params)
}
func (u *labelsAPI) FindLabelByID(ctx context.Context, labelID string) (*domain.Label, error) {
params := &domain.GetLabelsIDAllParams{
LabelID: labelID,
}
response, err := u.apiClient.GetLabelsID(ctx, params)
if err != nil {
return nil, err
}
return response.Label, nil
}
func (u *labelsAPI) FindLabelByName(ctx context.Context, orgID, labelName string) (*domain.Label, error) {
labels, err := u.FindLabelsByOrgID(ctx, orgID)
if err != nil {
return nil, err
}
var label *domain.Label
for _, u := range *labels {
if *u.Name == labelName {
label = &u
break
}
}
if label == nil {
return nil, fmt.Errorf("label '%s' not found", labelName)
}
return label, nil
}
func (u *labelsAPI) CreateLabelWithName(ctx context.Context, org *domain.Organization, labelName string, properties map[string]string) (*domain.Label, error) {
return u.CreateLabelWithNameWithID(ctx, *org.Id, labelName, properties)
}
func (u *labelsAPI) CreateLabelWithNameWithID(ctx context.Context, orgID, labelName string, properties map[string]string) (*domain.Label, error) {
props := &domain.LabelCreateRequest_Properties{AdditionalProperties: properties}
label := &domain.LabelCreateRequest{Name: labelName, OrgID: orgID, Properties: props}
return u.CreateLabel(ctx, label)
}
func (u *labelsAPI) CreateLabel(ctx context.Context, label *domain.LabelCreateRequest) (*domain.Label, error) {
params := &domain.PostLabelsAllParams{
Body: domain.PostLabelsJSONRequestBody(*label),
}
response, err := u.apiClient.PostLabels(ctx, params)
if err != nil {
return nil, err
}
return response.Label, nil
}
func (u *labelsAPI) UpdateLabel(ctx context.Context, label *domain.Label) (*domain.Label, error) {
var props *domain.LabelUpdate_Properties
if label.Properties != nil {
props = &domain.LabelUpdate_Properties{AdditionalProperties: label.Properties.AdditionalProperties}
}
params := &domain.PatchLabelsIDAllParams{
Body: domain.PatchLabelsIDJSONRequestBody(domain.LabelUpdate{
Name: label.Name,
Properties: props,
}),
LabelID: *label.Id,
}
response, err := u.apiClient.PatchLabelsID(ctx, params)
if err != nil {
return nil, err
}
return response.Label, nil
}
func (u *labelsAPI) DeleteLabel(ctx context.Context, label *domain.Label) error {
return u.DeleteLabelWithID(ctx, *label.Id)
}
func (u *labelsAPI) DeleteLabelWithID(ctx context.Context, labelID string) error {
params := &domain.DeleteLabelsIDAllParams{
LabelID: labelID,
}
return u.apiClient.DeleteLabelsID(ctx, params)
}
+233
View File
@@ -0,0 +1,233 @@
// Copyright 2020-2021 InfluxData, Inc. All rights reserved.
// Use of this source code is governed by MIT
// license that can be found in the LICENSE file.
package api
import (
"context"
"fmt"
"github.com/influxdata/influxdb-client-go/v2/domain"
)
// OrganizationsAPI provides methods for managing Organizations in a InfluxDB server.
type OrganizationsAPI interface {
// GetOrganizations returns all organizations.
// GetOrganizations supports PagingOptions: Offset, Limit, Descending
GetOrganizations(ctx context.Context, pagingOptions ...PagingOption) (*[]domain.Organization, error)
// FindOrganizationByName returns an organization found using orgName.
FindOrganizationByName(ctx context.Context, orgName string) (*domain.Organization, error)
// FindOrganizationByID returns an organization found using orgID.
FindOrganizationByID(ctx context.Context, orgID string) (*domain.Organization, error)
// FindOrganizationsByUserID returns organizations an user with userID belongs to.
// FindOrganizationsByUserID supports PagingOptions: Offset, Limit, Descending
FindOrganizationsByUserID(ctx context.Context, userID string, pagingOptions ...PagingOption) (*[]domain.Organization, error)
// CreateOrganization creates new organization.
CreateOrganization(ctx context.Context, org *domain.Organization) (*domain.Organization, error)
// CreateOrganizationWithName creates new organization with orgName and with status active.
CreateOrganizationWithName(ctx context.Context, orgName string) (*domain.Organization, error)
// UpdateOrganization updates organization.
UpdateOrganization(ctx context.Context, org *domain.Organization) (*domain.Organization, error)
// DeleteOrganization deletes an organization.
DeleteOrganization(ctx context.Context, org *domain.Organization) error
// DeleteOrganizationWithID deletes an organization with orgID.
DeleteOrganizationWithID(ctx context.Context, orgID string) error
// GetMembers returns members of an organization.
GetMembers(ctx context.Context, org *domain.Organization) (*[]domain.ResourceMember, error)
// GetMembersWithID returns members of an organization with orgID.
GetMembersWithID(ctx context.Context, orgID string) (*[]domain.ResourceMember, error)
// AddMember adds a member to an organization.
AddMember(ctx context.Context, org *domain.Organization, user *domain.User) (*domain.ResourceMember, error)
// AddMemberWithID adds a member with id memberID to an organization with orgID.
AddMemberWithID(ctx context.Context, orgID, memberID string) (*domain.ResourceMember, error)
// RemoveMember removes a member from an organization.
RemoveMember(ctx context.Context, org *domain.Organization, user *domain.User) error
// RemoveMemberWithID removes a member with id memberID from an organization with orgID.
RemoveMemberWithID(ctx context.Context, orgID, memberID string) error
// GetOwners returns owners of an organization.
GetOwners(ctx context.Context, org *domain.Organization) (*[]domain.ResourceOwner, error)
// GetOwnersWithID returns owners of an organization with orgID.
GetOwnersWithID(ctx context.Context, orgID string) (*[]domain.ResourceOwner, error)
// AddOwner adds an owner to an organization.
AddOwner(ctx context.Context, org *domain.Organization, user *domain.User) (*domain.ResourceOwner, error)
// AddOwnerWithID adds an owner with id memberID to an organization with orgID.
AddOwnerWithID(ctx context.Context, orgID, memberID string) (*domain.ResourceOwner, error)
// RemoveOwner removes an owner from an organization.
RemoveOwner(ctx context.Context, org *domain.Organization, user *domain.User) error
// RemoveOwnerWithID removes an owner with id memberID from an organization with orgID.
RemoveOwnerWithID(ctx context.Context, orgID, memberID string) error
}
// organizationsAPI implements OrganizationsAPI
type organizationsAPI struct {
apiClient *domain.Client
}
// NewOrganizationsAPI creates new instance of OrganizationsAPI
func NewOrganizationsAPI(apiClient *domain.Client) OrganizationsAPI {
return &organizationsAPI{
apiClient: apiClient,
}
}
func (o *organizationsAPI) getOrganizations(ctx context.Context, params *domain.GetOrgsParams, pagingOptions ...PagingOption) (*[]domain.Organization, error) {
options := defaultPaging()
for _, opt := range pagingOptions {
opt(options)
}
if options.limit > 0 {
params.Limit = &options.limit
}
params.Offset = &options.offset
params.Descending = &options.descending
response, err := o.apiClient.GetOrgs(ctx, params)
if err != nil {
return nil, err
}
return response.Orgs, nil
}
func (o *organizationsAPI) GetOrganizations(ctx context.Context, pagingOptions ...PagingOption) (*[]domain.Organization, error) {
params := &domain.GetOrgsParams{}
return o.getOrganizations(ctx, params, pagingOptions...)
}
func (o *organizationsAPI) FindOrganizationByName(ctx context.Context, orgName string) (*domain.Organization, error) {
params := &domain.GetOrgsParams{Org: &orgName}
organizations, err := o.getOrganizations(ctx, params)
if err != nil {
return nil, err
}
if organizations != nil && len(*organizations) > 0 {
return &(*organizations)[0], nil
}
return nil, fmt.Errorf("organization '%s' not found", orgName)
}
func (o *organizationsAPI) FindOrganizationByID(ctx context.Context, orgID string) (*domain.Organization, error) {
params := &domain.GetOrgsIDAllParams{
OrgID: orgID,
}
return o.apiClient.GetOrgsID(ctx, params)
}
func (o *organizationsAPI) FindOrganizationsByUserID(ctx context.Context, userID string, pagingOptions ...PagingOption) (*[]domain.Organization, error) {
params := &domain.GetOrgsParams{UserID: &userID}
return o.getOrganizations(ctx, params, pagingOptions...)
}
func (o *organizationsAPI) CreateOrganization(ctx context.Context, org *domain.Organization) (*domain.Organization, error) {
params := &domain.PostOrgsAllParams{
Body: domain.PostOrgsJSONRequestBody{
Name: org.Name,
Description: org.Description,
},
}
return o.apiClient.PostOrgs(ctx, params)
}
func (o *organizationsAPI) CreateOrganizationWithName(ctx context.Context, orgName string) (*domain.Organization, error) {
status := domain.OrganizationStatusActive
org := &domain.Organization{Name: orgName, Status: &status}
return o.CreateOrganization(ctx, org)
}
func (o *organizationsAPI) DeleteOrganization(ctx context.Context, org *domain.Organization) error {
return o.DeleteOrganizationWithID(ctx, *org.Id)
}
func (o *organizationsAPI) DeleteOrganizationWithID(ctx context.Context, orgID string) error {
params := &domain.DeleteOrgsIDAllParams{
OrgID: orgID,
}
return o.apiClient.DeleteOrgsID(ctx, params)
}
func (o *organizationsAPI) UpdateOrganization(ctx context.Context, org *domain.Organization) (*domain.Organization, error) {
params := &domain.PatchOrgsIDAllParams{
Body: domain.PatchOrgsIDJSONRequestBody{
Name: &org.Name,
Description: org.Description,
},
OrgID: *org.Id,
}
return o.apiClient.PatchOrgsID(ctx, params)
}
func (o *organizationsAPI) GetMembers(ctx context.Context, org *domain.Organization) (*[]domain.ResourceMember, error) {
return o.GetMembersWithID(ctx, *org.Id)
}
func (o *organizationsAPI) GetMembersWithID(ctx context.Context, orgID string) (*[]domain.ResourceMember, error) {
params := &domain.GetOrgsIDMembersAllParams{
OrgID: orgID,
}
response, err := o.apiClient.GetOrgsIDMembers(ctx, params)
if err != nil {
return nil, err
}
return response.Users, nil
}
func (o *organizationsAPI) AddMember(ctx context.Context, org *domain.Organization, user *domain.User) (*domain.ResourceMember, error) {
return o.AddMemberWithID(ctx, *org.Id, *user.Id)
}
func (o *organizationsAPI) AddMemberWithID(ctx context.Context, orgID, memberID string) (*domain.ResourceMember, error) {
params := &domain.PostOrgsIDMembersAllParams{
Body: domain.PostOrgsIDMembersJSONRequestBody{Id: memberID},
OrgID: orgID,
}
return o.apiClient.PostOrgsIDMembers(ctx, params)
}
func (o *organizationsAPI) RemoveMember(ctx context.Context, org *domain.Organization, user *domain.User) error {
return o.RemoveMemberWithID(ctx, *org.Id, *user.Id)
}
func (o *organizationsAPI) RemoveMemberWithID(ctx context.Context, orgID, memberID string) error {
params := &domain.DeleteOrgsIDMembersIDAllParams{
OrgID: orgID,
UserID: memberID,
}
return o.apiClient.DeleteOrgsIDMembersID(ctx, params)
}
func (o *organizationsAPI) GetOwners(ctx context.Context, org *domain.Organization) (*[]domain.ResourceOwner, error) {
return o.GetOwnersWithID(ctx, *org.Id)
}
func (o *organizationsAPI) GetOwnersWithID(ctx context.Context, orgID string) (*[]domain.ResourceOwner, error) {
params := &domain.GetOrgsIDOwnersAllParams{
OrgID: orgID,
}
response, err := o.apiClient.GetOrgsIDOwners(ctx, params)
if err != nil {
return nil, err
}
return response.Users, nil
}
func (o *organizationsAPI) AddOwner(ctx context.Context, org *domain.Organization, user *domain.User) (*domain.ResourceOwner, error) {
return o.AddOwnerWithID(ctx, *org.Id, *user.Id)
}
func (o *organizationsAPI) AddOwnerWithID(ctx context.Context, orgID, memberID string) (*domain.ResourceOwner, error) {
params := &domain.PostOrgsIDOwnersAllParams{
Body: domain.PostOrgsIDOwnersJSONRequestBody{Id: memberID},
OrgID: orgID,
}
return o.apiClient.PostOrgsIDOwners(ctx, params)
}
func (o *organizationsAPI) RemoveOwner(ctx context.Context, org *domain.Organization, user *domain.User) error {
return o.RemoveOwnerWithID(ctx, *org.Id, *user.Id)
}
func (o *organizationsAPI) RemoveOwnerWithID(ctx context.Context, orgID, memberID string) error {
params := &domain.DeleteOrgsIDOwnersIDAllParams{
OrgID: orgID,
UserID: memberID,
}
return o.apiClient.DeleteOrgsIDOwnersID(ctx, params)
}
+69
View File
@@ -0,0 +1,69 @@
// Copyright 2020-2021 InfluxData, Inc. All rights reserved.
// Use of this source code is governed by MIT
// license that can be found in the LICENSE file.
package api
import "github.com/influxdata/influxdb-client-go/v2/domain"
// PagingOption is the function type for applying paging option
type PagingOption func(p *Paging)
// Paging holds pagination parameters for various Get* functions of InfluxDB 2 API
// Not the all options are usable for some Get* functions
type Paging struct {
// Starting offset for returning items
// Default 0.
offset domain.Offset
// Maximum number of items returned.
// Default 0 - not applied
limit domain.Limit
// What field should be used for sorting
sortBy string
// Changes sorting direction
descending domain.Descending
// The last resource ID from which to seek from (but not including).
// This is to be used instead of `offset`.
after domain.After
}
// defaultPagingOptions returns default paging options: offset 0, limit 0 (not applied), default sorting, ascending
func defaultPaging() *Paging {
return &Paging{limit: 0, offset: 0, sortBy: "", descending: false, after: ""}
}
// PagingWithLimit sets limit option - maximum number of items returned.
func PagingWithLimit(limit int) PagingOption {
return func(p *Paging) {
p.limit = domain.Limit(limit)
}
}
// PagingWithOffset set starting offset for returning items. Default 0.
func PagingWithOffset(offset int) PagingOption {
return func(p *Paging) {
p.offset = domain.Offset(offset)
}
}
// PagingWithSortBy sets field name which should be used for sorting
func PagingWithSortBy(sortBy string) PagingOption {
return func(p *Paging) {
p.sortBy = sortBy
}
}
// PagingWithDescending changes sorting direction
func PagingWithDescending(descending bool) PagingOption {
return func(p *Paging) {
p.descending = domain.Descending(descending)
}
}
// PagingWithAfter set after option - the last resource ID from which to seek from (but not including).
// This is to be used instead of `offset`.
func PagingWithAfter(after string) PagingOption {
return func(p *Paging) {
p.after = domain.After(after)
}
}
+532
View File
@@ -0,0 +1,532 @@
// Copyright 2020-2021 InfluxData, Inc. All rights reserved.
// Use of this source code is governed by MIT
// license that can be found in the LICENSE file.
package api
import (
"bytes"
"compress/gzip"
"context"
"encoding/base64"
"encoding/csv"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"path"
"reflect"
"strconv"
"strings"
"sync"
"time"
http2 "github.com/influxdata/influxdb-client-go/v2/api/http"
"github.com/influxdata/influxdb-client-go/v2/api/query"
"github.com/influxdata/influxdb-client-go/v2/domain"
"github.com/influxdata/influxdb-client-go/v2/internal/log"
ilog "github.com/influxdata/influxdb-client-go/v2/log"
)
const (
stringDatatype = "string"
doubleDatatype = "double"
boolDatatype = "boolean"
longDatatype = "long"
uLongDatatype = "unsignedLong"
durationDatatype = "duration"
base64BinaryDataType = "base64Binary"
timeDatatypeRFC = "dateTime:RFC3339"
timeDatatypeRFCNano = "dateTime:RFC3339Nano"
)
// QueryAPI provides methods for performing synchronously flux query against InfluxDB server.
//
// Flux query can contain reference to parameters, which must be passed via queryParams.
// it can be a struct or map. Param values can be only simple types or time.Time.
// The name of a struct field or a map key (must be a string) will be a param name.
// The name of the parameter represented by a struct field can be specified by JSON annotation:
//
// type Condition struct {
// Start time.Time `json:"start"`
// Field string `json:"field"`
// Value float64 `json:"value"`
// }
//
// Parameters are then accessed via the Flux params object:
//
// query:= `from(bucket: "environment")
// |> range(start: time(v: params.start))
// |> filter(fn: (r) => r._measurement == "air")
// |> filter(fn: (r) => r._field == params.field)
// |> filter(fn: (r) => r._value > params.value)`
//
type QueryAPI interface {
// QueryRaw executes flux query on the InfluxDB server and returns complete query result as a string with table annotations according to dialect
QueryRaw(ctx context.Context, query string, dialect *domain.Dialect) (string, error)
// QueryRawWithParams executes flux parametrized query on the InfluxDB server and returns complete query result as a string with table annotations according to dialect
QueryRawWithParams(ctx context.Context, query string, dialect *domain.Dialect, params interface{}) (string, error)
// Query executes flux query on the InfluxDB server and returns QueryTableResult which parses streamed response into structures representing flux table parts
Query(ctx context.Context, query string) (*QueryTableResult, error)
// QueryWithParams executes flux parametrized query on the InfluxDB server and returns QueryTableResult which parses streamed response into structures representing flux table parts
QueryWithParams(ctx context.Context, query string, params interface{}) (*QueryTableResult, error)
}
// NewQueryAPI returns new query client for querying buckets belonging to org
func NewQueryAPI(org string, service http2.Service) QueryAPI {
return &queryAPI{
org: org,
httpService: service,
}
}
// QueryTableResult parses streamed flux query response into structures representing flux table parts
// Walking though the result is done by repeatedly calling Next() until returns false.
// Actual flux table info (columns with names, data types, etc) is returned by TableMetadata() method.
// Data are acquired by Record() method.
// Preliminary end can be caused by an error, so when Next() return false, check Err() for an error
type QueryTableResult struct {
io.Closer
csvReader *csv.Reader
tablePosition int
tableChanged bool
table *query.FluxTableMetadata
record *query.FluxRecord
err error
}
// NewQueryTableResult returns new QueryTableResult
func NewQueryTableResult(rawResponse io.ReadCloser) *QueryTableResult {
csvReader := csv.NewReader(rawResponse)
csvReader.FieldsPerRecord = -1
return &QueryTableResult{Closer: rawResponse, csvReader: csvReader}
}
// queryAPI implements QueryAPI interface
type queryAPI struct {
org string
httpService http2.Service
url string
lock sync.Mutex
}
// queryBody holds the body for an HTTP query request.
type queryBody struct {
Dialect *domain.Dialect `json:"dialect,omitempty"`
Query string `json:"query"`
Type domain.QueryType `json:"type"`
Params interface{} `json:"params,omitempty"`
}
func (q *queryAPI) QueryRaw(ctx context.Context, query string, dialect *domain.Dialect) (string, error) {
return q.QueryRawWithParams(ctx, query, dialect, nil)
}
func (q *queryAPI) QueryRawWithParams(ctx context.Context, query string, dialect *domain.Dialect, params interface{}) (string, error) {
if err := checkParamsType(params); err != nil {
return "", err
}
queryURL, err := q.queryURL()
if err != nil {
return "", err
}
qr := queryBody{
Query: query,
Type: domain.QueryTypeFlux,
Dialect: dialect,
Params: params,
}
qrJSON, err := json.Marshal(qr)
if err != nil {
return "", err
}
if log.Level() >= ilog.DebugLevel {
log.Debugf("Query: %s", qrJSON)
}
var body string
perror := q.httpService.DoPostRequest(ctx, queryURL, bytes.NewReader(qrJSON), func(req *http.Request) {
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept-Encoding", "gzip")
},
func(resp *http.Response) error {
if resp.Header.Get("Content-Encoding") == "gzip" {
resp.Body, err = gzip.NewReader(resp.Body)
if err != nil {
return err
}
}
respBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
body = string(respBody)
return nil
})
if perror != nil {
return "", perror
}
return body, nil
}
// DefaultDialect return flux query Dialect with full annotations (datatype, group, default), header and comma char as a delimiter
func DefaultDialect() *domain.Dialect {
annotations := []domain.DialectAnnotations{domain.DialectAnnotationsDatatype, domain.DialectAnnotationsGroup, domain.DialectAnnotationsDefault}
delimiter := ","
header := true
return &domain.Dialect{
Annotations: &annotations,
Delimiter: &delimiter,
Header: &header,
}
}
func (q *queryAPI) Query(ctx context.Context, query string) (*QueryTableResult, error) {
return q.QueryWithParams(ctx, query, nil)
}
func (q *queryAPI) QueryWithParams(ctx context.Context, query string, params interface{}) (*QueryTableResult, error) {
var queryResult *QueryTableResult
if err := checkParamsType(params); err != nil {
return nil, err
}
queryURL, err := q.queryURL()
if err != nil {
return nil, err
}
qr := queryBody{
Query: query,
Type: domain.QueryTypeFlux,
Dialect: DefaultDialect(),
Params: params,
}
qrJSON, err := json.Marshal(qr)
if err != nil {
return nil, err
}
if log.Level() >= ilog.DebugLevel {
log.Debugf("Query: %s", qrJSON)
}
perror := q.httpService.DoPostRequest(ctx, queryURL, bytes.NewReader(qrJSON), func(req *http.Request) {
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept-Encoding", "gzip")
},
func(resp *http.Response) error {
if resp.Header.Get("Content-Encoding") == "gzip" {
resp.Body, err = gzip.NewReader(resp.Body)
if err != nil {
return err
}
}
csvReader := csv.NewReader(resp.Body)
csvReader.FieldsPerRecord = -1
queryResult = &QueryTableResult{Closer: resp.Body, csvReader: csvReader}
return nil
})
if perror != nil {
return queryResult, perror
}
return queryResult, nil
}
func (q *queryAPI) queryURL() (string, error) {
if q.url == "" {
u, err := url.Parse(q.httpService.ServerAPIURL())
if err != nil {
return "", err
}
u.Path = path.Join(u.Path, "query")
params := u.Query()
params.Set("org", q.org)
u.RawQuery = params.Encode()
q.lock.Lock()
q.url = u.String()
q.lock.Unlock()
}
return q.url, nil
}
// checkParamsType validates the value is struct with simple type fields
// or a map with key as string and value as a simple type
func checkParamsType(p interface{}) error {
if p == nil {
return nil
}
t := reflect.TypeOf(p)
v := reflect.ValueOf(p)
if t.Kind() == reflect.Ptr {
t = t.Elem()
v = v.Elem()
}
if t.Kind() != reflect.Struct && t.Kind() != reflect.Map {
return fmt.Errorf("cannot use %v as query params", t)
}
switch t.Kind() {
case reflect.Struct:
fields := reflect.VisibleFields(t)
for _, f := range fields {
fv := v.FieldByIndex(f.Index)
t := getFieldType(fv)
if !validParamType(t) {
return fmt.Errorf("cannot use field '%s' of type '%v' as a query param", f.Name, t)
}
}
case reflect.Map:
key := t.Key()
if key.Kind() != reflect.String {
return fmt.Errorf("cannot use map key of type '%v' for query param name", key)
}
for _, k := range v.MapKeys() {
f := v.MapIndex(k)
t := getFieldType(f)
if !validParamType(t) {
return fmt.Errorf("cannot use map value type '%v' as a query param", t)
}
}
}
return nil
}
// getFieldType extracts type of value
func getFieldType(v reflect.Value) reflect.Type {
t := v.Type()
if t.Kind() == reflect.Ptr {
t = t.Elem()
v = v.Elem()
}
if t.Kind() == reflect.Interface && !v.IsNil() {
t = reflect.ValueOf(v.Interface()).Type()
}
return t
}
// timeType is the exact type for the Time
var timeType = reflect.TypeOf(time.Time{})
// validParamType validates that t is primitive type or string or interface
func validParamType(t reflect.Type) bool {
return (t.Kind() > reflect.Invalid && t.Kind() < reflect.Complex64) ||
t.Kind() == reflect.String ||
t == timeType
}
// TablePosition returns actual flux table position in the result, or -1 if no table was found yet
// Each new table is introduced by an annotation in csv
func (q *QueryTableResult) TablePosition() int {
if q.table != nil {
return q.table.Position()
}
return -1
}
// TableMetadata returns actual flux table metadata
func (q *QueryTableResult) TableMetadata() *query.FluxTableMetadata {
return q.table
}
// TableChanged returns true if last call of Next() found also new result table
// Table information is available via TableMetadata method
func (q *QueryTableResult) TableChanged() bool {
return q.tableChanged
}
// Record returns last parsed flux table data row
// Use Record methods to access value and row properties
func (q *QueryTableResult) Record() *query.FluxRecord {
return q.record
}
type parsingState int
const (
parsingStateNormal parsingState = iota
parsingStateAnnotation
parsingStateNameRow
parsingStateError
)
// Next advances to next row in query result.
// During the first time it is called, Next creates also table metadata
// Actual parsed row is available through Record() function
// Returns false in case of end or an error, otherwise true
func (q *QueryTableResult) Next() bool {
var row []string
// set closing query in case of preliminary return
closer := func() {
if err := q.Close(); err != nil {
message := err.Error()
if q.err != nil {
message = fmt.Sprintf("%s,%s", message, q.err.Error())
}
q.err = errors.New(message)
}
}
defer func() {
closer()
}()
parsingState := parsingStateNormal
q.tableChanged = false
dataTypeAnnotationFound := false
readRow:
row, q.err = q.csvReader.Read()
if q.err == io.EOF {
q.err = nil
return false
}
if q.err != nil {
return false
}
if len(row) <= 1 {
goto readRow
}
if len(row[0]) > 0 && row[0][0] == '#' {
if parsingState == parsingStateNormal {
q.table = query.NewFluxTableMetadata(q.tablePosition)
q.tablePosition++
q.tableChanged = true
for i := range row[1:] {
q.table.AddColumn(query.NewFluxColumn(i))
}
parsingState = parsingStateAnnotation
}
}
if q.table == nil {
q.err = errors.New("parsing error, annotations not found")
return false
}
if len(row)-1 != len(q.table.Columns()) {
q.err = fmt.Errorf("parsing error, row has different number of columns than the table: %d vs %d", len(row)-1, len(q.table.Columns()))
return false
}
switch row[0] {
case "":
switch parsingState {
case parsingStateAnnotation:
if !dataTypeAnnotationFound {
q.err = errors.New("parsing error, datatype annotation not found")
return false
}
parsingState = parsingStateNameRow
fallthrough
case parsingStateNameRow:
if row[1] == "error" {
parsingState = parsingStateError
} else {
for i, n := range row[1:] {
if q.table.Column(i) != nil {
q.table.Column(i).SetName(n)
}
}
parsingState = parsingStateNormal
}
goto readRow
case parsingStateError:
var message string
if len(row) > 1 && len(row[1]) > 0 {
message = row[1]
} else {
message = "unknown query error"
}
reference := ""
if len(row) > 2 && len(row[2]) > 0 {
reference = fmt.Sprintf(",%s", row[2])
}
q.err = fmt.Errorf("%s%s", message, reference)
return false
}
values := make(map[string]interface{})
for i, v := range row[1:] {
if q.table.Column(i) != nil {
values[q.table.Column(i).Name()], q.err = toValue(stringTernary(v, q.table.Column(i).DefaultValue()), q.table.Column(i).DataType(), q.table.Column(i).Name())
if q.err != nil {
return false
}
}
}
q.record = query.NewFluxRecord(q.table.Position(), values)
case "#datatype":
dataTypeAnnotationFound = true
for i, d := range row[1:] {
if q.table.Column(i) != nil {
q.table.Column(i).SetDataType(d)
}
}
goto readRow
case "#group":
for i, g := range row[1:] {
if q.table.Column(i) != nil {
q.table.Column(i).SetGroup(g == "true")
}
}
goto readRow
case "#default":
for i, c := range row[1:] {
if q.table.Column(i) != nil {
q.table.Column(i).SetDefaultValue(c)
}
}
goto readRow
}
// don't close query
closer = func() {}
return true
}
// Err returns an error raised during flux query response parsing
func (q *QueryTableResult) Err() error {
return q.err
}
// Close reads remaining data and closes underlying Closer
func (q *QueryTableResult) Close() error {
var err error
for err == nil {
_, err = q.csvReader.Read()
}
return q.Closer.Close()
}
// stringTernary returns a if not empty, otherwise b
func stringTernary(a, b string) string {
if a == "" {
return b
}
return a
}
// toValues converts s into type by t
func toValue(s, t, name string) (interface{}, error) {
if s == "" {
return nil, nil
}
switch t {
case stringDatatype:
return s, nil
case timeDatatypeRFC:
return time.Parse(time.RFC3339, s)
case timeDatatypeRFCNano:
return time.Parse(time.RFC3339Nano, s)
case durationDatatype:
return time.ParseDuration(s)
case doubleDatatype:
return strconv.ParseFloat(s, 64)
case boolDatatype:
if strings.ToLower(s) == "false" {
return false, nil
}
return true, nil
case longDatatype:
return strconv.ParseInt(s, 10, 64)
case uLongDatatype:
return strconv.ParseUint(s, 10, 64)
case base64BinaryDataType:
return base64.StdEncoding.DecodeString(s)
default:
return nil, fmt.Errorf("%s has unknown data type %s", name, t)
}
}
+260
View File
@@ -0,0 +1,260 @@
// Copyright 2020-2021 InfluxData, Inc. All rights reserved.
// Use of this source code is governed by MIT
// license that can be found in the LICENSE file.
// Package query defined types for representing flux query result
package query
import (
"fmt"
"sort"
"strings"
"time"
)
// FluxTableMetadata holds flux query result table information represented by collection of columns.
// Each new table is introduced by annotations
type FluxTableMetadata struct {
position int
columns []*FluxColumn
}
// FluxColumn holds flux query table column properties
type FluxColumn struct {
index int
name string
dataType string
group bool
defaultValue string
}
// FluxRecord represents row in the flux query result table
type FluxRecord struct {
table int
values map[string]interface{}
}
// NewFluxTableMetadata creates FluxTableMetadata for the table on position
func NewFluxTableMetadata(position int) *FluxTableMetadata {
return NewFluxTableMetadataFull(position, make([]*FluxColumn, 0, 10))
}
// NewFluxTableMetadataFull creates FluxTableMetadata
func NewFluxTableMetadataFull(position int, columns []*FluxColumn) *FluxTableMetadata {
return &FluxTableMetadata{position: position, columns: columns}
}
// Position returns position of the table in the flux query result
func (f *FluxTableMetadata) Position() int {
return f.position
}
// Columns returns slice of flux query result table
func (f *FluxTableMetadata) Columns() []*FluxColumn {
return f.columns
}
// AddColumn adds column definition to table metadata
func (f *FluxTableMetadata) AddColumn(column *FluxColumn) *FluxTableMetadata {
f.columns = append(f.columns, column)
return f
}
// Column returns flux table column by index.
// Returns nil if index is out of the bounds.
func (f *FluxTableMetadata) Column(index int) *FluxColumn {
if len(f.columns) == 0 || index < 0 || index >= len(f.columns) {
return nil
}
return f.columns[index]
}
// String returns FluxTableMetadata string dump
func (f *FluxTableMetadata) String() string {
var buffer strings.Builder
for i, c := range f.columns {
if i > 0 {
buffer.WriteString(",")
}
buffer.WriteString("col")
buffer.WriteString(c.String())
}
return buffer.String()
}
// NewFluxColumn creates FluxColumn for position
func NewFluxColumn(index int) *FluxColumn {
return &FluxColumn{index: index}
}
// NewFluxColumnFull creates FluxColumn
func NewFluxColumnFull(dataType string, defaultValue string, name string, group bool, index int) *FluxColumn {
return &FluxColumn{index: index, name: name, dataType: dataType, group: group, defaultValue: defaultValue}
}
// SetDefaultValue sets default value for the column
func (f *FluxColumn) SetDefaultValue(defaultValue string) {
f.defaultValue = defaultValue
}
// SetGroup set group flag for the column
func (f *FluxColumn) SetGroup(group bool) {
f.group = group
}
// SetDataType sets data type for the column
func (f *FluxColumn) SetDataType(dataType string) {
f.dataType = dataType
}
// SetName sets name of the column
func (f *FluxColumn) SetName(name string) {
f.name = name
}
// DefaultValue returns default value of the column
func (f *FluxColumn) DefaultValue() string {
return f.defaultValue
}
// IsGroup return true if the column is grouping column
func (f *FluxColumn) IsGroup() bool {
return f.group
}
// DataType returns data type of the column
func (f *FluxColumn) DataType() string {
return f.dataType
}
// Name returns name of the column
func (f *FluxColumn) Name() string {
return f.name
}
// Index returns index of the column
func (f *FluxColumn) Index() int {
return f.index
}
// String returns FluxColumn string dump
func (f *FluxColumn) String() string {
return fmt.Sprintf("{%d: name: %s, datatype: %s, defaultValue: %s, group: %v}", f.index, f.name, f.dataType, f.defaultValue, f.group)
}
// NewFluxRecord returns new record for the table with values
func NewFluxRecord(table int, values map[string]interface{}) *FluxRecord {
return &FluxRecord{table: table, values: values}
}
// Table returns value of the table column
// It returns zero if the table column is not found
func (r *FluxRecord) Table() int {
return int(intValue(r.values, "table"))
}
// Start returns the inclusive lower time bound of all records in the current table.
// Returns empty time.Time if there is no column "_start".
func (r *FluxRecord) Start() time.Time {
return timeValue(r.values, "_start")
}
// Stop returns the exclusive upper time bound of all records in the current table.
// Returns empty time.Time if there is no column "_stop".
func (r *FluxRecord) Stop() time.Time {
return timeValue(r.values, "_stop")
}
// Time returns the time of the record.
// Returns empty time.Time if there is no column "_time".
func (r *FluxRecord) Time() time.Time {
return timeValue(r.values, "_time")
}
// Value returns the default _value column value or nil if not present
func (r *FluxRecord) Value() interface{} {
return r.ValueByKey("_value")
}
// Field returns the field name.
// Returns empty string if there is no column "_field".
func (r *FluxRecord) Field() string {
return stringValue(r.values, "_field")
}
// Result returns the value of the _result column, which represents result name.
// Returns empty string if there is no column "result".
func (r *FluxRecord) Result() string {
return stringValue(r.values, "result")
}
// Measurement returns the measurement name of the record
// Returns empty string if there is no column "_measurement".
func (r *FluxRecord) Measurement() string {
return stringValue(r.values, "_measurement")
}
// Values returns map of the values where key is the column name
func (r *FluxRecord) Values() map[string]interface{} {
return r.values
}
// ValueByKey returns value for given column key for the record or nil of result has no value the column key
func (r *FluxRecord) ValueByKey(key string) interface{} {
return r.values[key]
}
// String returns FluxRecord string dump
func (r *FluxRecord) String() string {
if len(r.values) == 0 {
return ""
}
i := 0
keys := make([]string, len(r.values))
for k := range r.values {
keys[i] = k
i++
}
sort.Strings(keys)
var buffer strings.Builder
buffer.WriteString(fmt.Sprintf("%s:%v", keys[0], r.values[keys[0]]))
for _, k := range keys[1:] {
buffer.WriteString(",")
buffer.WriteString(fmt.Sprintf("%s:%v", k, r.values[k]))
}
return buffer.String()
}
// timeValue returns time.Time value from values map according to the key
// Empty time.Time value is returned if key is not found
func timeValue(values map[string]interface{}, key string) time.Time {
if val, ok := values[key]; ok {
if t, ok := val.(time.Time); ok {
return t
}
}
return time.Time{}
}
// stringValue returns string value from values map according to the key
// Empty string is returned if key is not found
func stringValue(values map[string]interface{}, key string) string {
if val, ok := values[key]; ok {
if s, ok := val.(string); ok {
return s
}
}
return ""
}
// intValue returns int64 value from values map according to the key
// Zero value is returned if key is not found
func intValue(values map[string]interface{}, key string) int64 {
if val, ok := values[key]; ok {
if i, ok := val.(int64); ok {
return i
}
}
return 0
}
+511
View File
@@ -0,0 +1,511 @@
// Copyright 2020-2021 InfluxData, Inc. All rights reserved.
// Use of this source code is governed by MIT
// license that can be found in the LICENSE file.
package api
import (
"context"
"fmt"
"time"
"github.com/influxdata/influxdb-client-go/v2/domain"
)
// TaskFilter defines filtering options for FindTasks functions.
type TaskFilter struct {
// Returns task with a specific name
Name string
// Filter tasks to a specific organization name.
OrgName string
// Filter tasks to a specific organization ID.
OrgID string
// Filter tasks to a specific user ID.
User string
// Filter tasks by a status--"inactive" or "active".
Status domain.TaskStatusType
// Return tasks after a specified ID.
After string
// The number of tasks to return.
// Default 100, minimum: 1, maximum 500
Limit int
}
// RunFilter defines filtering options for FindRun* functions.
type RunFilter struct {
// Return runs after a specified ID.
After string
// The number of runs to return.
// Default 100, minimum 1, maximum 500.
Limit int
// Filter runs to those scheduled before this time.
BeforeTime time.Time
// Filter runs to those scheduled after this time.
AfterTime time.Time
}
// TasksAPI provides methods for managing tasks and task runs in an InfluxDB server.
type TasksAPI interface {
// FindTasks retrieves tasks according to the filter. More fields can be applied. Filter can be nil.
FindTasks(ctx context.Context, filter *TaskFilter) ([]domain.Task, error)
// GetTask retrieves a refreshed instance of task.
GetTask(ctx context.Context, task *domain.Task) (*domain.Task, error)
// GetTaskByID retrieves a task found using taskID.
GetTaskByID(ctx context.Context, taskID string) (*domain.Task, error)
// CreateTask creates a new task according the task object.
// It copies OrgId, Name, Description, Flux, Status and Every or Cron properties. Every and Cron are mutually exclusive.
// Every has higher priority.
CreateTask(ctx context.Context, task *domain.Task) (*domain.Task, error)
// CreateTaskWithEvery creates a new task with the name, flux script and every repetition setting, in the org orgID.
// Every means duration values.
CreateTaskWithEvery(ctx context.Context, name, flux, every, orgID string) (*domain.Task, error)
// CreateTaskWithCron creates a new task with the name, flux script and cron repetition setting, in the org orgID
// Cron holds cron-like setting, e.g. once an hour at beginning of the hour "0 * * * *".
CreateTaskWithCron(ctx context.Context, name, flux, cron, orgID string) (*domain.Task, error)
// CreateTaskByFlux creates a new task with complete definition in flux script, in the org orgID
CreateTaskByFlux(ctx context.Context, flux, orgID string) (*domain.Task, error)
// UpdateTask updates a task.
// It copies Description, Flux, Status, Offset and Every or Cron properties. Every and Cron are mutually exclusive.
// Every has higher priority.
UpdateTask(ctx context.Context, task *domain.Task) (*domain.Task, error)
// DeleteTask deletes a task.
DeleteTask(ctx context.Context, task *domain.Task) error
// DeleteTaskWithID deletes a task with taskID.
DeleteTaskWithID(ctx context.Context, taskID string) error
// FindMembers retrieves members of a task.
FindMembers(ctx context.Context, task *domain.Task) ([]domain.ResourceMember, error)
// FindMembersWithID retrieves members of a task with taskID.
FindMembersWithID(ctx context.Context, taskID string) ([]domain.ResourceMember, error)
// AddMember adds a member to a task.
AddMember(ctx context.Context, task *domain.Task, user *domain.User) (*domain.ResourceMember, error)
// AddMemberWithID adds a member with id memberID to a task with taskID.
AddMemberWithID(ctx context.Context, taskID, memberID string) (*domain.ResourceMember, error)
// RemoveMember removes a member from a task.
RemoveMember(ctx context.Context, task *domain.Task, user *domain.User) error
// RemoveMemberWithID removes a member with id memberID from a task with taskID.
RemoveMemberWithID(ctx context.Context, taskID, memberID string) error
// FindOwners retrieves owners of a task.
FindOwners(ctx context.Context, task *domain.Task) ([]domain.ResourceOwner, error)
// FindOwnersWithID retrieves owners of a task with taskID.
FindOwnersWithID(ctx context.Context, taskID string) ([]domain.ResourceOwner, error)
// AddOwner adds an owner to a task.
AddOwner(ctx context.Context, task *domain.Task, user *domain.User) (*domain.ResourceOwner, error)
// AddOwnerWithID adds an owner with id memberID to a task with taskID.
AddOwnerWithID(ctx context.Context, taskID, memberID string) (*domain.ResourceOwner, error)
// RemoveOwner removes an owner from a task.
RemoveOwner(ctx context.Context, task *domain.Task, user *domain.User) error
// RemoveOwnerWithID removes a member with id memberID from a task with taskID.
RemoveOwnerWithID(ctx context.Context, taskID, memberID string) error
// FindRuns retrieves a task runs according the filter. More fields can be applied. Filter can be nil.
FindRuns(ctx context.Context, task *domain.Task, filter *RunFilter) ([]domain.Run, error)
// FindRunsWithID retrieves runs of a task with taskID according the filter. More fields can be applied. Filter can be nil.
FindRunsWithID(ctx context.Context, taskID string, filter *RunFilter) ([]domain.Run, error)
// GetRun retrieves a refreshed instance if a task run.
GetRun(ctx context.Context, run *domain.Run) (*domain.Run, error)
// GetRunByID retrieves a specific task run by taskID and runID
GetRunByID(ctx context.Context, taskID, runID string) (*domain.Run, error)
// FindRunLogs return all log events for a task run.
FindRunLogs(ctx context.Context, run *domain.Run) ([]domain.LogEvent, error)
// FindRunLogsWithID return all log events for a run with runID of a task with taskID.
FindRunLogsWithID(ctx context.Context, taskID, runID string) ([]domain.LogEvent, error)
// RunManually manually start a run of the task now, overriding the current schedule.
RunManually(ctx context.Context, task *domain.Task) (*domain.Run, error)
// RunManuallyWithID manually start a run of a task with taskID now, overriding the current schedule.
RunManuallyWithID(ctx context.Context, taskID string) (*domain.Run, error)
// RetryRun retry a task run.
RetryRun(ctx context.Context, run *domain.Run) (*domain.Run, error)
// RetryRunWithID retry a run with runID of a task with taskID.
RetryRunWithID(ctx context.Context, taskID, runID string) (*domain.Run, error)
// CancelRun cancels a running task.
CancelRun(ctx context.Context, run *domain.Run) error
// CancelRunWithID cancels a running task.
CancelRunWithID(ctx context.Context, taskID, runID string) error
// FindLogs retrieves all logs for a task.
FindLogs(ctx context.Context, task *domain.Task) ([]domain.LogEvent, error)
// FindLogsWithID retrieves all logs for a task with taskID.
FindLogsWithID(ctx context.Context, taskID string) ([]domain.LogEvent, error)
// FindLabels retrieves labels of a task.
FindLabels(ctx context.Context, task *domain.Task) ([]domain.Label, error)
// FindLabelsWithID retrieves labels of a task with taskID.
FindLabelsWithID(ctx context.Context, taskID string) ([]domain.Label, error)
// AddLabel adds a label to a task.
AddLabel(ctx context.Context, task *domain.Task, label *domain.Label) (*domain.Label, error)
// AddLabelWithID adds a label with id labelID to a task with taskID.
AddLabelWithID(ctx context.Context, taskID, labelID string) (*domain.Label, error)
// RemoveLabel removes a label from a task.
RemoveLabel(ctx context.Context, task *domain.Task, label *domain.Label) error
// RemoveLabelWithID removes a label with id labelID from a task with taskID.
RemoveLabelWithID(ctx context.Context, taskID, labelID string) error
}
// tasksAPI implements TasksAPI
type tasksAPI struct {
apiClient *domain.Client
}
// NewTasksAPI creates new instance of TasksAPI
func NewTasksAPI(apiClient *domain.Client) TasksAPI {
return &tasksAPI{
apiClient: apiClient,
}
}
func (t *tasksAPI) FindTasks(ctx context.Context, filter *TaskFilter) ([]domain.Task, error) {
params := &domain.GetTasksParams{}
if filter != nil {
if filter.Name != "" {
params.Name = &filter.Name
}
if filter.User != "" {
params.User = &filter.User
}
if filter.OrgID != "" {
params.OrgID = &filter.OrgID
}
if filter.OrgName != "" {
params.Org = &filter.OrgName
}
if filter.Status != "" {
status := domain.GetTasksParamsStatus(filter.Status)
params.Status = &status
}
if filter.Limit > 0 {
params.Limit = &filter.Limit
}
if filter.After != "" {
params.After = &filter.After
}
}
response, err := t.apiClient.GetTasks(ctx, params)
if err != nil {
return nil, err
}
return *response.Tasks, nil
}
func (t *tasksAPI) GetTask(ctx context.Context, task *domain.Task) (*domain.Task, error) {
return t.GetTaskByID(ctx, task.Id)
}
func (t *tasksAPI) GetTaskByID(ctx context.Context, taskID string) (*domain.Task, error) {
params := &domain.GetTasksIDAllParams{
TaskID: taskID,
}
return t.apiClient.GetTasksID(ctx, params)
}
func (t *tasksAPI) createTask(ctx context.Context, taskReq *domain.TaskCreateRequest) (*domain.Task, error) {
params := &domain.PostTasksAllParams{
Body: domain.PostTasksJSONRequestBody(*taskReq),
}
return t.apiClient.PostTasks(ctx, params)
}
func createTaskReqDetailed(name, flux string, every, cron *string, orgID string) *domain.TaskCreateRequest {
repetition := ""
if every != nil {
repetition = fmt.Sprintf("every: %s", *every)
} else if cron != nil {
repetition = fmt.Sprintf(`cron: "%s"`, *cron)
}
fullFlux := fmt.Sprintf(`option task = { name: "%s", %s } %s`, name, repetition, flux)
return createTaskReq(fullFlux, orgID)
}
func createTaskReq(flux string, orgID string) *domain.TaskCreateRequest {
status := domain.TaskStatusTypeActive
taskReq := &domain.TaskCreateRequest{
Flux: flux,
Status: &status,
OrgID: &orgID,
}
return taskReq
}
func (t *tasksAPI) CreateTask(ctx context.Context, task *domain.Task) (*domain.Task, error) {
taskReq := createTaskReqDetailed(task.Name, task.Flux, task.Every, task.Cron, task.OrgID)
taskReq.Description = task.Description
taskReq.Status = task.Status
return t.createTask(ctx, taskReq)
}
func (t *tasksAPI) CreateTaskWithEvery(ctx context.Context, name, flux, every, orgID string) (*domain.Task, error) {
taskReq := createTaskReqDetailed(name, flux, &every, nil, orgID)
return t.createTask(ctx, taskReq)
}
func (t *tasksAPI) CreateTaskWithCron(ctx context.Context, name, flux, cron, orgID string) (*domain.Task, error) {
taskReq := createTaskReqDetailed(name, flux, nil, &cron, orgID)
return t.createTask(ctx, taskReq)
}
func (t *tasksAPI) CreateTaskByFlux(ctx context.Context, flux, orgID string) (*domain.Task, error) {
taskReq := createTaskReq(flux, orgID)
return t.createTask(ctx, taskReq)
}
func (t *tasksAPI) DeleteTask(ctx context.Context, task *domain.Task) error {
return t.DeleteTaskWithID(ctx, task.Id)
}
func (t *tasksAPI) DeleteTaskWithID(ctx context.Context, taskID string) error {
params := &domain.DeleteTasksIDAllParams{
TaskID: taskID,
}
return t.apiClient.DeleteTasksID(ctx, params)
}
func (t *tasksAPI) UpdateTask(ctx context.Context, task *domain.Task) (*domain.Task, error) {
params := &domain.PatchTasksIDAllParams{
Body: domain.PatchTasksIDJSONRequestBody(domain.TaskUpdateRequest{
Description: task.Description,
Flux: &task.Flux,
Name: &task.Name,
Offset: task.Offset,
Status: task.Status,
}),
TaskID: task.Id,
}
if task.Every != nil {
params.Body.Every = task.Every
} else {
params.Body.Cron = task.Cron
}
return t.apiClient.PatchTasksID(ctx, params)
}
func (t *tasksAPI) FindMembers(ctx context.Context, task *domain.Task) ([]domain.ResourceMember, error) {
return t.FindMembersWithID(ctx, task.Id)
}
func (t *tasksAPI) FindMembersWithID(ctx context.Context, taskID string) ([]domain.ResourceMember, error) {
params := &domain.GetTasksIDMembersAllParams{
TaskID: taskID,
}
response, err := t.apiClient.GetTasksIDMembers(ctx, params)
if err != nil {
return nil, err
}
return *response.Users, nil
}
func (t *tasksAPI) AddMember(ctx context.Context, task *domain.Task, user *domain.User) (*domain.ResourceMember, error) {
return t.AddMemberWithID(ctx, task.Id, *user.Id)
}
func (t *tasksAPI) AddMemberWithID(ctx context.Context, taskID, memberID string) (*domain.ResourceMember, error) {
params := &domain.PostTasksIDMembersAllParams{
TaskID: taskID,
Body: domain.PostTasksIDMembersJSONRequestBody{Id: memberID},
}
return t.apiClient.PostTasksIDMembers(ctx, params)
}
func (t *tasksAPI) RemoveMember(ctx context.Context, task *domain.Task, user *domain.User) error {
return t.RemoveMemberWithID(ctx, task.Id, *user.Id)
}
func (t *tasksAPI) RemoveMemberWithID(ctx context.Context, taskID, memberID string) error {
params := &domain.DeleteTasksIDMembersIDAllParams{
TaskID: taskID,
UserID: memberID,
}
return t.apiClient.DeleteTasksIDMembersID(ctx, params)
}
func (t *tasksAPI) FindOwners(ctx context.Context, task *domain.Task) ([]domain.ResourceOwner, error) {
return t.FindOwnersWithID(ctx, task.Id)
}
func (t *tasksAPI) FindOwnersWithID(ctx context.Context, taskID string) ([]domain.ResourceOwner, error) {
params := &domain.GetTasksIDOwnersAllParams{
TaskID: taskID,
}
response, err := t.apiClient.GetTasksIDOwners(ctx, params)
if err != nil {
return nil, err
}
return *response.Users, nil
}
func (t *tasksAPI) AddOwner(ctx context.Context, task *domain.Task, user *domain.User) (*domain.ResourceOwner, error) {
return t.AddOwnerWithID(ctx, task.Id, *user.Id)
}
func (t *tasksAPI) AddOwnerWithID(ctx context.Context, taskID, memberID string) (*domain.ResourceOwner, error) {
params := &domain.PostTasksIDOwnersAllParams{
Body: domain.PostTasksIDOwnersJSONRequestBody{Id: memberID},
TaskID: taskID,
}
return t.apiClient.PostTasksIDOwners(ctx, params)
}
func (t *tasksAPI) RemoveOwner(ctx context.Context, task *domain.Task, user *domain.User) error {
return t.RemoveOwnerWithID(ctx, task.Id, *user.Id)
}
func (t *tasksAPI) RemoveOwnerWithID(ctx context.Context, taskID, memberID string) error {
params := &domain.DeleteTasksIDOwnersIDAllParams{
TaskID: taskID,
UserID: memberID,
}
return t.apiClient.DeleteTasksIDOwnersID(ctx, params)
}
func (t *tasksAPI) FindRuns(ctx context.Context, task *domain.Task, filter *RunFilter) ([]domain.Run, error) {
return t.FindRunsWithID(ctx, task.Id, filter)
}
func (t *tasksAPI) FindRunsWithID(ctx context.Context, taskID string, filter *RunFilter) ([]domain.Run, error) {
params := &domain.GetTasksIDRunsAllParams{TaskID: taskID}
if filter != nil {
if !filter.AfterTime.IsZero() {
params.AfterTime = &filter.AfterTime
}
if !filter.BeforeTime.IsZero() {
params.BeforeTime = &filter.BeforeTime
}
if filter.Limit > 0 {
params.Limit = &filter.Limit
}
if filter.After != "" {
params.After = &filter.After
}
}
response, err := t.apiClient.GetTasksIDRuns(ctx, params)
if err != nil {
return nil, err
}
return *response.Runs, nil
}
func (t *tasksAPI) GetRun(ctx context.Context, run *domain.Run) (*domain.Run, error) {
return t.GetRunByID(ctx, *run.TaskID, *run.Id)
}
func (t *tasksAPI) GetRunByID(ctx context.Context, taskID, runID string) (*domain.Run, error) {
params := &domain.GetTasksIDRunsIDAllParams{
TaskID: taskID,
RunID: runID,
}
return t.apiClient.GetTasksIDRunsID(ctx, params)
}
func (t *tasksAPI) FindRunLogs(ctx context.Context, run *domain.Run) ([]domain.LogEvent, error) {
return t.FindRunLogsWithID(ctx, *run.TaskID, *run.Id)
}
func (t *tasksAPI) FindRunLogsWithID(ctx context.Context, taskID, runID string) ([]domain.LogEvent, error) {
params := &domain.GetTasksIDRunsIDLogsAllParams{
TaskID: taskID,
RunID: runID,
}
response, err := t.apiClient.GetTasksIDRunsIDLogs(ctx, params)
if err != nil {
return nil, err
}
if response.Events == nil {
return nil, fmt.Errorf("logs for task '%s' run '%s 'not found", taskID, runID)
}
return *response.Events, nil
}
func (t *tasksAPI) RunManually(ctx context.Context, task *domain.Task) (*domain.Run, error) {
return t.RunManuallyWithID(ctx, task.Id)
}
func (t *tasksAPI) RunManuallyWithID(ctx context.Context, taskID string) (*domain.Run, error) {
params := &domain.PostTasksIDRunsAllParams{
TaskID: taskID,
}
return t.apiClient.PostTasksIDRuns(ctx, params)
}
func (t *tasksAPI) RetryRun(ctx context.Context, run *domain.Run) (*domain.Run, error) {
return t.RetryRunWithID(ctx, *run.TaskID, *run.Id)
}
func (t *tasksAPI) RetryRunWithID(ctx context.Context, taskID, runID string) (*domain.Run, error) {
params := &domain.PostTasksIDRunsIDRetryAllParams{
TaskID: taskID,
RunID: runID,
}
return t.apiClient.PostTasksIDRunsIDRetry(ctx, params)
}
func (t *tasksAPI) CancelRun(ctx context.Context, run *domain.Run) error {
return t.CancelRunWithID(ctx, *run.TaskID, *run.Id)
}
func (t *tasksAPI) CancelRunWithID(ctx context.Context, taskID, runID string) error {
params := &domain.DeleteTasksIDRunsIDAllParams{
TaskID: taskID,
RunID: runID,
}
return t.apiClient.DeleteTasksIDRunsID(ctx, params)
}
func (t *tasksAPI) FindLogs(ctx context.Context, task *domain.Task) ([]domain.LogEvent, error) {
return t.FindLogsWithID(ctx, task.Id)
}
func (t *tasksAPI) FindLogsWithID(ctx context.Context, taskID string) ([]domain.LogEvent, error) {
params := &domain.GetTasksIDLogsAllParams{
TaskID: taskID,
}
response, err := t.apiClient.GetTasksIDLogs(ctx, params)
if err != nil {
return nil, err
}
if response.Events == nil {
return nil, fmt.Errorf("logs for task '%s' not found", taskID)
}
return *response.Events, nil
}
func (t *tasksAPI) FindLabels(ctx context.Context, task *domain.Task) ([]domain.Label, error) {
return t.FindLabelsWithID(ctx, task.Id)
}
func (t *tasksAPI) FindLabelsWithID(ctx context.Context, taskID string) ([]domain.Label, error) {
params := &domain.GetTasksIDLabelsAllParams{
TaskID: taskID,
}
response, err := t.apiClient.GetTasksIDLabels(ctx, params)
if err != nil {
return nil, err
}
if response.Labels == nil {
return nil, fmt.Errorf("lables for task '%s' not found", taskID)
}
return *response.Labels, nil
}
func (t *tasksAPI) AddLabel(ctx context.Context, task *domain.Task, label *domain.Label) (*domain.Label, error) {
return t.AddLabelWithID(ctx, task.Id, *label.Id)
}
func (t *tasksAPI) AddLabelWithID(ctx context.Context, taskID, labelID string) (*domain.Label, error) {
params := &domain.PostTasksIDLabelsAllParams{
Body: domain.PostTasksIDLabelsJSONRequestBody{LabelID: &labelID},
TaskID: taskID,
}
response, err := t.apiClient.PostTasksIDLabels(ctx, params)
if err != nil {
return nil, err
}
return response.Label, nil
}
func (t *tasksAPI) RemoveLabel(ctx context.Context, task *domain.Task, label *domain.Label) error {
return t.RemoveLabelWithID(ctx, task.Id, *label.Id)
}
func (t *tasksAPI) RemoveLabelWithID(ctx context.Context, taskID, labelID string) error {
params := &domain.DeleteTasksIDLabelsIDAllParams{
TaskID: taskID,
LabelID: labelID,
}
return t.apiClient.DeleteTasksIDLabelsID(ctx, params)
}
+241
View File
@@ -0,0 +1,241 @@
// Copyright 2020-2021 InfluxData, Inc. All rights reserved.
// Use of this source code is governed by MIT
// license that can be found in the LICENSE file.
package api
import (
"context"
"encoding/base64"
"fmt"
nethttp "net/http"
"net/http/cookiejar"
"sync"
"github.com/influxdata/influxdb-client-go/v2/api/http"
"github.com/influxdata/influxdb-client-go/v2/domain"
"golang.org/x/net/publicsuffix"
)
// UsersAPI provides methods for managing users in a InfluxDB server
type UsersAPI interface {
// GetUsers returns all users
GetUsers(ctx context.Context) (*[]domain.User, error)
// FindUserByID returns user with userID
FindUserByID(ctx context.Context, userID string) (*domain.User, error)
// FindUserByName returns user with name userName
FindUserByName(ctx context.Context, userName string) (*domain.User, error)
// CreateUser creates new user
CreateUser(ctx context.Context, user *domain.User) (*domain.User, error)
// CreateUserWithName creates new user with userName
CreateUserWithName(ctx context.Context, userName string) (*domain.User, error)
// UpdateUser updates user
UpdateUser(ctx context.Context, user *domain.User) (*domain.User, error)
// UpdateUserPassword sets password for a user
UpdateUserPassword(ctx context.Context, user *domain.User, password string) error
// UpdateUserPasswordWithID sets password for a user with userID
UpdateUserPasswordWithID(ctx context.Context, userID string, password string) error
// DeleteUserWithID deletes an user with userID
DeleteUserWithID(ctx context.Context, userID string) error
// DeleteUser deletes an user
DeleteUser(ctx context.Context, user *domain.User) error
// Me returns actual user
Me(ctx context.Context) (*domain.User, error)
// MeUpdatePassword set password of actual user
MeUpdatePassword(ctx context.Context, oldPassword, newPassword string) error
// SignIn exchanges username and password credentials to establish an authenticated session with the InfluxDB server. The Client's authentication token is then ignored, it can be empty.
SignIn(ctx context.Context, username, password string) error
// SignOut signs out previously signed-in user
SignOut(ctx context.Context) error
}
// usersAPI implements UsersAPI
type usersAPI struct {
apiClient *domain.Client
httpService http.Service
httpClient *nethttp.Client
deleteCookieJar bool
lock sync.Mutex
}
// NewUsersAPI creates new instance of UsersAPI
func NewUsersAPI(apiClient *domain.Client, httpService http.Service, httpClient *nethttp.Client) UsersAPI {
return &usersAPI{
apiClient: apiClient,
httpService: httpService,
httpClient: httpClient,
}
}
func (u *usersAPI) GetUsers(ctx context.Context) (*[]domain.User, error) {
params := &domain.GetUsersParams{}
response, err := u.apiClient.GetUsers(ctx, params)
if err != nil {
return nil, err
}
return userResponsesToUsers(response.Users), nil
}
func (u *usersAPI) FindUserByID(ctx context.Context, userID string) (*domain.User, error) {
params := &domain.GetUsersIDAllParams{
UserID: userID,
}
response, err := u.apiClient.GetUsersID(ctx, params)
if err != nil {
return nil, err
}
return userResponseToUser(response), nil
}
func (u *usersAPI) FindUserByName(ctx context.Context, userName string) (*domain.User, error) {
users, err := u.GetUsers(ctx)
if err != nil {
return nil, err
}
var user *domain.User
for _, u := range *users {
if u.Name == userName {
user = &u
break
}
}
if user == nil {
return nil, fmt.Errorf("user '%s' not found", userName)
}
return user, nil
}
func (u *usersAPI) CreateUserWithName(ctx context.Context, userName string) (*domain.User, error) {
user := &domain.User{Name: userName}
return u.CreateUser(ctx, user)
}
func (u *usersAPI) CreateUser(ctx context.Context, user *domain.User) (*domain.User, error) {
params := &domain.PostUsersAllParams{
Body: domain.PostUsersJSONRequestBody(*user),
}
response, err := u.apiClient.PostUsers(ctx, params)
if err != nil {
return nil, err
}
return userResponseToUser(response), nil
}
func (u *usersAPI) UpdateUser(ctx context.Context, user *domain.User) (*domain.User, error) {
params := &domain.PatchUsersIDAllParams{
Body: domain.PatchUsersIDJSONRequestBody(*user),
UserID: *user.Id,
}
response, err := u.apiClient.PatchUsersID(ctx, params)
if err != nil {
return nil, err
}
return userResponseToUser(response), nil
}
func (u *usersAPI) UpdateUserPassword(ctx context.Context, user *domain.User, password string) error {
return u.UpdateUserPasswordWithID(ctx, *user.Id, password)
}
func (u *usersAPI) UpdateUserPasswordWithID(ctx context.Context, userID string, password string) error {
params := &domain.PostUsersIDPasswordAllParams{
UserID: userID,
Body: domain.PostUsersIDPasswordJSONRequestBody(domain.PasswordResetBody{Password: password}),
}
return u.apiClient.PostUsersIDPassword(ctx, params)
}
func (u *usersAPI) DeleteUser(ctx context.Context, user *domain.User) error {
return u.DeleteUserWithID(ctx, *user.Id)
}
func (u *usersAPI) DeleteUserWithID(ctx context.Context, userID string) error {
params := &domain.DeleteUsersIDAllParams{
UserID: userID,
}
return u.apiClient.DeleteUsersID(ctx, params)
}
func (u *usersAPI) Me(ctx context.Context) (*domain.User, error) {
params := &domain.GetMeParams{}
response, err := u.apiClient.GetMe(ctx, params)
if err != nil {
return nil, err
}
return userResponseToUser(response), nil
}
func (u *usersAPI) MeUpdatePassword(ctx context.Context, oldPassword, newPassword string) error {
u.lock.Lock()
defer u.lock.Unlock()
me, err := u.Me(ctx)
if err != nil {
return err
}
creds := base64.StdEncoding.EncodeToString([]byte(me.Name + ":" + oldPassword))
auth := u.httpService.Authorization()
defer u.httpService.SetAuthorization(auth)
u.httpService.SetAuthorization("Basic " + creds)
params := &domain.PutMePasswordAllParams{
Body: domain.PutMePasswordJSONRequestBody(domain.PasswordResetBody{Password: newPassword}),
}
return u.apiClient.PutMePassword(ctx, params)
}
func (u *usersAPI) SignIn(ctx context.Context, username, password string) error {
u.lock.Lock()
defer u.lock.Unlock()
if u.httpClient.Jar == nil {
jar, err := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List})
if err != nil {
return err
}
u.httpClient.Jar = jar
u.deleteCookieJar = true
}
creds := base64.StdEncoding.EncodeToString([]byte(username + ":" + password))
u.httpService.SetAuthorization("Basic " + creds)
defer u.httpService.SetAuthorization("")
return u.apiClient.PostSignin(ctx, &domain.PostSigninParams{})
}
func (u *usersAPI) SignOut(ctx context.Context) error {
u.lock.Lock()
defer u.lock.Unlock()
err := u.apiClient.PostSignout(ctx, &domain.PostSignoutParams{})
if u.deleteCookieJar {
u.httpClient.Jar = nil
}
return err
}
func userResponseToUser(ur *domain.UserResponse) *domain.User {
if ur == nil {
return nil
}
user := &domain.User{
Id: ur.Id,
Name: ur.Name,
Status: userResponseStatusToUserStatus(ur.Status),
}
return user
}
func userResponseStatusToUserStatus(urs *domain.UserResponseStatus) *domain.UserStatus {
if urs == nil {
return nil
}
us := domain.UserStatus(*urs)
return &us
}
func userResponsesToUsers(urs *[]domain.UserResponse) *[]domain.User {
if urs == nil {
return nil
}
us := make([]domain.User, len(*urs))
for i, ur := range *urs {
us[i] = *userResponseToUser(&ur)
}
return &us
}
+268
View File
@@ -0,0 +1,268 @@
// Copyright 2020-2021 InfluxData, Inc. All rights reserved.
// Use of this source code is governed by MIT
// license that can be found in the LICENSE file.
package api
import (
"context"
"strings"
"sync"
"sync/atomic"
"time"
http2 "github.com/influxdata/influxdb-client-go/v2/api/http"
"github.com/influxdata/influxdb-client-go/v2/api/write"
"github.com/influxdata/influxdb-client-go/v2/internal/log"
iwrite "github.com/influxdata/influxdb-client-go/v2/internal/write"
)
// WriteFailedCallback is synchronously notified in case non-blocking write fails.
// batch contains complete payload, error holds detailed error information,
// retryAttempts means number of retries, 0 if it failed during first write.
// It must return true if WriteAPI should continue with retrying, false will discard the batch.
type WriteFailedCallback func(batch string, error http2.Error, retryAttempts uint) bool
// WriteAPI is Write client interface with non-blocking methods for writing time series data asynchronously in batches into an InfluxDB server.
// WriteAPI can be used concurrently.
// When using multiple goroutines for writing, use a single WriteAPI instance in all goroutines.
type WriteAPI interface {
// WriteRecord writes asynchronously line protocol record into bucket.
// WriteRecord adds record into the buffer which is sent on the background when it reaches the batch size.
// Blocking alternative is available in the WriteAPIBlocking interface
WriteRecord(line string)
// WritePoint writes asynchronously Point into bucket.
// WritePoint adds Point into the buffer which is sent on the background when it reaches the batch size.
// Blocking alternative is available in the WriteAPIBlocking interface
WritePoint(point *write.Point)
// Flush forces all pending writes from the buffer to be sent
Flush()
// Errors returns a channel for reading errors which occurs during async writes.
// Must be called before performing any writes for errors to be collected.
// The chan is unbuffered and must be drained or the writer will block.
Errors() <-chan error
// SetWriteFailedCallback sets callback allowing custom handling of failed writes.
// If callback returns true, failed batch will be retried, otherwise discarded.
SetWriteFailedCallback(cb WriteFailedCallback)
}
// WriteAPIImpl provides main implementation for WriteAPI
type WriteAPIImpl struct {
service *iwrite.Service
writeBuffer []string
errCh chan error
writeCh chan *iwrite.Batch
bufferCh chan string
writeStop chan struct{}
bufferStop chan struct{}
bufferFlush chan struct{}
doneCh chan struct{}
bufferInfoCh chan writeBuffInfoReq
writeInfoCh chan writeBuffInfoReq
writeOptions *write.Options
closingMu *sync.Mutex
// more appropriate Bool type from sync/atomic cannot be used because it is available since go 1.19
isErrChReader int32
}
type writeBuffInfoReq struct {
writeBuffLen int
}
// NewWriteAPI returns new non-blocking write client for writing data to bucket belonging to org
func NewWriteAPI(org string, bucket string, service http2.Service, writeOptions *write.Options) *WriteAPIImpl {
w := &WriteAPIImpl{
service: iwrite.NewService(org, bucket, service, writeOptions),
errCh: make(chan error, 1),
writeBuffer: make([]string, 0, writeOptions.BatchSize()+1),
writeCh: make(chan *iwrite.Batch),
bufferCh: make(chan string),
bufferStop: make(chan struct{}),
writeStop: make(chan struct{}),
bufferFlush: make(chan struct{}),
doneCh: make(chan struct{}),
bufferInfoCh: make(chan writeBuffInfoReq),
writeInfoCh: make(chan writeBuffInfoReq),
writeOptions: writeOptions,
closingMu: &sync.Mutex{},
}
go w.bufferProc()
go w.writeProc()
return w
}
// SetWriteFailedCallback sets callback allowing custom handling of failed writes.
// If callback returns true, failed batch will be retried, otherwise discarded.
func (w *WriteAPIImpl) SetWriteFailedCallback(cb WriteFailedCallback) {
w.service.SetBatchErrorCallback(func(batch *iwrite.Batch, error2 http2.Error) bool {
return cb(batch.Batch, error2, batch.RetryAttempts)
})
}
// Errors returns a channel for reading errors which occurs during async writes.
// Must be called before performing any writes for errors to be collected.
// New error is skipped when channel is not read.
func (w *WriteAPIImpl) Errors() <-chan error {
w.setErrChanRead()
return w.errCh
}
// Flush forces all pending writes from the buffer to be sent.
// Flush also tries sending batches from retry queue without additional retrying.
func (w *WriteAPIImpl) Flush() {
w.bufferFlush <- struct{}{}
w.waitForFlushing()
w.service.Flush()
}
func (w *WriteAPIImpl) waitForFlushing() {
for {
w.bufferInfoCh <- writeBuffInfoReq{}
writeBuffInfo := <-w.bufferInfoCh
if writeBuffInfo.writeBuffLen == 0 {
break
}
log.Info("Waiting buffer is flushed")
<-time.After(time.Millisecond)
}
for {
w.writeInfoCh <- writeBuffInfoReq{}
writeBuffInfo := <-w.writeInfoCh
if writeBuffInfo.writeBuffLen == 0 {
break
}
log.Info("Waiting buffer is flushed")
<-time.After(time.Millisecond)
}
}
func (w *WriteAPIImpl) bufferProc() {
log.Info("Buffer proc started")
ticker := time.NewTicker(time.Duration(w.writeOptions.FlushInterval()) * time.Millisecond)
x:
for {
select {
case line := <-w.bufferCh:
w.writeBuffer = append(w.writeBuffer, line)
if len(w.writeBuffer) == int(w.writeOptions.BatchSize()) {
w.flushBuffer()
}
case <-ticker.C:
w.flushBuffer()
case <-w.bufferFlush:
w.flushBuffer()
case <-w.bufferStop:
ticker.Stop()
w.flushBuffer()
break x
case buffInfo := <-w.bufferInfoCh:
buffInfo.writeBuffLen = len(w.bufferInfoCh)
w.bufferInfoCh <- buffInfo
}
}
log.Info("Buffer proc finished")
w.doneCh <- struct{}{}
}
func (w *WriteAPIImpl) flushBuffer() {
if len(w.writeBuffer) > 0 {
log.Info("sending batch")
batch := iwrite.NewBatch(buffer(w.writeBuffer), w.writeOptions.MaxRetryTime())
w.writeCh <- batch
w.writeBuffer = w.writeBuffer[:0]
}
}
func (w *WriteAPIImpl) isErrChanRead() bool {
return atomic.LoadInt32(&w.isErrChReader) > 0
}
func (w *WriteAPIImpl) setErrChanRead() {
atomic.StoreInt32(&w.isErrChReader, 1)
}
func (w *WriteAPIImpl) writeProc() {
log.Info("Write proc started")
x:
for {
select {
case batch := <-w.writeCh:
err := w.service.HandleWrite(context.Background(), batch)
if err != nil && w.isErrChanRead() {
select {
case w.errCh <- err:
default:
log.Warn("Cannot write error to error channel, it is not read")
}
}
case <-w.writeStop:
log.Info("Write proc: received stop")
break x
case buffInfo := <-w.writeInfoCh:
buffInfo.writeBuffLen = len(w.writeCh)
w.writeInfoCh <- buffInfo
}
}
log.Info("Write proc finished")
w.doneCh <- struct{}{}
}
// Close finishes outstanding write operations,
// stop background routines and closes all channels
func (w *WriteAPIImpl) Close() {
w.closingMu.Lock()
defer w.closingMu.Unlock()
if w.writeCh != nil {
// Flush outstanding metrics
w.Flush()
// stop and wait for buffer proc
close(w.bufferStop)
<-w.doneCh
close(w.bufferFlush)
close(w.bufferCh)
// stop and wait for write proc
close(w.writeStop)
<-w.doneCh
close(w.writeCh)
close(w.writeInfoCh)
close(w.bufferInfoCh)
w.writeCh = nil
close(w.errCh)
w.errCh = nil
}
}
// WriteRecord writes asynchronously line protocol record into bucket.
// WriteRecord adds record into the buffer which is sent on the background when it reaches the batch size.
// Blocking alternative is available in the WriteAPIBlocking interface
func (w *WriteAPIImpl) WriteRecord(line string) {
b := []byte(line)
b = append(b, 0xa)
w.bufferCh <- string(b)
}
// WritePoint writes asynchronously Point into bucket.
// WritePoint adds Point into the buffer which is sent on the background when it reaches the batch size.
// Blocking alternative is available in the WriteAPIBlocking interface
func (w *WriteAPIImpl) WritePoint(point *write.Point) {
line, err := w.service.EncodePoints(point)
if err != nil {
log.Errorf("point encoding error: %s\n", err.Error())
if w.errCh != nil {
w.errCh <- err
}
} else {
w.bufferCh <- line
}
}
func buffer(lines []string) string {
return strings.Join(lines, "")
}
+106
View File
@@ -0,0 +1,106 @@
// Copyright 2020-2021 InfluxData, Inc. All rights reserved.
// Use of this source code is governed by MIT
// license that can be found in the LICENSE file.
package write
import (
"fmt"
"strconv"
"strings"
"time"
)
// Point extension methods for test
// PointToLineProtocolBuffer creates InfluxDB line protocol string from the Point, converting associated timestamp according to precision
// and write result to the string builder
func PointToLineProtocolBuffer(p *Point, sb *strings.Builder, precision time.Duration) {
escapeKey(sb, p.Name(), false)
sb.WriteRune(',')
for i, t := range p.TagList() {
if i > 0 {
sb.WriteString(",")
}
escapeKey(sb, t.Key, true)
sb.WriteString("=")
escapeKey(sb, t.Value, true)
}
sb.WriteString(" ")
for i, f := range p.FieldList() {
if i > 0 {
sb.WriteString(",")
}
escapeKey(sb, f.Key, true)
sb.WriteString("=")
switch f.Value.(type) {
case string:
sb.WriteString(`"`)
escapeValue(sb, f.Value.(string))
sb.WriteString(`"`)
default:
sb.WriteString(fmt.Sprintf("%v", f.Value))
}
switch f.Value.(type) {
case int64:
sb.WriteString("i")
case uint64:
sb.WriteString("u")
}
}
if !p.Time().IsZero() {
sb.WriteString(" ")
switch precision {
case time.Microsecond:
sb.WriteString(strconv.FormatInt(p.Time().UnixNano()/1000, 10))
case time.Millisecond:
sb.WriteString(strconv.FormatInt(p.Time().UnixNano()/1000000, 10))
case time.Second:
sb.WriteString(strconv.FormatInt(p.Time().Unix(), 10))
default:
sb.WriteString(strconv.FormatInt(p.Time().UnixNano(), 10))
}
}
sb.WriteString("\n")
}
// PointToLineProtocol creates InfluxDB line protocol string from the Point, converting associated timestamp according to precision
func PointToLineProtocol(p *Point, precision time.Duration) string {
var sb strings.Builder
sb.Grow(1024)
PointToLineProtocolBuffer(p, &sb, precision)
return sb.String()
}
func escapeKey(sb *strings.Builder, key string, escapeEqual bool) {
for _, r := range key {
switch r {
case '\n':
sb.WriteString(`\\n`)
continue
case '\r':
sb.WriteString(`\\r`)
continue
case '\t':
sb.WriteString(`\\t`)
continue
case ' ', ',':
sb.WriteString(`\`)
case '=':
if escapeEqual {
sb.WriteString(`\`)
}
}
sb.WriteRune(r)
}
}
func escapeValue(sb *strings.Builder, value string) {
for _, r := range value {
switch r {
case '\\', '"':
sb.WriteString(`\`)
}
sb.WriteRune(r)
}
}
+199
View File
@@ -0,0 +1,199 @@
// Copyright 2020-2021 InfluxData, Inc. All rights reserved.
// Use of this source code is governed by MIT
// license that can be found in the LICENSE file.
package write
import (
"time"
)
// Options holds write configuration properties
type Options struct {
// Maximum number of points sent to server in single request. Default 5000
batchSize uint
// Interval, in ms, in which is buffer flushed if it has not been already written (by reaching batch size) . Default 1000ms
flushInterval uint
// Precision to use in writes for timestamp. In unit of duration: time.Nanosecond, time.Microsecond, time.Millisecond, time.Second
// Default time.Nanosecond
precision time.Duration
// Whether to use GZip compression in requests. Default false
useGZip bool
// Tags added to each point during writing. If a point already has a tag with the same key, it is left unchanged.
defaultTags map[string]string
// Default retry interval in ms, if not sent by server. Default 5,000.
retryInterval uint
// Maximum count of retry attempts of failed writes, default 5.
maxRetries uint
// Maximum number of points to keep for retry. Should be multiple of BatchSize. Default 50,000.
retryBufferLimit uint
// The maximum delay between each retry attempt in milliseconds, default 125,000.
maxRetryInterval uint
// The maximum total retry timeout in millisecond, default 180,000.
maxRetryTime uint
// The base for the exponential retry delay
exponentialBase uint
// InfluxDB Enterprise write consistency as explained in https://docs.influxdata.com/enterprise_influxdb/v1.9/concepts/clustering/#write-consistency
consistency Consistency
}
const (
// ConsistencyOne requires at least one data node acknowledged a write.
ConsistencyOne Consistency = "one"
// ConsistencyAll requires all data nodes to acknowledge a write.
ConsistencyAll Consistency = "all"
// ConsistencyQuorum requires a quorum of data nodes to acknowledge a write.
ConsistencyQuorum Consistency = "quorum"
// ConsistencyAny allows for hinted hand off, potentially no write happened yet.
ConsistencyAny Consistency = "any"
)
// Consistency defines enum for allows consistency values for InfluxDB Enterprise, as explained https://docs.influxdata.com/enterprise_influxdb/v1.9/concepts/clustering/#write-consistency
type Consistency string
// BatchSize returns size of batch
func (o *Options) BatchSize() uint {
return o.batchSize
}
// SetBatchSize sets number of points sent in single request
func (o *Options) SetBatchSize(batchSize uint) *Options {
o.batchSize = batchSize
return o
}
// FlushInterval returns flush interval in ms
func (o *Options) FlushInterval() uint {
return o.flushInterval
}
// SetFlushInterval sets flush interval in ms in which is buffer flushed if it has not been already written
func (o *Options) SetFlushInterval(flushIntervalMs uint) *Options {
o.flushInterval = flushIntervalMs
return o
}
// RetryInterval returns the default retry interval in ms, if not sent by server. Default 5,000.
func (o *Options) RetryInterval() uint {
return o.retryInterval
}
// SetRetryInterval sets the time to wait before retry unsuccessful write in ms, if not sent by server
func (o *Options) SetRetryInterval(retryIntervalMs uint) *Options {
o.retryInterval = retryIntervalMs
return o
}
// MaxRetries returns maximum count of retry attempts of failed writes, default 5.
func (o *Options) MaxRetries() uint {
return o.maxRetries
}
// SetMaxRetries sets maximum count of retry attempts of failed writes.
// Setting zero value disables retry strategy.
func (o *Options) SetMaxRetries(maxRetries uint) *Options {
o.maxRetries = maxRetries
return o
}
// RetryBufferLimit returns retry buffer limit.
func (o *Options) RetryBufferLimit() uint {
return o.retryBufferLimit
}
// SetRetryBufferLimit sets maximum number of points to keep for retry. Should be multiple of BatchSize.
func (o *Options) SetRetryBufferLimit(retryBufferLimit uint) *Options {
o.retryBufferLimit = retryBufferLimit
return o
}
// MaxRetryInterval returns the maximum delay between each retry attempt in milliseconds, default 125,000.
func (o *Options) MaxRetryInterval() uint {
return o.maxRetryInterval
}
// SetMaxRetryInterval sets the maximum delay between each retry attempt in millisecond
func (o *Options) SetMaxRetryInterval(maxRetryIntervalMs uint) *Options {
o.maxRetryInterval = maxRetryIntervalMs
return o
}
// MaxRetryTime returns the maximum total retry timeout in millisecond, default 180,000.
func (o *Options) MaxRetryTime() uint {
return o.maxRetryTime
}
// SetMaxRetryTime sets the maximum total retry timeout in millisecond.
func (o *Options) SetMaxRetryTime(maxRetryTimeMs uint) *Options {
o.maxRetryTime = maxRetryTimeMs
return o
}
// ExponentialBase returns the base for the exponential retry delay. Default 2.
func (o *Options) ExponentialBase() uint {
return o.exponentialBase
}
// SetExponentialBase sets the base for the exponential retry delay.
func (o *Options) SetExponentialBase(retryExponentialBase uint) *Options {
o.exponentialBase = retryExponentialBase
return o
}
// Precision returns time precision for writes
func (o *Options) Precision() time.Duration {
return o.precision
}
// SetPrecision sets time precision to use in writes for timestamp. In unit of duration: time.Nanosecond, time.Microsecond, time.Millisecond, time.Second
func (o *Options) SetPrecision(precision time.Duration) *Options {
o.precision = precision
return o
}
// UseGZip returns true if write request are gzip`ed
func (o *Options) UseGZip() bool {
return o.useGZip
}
// SetUseGZip specifies whether to use GZip compression in write requests.
func (o *Options) SetUseGZip(useGZip bool) *Options {
o.useGZip = useGZip
return o
}
// AddDefaultTag adds a default tag. DefaultTags are added to each written point.
// If a tag with the same key already exist it is overwritten.
// If a point already defines such a tag, it is left unchanged.
func (o *Options) AddDefaultTag(key, value string) *Options {
o.DefaultTags()[key] = value
return o
}
// DefaultTags returns set of default tags
func (o *Options) DefaultTags() map[string]string {
if o.defaultTags == nil {
o.defaultTags = make(map[string]string)
}
return o.defaultTags
}
// Consistency returns consistency for param value
func (o *Options) Consistency() Consistency {
return o.consistency
}
// SetConsistency allows setting InfluxDB Enterprise write consistency, as explained in https://docs.influxdata.com/enterprise_influxdb/v1.9/concepts/clustering/#write-consistency */
func (o *Options) SetConsistency(consistency Consistency) *Options {
o.consistency = consistency
return o
}
// DefaultOptions returns Options object with default values
func DefaultOptions() *Options {
return &Options{batchSize: 5_000, flushInterval: 1_000, precision: time.Nanosecond, useGZip: false, retryBufferLimit: 50_000, defaultTags: make(map[string]string),
maxRetries: 5, retryInterval: 5_000, maxRetryInterval: 125_000, maxRetryTime: 180_000, exponentialBase: 2}
}
+162
View File
@@ -0,0 +1,162 @@
// Copyright 2020-2021 InfluxData, Inc. All rights reserved.
// Use of this source code is governed by MIT
// license that can be found in the LICENSE file.
// Package write provides the Point struct
package write
import (
"fmt"
"sort"
"time"
lp "github.com/influxdata/line-protocol"
)
// Point is represents InfluxDB time series point, holding tags and fields
type Point struct {
measurement string
tags []*lp.Tag
fields []*lp.Field
timestamp time.Time
}
// TagList returns a slice containing tags of a Point.
func (m *Point) TagList() []*lp.Tag {
return m.tags
}
// FieldList returns a slice containing the fields of a Point.
func (m *Point) FieldList() []*lp.Field {
return m.fields
}
// SetTime set timestamp for a Point.
func (m *Point) SetTime(timestamp time.Time) *Point {
m.timestamp = timestamp
return m
}
// Time is the timestamp of a Point.
func (m *Point) Time() time.Time {
return m.timestamp
}
// SortTags orders the tags of a point alphanumerically by key.
// This is just here as a helper, to make it easy to keep tags sorted if you are creating a Point manually.
func (m *Point) SortTags() *Point {
sort.Slice(m.tags, func(i, j int) bool { return m.tags[i].Key < m.tags[j].Key })
return m
}
// SortFields orders the fields of a point alphanumerically by key.
func (m *Point) SortFields() *Point {
sort.Slice(m.fields, func(i, j int) bool { return m.fields[i].Key < m.fields[j].Key })
return m
}
// AddTag adds a tag to a point.
func (m *Point) AddTag(k, v string) *Point {
for i, tag := range m.tags {
if k == tag.Key {
m.tags[i].Value = v
return m
}
}
m.tags = append(m.tags, &lp.Tag{Key: k, Value: v})
return m
}
// AddField adds a field to a point.
func (m *Point) AddField(k string, v interface{}) *Point {
for i, field := range m.fields {
if k == field.Key {
m.fields[i].Value = v
return m
}
}
m.fields = append(m.fields, &lp.Field{Key: k, Value: convertField(v)})
return m
}
// Name returns the name of measurement of a point.
func (m *Point) Name() string {
return m.measurement
}
// NewPointWithMeasurement creates a empty Point
// Use AddTag and AddField to fill point with data
func NewPointWithMeasurement(measurement string) *Point {
return &Point{measurement: measurement}
}
// NewPoint creates a Point from measurement name, tags, fields and a timestamp.
func NewPoint(
measurement string,
tags map[string]string,
fields map[string]interface{},
ts time.Time,
) *Point {
m := &Point{
measurement: measurement,
tags: nil,
fields: nil,
timestamp: ts,
}
if len(tags) > 0 {
m.tags = make([]*lp.Tag, 0, len(tags))
for k, v := range tags {
m.tags = append(m.tags,
&lp.Tag{Key: k, Value: v})
}
}
m.fields = make([]*lp.Field, 0, len(fields))
for k, v := range fields {
v := convertField(v)
if v == nil {
continue
}
m.fields = append(m.fields, &lp.Field{Key: k, Value: v})
}
m.SortFields()
m.SortTags()
return m
}
// convertField converts any primitive type to types supported by line protocol
func convertField(v interface{}) interface{} {
switch v := v.(type) {
case bool, int64, string, float64:
return v
case int:
return int64(v)
case uint:
return uint64(v)
case uint64:
return v
case []byte:
return string(v)
case int32:
return int64(v)
case int16:
return int64(v)
case int8:
return int64(v)
case uint32:
return uint64(v)
case uint16:
return uint64(v)
case uint8:
return uint64(v)
case float32:
return float64(v)
case time.Time:
return v.Format(time.RFC3339Nano)
case time.Duration:
return v.String()
default:
return fmt.Sprintf("%v", v)
}
}
@@ -0,0 +1,135 @@
// Copyright 2020-2021 InfluxData, Inc. All rights reserved.
// Use of this source code is governed by MIT
// license that can be found in the LICENSE file.
package api
import (
"context"
"strings"
"sync"
"sync/atomic"
http2 "github.com/influxdata/influxdb-client-go/v2/api/http"
"github.com/influxdata/influxdb-client-go/v2/api/write"
iwrite "github.com/influxdata/influxdb-client-go/v2/internal/write"
)
// WriteAPIBlocking offers blocking methods for writing time series data synchronously into an InfluxDB server.
// It doesn't implicitly create batches of points by default. Batches are created from array of points/records.
//
// Implicit batching is enabled with EnableBatching(). In this mode, each call to WritePoint or WriteRecord adds a line
// to internal buffer. If length ot the buffer is equal to the batch-size (set in write.Options), the buffer is sent to the server
// and the result of the operation is returned.
// When a point is written to the buffer, nil error is always returned.
// Flush() can be used to trigger sending of batch when it doesn't have the batch-size.
//
// Synchronous writing is intended to use for writing less frequent data, such as a weather sensing, or if there is a need to have explicit control of failed batches.
//
// WriteAPIBlocking can be used concurrently.
// When using multiple goroutines for writing, use a single WriteAPIBlocking instance in all goroutines.
type WriteAPIBlocking interface {
// WriteRecord writes line protocol record(s) into bucket.
// WriteRecord writes lines without implicit batching by default, batch is created from given number of records.
// Automatic batching can be enabled by EnableBatching()
// Individual arguments can also be batches (multiple records separated by newline).
// Non-blocking alternative is available in the WriteAPI interface
WriteRecord(ctx context.Context, line ...string) error
// WritePoint data point into bucket.
// WriteRecord writes points without implicit batching by default, batch is created from given number of points.
// Automatic batching can be enabled by EnableBatching().
// Non-blocking alternative is available in the WriteAPI interface
WritePoint(ctx context.Context, point ...*write.Point) error
// EnableBatching turns on implicit batching
// Batch size is controlled via write.Options
EnableBatching()
// Flush forces write of buffer if batching is enabled, even buffer doesn't have the batch-size.
Flush(ctx context.Context) error
}
// writeAPIBlocking implements WriteAPIBlocking interface
type writeAPIBlocking struct {
service *iwrite.Service
writeOptions *write.Options
// more appropriate Bool type from sync/atomic cannot be used because it is available since go 1.19
batching int32
batch []string
mu sync.Mutex
}
// NewWriteAPIBlocking creates new instance of blocking write client for writing data to bucket belonging to org
func NewWriteAPIBlocking(org string, bucket string, service http2.Service, writeOptions *write.Options) WriteAPIBlocking {
return &writeAPIBlocking{service: iwrite.NewService(org, bucket, service, writeOptions), writeOptions: writeOptions}
}
// NewWriteAPIBlockingWithBatching creates new instance of blocking write client for writing data to bucket belonging to org with batching enabled
func NewWriteAPIBlockingWithBatching(org string, bucket string, service http2.Service, writeOptions *write.Options) WriteAPIBlocking {
api := &writeAPIBlocking{service: iwrite.NewService(org, bucket, service, writeOptions), writeOptions: writeOptions}
api.EnableBatching()
return api
}
func (w *writeAPIBlocking) EnableBatching() {
if atomic.LoadInt32(&w.batching) == 0 {
w.mu.Lock()
w.batching = 1
w.batch = make([]string, 0, w.writeOptions.BatchSize())
w.mu.Unlock()
}
}
func (w *writeAPIBlocking) write(ctx context.Context, line string) error {
if atomic.LoadInt32(&w.batching) > 0 {
w.mu.Lock()
defer w.mu.Unlock()
w.batch = append(w.batch, line)
if len(w.batch) == int(w.writeOptions.BatchSize()) {
return w.flush(ctx)
}
return nil
}
err := w.service.WriteBatch(ctx, iwrite.NewBatch(line, w.writeOptions.MaxRetryTime()))
if err != nil {
return err
}
return nil
}
func (w *writeAPIBlocking) WriteRecord(ctx context.Context, line ...string) error {
if len(line) == 0 {
return nil
}
return w.write(ctx, strings.Join(line, "\n"))
}
func (w *writeAPIBlocking) WritePoint(ctx context.Context, point ...*write.Point) error {
line, err := w.service.EncodePoints(point...)
if err != nil {
return err
}
return w.write(ctx, line)
}
// flush is unsychronized helper for creating and sending batch
// Must be called from synchronized block
func (w *writeAPIBlocking) flush(ctx context.Context) error {
if len(w.batch) > 0 {
body := strings.Join(w.batch, "\n")
w.batch = w.batch[:0]
b := iwrite.NewBatch(body, w.writeOptions.MaxRetryTime())
if err:= w.service.WriteBatch(ctx, b); err != nil {
return err
}
}
return nil
}
func (w *writeAPIBlocking) Flush(ctx context.Context) error {
if atomic.LoadInt32(&w.batching) > 0 {
w.mu.Lock()
defer w.mu.Unlock()
return w.flush(ctx)
}
return nil
}
+324
View File
@@ -0,0 +1,324 @@
// Copyright 2020-2021 InfluxData, Inc. All rights reserved.
// Use of this source code is governed by MIT
// license that can be found in the LICENSE file.
// Package influxdb2 provides API for using InfluxDB client in Go.
// It's intended to use with InfluxDB 2 server. WriteAPI, QueryAPI and Health work also with InfluxDB 1.8
package influxdb2
import (
"context"
"errors"
httpnet "net/http"
"strings"
"sync"
"time"
"github.com/influxdata/influxdb-client-go/v2/api"
"github.com/influxdata/influxdb-client-go/v2/api/http"
"github.com/influxdata/influxdb-client-go/v2/domain"
ilog "github.com/influxdata/influxdb-client-go/v2/internal/log"
"github.com/influxdata/influxdb-client-go/v2/log"
)
// Client provides API to communicate with InfluxDBServer.
// There two APIs for writing, WriteAPI and WriteAPIBlocking.
// WriteAPI provides asynchronous, non-blocking, methods for writing time series data.
// WriteAPIBlocking provides blocking methods for writing time series data.
type Client interface {
// Setup sends request to initialise new InfluxDB server with user, org and bucket, and data retention period
// and returns details about newly created entities along with the authorization object.
// Retention period of zero will result to infinite retention.
Setup(ctx context.Context, username, password, org, bucket string, retentionPeriodHours int) (*domain.OnboardingResponse, error)
// SetupWithToken sends request to initialise new InfluxDB server with user, org and bucket, data retention period and token
// and returns details about newly created entities along with the authorization object.
// Retention period of zero will result to infinite retention.
SetupWithToken(ctx context.Context, username, password, org, bucket string, retentionPeriodHours int, token string) (*domain.OnboardingResponse, error)
// Ready returns InfluxDB uptime info of server. It doesn't validate authentication params.
Ready(ctx context.Context) (*domain.Ready, error)
// Health returns an InfluxDB server health check result. Read the HealthCheck.Status field to get server status.
// Health doesn't validate authentication params.
Health(ctx context.Context) (*domain.HealthCheck, error)
// Ping validates whether InfluxDB server is running. It doesn't validate authentication params.
Ping(ctx context.Context) (bool, error)
// Close ensures all ongoing asynchronous write clients finish.
// Also closes all idle connections, in case of HTTP client was created internally.
Close()
// Options returns the options associated with client
Options() *Options
// ServerURL returns the url of the server url client talks to
ServerURL() string
// HTTPService returns underlying HTTP service object used by client
HTTPService() http.Service
// WriteAPI returns the asynchronous, non-blocking, Write client.
// Ensures using a single WriteAPI instance for each org/bucket pair.
WriteAPI(org, bucket string) api.WriteAPI
// WriteAPIBlocking returns the synchronous, blocking, Write client.
// Ensures using a single WriteAPIBlocking instance for each org/bucket pair.
WriteAPIBlocking(org, bucket string) api.WriteAPIBlocking
// QueryAPI returns Query client.
// Ensures using a single QueryAPI instance each org.
QueryAPI(org string) api.QueryAPI
// AuthorizationsAPI returns Authorizations API client.
AuthorizationsAPI() api.AuthorizationsAPI
// OrganizationsAPI returns Organizations API client
OrganizationsAPI() api.OrganizationsAPI
// UsersAPI returns Users API client.
UsersAPI() api.UsersAPI
// DeleteAPI returns Delete API client
DeleteAPI() api.DeleteAPI
// BucketsAPI returns Buckets API client
BucketsAPI() api.BucketsAPI
// LabelsAPI returns Labels API client
LabelsAPI() api.LabelsAPI
// TasksAPI returns Tasks API client
TasksAPI() api.TasksAPI
APIClient() *domain.Client
}
// clientImpl implements Client interface
type clientImpl struct {
serverURL string
options *Options
writeAPIs map[string]api.WriteAPI
syncWriteAPIs map[string]api.WriteAPIBlocking
lock sync.Mutex
httpService http.Service
apiClient *domain.Client
authAPI api.AuthorizationsAPI
orgAPI api.OrganizationsAPI
usersAPI api.UsersAPI
deleteAPI api.DeleteAPI
bucketsAPI api.BucketsAPI
labelsAPI api.LabelsAPI
tasksAPI api.TasksAPI
}
type clientDoer struct {
service http.Service
}
// NewClient creates Client for connecting to given serverURL with provided authentication token, with the default options.
// serverURL is the InfluxDB server base URL, e.g. http://localhost:8086,
// authToken is an authentication token. It can be empty in case of connecting to newly installed InfluxDB server, which has not been set up yet.
// In such case, calling Setup() will set the authentication token.
func NewClient(serverURL string, authToken string) Client {
return NewClientWithOptions(serverURL, authToken, DefaultOptions())
}
// NewClientWithOptions creates Client for connecting to given serverURL with provided authentication token
// and configured with custom Options.
// serverURL is the InfluxDB server base URL, e.g. http://localhost:8086,
// authToken is an authentication token. It can be empty in case of connecting to newly installed InfluxDB server, which has not been set up yet.
// In such case, calling Setup() will set authentication token
func NewClientWithOptions(serverURL string, authToken string, options *Options) Client {
normServerURL := serverURL
if !strings.HasSuffix(normServerURL, "/") {
// For subsequent path parts concatenation, url has to end with '/'
normServerURL = serverURL + "/"
}
authorization := ""
if len(authToken) > 0 {
authorization = "Token " + authToken
}
service := http.NewService(normServerURL, authorization, options.httpOptions)
doer := &clientDoer{service}
apiClient, _ := domain.NewClient(service.ServerURL(), doer)
client := &clientImpl{
serverURL: serverURL,
options: options,
writeAPIs: make(map[string]api.WriteAPI, 5),
syncWriteAPIs: make(map[string]api.WriteAPIBlocking, 5),
httpService: service,
apiClient: apiClient,
}
if log.Log != nil {
log.Log.SetLogLevel(options.LogLevel())
}
if ilog.Level() >= log.InfoLevel {
tokenStr := ""
if len(authToken) > 0 {
tokenStr = ", token '******'"
}
ilog.Infof("Using URL '%s'%s", serverURL, tokenStr)
}
if options.ApplicationName() == "" {
ilog.Warn("Application name is not set")
}
return client
}
func (c *clientImpl) APIClient() *domain.Client {
return c.apiClient
}
func (c *clientImpl) Options() *Options {
return c.options
}
func (c *clientImpl) ServerURL() string {
return c.serverURL
}
func (c *clientImpl) HTTPService() http.Service {
return c.httpService
}
func (c *clientDoer) Do(req *httpnet.Request) (*httpnet.Response, error) {
return c.service.DoHTTPRequestWithResponse(req, nil)
}
func (c *clientImpl) Ready(ctx context.Context) (*domain.Ready, error) {
params := &domain.GetReadyParams{}
return c.apiClient.GetReady(ctx, params)
}
func (c *clientImpl) Setup(ctx context.Context, username, password, org, bucket string, retentionPeriodHours int) (*domain.OnboardingResponse, error) {
return c.SetupWithToken(ctx, username, password, org, bucket, retentionPeriodHours, "")
}
func (c *clientImpl) SetupWithToken(ctx context.Context, username, password, org, bucket string, retentionPeriodHours int, token string) (*domain.OnboardingResponse, error) {
if username == "" || password == "" {
return nil, errors.New("a username and a password is required for a setup")
}
c.lock.Lock()
defer c.lock.Unlock()
params := &domain.PostSetupAllParams{}
retentionPeriodSeconds := int64(retentionPeriodHours * 3600)
retentionPeriodHrs := int(time.Duration(retentionPeriodSeconds) * time.Second)
params.Body = domain.PostSetupJSONRequestBody{
Bucket: bucket,
Org: org,
Password: &password,
RetentionPeriodSeconds: &retentionPeriodSeconds,
RetentionPeriodHrs: &retentionPeriodHrs,
Username: username,
}
if token != "" {
params.Body.Token = &token
}
return c.apiClient.PostSetup(ctx, params)
}
func (c *clientImpl) Health(ctx context.Context) (*domain.HealthCheck, error) {
params := &domain.GetHealthParams{}
return c.apiClient.GetHealth(ctx, params)
}
func (c *clientImpl) Ping(ctx context.Context) (bool, error) {
err := c.apiClient.GetPing(ctx)
if err != nil {
return false, err
}
return true, nil
}
func createKey(org, bucket string) string {
return org + "\t" + bucket
}
func (c *clientImpl) WriteAPI(org, bucket string) api.WriteAPI {
c.lock.Lock()
defer c.lock.Unlock()
key := createKey(org, bucket)
if _, ok := c.writeAPIs[key]; !ok {
w := api.NewWriteAPI(org, bucket, c.httpService, c.options.writeOptions)
c.writeAPIs[key] = w
}
return c.writeAPIs[key]
}
func (c *clientImpl) WriteAPIBlocking(org, bucket string) api.WriteAPIBlocking {
c.lock.Lock()
defer c.lock.Unlock()
key := createKey(org, bucket)
if _, ok := c.syncWriteAPIs[key]; !ok {
w := api.NewWriteAPIBlocking(org, bucket, c.httpService, c.options.writeOptions)
c.syncWriteAPIs[key] = w
}
return c.syncWriteAPIs[key]
}
func (c *clientImpl) Close() {
for key, w := range c.writeAPIs {
wa := w.(*api.WriteAPIImpl)
wa.Close()
delete(c.writeAPIs, key)
}
for key := range c.syncWriteAPIs {
delete(c.syncWriteAPIs, key)
}
if c.options.HTTPOptions().OwnHTTPClient() {
c.options.HTTPOptions().HTTPClient().CloseIdleConnections()
}
}
func (c *clientImpl) QueryAPI(org string) api.QueryAPI {
return api.NewQueryAPI(org, c.httpService)
}
func (c *clientImpl) AuthorizationsAPI() api.AuthorizationsAPI {
c.lock.Lock()
defer c.lock.Unlock()
if c.authAPI == nil {
c.authAPI = api.NewAuthorizationsAPI(c.apiClient)
}
return c.authAPI
}
func (c *clientImpl) OrganizationsAPI() api.OrganizationsAPI {
c.lock.Lock()
defer c.lock.Unlock()
if c.orgAPI == nil {
c.orgAPI = api.NewOrganizationsAPI(c.apiClient)
}
return c.orgAPI
}
func (c *clientImpl) UsersAPI() api.UsersAPI {
c.lock.Lock()
defer c.lock.Unlock()
if c.usersAPI == nil {
c.usersAPI = api.NewUsersAPI(c.apiClient, c.httpService, c.options.HTTPClient())
}
return c.usersAPI
}
func (c *clientImpl) DeleteAPI() api.DeleteAPI {
c.lock.Lock()
defer c.lock.Unlock()
if c.deleteAPI == nil {
c.deleteAPI = api.NewDeleteAPI(c.apiClient)
}
return c.deleteAPI
}
func (c *clientImpl) BucketsAPI() api.BucketsAPI {
c.lock.Lock()
defer c.lock.Unlock()
if c.bucketsAPI == nil {
c.bucketsAPI = api.NewBucketsAPI(c.apiClient)
}
return c.bucketsAPI
}
func (c *clientImpl) LabelsAPI() api.LabelsAPI {
c.lock.Lock()
defer c.lock.Unlock()
if c.labelsAPI == nil {
c.labelsAPI = api.NewLabelsAPI(c.apiClient)
}
return c.labelsAPI
}
func (c *clientImpl) TasksAPI() api.TasksAPI {
c.lock.Lock()
defer c.lock.Unlock()
if c.tasksAPI == nil {
c.tasksAPI = api.NewTasksAPI(c.apiClient)
}
return c.tasksAPI
}
+35
View File
@@ -0,0 +1,35 @@
// Copyright 2020-2021 InfluxData, Inc. All rights reserved.
// Use of this source code is governed by MIT
// license that can be found in the LICENSE file.
package influxdb2
import (
"github.com/influxdata/influxdb-client-go/v2/api"
"github.com/influxdata/influxdb-client-go/v2/api/write"
"github.com/influxdata/influxdb-client-go/v2/domain"
"time"
)
// Proxy methods for backward compatibility
// NewPointWithMeasurement creates a empty Point
// Use AddTag and AddField to fill point with data
func NewPointWithMeasurement(measurement string) *write.Point {
return write.NewPointWithMeasurement(measurement)
}
// NewPoint creates a Point from measurement name, tags, fields and a timestamp.
func NewPoint(
measurement string,
tags map[string]string,
fields map[string]interface{},
ts time.Time,
) *write.Point {
return write.NewPoint(measurement, tags, fields, ts)
}
// DefaultDialect return flux query Dialect with full annotations (datatype, group, default), header and comma char as a delimiter
func DefaultDialect() *domain.Dialect {
return api.DefaultDialect()
}
+23
View File
@@ -0,0 +1,23 @@
# Generated types and API client
`oss.yml`must be periodically synced with latest changes and types and client must be re-generated
to maintain full compatibility with the latest InfluxDB release
## Install oapi generator
`git clone git@github.com:bonitoo-io/oapi-codegen.git`
`cd oapi-codegen`
`git checkout feat/template_helpers`
`go install ./cmd/oapi-codegen/oapi-codegen.go`
## Download latest swagger
`wget https://raw.githubusercontent.com/influxdata/openapi/master/contracts/oss.yml`
`cd domain`
## Generate
### Generate types
`oapi-codegen -generate types -o types.gen.go -package domain -templates .\templates oss.yml`
### Generate client
`oapi-codegen -generate client -o client.gen.go -package domain -templates .\templates oss.yml`
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,52 @@
// Copyright 2020-2021 InfluxData, Inc.. All rights reserved.
// Use of this source code is governed by MIT
// license that can be found in the LICENSE file.
// Package gzip provides GZip related functionality
package gzip
import (
"compress/gzip"
"io"
"sync"
)
// ReadWaitCloser is ReadCloser that waits for finishing underlying reader
type ReadWaitCloser struct {
pipeReader *io.PipeReader
wg sync.WaitGroup
}
// Close closes underlying reader and waits for finishing operations
func (r *ReadWaitCloser) Close() error {
err := r.pipeReader.Close()
r.wg.Wait() // wait for the gzip goroutine finish
return err
}
// CompressWithGzip takes an io.Reader as input and pipes
// it through a gzip.Writer returning an io.Reader containing
// the gzipped data.
// An error is returned if passing data to the gzip.Writer fails
// this is shamelessly stolen from https://github.com/influxdata/telegraf
func CompressWithGzip(data io.Reader) (io.ReadCloser, error) {
pipeReader, pipeWriter := io.Pipe()
gzipWriter := gzip.NewWriter(pipeWriter)
rc := &ReadWaitCloser{
pipeReader: pipeReader,
}
rc.wg.Add(1)
var err error
go func() {
_, err = io.Copy(gzipWriter, data)
gzipWriter.Close()
// subsequent reads from the read half of the pipe will
// return no bytes and the error err, or EOF if err is nil.
pipeWriter.CloseWithError(err)
rc.wg.Done()
}()
return pipeReader, err
}
@@ -0,0 +1,21 @@
// Copyright 2020-2021 InfluxData, Inc. All rights reserved.
// Use of this source code is governed by MIT
// license that can be found in the LICENSE file.
// Package http hold internal HTTP related stuff
package http
import (
"fmt"
)
// UserAgentBase keeps once created base User-Agent string
var UserAgentBase string
// FormatUserAgent creates User-Agent header value for application name
func FormatUserAgent(appName string) string {
if appName != "" {
return fmt.Sprintf("%s %s", UserAgentBase, appName)
}
return UserAgentBase
}
@@ -0,0 +1,74 @@
// Copyright 2020-2021 InfluxData, Inc. All rights reserved.
// Use of this source code is governed by MIT
// license that can be found in the LICENSE file.
// Package log provides internal logging infrastructure
package log
import (
ilog "github.com/influxdata/influxdb-client-go/v2/log"
)
// Debugf writes formatted debug message to the Logger instance
func Debugf(format string, v ...interface{}) {
if ilog.Log != nil {
ilog.Log.Debugf(format, v...)
}
}
// Debug writes debug message message to the Logger instance
func Debug(msg string) {
if ilog.Log != nil {
ilog.Log.Debug(msg)
}
}
// Infof writes formatted info message to the Logger instance
func Infof(format string, v ...interface{}) {
if ilog.Log != nil {
ilog.Log.Infof(format, v...)
}
}
// Info writes info message message to the Logger instance
func Info(msg string) {
if ilog.Log != nil {
ilog.Log.Info(msg)
}
}
// Warnf writes formatted warning message to the Logger instance
func Warnf(format string, v ...interface{}) {
if ilog.Log != nil {
ilog.Log.Warnf(format, v...)
}
}
// Warn writes warning message message to the Logger instance
func Warn(msg string) {
if ilog.Log != nil {
ilog.Log.Warn(msg)
}
}
// Errorf writes formatted error message to the Logger instance
func Errorf(format string, v ...interface{}) {
if ilog.Log != nil {
ilog.Log.Errorf(format, v...)
}
}
// Error writes error message message to the Logger instance
func Error(msg string) {
if ilog.Log != nil {
ilog.Log.Error(msg)
}
}
// Level retrieves current logging level form the Logger instance
func Level() uint {
if ilog.Log != nil {
return ilog.Log.LogLevel()
}
return ilog.ErrorLevel
}
@@ -0,0 +1,50 @@
// Copyright 2020-2021 InfluxData, Inc. All rights reserved.
// Use of this source code is governed by MIT
// license that can be found in the LICENSE file.
package write
import (
"container/list"
)
type queue struct {
list *list.List
limit int
}
func newQueue(limit int) *queue {
return &queue{list: list.New(), limit: limit}
}
func (q *queue) push(batch *Batch) bool {
overWrite := false
if q.list.Len() == q.limit {
q.pop()
overWrite = true
}
q.list.PushBack(batch)
return overWrite
}
func (q *queue) pop() *Batch {
el := q.list.Front()
if el != nil {
q.list.Remove(el)
batch := el.Value.(*Batch)
batch.Evicted = true
return batch
}
return nil
}
func (q *queue) first() *Batch {
el := q.list.Front()
if el != nil {
return el.Value.(*Batch)
}
return nil
}
func (q *queue) isEmpty() bool {
return q.list.Len() == 0
}
@@ -0,0 +1,417 @@
// Copyright 2020-2021 InfluxData, Inc. All rights reserved.
// Use of this source code is governed by MIT
// license that can be found in the LICENSE file.
// Package write provides service and its stuff
package write
import (
"bytes"
"context"
"fmt"
"io"
"math/rand"
"net/http"
"net/url"
"sort"
"strings"
"sync"
"time"
http2 "github.com/influxdata/influxdb-client-go/v2/api/http"
"github.com/influxdata/influxdb-client-go/v2/api/write"
"github.com/influxdata/influxdb-client-go/v2/internal/gzip"
"github.com/influxdata/influxdb-client-go/v2/internal/log"
ilog "github.com/influxdata/influxdb-client-go/v2/log"
lp "github.com/influxdata/line-protocol"
)
// Batch holds information for sending points batch
type Batch struct {
// lines to send
Batch string
// retry attempts so far
RetryAttempts uint
// true if it was removed from queue
Evicted bool
// time when this batch expires
Expires time.Time
}
// NewBatch creates new batch
func NewBatch(data string, expireDelayMs uint) *Batch {
return &Batch{
Batch: data,
Expires: time.Now().Add(time.Duration(expireDelayMs) * time.Millisecond),
}
}
// BatchErrorCallback is synchronously notified in case non-blocking write fails.
// It returns true if WriteAPI should continue with retrying, false will discard the batch.
type BatchErrorCallback func(batch *Batch, error2 http2.Error) bool
// Service is responsible for reliable writing of batches
type Service struct {
org string
bucket string
httpService http2.Service
url string
lastWriteAttempt time.Time
retryQueue *queue
lock sync.Mutex
writeOptions *write.Options
retryExponentialBase uint
errorCb BatchErrorCallback
retryDelay uint
retryAttempts uint
}
// NewService creates new write service
func NewService(org string, bucket string, httpService http2.Service, options *write.Options) *Service {
retryBufferLimit := options.RetryBufferLimit() / options.BatchSize()
if retryBufferLimit == 0 {
retryBufferLimit = 1
}
u, _ := url.Parse(httpService.ServerAPIURL())
u, _ = u.Parse("write")
params := u.Query()
params.Set("org", org)
params.Set("bucket", bucket)
params.Set("precision", precisionToString(options.Precision()))
if options.Consistency() != "" {
params.Set("consistency", string(options.Consistency()))
}
u.RawQuery = params.Encode()
writeURL := u.String()
return &Service{
org: org,
bucket: bucket,
httpService: httpService,
url: writeURL,
writeOptions: options,
retryQueue: newQueue(int(retryBufferLimit)),
retryExponentialBase: 2,
retryDelay: options.RetryInterval(),
retryAttempts: 0,
}
}
// SetBatchErrorCallback sets callback allowing custom handling of failed writes.
// If callback returns true, failed batch will be retried, otherwise discarded.
func (w *Service) SetBatchErrorCallback(cb BatchErrorCallback) {
w.errorCb = cb
}
// HandleWrite handles writes of batches and handles retrying.
// Retrying is triggered by new writes, there is no scheduler.
// It first checks retry queue, because it has the highest priority.
// If there are some batches in retry queue, those are written and incoming batch is added to end of retry queue.
// Immediate write is allowed only in case there was success or not retryable error.
// Otherwise, delay is checked based on recent batch.
// If write of batch fails with retryable error (connection errors and HTTP code >= 429),
// Batch retry time is calculated based on #of attempts.
// If writes continues failing and # of attempts reaches maximum or total retry time reaches maxRetryTime,
// batch is discarded.
func (w *Service) HandleWrite(ctx context.Context, batch *Batch) error {
log.Debug("Write proc: received write request")
batchToWrite := batch
retrying := false
for {
select {
case <-ctx.Done():
log.Debug("Write proc: ctx cancelled req")
return ctx.Err()
default:
}
if !w.retryQueue.isEmpty() {
log.Debug("Write proc: taking batch from retry queue")
if !retrying {
b := w.retryQueue.first()
// Discard batches at beginning of retryQueue that have already expired
if time.Now().After(b.Expires) {
log.Error("Write proc: oldest batch in retry queue expired, discarding")
if !b.Evicted {
w.retryQueue.pop()
}
continue
}
// Can we write? In case of retryable error we must wait a bit
if w.lastWriteAttempt.IsZero() || time.Now().After(w.lastWriteAttempt.Add(time.Millisecond*time.Duration(w.retryDelay))) {
retrying = true
} else {
log.Warn("Write proc: cannot write yet, storing batch to queue")
if w.retryQueue.push(batch) {
log.Error("Write proc: Retry buffer full, discarding oldest batch")
}
batchToWrite = nil
}
}
if retrying {
batchToWrite = w.retryQueue.first()
if batch != nil { //store actual batch to retry queue
if w.retryQueue.push(batch) {
log.Error("Write proc: Retry buffer full, discarding oldest batch")
}
batch = nil
}
}
}
// write batch
if batchToWrite != nil {
perror := w.WriteBatch(ctx, batchToWrite)
if perror != nil {
if isIgnorableError(perror) {
log.Warnf("Write error: %s", perror.Error())
} else {
if w.writeOptions.MaxRetries() != 0 && (perror.StatusCode == 0 || perror.StatusCode >= http.StatusTooManyRequests) {
log.Errorf("Write error: %s, batch kept for retrying\n", perror.Error())
if perror.RetryAfter > 0 {
w.retryDelay = perror.RetryAfter * 1000
} else {
w.retryDelay = w.computeRetryDelay(w.retryAttempts)
}
if w.errorCb != nil && !w.errorCb(batchToWrite, *perror) {
log.Error("Callback rejected batch, discarding")
if !batchToWrite.Evicted {
w.retryQueue.pop()
}
return perror
}
// store new batch (not taken from queue)
if !batchToWrite.Evicted && batchToWrite != w.retryQueue.first() {
if w.retryQueue.push(batch) {
log.Error("Retry buffer full, discarding oldest batch")
}
} else if batchToWrite.RetryAttempts == w.writeOptions.MaxRetries() {
log.Error("Reached maximum number of retries, discarding batch")
if !batchToWrite.Evicted {
w.retryQueue.pop()
}
}
batchToWrite.RetryAttempts++
w.retryAttempts++
log.Debugf("Write proc: next wait for write is %dms\n", w.retryDelay)
} else {
log.Errorf("Write error: %s\n", perror.Error())
}
return fmt.Errorf("write failed (attempts %d): %w", batchToWrite.RetryAttempts, perror)
}
}
w.retryDelay = w.writeOptions.RetryInterval()
w.retryAttempts = 0
if retrying && !batchToWrite.Evicted {
w.retryQueue.pop()
}
batchToWrite = nil
} else {
break
}
}
return nil
}
// Non-retryable errors
const (
errStringHintedHandoffNotEmpty = "hinted handoff queue not empty"
errStringPartialWrite = "partial write"
errStringPointsBeyondRP = "points beyond retention policy"
errStringUnableToParse = "unable to parse"
)
func isIgnorableError(error *http2.Error) bool {
// This "error" is an informational message about the state of the
// InfluxDB cluster.
if strings.Contains(error.Message, errStringHintedHandoffNotEmpty) {
return true
}
// Points beyond retention policy is returned when points are immediately
// discarded for being older than the retention policy. Usually this not
// a cause for concern, and we don't want to retry.
if strings.Contains(error.Message, errStringPointsBeyondRP) {
return true
}
// Other partial write errors, such as "field type conflict", are not
// correctable at this point and so the point is dropped instead of
// retrying.
if strings.Contains(error.Message, errStringPartialWrite) {
return true
}
// This error indicates an error in line protocol
// serialization, retries would not be successful.
if strings.Contains(error.Message, errStringUnableToParse) {
return true
}
return false
}
// computeRetryDelay calculates retry delay.
// Retry delay is calculated as random value within the interval
// [retry_interval * exponential_base^(attempts) and retry_interval * exponential_base^(attempts+1)]
func (w *Service) computeRetryDelay(attempts uint) uint {
minDelay := int(w.writeOptions.RetryInterval() * pow(w.writeOptions.ExponentialBase(), attempts))
maxDelay := int(w.writeOptions.RetryInterval() * pow(w.writeOptions.ExponentialBase(), attempts+1))
diff := maxDelay - minDelay
if diff <= 0 { //check overflows
return w.writeOptions.MaxRetryInterval()
}
retryDelay := uint(rand.Intn(diff) + minDelay)
if retryDelay > w.writeOptions.MaxRetryInterval() {
retryDelay = w.writeOptions.MaxRetryInterval()
}
return retryDelay
}
// pow computes x**y
func pow(x, y uint) uint {
p := uint(1)
if y == 0 {
return 1
}
for i := uint(1); i <= y; i++ {
p = p * x
}
return p
}
// WriteBatch performs actual writing via HTTP service
func (w *Service) WriteBatch(ctx context.Context, batch *Batch) *http2.Error {
var body io.Reader
var err error
body = strings.NewReader(batch.Batch)
if log.Level() >= ilog.DebugLevel {
log.Debugf("Writing batch: %s", batch.Batch)
}
if w.writeOptions.UseGZip() {
body, err = gzip.CompressWithGzip(body)
if err != nil {
return http2.NewError(err)
}
}
w.lock.Lock()
w.lastWriteAttempt = time.Now()
w.lock.Unlock()
perror := w.httpService.DoPostRequest(ctx, w.url, body, func(req *http.Request) {
if w.writeOptions.UseGZip() {
req.Header.Set("Content-Encoding", "gzip")
}
}, func(r *http.Response) error {
return r.Body.Close()
})
return perror
}
// Flush sends batches from retry queue immediately, without retrying
func (w *Service) Flush() {
for !w.retryQueue.isEmpty() {
b := w.retryQueue.pop()
if time.Now().After(b.Expires) {
log.Error("Oldest batch in retry queue expired, discarding")
continue
}
if err := w.WriteBatch(context.Background(), b); err != nil {
log.Errorf("Error flushing batch from retry queue: %w", err.Unwrap())
}
}
}
// pointWithDefaultTags encapsulates Point with default tags
type pointWithDefaultTags struct {
point *write.Point
defaultTags map[string]string
}
// Name returns the name of measurement of a point.
func (p *pointWithDefaultTags) Name() string {
return p.point.Name()
}
// Time is the timestamp of a Point.
func (p *pointWithDefaultTags) Time() time.Time {
return p.point.Time()
}
// FieldList returns a slice containing the fields of a Point.
func (p *pointWithDefaultTags) FieldList() []*lp.Field {
return p.point.FieldList()
}
// TagList returns tags from point along with default tags
// If point of tag can override default tag
func (p *pointWithDefaultTags) TagList() []*lp.Tag {
tags := make([]*lp.Tag, 0, len(p.point.TagList())+len(p.defaultTags))
tags = append(tags, p.point.TagList()...)
for k, v := range p.defaultTags {
if !existTag(p.point.TagList(), k) {
tags = append(tags, &lp.Tag{
Key: k,
Value: v,
})
}
}
sort.Slice(tags, func(i, j int) bool { return tags[i].Key < tags[j].Key })
return tags
}
func existTag(tags []*lp.Tag, key string) bool {
for _, tag := range tags {
if key == tag.Key {
return true
}
}
return false
}
// EncodePoints creates line protocol string from points
func (w *Service) EncodePoints(points ...*write.Point) (string, error) {
var buffer bytes.Buffer
e := lp.NewEncoder(&buffer)
e.SetFieldTypeSupport(lp.UintSupport)
e.FailOnFieldErr(true)
e.SetPrecision(w.writeOptions.Precision())
for _, point := range points {
_, err := e.Encode(w.pointToEncode(point))
if err != nil {
return "", err
}
}
return buffer.String(), nil
}
// pointToEncode determines whether default tags should be applied
// and returns point with default tags instead of point
func (w *Service) pointToEncode(point *write.Point) lp.Metric {
var m lp.Metric
if len(w.writeOptions.DefaultTags()) > 0 {
m = &pointWithDefaultTags{
point: point,
defaultTags: w.writeOptions.DefaultTags(),
}
} else {
m = point
}
return m
}
// WriteURL returns current write URL
func (w *Service) WriteURL() string {
return w.url
}
func precisionToString(precision time.Duration) string {
prec := "ns"
switch precision {
case time.Microsecond:
prec = "us"
case time.Millisecond:
prec = "ms"
case time.Second:
prec = "s"
}
return prec
}
+133
View File
@@ -0,0 +1,133 @@
// Copyright 2020-2021 InfluxData, Inc. All rights reserved.
// Use of this source code is governed by MIT
// license that can be found in the LICENSE file.
// Package log defines Logging API.
// The global Log variable contains the actual logger. Set it to own implementation to override logging. Set it to nil to disable logging
package log
import (
"fmt"
"log"
"sync"
)
// Log is the library wide logger. Setting to nil disables logging.
var Log Logger = &logger{logLevel: ErrorLevel, prefix: "influxdb2client"}
// Log levels
const (
ErrorLevel uint = iota
WarningLevel
InfoLevel
DebugLevel
)
// Logger defines interface for logging
type Logger interface {
// Writes formatted debug message if debug logLevel is enabled.
Debugf(format string, v ...interface{})
// Writes debug message if debug is enabled.
Debug(msg string)
// Writes formatted info message if info logLevel is enabled.
Infof(format string, v ...interface{})
// Writes info message if info logLevel is enabled
Info(msg string)
// Writes formatted warning message if warning logLevel is enabled.
Warnf(format string, v ...interface{})
// Writes warning message if warning logLevel is enabled.
Warn(msg string)
// Writes formatted error message
Errorf(format string, v ...interface{})
// Writes error message
Error(msg string)
// SetLogLevel sets allowed logging level.
SetLogLevel(logLevel uint)
// LogLevel retrieves current logging level
LogLevel() uint
// SetPrefix sets logging prefix.
SetPrefix(prefix string)
}
// logger provides default implementation for Logger. It logs using Go log API
// mutex is needed in cases when multiple clients run concurrently
type logger struct {
prefix string
logLevel uint
lock sync.Mutex
}
func (l *logger) SetLogLevel(logLevel uint) {
l.lock.Lock()
defer l.lock.Unlock()
l.logLevel = logLevel
}
func (l *logger) LogLevel() uint {
l.lock.Lock()
defer l.lock.Unlock()
return l.logLevel
}
func (l *logger) SetPrefix(prefix string) {
l.lock.Lock()
defer l.lock.Unlock()
l.prefix = prefix
}
func (l *logger) Debugf(format string, v ...interface{}) {
l.lock.Lock()
defer l.lock.Unlock()
if l.logLevel >= DebugLevel {
log.Print(l.prefix, " D! ", fmt.Sprintf(format, v...))
}
}
func (l *logger) Debug(msg string) {
l.lock.Lock()
defer l.lock.Unlock()
if l.logLevel >= DebugLevel {
log.Print(l.prefix, " D! ", msg)
}
}
func (l *logger) Infof(format string, v ...interface{}) {
l.lock.Lock()
defer l.lock.Unlock()
if l.logLevel >= InfoLevel {
log.Print(l.prefix, " I! ", fmt.Sprintf(format, v...))
}
}
func (l *logger) Info(msg string) {
l.lock.Lock()
defer l.lock.Unlock()
if l.logLevel >= DebugLevel {
log.Print(l.prefix, " I! ", msg)
}
}
func (l *logger) Warnf(format string, v ...interface{}) {
l.lock.Lock()
defer l.lock.Unlock()
if l.logLevel >= WarningLevel {
log.Print(l.prefix, " W! ", fmt.Sprintf(format, v...))
}
}
func (l *logger) Warn(msg string) {
l.lock.Lock()
defer l.lock.Unlock()
if l.logLevel >= WarningLevel {
log.Print(l.prefix, " W! ", msg)
}
}
func (l *logger) Errorf(format string, v ...interface{}) {
l.lock.Lock()
defer l.lock.Unlock()
log.Print(l.prefix, " E! ", fmt.Sprintf(format, v...))
}
func (l *logger) Error(msg string) {
l.lock.Lock()
defer l.lock.Unlock()
log.Print(l.prefix, " E! ", msg)
}
+236
View File
@@ -0,0 +1,236 @@
// Copyright 2020-2021 InfluxData, Inc. All rights reserved.
// Use of this source code is governed by MIT
// license that can be found in the LICENSE file.
package influxdb2
import (
"crypto/tls"
nethttp "net/http"
"time"
"github.com/influxdata/influxdb-client-go/v2/api/http"
"github.com/influxdata/influxdb-client-go/v2/api/write"
)
// Options holds configuration properties for communicating with InfluxDB server
type Options struct {
// LogLevel to filter log messages. Each level mean to log all categories bellow. 0 error, 1 - warning, 2 - info, 3 - debug
logLevel uint
// Writing options
writeOptions *write.Options
// Http options
httpOptions *http.Options
}
// BatchSize returns size of batch
func (o *Options) BatchSize() uint {
return o.WriteOptions().BatchSize()
}
// SetBatchSize sets number of points sent in single request
func (o *Options) SetBatchSize(batchSize uint) *Options {
o.WriteOptions().SetBatchSize(batchSize)
return o
}
// FlushInterval returns flush interval in ms
func (o *Options) FlushInterval() uint {
return o.WriteOptions().FlushInterval()
}
// SetFlushInterval sets flush interval in ms in which is buffer flushed if it has not been already written
func (o *Options) SetFlushInterval(flushIntervalMs uint) *Options {
o.WriteOptions().SetFlushInterval(flushIntervalMs)
return o
}
// RetryInterval returns the retry interval in ms
func (o *Options) RetryInterval() uint {
return o.WriteOptions().RetryInterval()
}
// SetRetryInterval sets retry interval in ms, which is set if not sent by server
func (o *Options) SetRetryInterval(retryIntervalMs uint) *Options {
o.WriteOptions().SetRetryInterval(retryIntervalMs)
return o
}
// MaxRetries returns maximum count of retry attempts of failed writes, default 5.
func (o *Options) MaxRetries() uint {
return o.WriteOptions().MaxRetries()
}
// SetMaxRetries sets maximum count of retry attempts of failed writes.
// Setting zero value disables retry strategy.
func (o *Options) SetMaxRetries(maxRetries uint) *Options {
o.WriteOptions().SetMaxRetries(maxRetries)
return o
}
// RetryBufferLimit returns retry buffer limit
func (o *Options) RetryBufferLimit() uint {
return o.WriteOptions().RetryBufferLimit()
}
// SetRetryBufferLimit sets maximum number of points to keep for retry. Should be multiple of BatchSize.
func (o *Options) SetRetryBufferLimit(retryBufferLimit uint) *Options {
o.WriteOptions().SetRetryBufferLimit(retryBufferLimit)
return o
}
// MaxRetryInterval returns the maximum delay between each retry attempt in milliseconds, default 125,000.
func (o *Options) MaxRetryInterval() uint {
return o.WriteOptions().MaxRetryInterval()
}
// SetMaxRetryInterval sets the maximum delay between each retry attempt in millisecond.
func (o *Options) SetMaxRetryInterval(maxRetryIntervalMs uint) *Options {
o.WriteOptions().SetMaxRetryInterval(maxRetryIntervalMs)
return o
}
// MaxRetryTime returns the maximum total retry timeout in millisecond, default 180,000.
func (o *Options) MaxRetryTime() uint {
return o.WriteOptions().MaxRetryTime()
}
// SetMaxRetryTime sets the maximum total retry timeout in millisecond.
func (o *Options) SetMaxRetryTime(maxRetryTimeMs uint) *Options {
o.WriteOptions().SetMaxRetryTime(maxRetryTimeMs)
return o
}
// ExponentialBase returns the base for the exponential retry delay. Default 2.
func (o *Options) ExponentialBase() uint {
return o.WriteOptions().ExponentialBase()
}
// SetExponentialBase sets the base for the exponential retry delay.
func (o *Options) SetExponentialBase(exponentialBase uint) *Options {
o.WriteOptions().SetExponentialBase(exponentialBase)
return o
}
// LogLevel returns log level
func (o *Options) LogLevel() uint {
return o.logLevel
}
// SetLogLevel set level to filter log messages. Each level mean to log all categories bellow. Default is ErrorLevel.
// There are four level constant int the log package in this library:
// - ErrorLevel
// - WarningLevel
// - InfoLevel
// - DebugLevel
// The DebugLevel will print also content of writen batches, queries.
// The InfoLevel prints HTTP requests info, among others.
// Set log.Log to nil in order to completely disable logging.
func (o *Options) SetLogLevel(logLevel uint) *Options {
o.logLevel = logLevel
return o
}
// Precision returns time precision for writes
func (o *Options) Precision() time.Duration {
return o.WriteOptions().Precision()
}
// SetPrecision sets time precision to use in writes for timestamp. In unit of duration: time.Nanosecond, time.Microsecond, time.Millisecond, time.Second
func (o *Options) SetPrecision(precision time.Duration) *Options {
o.WriteOptions().SetPrecision(precision)
return o
}
// UseGZip returns true if write request are gzip`ed
func (o *Options) UseGZip() bool {
return o.WriteOptions().UseGZip()
}
// SetUseGZip specifies whether to use GZip compression in write requests.
func (o *Options) SetUseGZip(useGZip bool) *Options {
o.WriteOptions().SetUseGZip(useGZip)
return o
}
// HTTPClient returns the http.Client that is configured to be used
// for HTTP requests. It will return the one that has been set using
// SetHTTPClient or it will construct a default client using the
// other configured options.
func (o *Options) HTTPClient() *nethttp.Client {
return o.httpOptions.HTTPClient()
}
// SetHTTPClient will configure the http.Client that is used
// for HTTP requests. If set to nil, an HTTPClient will be
// generated.
//
// Setting the HTTPClient will cause the other HTTP options
// to be ignored.
// In case of UsersAPI.SignIn() is used, HTTPClient.Jar will be used for storing session cookie.
func (o *Options) SetHTTPClient(c *nethttp.Client) *Options {
o.httpOptions.SetHTTPClient(c)
return o
}
// TLSConfig returns TLS config
func (o *Options) TLSConfig() *tls.Config {
return o.HTTPOptions().TLSConfig()
}
// SetTLSConfig sets TLS configuration for secure connection
func (o *Options) SetTLSConfig(tlsConfig *tls.Config) *Options {
o.HTTPOptions().SetTLSConfig(tlsConfig)
return o
}
// HTTPRequestTimeout returns HTTP request timeout
func (o *Options) HTTPRequestTimeout() uint {
return o.HTTPOptions().HTTPRequestTimeout()
}
// SetHTTPRequestTimeout sets HTTP request timeout in sec
func (o *Options) SetHTTPRequestTimeout(httpRequestTimeout uint) *Options {
o.HTTPOptions().SetHTTPRequestTimeout(httpRequestTimeout)
return o
}
// WriteOptions returns write related options
func (o *Options) WriteOptions() *write.Options {
if o.writeOptions == nil {
o.writeOptions = write.DefaultOptions()
}
return o.writeOptions
}
// HTTPOptions returns HTTP related options
func (o *Options) HTTPOptions() *http.Options {
if o.httpOptions == nil {
o.httpOptions = http.DefaultOptions()
}
return o.httpOptions
}
// AddDefaultTag adds a default tag. DefaultTags are added to each written point.
// If a tag with the same key already exist it is overwritten.
// If a point already defines such a tag, it is left unchanged
func (o *Options) AddDefaultTag(key, value string) *Options {
o.WriteOptions().AddDefaultTag(key, value)
return o
}
// ApplicationName returns application name used in the User-Agent HTTP header
func (o *Options) ApplicationName() string {
return o.HTTPOptions().ApplicationName()
}
// SetApplicationName sets an application name to the User-Agent HTTP header
func (o *Options) SetApplicationName(appName string) *Options {
o.HTTPOptions().SetApplicationName(appName)
return o
}
// DefaultOptions returns Options object with default values
func DefaultOptions() *Options {
return &Options{logLevel: 0, writeOptions: write.DefaultOptions(), httpOptions: http.DefaultOptions()}
}
+21
View File
@@ -0,0 +1,21 @@
// Copyright 2020-2021 InfluxData, Inc. All rights reserved.
// Use of this source code is governed by MIT
// license that can be found in the LICENSE file.
package influxdb2
import (
"fmt"
"runtime"
"github.com/influxdata/influxdb-client-go/v2/internal/http"
)
const (
// Version defines current version
Version = "2.12.2"
)
func init() {
http.UserAgentBase = fmt.Sprintf("influxdb-client-go/%s (%s; %s)", Version, runtime.GOOS, runtime.GOARCH)
}
-763
View File
@@ -1,763 +0,0 @@
// Package client (v2) is the current official Go client for InfluxDB.
package client // import "github.com/influxdata/influxdb/client/v2"
import (
"bytes"
"context"
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"io"
"mime"
"net"
"net/http"
"net/url"
"path"
"strconv"
"strings"
"time"
"github.com/influxdata/influxdb/models"
)
// HTTPConfig is the config data needed to create an HTTP Client.
type HTTPConfig struct {
// Addr should be of the form "http://host:port"
// or "http://[ipv6-host%zone]:port".
Addr string
// Username is the influxdb username, optional.
Username string
// Password is the influxdb password, optional.
Password string
// UserAgent is the http User Agent, defaults to "InfluxDBClient".
UserAgent string
// Timeout for influxdb writes, defaults to no timeout.
Timeout time.Duration
// InsecureSkipVerify gets passed to the http client, if true, it will
// skip https certificate verification. Defaults to false.
InsecureSkipVerify bool
// TLSConfig allows the user to set their own TLS config for the HTTP
// Client. If set, this option overrides InsecureSkipVerify.
TLSConfig *tls.Config
// Proxy configures the Proxy function on the HTTP client.
Proxy func(req *http.Request) (*url.URL, error)
// DialContext specifies the dial function for creating unencrypted TCP connections.
// If DialContext is nil then the transport dials using package net.
DialContext func(ctx context.Context, network, addr string) (net.Conn, error)
}
// BatchPointsConfig is the config data needed to create an instance of the BatchPoints struct.
type BatchPointsConfig struct {
// Precision is the write precision of the points, defaults to "ns".
Precision string
// Database is the database to write points to.
Database string
// RetentionPolicy is the retention policy of the points.
RetentionPolicy string
// Write consistency is the number of servers required to confirm write.
WriteConsistency string
}
// Client is a client interface for writing & querying the database.
type Client interface {
// Ping checks that status of cluster, and will always return 0 time and no
// error for UDP clients.
Ping(timeout time.Duration) (time.Duration, string, error)
// Write takes a BatchPoints object and writes all Points to InfluxDB.
Write(bp BatchPoints) error
// WriteCtx takes a BatchPoints object and writes all Points to InfluxDB.
WriteCtx(ctx context.Context, bp BatchPoints) error
// Query makes an InfluxDB Query on the database. This will fail if using
// the UDP client.
Query(q Query) (*Response, error)
// QueryCtx makes an InfluxDB Query on the database. This will fail if using
// the UDP client.
QueryCtx(ctx context.Context, q Query) (*Response, error)
// QueryAsChunk makes an InfluxDB Query on the database. This will fail if using
// the UDP client.
QueryAsChunk(q Query) (*ChunkedResponse, error)
// Close releases any resources a Client may be using.
Close() error
}
// For added performance users may want to send pre-serialized points.
type HTTPClient interface {
Client
WriteRawCtx(ctx context.Context, bp BatchPoints, reqBody io.Reader) error
}
// NewHTTPClient returns a new Client from the provided config.
// Client is safe for concurrent use by multiple goroutines.
func NewHTTPClient(conf HTTPConfig) (HTTPClient, error) {
if conf.UserAgent == "" {
conf.UserAgent = "InfluxDBClient"
}
u, err := url.Parse(conf.Addr)
if err != nil {
return nil, err
} else if u.Scheme != "http" && u.Scheme != "https" {
m := fmt.Sprintf("Unsupported protocol scheme: %s, your address"+
" must start with http:// or https://", u.Scheme)
return nil, errors.New(m)
}
tr := &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: conf.InsecureSkipVerify,
},
Proxy: conf.Proxy,
DialContext: conf.DialContext,
}
if conf.TLSConfig != nil {
tr.TLSClientConfig = conf.TLSConfig
// Make sure to preserve the InsecureSkipVerify setting from the config.
tr.TLSClientConfig.InsecureSkipVerify = conf.InsecureSkipVerify
}
return &client{
url: *u,
username: conf.Username,
password: conf.Password,
useragent: conf.UserAgent,
httpClient: &http.Client{
Timeout: conf.Timeout,
Transport: tr,
},
transport: tr,
}, nil
}
// Ping will check to see if the server is up with an optional timeout on waiting for leader.
// Ping returns how long the request took, the version of the server it connected to, and an error if one occurred.
func (c *client) Ping(timeout time.Duration) (time.Duration, string, error) {
now := time.Now()
u := c.url
u.Path = path.Join(u.Path, "ping")
req, err := http.NewRequest("GET", u.String(), nil)
if err != nil {
return 0, "", err
}
req.Header.Set("User-Agent", c.useragent)
if c.username != "" {
req.SetBasicAuth(c.username, c.password)
}
if timeout > 0 {
params := req.URL.Query()
params.Set("wait_for_leader", fmt.Sprintf("%.0fs", timeout.Seconds()))
req.URL.RawQuery = params.Encode()
}
resp, err := c.httpClient.Do(req)
if err != nil {
return 0, "", err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return 0, "", err
}
if resp.StatusCode != http.StatusNoContent {
var err = errors.New(string(body))
return 0, "", err
}
version := resp.Header.Get("X-Influxdb-Version")
return time.Since(now), version, nil
}
// Close releases the client's resources.
func (c *client) Close() error {
c.transport.CloseIdleConnections()
return nil
}
// client is safe for concurrent use as the fields are all read-only
// once the client is instantiated.
type client struct {
// N.B - if url.UserInfo is accessed in future modifications to the
// methods on client, you will need to synchronize access to url.
url url.URL
username string
password string
useragent string
httpClient *http.Client
transport *http.Transport
}
// BatchPoints is an interface into a batched grouping of points to write into
// InfluxDB together. BatchPoints is NOT thread-safe, you must create a separate
// batch for each goroutine.
type BatchPoints interface {
// AddPoint adds the given point to the Batch of points.
AddPoint(p *Point)
// AddPoints adds the given points to the Batch of points.
AddPoints(ps []*Point)
// Points lists the points in the Batch.
Points() []*Point
// Precision returns the currently set precision of this Batch.
Precision() string
// SetPrecision sets the precision of this batch.
SetPrecision(s string) error
// Database returns the currently set database of this Batch.
Database() string
// SetDatabase sets the database of this Batch.
SetDatabase(s string)
// WriteConsistency returns the currently set write consistency of this Batch.
WriteConsistency() string
// SetWriteConsistency sets the write consistency of this Batch.
SetWriteConsistency(s string)
// RetentionPolicy returns the currently set retention policy of this Batch.
RetentionPolicy() string
// SetRetentionPolicy sets the retention policy of this Batch.
SetRetentionPolicy(s string)
}
// NewBatchPoints returns a BatchPoints interface based on the given config.
func NewBatchPoints(conf BatchPointsConfig) (BatchPoints, error) {
if conf.Precision == "" {
conf.Precision = "ns"
}
if _, err := time.ParseDuration("1" + conf.Precision); err != nil {
return nil, err
}
bp := &batchpoints{
database: conf.Database,
precision: conf.Precision,
retentionPolicy: conf.RetentionPolicy,
writeConsistency: conf.WriteConsistency,
}
return bp, nil
}
type batchpoints struct {
points []*Point
database string
precision string
retentionPolicy string
writeConsistency string
}
func (bp *batchpoints) AddPoint(p *Point) {
bp.points = append(bp.points, p)
}
func (bp *batchpoints) AddPoints(ps []*Point) {
bp.points = append(bp.points, ps...)
}
func (bp *batchpoints) Points() []*Point {
return bp.points
}
func (bp *batchpoints) Precision() string {
return bp.precision
}
func (bp *batchpoints) Database() string {
return bp.database
}
func (bp *batchpoints) WriteConsistency() string {
return bp.writeConsistency
}
func (bp *batchpoints) RetentionPolicy() string {
return bp.retentionPolicy
}
func (bp *batchpoints) SetPrecision(p string) error {
if _, err := time.ParseDuration("1" + p); err != nil {
return err
}
bp.precision = p
return nil
}
func (bp *batchpoints) SetDatabase(db string) {
bp.database = db
}
func (bp *batchpoints) SetWriteConsistency(wc string) {
bp.writeConsistency = wc
}
func (bp *batchpoints) SetRetentionPolicy(rp string) {
bp.retentionPolicy = rp
}
// Point represents a single data point.
type Point struct {
pt models.Point
}
// NewPoint returns a point with the given timestamp. If a timestamp is not
// given, then data is sent to the database without a timestamp, in which case
// the server will assign local time upon reception. NOTE: it is recommended to
// send data with a timestamp.
func NewPoint(
name string,
tags map[string]string,
fields map[string]interface{},
t ...time.Time,
) (*Point, error) {
var T time.Time
if len(t) > 0 {
T = t[0]
}
pt, err := models.NewPoint(name, models.NewTags(tags), fields, T)
if err != nil {
return nil, err
}
return &Point{
pt: pt,
}, nil
}
// String returns a line-protocol string of the Point.
func (p *Point) String() string {
return p.pt.String()
}
// PrecisionString returns a line-protocol string of the Point,
// with the timestamp formatted for the given precision.
func (p *Point) PrecisionString(precision string) string {
return p.pt.PrecisionString(precision)
}
// Name returns the measurement name of the point.
func (p *Point) Name() string {
return string(p.pt.Name())
}
// Tags returns the tags associated with the point.
func (p *Point) Tags() map[string]string {
return p.pt.Tags().Map()
}
// Time return the timestamp for the point.
func (p *Point) Time() time.Time {
return p.pt.Time()
}
// UnixNano returns timestamp of the point in nanoseconds since Unix epoch.
func (p *Point) UnixNano() int64 {
return p.pt.UnixNano()
}
// Fields returns the fields for the point.
func (p *Point) Fields() (map[string]interface{}, error) {
return p.pt.Fields()
}
// NewPointFrom returns a point from the provided models.Point.
func NewPointFrom(pt models.Point) *Point {
return &Point{pt: pt}
}
func (c *client) Write(bp BatchPoints) error {
return c.WriteCtx(context.Background(), bp)
}
func (c *client) WriteCtx(ctx context.Context, bp BatchPoints) error {
var b bytes.Buffer
for _, p := range bp.Points() {
if p == nil {
continue
}
if _, err := b.WriteString(p.pt.PrecisionString(bp.Precision())); err != nil {
return err
}
if err := b.WriteByte('\n'); err != nil {
return err
}
}
return c.WriteRawCtx(ctx, bp, &b)
}
// WriteRawCtx uses reqBody instead of parsing bp.Points. Metadata still comes from bp.
func (c *client) WriteRawCtx(ctx context.Context, bp BatchPoints, reqBody io.Reader) error {
u := c.url
u.Path = path.Join(u.Path, "write")
req, err := http.NewRequestWithContext(ctx, "POST", u.String(), reqBody)
if err != nil {
return err
}
req.Header.Set("Content-Type", "")
req.Header.Set("User-Agent", c.useragent)
if c.username != "" {
req.SetBasicAuth(c.username, c.password)
}
params := req.URL.Query()
params.Set("db", bp.Database())
params.Set("rp", bp.RetentionPolicy())
params.Set("precision", bp.Precision())
params.Set("consistency", bp.WriteConsistency())
req.URL.RawQuery = params.Encode()
resp, err := c.httpClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
if resp.StatusCode != http.StatusNoContent && resp.StatusCode != http.StatusOK {
var err = errors.New(string(body))
return err
}
return nil
}
// Query defines a query to send to the server.
type Query struct {
Command string
Database string
RetentionPolicy string
Precision string
Chunked bool
ChunkSize int
Parameters map[string]interface{}
}
// Params is a type alias to the query parameters.
type Params map[string]interface{}
// NewQuery returns a query object.
// The database and precision arguments can be empty strings if they are not needed for the query.
func NewQuery(command, database, precision string) Query {
return Query{
Command: command,
Database: database,
Precision: precision,
Parameters: make(map[string]interface{}),
}
}
// NewQueryWithRP returns a query object.
// The database, retention policy, and precision arguments can be empty strings if they are not needed
// for the query. Setting the retention policy only works on InfluxDB versions 1.6 or greater.
func NewQueryWithRP(command, database, retentionPolicy, precision string) Query {
return Query{
Command: command,
Database: database,
RetentionPolicy: retentionPolicy,
Precision: precision,
Parameters: make(map[string]interface{}),
}
}
// NewQueryWithParameters returns a query object.
// The database and precision arguments can be empty strings if they are not needed for the query.
// parameters is a map of the parameter names used in the command to their values.
func NewQueryWithParameters(command, database, precision string, parameters map[string]interface{}) Query {
return Query{
Command: command,
Database: database,
Precision: precision,
Parameters: parameters,
}
}
// Response represents a list of statement results.
type Response struct {
Results []Result
Err string `json:"error,omitempty"`
}
// Error returns the first error from any statement.
// It returns nil if no errors occurred on any statements.
func (r *Response) Error() error {
if r.Err != "" {
return errors.New(r.Err)
}
for _, result := range r.Results {
if result.Err != "" {
return errors.New(result.Err)
}
}
return nil
}
// Message represents a user message.
type Message struct {
Level string
Text string
}
// Result represents a resultset returned from a single statement.
type Result struct {
Series []models.Row
Messages []*Message
Err string `json:"error,omitempty"`
}
// Query sends a command to the server and returns the Response.
func (c *client) Query(q Query) (*Response, error) {
return c.QueryCtx(context.Background(), q)
}
// QueryCtx sends a command to the server and returns the Response.
func (c *client) QueryCtx(ctx context.Context, q Query) (*Response, error) {
req, err := c.createDefaultRequest(ctx, q)
if err != nil {
return nil, err
}
params := req.URL.Query()
if q.Chunked {
params.Set("chunked", "true")
if q.ChunkSize > 0 {
params.Set("chunk_size", strconv.Itoa(q.ChunkSize))
}
req.URL.RawQuery = params.Encode()
}
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if err := checkResponse(resp); err != nil {
return nil, err
}
var response Response
if q.Chunked {
cr := NewChunkedResponse(resp.Body)
for {
r, err := cr.NextResponse()
if err != nil {
if err == io.EOF {
break
}
// If we got an error while decoding the response, send that back.
return nil, err
}
if r == nil {
break
}
response.Results = append(response.Results, r.Results...)
if r.Err != "" {
response.Err = r.Err
break
}
}
} else {
dec := json.NewDecoder(resp.Body)
dec.UseNumber()
decErr := dec.Decode(&response)
// ignore this error if we got an invalid status code
if decErr != nil && decErr.Error() == "EOF" && resp.StatusCode != http.StatusOK {
decErr = nil
}
// If we got a valid decode error, send that back
if decErr != nil {
return nil, fmt.Errorf("unable to decode json: received status code %d err: %s", resp.StatusCode, decErr)
}
}
// If we don't have an error in our json response, and didn't get statusOK
// then send back an error
if resp.StatusCode != http.StatusOK && response.Error() == nil {
return &response, fmt.Errorf("received status code %d from server", resp.StatusCode)
}
return &response, nil
}
// QueryAsChunk sends a command to the server and returns the Response.
func (c *client) QueryAsChunk(q Query) (*ChunkedResponse, error) {
req, err := c.createDefaultRequest(context.Background(), q)
if err != nil {
return nil, err
}
params := req.URL.Query()
params.Set("chunked", "true")
if q.ChunkSize > 0 {
params.Set("chunk_size", strconv.Itoa(q.ChunkSize))
}
req.URL.RawQuery = params.Encode()
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, err
}
if err := checkResponse(resp); err != nil {
return nil, err
}
return NewChunkedResponse(resp.Body), nil
}
func checkResponse(resp *http.Response) error {
// If we lack a X-Influxdb-Version header, then we didn't get a response from influxdb
// but instead some other service. If the error code is also a 500+ code, then some
// downstream loadbalancer/proxy/etc had an issue and we should report that.
if resp.Header.Get("X-Influxdb-Version") == "" && resp.StatusCode >= http.StatusInternalServerError {
body, err := io.ReadAll(resp.Body)
if err != nil || len(body) == 0 {
return fmt.Errorf("received status code %d from downstream server", resp.StatusCode)
}
return fmt.Errorf("received status code %d from downstream server, with response body: %q", resp.StatusCode, body)
}
// If we get an unexpected content type, then it is also not from influx direct and therefore
// we want to know what we received and what status code was returned for debugging purposes.
if cType, _, _ := mime.ParseMediaType(resp.Header.Get("Content-Type")); cType != "application/json" {
// Read up to 1kb of the body to help identify downstream errors and limit the impact of things
// like downstream serving a large file
body, err := io.ReadAll(io.LimitReader(resp.Body, 1024))
if err != nil || len(body) == 0 {
return fmt.Errorf("expected json response, got empty body, with status: %v", resp.StatusCode)
}
return fmt.Errorf("expected json response, got %q, with status: %v and response body: %q", cType, resp.StatusCode, body)
}
return nil
}
func (c *client) createDefaultRequest(ctx context.Context, q Query) (*http.Request, error) {
u := c.url
u.Path = path.Join(u.Path, "query")
jsonParameters, err := json.Marshal(q.Parameters)
if err != nil {
return nil, err
}
req, err := http.NewRequest("POST", u.String(), nil)
if err != nil {
return nil, err
}
if ctx != nil {
req = req.WithContext(ctx)
}
req.Header.Set("Content-Type", "")
req.Header.Set("User-Agent", c.useragent)
if c.username != "" {
req.SetBasicAuth(c.username, c.password)
}
params := req.URL.Query()
params.Set("q", q.Command)
params.Set("db", q.Database)
if q.RetentionPolicy != "" {
params.Set("rp", q.RetentionPolicy)
}
params.Set("params", string(jsonParameters))
if q.Precision != "" {
params.Set("epoch", q.Precision)
}
req.URL.RawQuery = params.Encode()
return req, nil
}
// duplexReader reads responses and writes it to another writer while
// satisfying the reader interface.
type duplexReader struct {
r io.ReadCloser
w io.Writer
}
func (r *duplexReader) Read(p []byte) (n int, err error) {
n, err = r.r.Read(p)
if err == nil {
r.w.Write(p[:n])
}
return n, err
}
// Close closes the response.
func (r *duplexReader) Close() error {
return r.r.Close()
}
// ChunkedResponse represents a response from the server that
// uses chunking to stream the output.
type ChunkedResponse struct {
dec *json.Decoder
duplex *duplexReader
buf bytes.Buffer
}
// NewChunkedResponse reads a stream and produces responses from the stream.
func NewChunkedResponse(r io.Reader) *ChunkedResponse {
rc, ok := r.(io.ReadCloser)
if !ok {
rc = io.NopCloser(r)
}
resp := &ChunkedResponse{}
resp.duplex = &duplexReader{r: rc, w: &resp.buf}
resp.dec = json.NewDecoder(resp.duplex)
resp.dec.UseNumber()
return resp
}
// NextResponse reads the next line of the stream and returns a response.
func (r *ChunkedResponse) NextResponse() (*Response, error) {
var response Response
if err := r.dec.Decode(&response); err != nil {
if err == io.EOF {
return nil, err
}
// A decoding error happened. This probably means the server crashed
// and sent a last-ditch error message to us. Ensure we have read the
// entirety of the connection to get any remaining error text.
io.Copy(io.Discard, r.duplex)
return nil, errors.New(strings.TrimSpace(r.buf.String()))
}
r.buf.Reset()
return &response, nil
}
// Close closes the response.
func (r *ChunkedResponse) Close() error {
return r.duplex.Close()
}
-73
View File
@@ -1,73 +0,0 @@
package client
import (
"encoding/json"
"time"
)
type (
// Identifier is an identifier value.
Identifier string
// StringValue is a string literal.
StringValue string
// RegexValue is a regexp literal.
RegexValue string
// NumberValue is a number literal.
NumberValue float64
// IntegerValue is an integer literal.
IntegerValue int64
// BooleanValue is a boolean literal.
BooleanValue bool
// TimeValue is a time literal.
TimeValue time.Time
// DurationValue is a duration literal.
DurationValue time.Duration
)
func (v Identifier) MarshalJSON() ([]byte, error) {
m := map[string]string{"identifier": string(v)}
return json.Marshal(m)
}
func (v StringValue) MarshalJSON() ([]byte, error) {
m := map[string]string{"string": string(v)}
return json.Marshal(m)
}
func (v RegexValue) MarshalJSON() ([]byte, error) {
m := map[string]string{"regex": string(v)}
return json.Marshal(m)
}
func (v NumberValue) MarshalJSON() ([]byte, error) {
m := map[string]float64{"number": float64(v)}
return json.Marshal(m)
}
func (v IntegerValue) MarshalJSON() ([]byte, error) {
m := map[string]int64{"integer": int64(v)}
return json.Marshal(m)
}
func (v BooleanValue) MarshalJSON() ([]byte, error) {
m := map[string]bool{"boolean": bool(v)}
return json.Marshal(m)
}
func (v TimeValue) MarshalJSON() ([]byte, error) {
t := time.Time(v)
m := map[string]string{"string": t.Format(time.RFC3339Nano)}
return json.Marshal(m)
}
func (v DurationValue) MarshalJSON() ([]byte, error) {
m := map[string]int64{"duration": int64(v)}
return json.Marshal(m)
}
-125
View File
@@ -1,125 +0,0 @@
package client
import (
"context"
"fmt"
"io"
"net"
"time"
)
const (
// UDPPayloadSize is a reasonable default payload size for UDP packets that
// could be travelling over the internet.
UDPPayloadSize = 512
)
// UDPConfig is the config data needed to create a UDP Client.
type UDPConfig struct {
// Addr should be of the form "host:port"
// or "[ipv6-host%zone]:port".
Addr string
// PayloadSize is the maximum size of a UDP client message, optional
// Tune this based on your network. Defaults to UDPPayloadSize.
PayloadSize int
}
// NewUDPClient returns a client interface for writing to an InfluxDB UDP
// service from the given config.
func NewUDPClient(conf UDPConfig) (Client, error) {
var udpAddr *net.UDPAddr
udpAddr, err := net.ResolveUDPAddr("udp", conf.Addr)
if err != nil {
return nil, err
}
conn, err := net.DialUDP("udp", nil, udpAddr)
if err != nil {
return nil, err
}
payloadSize := conf.PayloadSize
if payloadSize == 0 {
payloadSize = UDPPayloadSize
}
return &udpclient{
conn: conn,
payloadSize: payloadSize,
}, nil
}
// Close releases the udpclient's resources.
func (uc *udpclient) Close() error {
return uc.conn.Close()
}
type udpclient struct {
conn io.WriteCloser
payloadSize int
}
func (uc *udpclient) Write(bp BatchPoints) error {
return uc.WriteCtx(context.Background(), bp)
}
func (uc *udpclient) WriteCtx(ctx context.Context, bp BatchPoints) error {
var b = make([]byte, 0, uc.payloadSize) // initial buffer size, it will grow as needed
var d, _ = time.ParseDuration("1" + bp.Precision())
var delayedError error
var checkBuffer = func(n int) {
if len(b) > 0 && len(b)+n > uc.payloadSize {
if _, err := uc.conn.Write(b); err != nil {
delayedError = err
}
b = b[:0]
}
}
for _, p := range bp.Points() {
p.pt.Round(d)
pointSize := p.pt.StringSize() + 1 // include newline in size
//point := p.pt.RoundedString(d) + "\n"
checkBuffer(pointSize)
if p.Time().IsZero() || pointSize <= uc.payloadSize {
b = p.pt.AppendString(b)
b = append(b, '\n')
continue
}
points := p.pt.Split(uc.payloadSize - 1) // account for newline character
for _, sp := range points {
checkBuffer(sp.StringSize() + 1)
b = sp.AppendString(b)
b = append(b, '\n')
}
}
if len(b) > 0 {
if _, err := uc.conn.Write(b); err != nil {
return err
}
}
return delayedError
}
func (uc *udpclient) Query(q Query) (*Response, error) {
return nil, fmt.Errorf("querying via UDP is not supported")
}
func (uc *udpclient) QueryCtx(ctx context.Context, q Query) (*Response, error) {
return nil, fmt.Errorf("querying via UDP is not supported")
}
func (uc *udpclient) QueryAsChunk(q Query) (*ChunkedResponse, error) {
return nil, fmt.Errorf("querying via UDP is not supported")
}
func (uc *udpclient) Ping(timeout time.Duration) (time.Duration, string, error) {
return 0, "", nil
}
-48
View File
@@ -1,48 +0,0 @@
package models
import (
"errors"
"strings"
)
// ConsistencyLevel represent a required replication criteria before a write can
// be returned as successful.
//
// The consistency level is handled in open-source InfluxDB but only applicable to clusters.
type ConsistencyLevel int
const (
// ConsistencyLevelAny allows for hinted handoff, potentially no write happened yet.
ConsistencyLevelAny ConsistencyLevel = iota
// ConsistencyLevelOne requires at least one data node acknowledged a write.
ConsistencyLevelOne
// ConsistencyLevelQuorum requires a quorum of data nodes to acknowledge a write.
ConsistencyLevelQuorum
// ConsistencyLevelAll requires all data nodes to acknowledge a write.
ConsistencyLevelAll
)
var (
// ErrInvalidConsistencyLevel is returned when parsing the string version
// of a consistency level.
ErrInvalidConsistencyLevel = errors.New("invalid consistency level")
)
// ParseConsistencyLevel converts a consistency level string to the corresponding ConsistencyLevel const.
func ParseConsistencyLevel(level string) (ConsistencyLevel, error) {
switch strings.ToLower(level) {
case "any":
return ConsistencyLevelAny, nil
case "one":
return ConsistencyLevelOne, nil
case "quorum":
return ConsistencyLevelQuorum, nil
case "all":
return ConsistencyLevelAll, nil
default:
return 0, ErrInvalidConsistencyLevel
}
}
-28
View File
@@ -1,28 +0,0 @@
// Code generated by "stringer -type=FieldType"; DO NOT EDIT.
package models
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[Integer-0]
_ = x[Float-1]
_ = x[Boolean-2]
_ = x[String-3]
_ = x[Empty-4]
_ = x[Unsigned-5]
}
const _FieldType_name = "IntegerFloatBooleanStringEmptyUnsigned"
var _FieldType_index = [...]uint8{0, 7, 12, 19, 25, 30, 38}
func (i FieldType) String() string {
if i < 0 || i >= FieldType(len(_FieldType_index)-1) {
return "FieldType(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _FieldType_name[_FieldType_index[i]:_FieldType_index[i+1]]
}
-3
View File
@@ -1,3 +0,0 @@
package models
//go:generate stringer -type=FieldType
-32
View File
@@ -1,32 +0,0 @@
package models // import "github.com/influxdata/influxdb/models"
// from stdlib hash/fnv/fnv.go
const (
prime64 = 1099511628211
offset64 = 14695981039346656037
)
// InlineFNV64a is an alloc-free port of the standard library's fnv64a.
// See https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function.
type InlineFNV64a uint64
// NewInlineFNV64a returns a new instance of InlineFNV64a.
func NewInlineFNV64a() InlineFNV64a {
return offset64
}
// Write adds data to the running hash.
func (s *InlineFNV64a) Write(data []byte) (int, error) {
hash := uint64(*s)
for _, c := range data {
hash ^= uint64(c)
hash *= prime64
}
*s = InlineFNV64a(hash)
return len(data), nil
}
// Sum64 returns the uint64 of the current resulting hash.
func (s *InlineFNV64a) Sum64() uint64 {
return uint64(*s)
}
-34
View File
@@ -1,34 +0,0 @@
package models // import "github.com/influxdata/influxdb/models"
import (
"strconv"
"unsafe"
)
// parseIntBytes is a zero-alloc wrapper around strconv.ParseInt.
func parseIntBytes(b []byte, base int, bitSize int) (i int64, err error) {
s := unsafeBytesToString(b)
return strconv.ParseInt(s, base, bitSize)
}
// parseUintBytes is a zero-alloc wrapper around strconv.ParseUint.
func parseUintBytes(b []byte, base int, bitSize int) (i uint64, err error) {
s := unsafeBytesToString(b)
return strconv.ParseUint(s, base, bitSize)
}
// parseFloatBytes is a zero-alloc wrapper around strconv.ParseFloat.
func parseFloatBytes(b []byte, bitSize int) (float64, error) {
s := unsafeBytesToString(b)
return strconv.ParseFloat(s, bitSize)
}
// parseBoolBytes is a zero-alloc wrapper around strconv.ParseBool.
func parseBoolBytes(b []byte) (bool, error) {
return strconv.ParseBool(unsafeBytesToString(b))
}
// unsafeBytesToString converts a []byte to a string without a heap allocation.
func unsafeBytesToString(in []byte) string {
return *(*string)(unsafe.Pointer(&in))
}
File diff suppressed because it is too large Load Diff
-62
View File
@@ -1,62 +0,0 @@
package models
import (
"sort"
)
// Row represents a single row returned from the execution of a statement.
type Row struct {
Name string `json:"name,omitempty"`
Tags map[string]string `json:"tags,omitempty"`
Columns []string `json:"columns,omitempty"`
Values [][]interface{} `json:"values,omitempty"`
Partial bool `json:"partial,omitempty"`
}
// SameSeries returns true if r contains values for the same series as o.
func (r *Row) SameSeries(o *Row) bool {
return r.tagsHash() == o.tagsHash() && r.Name == o.Name
}
// tagsHash returns a hash of tag key/value pairs.
func (r *Row) tagsHash() uint64 {
h := NewInlineFNV64a()
keys := r.tagsKeys()
for _, k := range keys {
h.Write([]byte(k))
h.Write([]byte(r.Tags[k]))
}
return h.Sum64()
}
// tagKeys returns a sorted list of tag keys.
func (r *Row) tagsKeys() []string {
a := make([]string, 0, len(r.Tags))
for k := range r.Tags {
a = append(a, k)
}
sort.Strings(a)
return a
}
// Rows represents a collection of rows. Rows implements sort.Interface.
type Rows []*Row
// Len implements sort.Interface.
func (p Rows) Len() int { return len(p) }
// Less implements sort.Interface.
func (p Rows) Less(i, j int) bool {
// Sort by name first.
if p[i].Name != p[j].Name {
return p[i].Name < p[j].Name
}
// Sort by tag set hash. Tags don't have a meaningful sort order so we
// just compute a hash and sort by that instead. This allows the tests
// to receive rows in a predictable order every time.
return p[i].tagsHash() < p[j].tagsHash()
}
// Swap implements sort.Interface.
func (p Rows) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
-42
View File
@@ -1,42 +0,0 @@
package models
// Statistic is the representation of a statistic used by the monitoring service.
type Statistic struct {
Name string `json:"name"`
Tags map[string]string `json:"tags"`
Values map[string]interface{} `json:"values"`
}
// NewStatistic returns an initialized Statistic.
func NewStatistic(name string) Statistic {
return Statistic{
Name: name,
Tags: make(map[string]string),
Values: make(map[string]interface{}),
}
}
// StatisticTags is a map that can be merged with others without causing
// mutations to either map.
type StatisticTags map[string]string
// Merge creates a new map containing the merged contents of tags and t.
// If both tags and the receiver map contain the same key, the value in tags
// is used in the resulting map.
//
// Merge always returns a usable map.
func (t StatisticTags) Merge(tags map[string]string) map[string]string {
// Add everything in tags to the result.
out := make(map[string]string, len(tags))
for k, v := range tags {
out[k] = v
}
// Only add values from t that don't appear in tags.
for k, v := range t {
if _, ok := tags[k]; !ok {
out[k] = v
}
}
return out
}
-156
View File
@@ -1,156 +0,0 @@
package models
import (
"bytes"
"strings"
)
// TagKeysSet provides set operations for combining Tags.
type TagKeysSet struct {
i int
keys [2][][]byte
tmp [][]byte
}
// Clear removes all the elements of TagKeysSet and ensures all internal
// buffers are reset.
func (set *TagKeysSet) Clear() {
set.clear(set.keys[0])
set.clear(set.keys[1])
set.clear(set.tmp)
set.i = 0
set.keys[0] = set.keys[0][:0]
}
func (set *TagKeysSet) clear(b [][]byte) {
b = b[:cap(b)]
for i := range b {
b[i] = nil
}
}
// KeysBytes returns the merged keys in lexicographical order.
// The slice is valid until the next call to UnionKeys, UnionBytes or Reset.
func (set *TagKeysSet) KeysBytes() [][]byte {
return set.keys[set.i&1]
}
// Keys returns a copy of the merged keys in lexicographical order.
func (set *TagKeysSet) Keys() []string {
keys := set.KeysBytes()
s := make([]string, 0, len(keys))
for i := range keys {
s = append(s, string(keys[i]))
}
return s
}
func (set *TagKeysSet) String() string {
var s []string
for _, k := range set.KeysBytes() {
s = append(s, string(k))
}
return strings.Join(s, ",")
}
// IsSupersetKeys returns true if the TagKeysSet is a superset of all the keys
// contained in other.
func (set *TagKeysSet) IsSupersetKeys(other Tags) bool {
keys := set.keys[set.i&1]
i, j := 0, 0
for i < len(keys) && j < len(other) {
if cmp := bytes.Compare(keys[i], other[j].Key); cmp > 0 {
return false
} else if cmp == 0 {
j++
}
i++
}
return j == len(other)
}
// IsSupersetBytes returns true if the TagKeysSet is a superset of all the keys
// in other.
// Other must be lexicographically sorted or the results are undefined.
func (set *TagKeysSet) IsSupersetBytes(other [][]byte) bool {
keys := set.keys[set.i&1]
i, j := 0, 0
for i < len(keys) && j < len(other) {
if cmp := bytes.Compare(keys[i], other[j]); cmp > 0 {
return false
} else if cmp == 0 {
j++
}
i++
}
return j == len(other)
}
// UnionKeys updates the set so that it is the union of itself and all the
// keys contained in other.
func (set *TagKeysSet) UnionKeys(other Tags) {
if set.IsSupersetKeys(other) {
return
}
if l := len(other); cap(set.tmp) < l {
set.tmp = make([][]byte, l)
} else {
set.tmp = set.tmp[:l]
}
for i := range other {
set.tmp[i] = other[i].Key
}
set.merge(set.tmp)
}
// UnionBytes updates the set so that it is the union of itself and all the
// keys contained in other.
// Other must be lexicographically sorted or the results are undefined.
func (set *TagKeysSet) UnionBytes(other [][]byte) {
if set.IsSupersetBytes(other) {
return
}
set.merge(other)
}
func (set *TagKeysSet) merge(in [][]byte) {
keys := set.keys[set.i&1]
l := len(keys) + len(in)
set.i = (set.i + 1) & 1
keya := set.keys[set.i&1]
if cap(keya) < l {
keya = make([][]byte, 0, l)
} else {
keya = keya[:0]
}
i, j := 0, 0
for i < len(keys) && j < len(in) {
ki, kj := keys[i], in[j]
if cmp := bytes.Compare(ki, kj); cmp < 0 {
i++
} else if cmp > 0 {
ki = kj
j++
} else {
i++
j++
}
keya = append(keya, ki)
}
if i < len(keys) {
keya = append(keya, keys[i:]...)
} else if j < len(in) {
keya = append(keya, in[j:]...)
}
set.keys[set.i&1] = keya
}
-74
View File
@@ -1,74 +0,0 @@
package models
// Helper time methods since parsing time can easily overflow and we only support a
// specific time range.
import (
"fmt"
"math"
"time"
)
const (
// MinNanoTime is the minimum time that can be represented.
//
// 1677-09-21 00:12:43.145224194 +0000 UTC
//
// The two lowest minimum integers are used as sentinel values. The
// minimum value needs to be used as a value lower than any other value for
// comparisons and another separate value is needed to act as a sentinel
// default value that is unusable by the user, but usable internally.
// Because these two values need to be used for a special purpose, we do
// not allow users to write points at these two times.
MinNanoTime = int64(math.MinInt64) + 2
// MaxNanoTime is the maximum time that can be represented.
//
// 2262-04-11 23:47:16.854775806 +0000 UTC
//
// The highest time represented by a nanosecond needs to be used for an
// exclusive range in the shard group, so the maximum time needs to be one
// less than the possible maximum number of nanoseconds representable by an
// int64 so that we don't lose a point at that one time.
MaxNanoTime = int64(math.MaxInt64) - 1
)
var (
minNanoTime = time.Unix(0, MinNanoTime).UTC()
maxNanoTime = time.Unix(0, MaxNanoTime).UTC()
// ErrTimeOutOfRange gets returned when time is out of the representable range using int64 nanoseconds since the epoch.
ErrTimeOutOfRange = fmt.Errorf("time outside range %d - %d", MinNanoTime, MaxNanoTime)
)
// SafeCalcTime safely calculates the time given. Will return error if the time is outside the
// supported range.
func SafeCalcTime(timestamp int64, precision string) (time.Time, error) {
mult := GetPrecisionMultiplier(precision)
if t, ok := safeSignedMult(timestamp, mult); ok {
tme := time.Unix(0, t).UTC()
return tme, CheckTime(tme)
}
return time.Time{}, ErrTimeOutOfRange
}
// CheckTime checks that a time is within the safe range.
func CheckTime(t time.Time) error {
if t.Before(minNanoTime) || t.After(maxNanoTime) {
return ErrTimeOutOfRange
}
return nil
}
// Perform the multiplication and check to make sure it didn't overflow.
func safeSignedMult(a, b int64) (int64, bool) {
if a == 0 || b == 0 || a == 1 || b == 1 {
return a * b, true
}
if a == MinNanoTime || b == MaxNanoTime {
return 0, false
}
c := a * b
return c, c/b == a
}
-7
View File
@@ -1,7 +0,0 @@
//go:build uint || uint64
package models
func init() {
EnableUintSupport()
}
-115
View File
@@ -1,115 +0,0 @@
// Package escape contains utilities for escaping parts of InfluxQL
// and InfluxDB line protocol.
package escape // import "github.com/influxdata/influxdb/pkg/escape"
import (
"bytes"
"strings"
)
// Codes is a map of bytes to be escaped.
var Codes = map[byte][]byte{
',': []byte(`\,`),
'"': []byte(`\"`),
' ': []byte(`\ `),
'=': []byte(`\=`),
}
// Bytes escapes characters on the input slice, as defined by Codes.
func Bytes(in []byte) []byte {
for b, esc := range Codes {
in = bytes.Replace(in, []byte{b}, esc, -1)
}
return in
}
const escapeChars = `," =`
// IsEscaped returns whether b has any escaped characters,
// i.e. whether b seems to have been processed by Bytes.
func IsEscaped(b []byte) bool {
for len(b) > 0 {
i := bytes.IndexByte(b, '\\')
if i < 0 {
return false
}
if i+1 < len(b) && strings.IndexByte(escapeChars, b[i+1]) >= 0 {
return true
}
b = b[i+1:]
}
return false
}
// AppendUnescaped appends the unescaped version of src to dst
// and returns the resulting slice.
func AppendUnescaped(dst, src []byte) []byte {
var pos int
for len(src) > 0 {
next := bytes.IndexByte(src[pos:], '\\')
if next < 0 || pos+next+1 >= len(src) {
return append(dst, src...)
}
if pos+next+1 < len(src) && strings.IndexByte(escapeChars, src[pos+next+1]) >= 0 {
if pos+next > 0 {
dst = append(dst, src[:pos+next]...)
}
src = src[pos+next+1:]
pos = 0
} else {
pos += next + 1
}
}
return dst
}
// Unescape returns a new slice containing the unescaped version of in.
func Unescape(in []byte) []byte {
if len(in) == 0 {
return nil
}
if bytes.IndexByte(in, '\\') == -1 {
return in
}
i := 0
inLen := len(in)
// The output size will be no more than inLen. Preallocating the
// capacity of the output is faster and uses less memory than
// letting append() do its own (over)allocation.
out := make([]byte, 0, inLen)
for {
if i >= inLen {
break
}
if in[i] == '\\' && i+1 < inLen {
switch in[i+1] {
case ',':
out = append(out, ',')
i += 2
continue
case '"':
out = append(out, '"')
i += 2
continue
case ' ':
out = append(out, ' ')
i += 2
continue
case '=':
out = append(out, '=')
i += 2
continue
}
}
out = append(out, in[i])
i += 1
}
return out
}
-21
View File
@@ -1,21 +0,0 @@
package escape
import "strings"
var (
escaper = strings.NewReplacer(`,`, `\,`, `"`, `\"`, ` `, `\ `, `=`, `\=`)
unescaper = strings.NewReplacer(`\,`, `,`, `\"`, `"`, `\ `, ` `, `\=`, `=`)
)
// UnescapeString returns unescaped version of in.
func UnescapeString(in string) string {
if strings.IndexByte(in, '\\') == -1 {
return in
}
return unescaper.Replace(in)
}
// String returns the escaped version of in.
func String(in string) string {
return escaper.Replace(in)
}
+5
View File
@@ -0,0 +1,5 @@
# Test binary, build with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
+22
View File
@@ -0,0 +1,22 @@
# line-protocol
This is an encoder for the influx [line protocol.](https://docs.influxdata.com/influxdb/latest/write_protocols/line_protocol_reference/)
It has an interface similar to the standard library's `json.Encoder`.
### some caveats.
- It is not concurrency-safe. If you want to make multiple calls to `Encoder.Encode` concurrently you have to manage the concurrency yourself.
- It can only encode values that are uint64, int64, int, float32, float64, string, or bool.
- Ints are converted to int64, float32's to float64.
- If UintSupport is not set, uint64s are converted to int64's and if they are larger than the max int64, they get truncated to the max int64 instead of overflowing.
### Example:
```go
buf := &bytes.Buffer{}
serializer := protocol.NewEncoder(buf)
serializer.SetMaxLineBytes(1024)
serializer.SetFieldTypeSupport(UintSupport)
serializer.Encode(e) // where e is something that implements the protocol.Metric interface
```
+299
View File
@@ -0,0 +1,299 @@
package protocol
import (
"fmt"
"io"
"math"
"sort"
"strconv"
"time"
)
// ErrIsNaN is a field error for when a float field is NaN.
var ErrIsNaN = &FieldError{"is NaN"}
// ErrIsInf is a field error for when a float field is Inf.
var ErrIsInf = &FieldError{"is Inf"}
// Encoder marshals Metrics into influxdb line protocol.
// It is not safe for concurrent use, make a new one!
// The default behavior when encountering a field error is to ignore the field and move on.
// If you wish it to error out on field errors, use Encoder.FailOnFieldErr(true)
type Encoder struct {
w io.Writer
fieldSortOrder FieldSortOrder
fieldTypeSupport FieldTypeSupport
failOnFieldError bool
maxLineBytes int
fieldList []*Field
header []byte
footer []byte
pair []byte
precision time.Duration
}
// SetMaxLineBytes sets a maximum length for a line, Encode will error if the generated line is longer
func (e *Encoder) SetMaxLineBytes(i int) {
e.maxLineBytes = i
}
// SetFieldSortOrder sets a sort order for the data.
// The options are:
// NoSortFields (doesn't sort the fields)
// SortFields (sorts the keys in alphabetical order)
func (e *Encoder) SetFieldSortOrder(s FieldSortOrder) {
e.fieldSortOrder = s
}
// SetFieldTypeSupport sets flags for if the encoder supports certain optional field types such as uint64
func (e *Encoder) SetFieldTypeSupport(s FieldTypeSupport) {
e.fieldTypeSupport = s
}
// FailOnFieldErr whether or not to fail on a field error or just move on.
// The default behavior to move on
func (e *Encoder) FailOnFieldErr(s bool) {
e.failOnFieldError = s
}
// SetPrecision sets time precision for writes
// Default is nanoseconds precision
func (e *Encoder) SetPrecision(p time.Duration) {
e.precision = p
}
// NewEncoder gives us an encoder that marshals to a writer in influxdb line protocol
// as defined by:
// https://docs.influxdata.com/influxdb/v1.5/write_protocols/line_protocol_reference/
func NewEncoder(w io.Writer) *Encoder {
return &Encoder{
w: w,
header: make([]byte, 0, 128),
footer: make([]byte, 0, 128),
pair: make([]byte, 0, 128),
fieldList: make([]*Field, 0, 16),
precision: time.Nanosecond,
}
}
// This is here to significantly reduce allocations, wish that we had constant/immutable keyword that applied to
// more complex objects
var comma = []byte(",")
// Encode marshals a Metric to the io.Writer in the Encoder
func (e *Encoder) Encode(m Metric) (int, error) {
err := e.buildHeader(m)
if err != nil {
return 0, err
}
e.buildFooter(m.Time())
// here we make a copy of the *fields so we can do an in-place sort
e.fieldList = append(e.fieldList[:0], m.FieldList()...)
if e.fieldSortOrder == SortFields {
sort.Slice(e.fieldList, func(i, j int) bool {
return e.fieldList[i].Key < e.fieldList[j].Key
})
}
i := 0
totalWritten := 0
pairsLen := 0
firstField := true
for _, field := range e.fieldList {
err = e.buildFieldPair(field.Key, field.Value)
if err != nil {
if e.failOnFieldError {
return 0, err
}
continue
}
bytesNeeded := len(e.header) + pairsLen + len(e.pair) + len(e.footer)
// Additional length needed for field separator `,`
if !firstField {
bytesNeeded++
}
if e.maxLineBytes > 0 && bytesNeeded > e.maxLineBytes {
// Need at least one field per line
if firstField {
return 0, ErrNeedMoreSpace
}
i, err = e.w.Write(e.footer)
if err != nil {
return 0, err
}
pairsLen = 0
totalWritten += i
bytesNeeded = len(e.header) + len(e.pair) + len(e.footer)
if e.maxLineBytes > 0 && bytesNeeded > e.maxLineBytes {
return 0, ErrNeedMoreSpace
}
i, err = e.w.Write(e.header)
if err != nil {
return 0, err
}
totalWritten += i
i, err = e.w.Write(e.pair)
if err != nil {
return 0, err
}
totalWritten += i
pairsLen += len(e.pair)
firstField = false
continue
}
if firstField {
i, err = e.w.Write(e.header)
if err != nil {
return 0, err
}
totalWritten += i
} else {
i, err = e.w.Write(comma)
if err != nil {
return 0, err
}
totalWritten += i
}
e.w.Write(e.pair)
pairsLen += len(e.pair)
firstField = false
}
if firstField {
return 0, ErrNoFields
}
i, err = e.w.Write(e.footer)
if err != nil {
return 0, err
}
totalWritten += i
return totalWritten, nil
}
func (e *Encoder) buildHeader(m Metric) error {
e.header = e.header[:0]
name := nameEscape(m.Name())
if name == "" {
return ErrInvalidName
}
e.header = append(e.header, name...)
for _, tag := range m.TagList() {
key := escape(tag.Key)
value := escape(tag.Value)
// Some keys and values are not encodeable as line protocol, such as
// those with a trailing '\' or empty strings.
if key == "" || value == "" {
continue
}
e.header = append(e.header, ',')
e.header = append(e.header, key...)
e.header = append(e.header, '=')
e.header = append(e.header, value...)
}
e.header = append(e.header, ' ')
return nil
}
func (e *Encoder) buildFieldVal(value interface{}) error {
switch v := value.(type) {
case uint64:
if e.fieldTypeSupport&UintSupport != 0 {
e.pair = append(strconv.AppendUint(e.pair, v, 10), 'u')
} else if v <= uint64(math.MaxInt64) {
e.pair = append(strconv.AppendInt(e.pair, int64(v), 10), 'i')
} else {
e.pair = append(strconv.AppendInt(e.pair, math.MaxInt64, 10), 'i')
}
case int64:
e.pair = append(strconv.AppendInt(e.pair, v, 10), 'i')
case int:
e.pair = append(strconv.AppendInt(e.pair, int64(v), 10), 'i')
case float64:
if math.IsNaN(v) {
return ErrIsNaN
}
if math.IsInf(v, 0) {
return ErrIsInf
}
e.pair = strconv.AppendFloat(e.pair, v, 'f', -1, 64)
case float32:
v32 := float64(v)
if math.IsNaN(v32) {
return ErrIsNaN
}
if math.IsInf(v32, 0) {
return ErrIsInf
}
e.pair = strconv.AppendFloat(e.pair, v32, 'f', -1, 64)
case string:
e.pair = append(e.pair, '"')
e.pair = append(e.pair, stringFieldEscape(v)...)
e.pair = append(e.pair, '"')
case []byte:
e.pair = append(e.pair, '"')
stringFieldEscapeBytes(&e.pair, v)
e.pair = append(e.pair, '"')
case bool:
e.pair = strconv.AppendBool(e.pair, v)
default:
return &FieldError{fmt.Sprintf("invalid value type: %T", v)}
}
return nil
}
func (e *Encoder) buildFieldPair(key string, value interface{}) error {
e.pair = e.pair[:0]
key = escape(key)
// Some keys are not encodeable as line protocol, such as those with a
// trailing '\' or empty strings.
if key == "" || key[:len(key)-1] == "\\" {
return &FieldError{"invalid field key"}
}
e.pair = append(e.pair, key...)
e.pair = append(e.pair, '=')
return e.buildFieldVal(value)
}
func (e *Encoder) buildFooter(t time.Time) {
e.footer = e.footer[:0]
if !t.IsZero() {
e.footer = append(e.footer, ' ')
switch e.precision {
case time.Microsecond:
e.footer = strconv.AppendInt(e.footer, t.UnixNano()/1000, 10)
case time.Millisecond:
e.footer = strconv.AppendInt(e.footer, t.UnixNano()/1000000, 10)
case time.Second:
e.footer = strconv.AppendInt(e.footer, t.Unix(), 10)
default:
e.footer = strconv.AppendInt(e.footer, t.UnixNano(), 10)
}
}
e.footer = append(e.footer, '\n')
}
+264
View File
@@ -0,0 +1,264 @@
package protocol
import (
"bytes"
"reflect"
"strconv"
"strings"
"unicode/utf8"
"unsafe"
)
const (
escapes = "\t\n\f\r ,="
nameEscapes = "\t\n\f\r ,"
stringFieldEscapes = "\t\n\f\r\\\""
)
var (
stringEscaper = strings.NewReplacer(
"\t", `\t`,
"\n", `\n`,
"\f", `\f`,
"\r", `\r`,
`,`, `\,`,
` `, `\ `,
`=`, `\=`,
)
nameEscaper = strings.NewReplacer(
"\t", `\t`,
"\n", `\n`,
"\f", `\f`,
"\r", `\r`,
`,`, `\,`,
` `, `\ `,
)
stringFieldEscaper = strings.NewReplacer(
"\t", `\t`,
"\n", `\n`,
"\f", `\f`,
"\r", `\r`,
`"`, `\"`,
`\`, `\\`,
)
)
var (
unescaper = strings.NewReplacer(
`\,`, `,`,
`\"`, `"`, // ???
`\ `, ` `,
`\=`, `=`,
)
nameUnescaper = strings.NewReplacer(
`\,`, `,`,
`\ `, ` `,
)
stringFieldUnescaper = strings.NewReplacer(
`\"`, `"`,
`\\`, `\`,
)
)
// The various escape functions allocate, I'd like to fix that.
// TODO: make escape not allocate
// Escape a tagkey, tagvalue, or fieldkey
func escape(s string) string {
if strings.ContainsAny(s, escapes) {
return stringEscaper.Replace(s)
}
return s
}
// Escape a measurement name
func nameEscape(s string) string {
if strings.ContainsAny(s, nameEscapes) {
return nameEscaper.Replace(s)
}
return s
}
// Escape a string field
func stringFieldEscape(s string) string {
if strings.ContainsAny(s, stringFieldEscapes) {
return stringFieldEscaper.Replace(s)
}
return s
}
const (
utf8mask = byte(0x3F)
utf8bytex = byte(0x80) // 1000 0000
utf8len2 = byte(0xC0) // 1100 0000
utf8len3 = byte(0xE0) // 1110 0000
utf8len4 = byte(0xF0) // 1111 0000
)
func escapeBytes(dest *[]byte, b []byte) {
if bytes.ContainsAny(b, escapes) {
var r rune
for i, j := 0, 0; i < len(b); i += j {
r, j = utf8.DecodeRune(b[i:])
switch {
case r == '\t':
*dest = append(*dest, `\t`...)
case r == '\n':
*dest = append(*dest, `\n`...)
case r == '\f':
*dest = append(*dest, `\f`...)
case r == '\r':
*dest = append(*dest, `\r`...)
case r == ',':
*dest = append(*dest, `\,`...)
case r == ' ':
*dest = append(*dest, `\ `...)
case r == '=':
*dest = append(*dest, `\=`...)
case r <= 1<<7-1:
*dest = append(*dest, byte(r))
case r <= 1<<11-1:
*dest = append(*dest, utf8len2|byte(r>>6), utf8bytex|byte(r)&utf8mask)
case r <= 1<<16-1:
*dest = append(*dest, utf8len3|byte(r>>12), utf8bytex|byte(r>>6)&utf8mask, utf8bytex|byte(r)&utf8mask)
default:
*dest = append(*dest, utf8len4|byte(r>>18), utf8bytex|byte(r>>12)&utf8mask, utf8bytex|byte(r>>6)&utf8mask, utf8bytex|byte(r)&utf8mask)
}
}
return
}
*dest = append(*dest, b...)
}
// Escape a measurement name
func nameEscapeBytes(dest *[]byte, b []byte) {
if bytes.ContainsAny(b, nameEscapes) {
var r rune
for i, j := 0, 0; i < len(b); i += j {
r, j = utf8.DecodeRune(b[i:])
switch {
case r == '\t':
*dest = append(*dest, `\t`...)
case r == '\n':
*dest = append(*dest, `\n`...)
case r == '\f':
*dest = append(*dest, `\f`...)
case r == '\r':
*dest = append(*dest, `\r`...)
case r == ',':
*dest = append(*dest, `\,`...)
case r == ' ':
*dest = append(*dest, `\ `...)
case r == '\\':
*dest = append(*dest, `\\`...)
case r <= 1<<7-1:
*dest = append(*dest, byte(r))
case r <= 1<<11-1:
*dest = append(*dest, utf8len2|byte(r>>6), utf8bytex|byte(r)&utf8mask)
case r <= 1<<16-1:
*dest = append(*dest, utf8len3|byte(r>>12), utf8bytex|byte(r>>6)&utf8mask, utf8bytex|byte(r)&utf8mask)
default:
*dest = append(*dest, utf8len4|byte(r>>18), utf8bytex|byte(r>>12)&utf8mask, utf8bytex|byte(r>>6)&utf8mask, utf8bytex|byte(r)&utf8mask)
}
}
return
}
*dest = append(*dest, b...)
}
func stringFieldEscapeBytes(dest *[]byte, b []byte) {
if bytes.ContainsAny(b, stringFieldEscapes) {
var r rune
for i, j := 0, 0; i < len(b); i += j {
r, j = utf8.DecodeRune(b[i:])
switch {
case r == '\t':
*dest = append(*dest, `\t`...)
case r == '\n':
*dest = append(*dest, `\n`...)
case r == '\f':
*dest = append(*dest, `\f`...)
case r == '\r':
*dest = append(*dest, `\r`...)
case r == ',':
*dest = append(*dest, `\,`...)
case r == ' ':
*dest = append(*dest, `\ `...)
case r == '\\':
*dest = append(*dest, `\\`...)
case r <= 1<<7-1:
*dest = append(*dest, byte(r))
case r <= 1<<11-1:
*dest = append(*dest, utf8len2|byte(r>>6), utf8bytex|byte(r)&utf8mask)
case r <= 1<<16-1:
*dest = append(*dest, utf8len3|byte(r>>12), utf8bytex|byte(r>>6)&utf8mask, utf8bytex|byte(r)&utf8mask)
default:
*dest = append(*dest, utf8len4|byte(r>>18), utf8bytex|byte(r>>12)&utf8mask, utf8bytex|byte(r>>6)&utf8mask, utf8bytex|byte(r)&utf8mask)
}
}
return
}
*dest = append(*dest, b...)
}
func unescape(b []byte) string {
if bytes.ContainsAny(b, escapes) {
return unescaper.Replace(unsafeBytesToString(b))
}
return string(b)
}
func nameUnescape(b []byte) string {
if bytes.ContainsAny(b, nameEscapes) {
return nameUnescaper.Replace(unsafeBytesToString(b))
}
return string(b)
}
// unsafeBytesToString converts a []byte to a string without a heap allocation.
//
// It is unsafe, and is intended to prepare input to short-lived functions
// that require strings.
func unsafeBytesToString(in []byte) string {
src := *(*reflect.SliceHeader)(unsafe.Pointer(&in))
dst := reflect.StringHeader{
Data: src.Data,
Len: src.Len,
}
s := *(*string)(unsafe.Pointer(&dst))
return s
}
// parseIntBytes is a zero-alloc wrapper around strconv.ParseInt.
func parseIntBytes(b []byte, base int, bitSize int) (i int64, err error) {
s := unsafeBytesToString(b)
return strconv.ParseInt(s, base, bitSize)
}
// parseUintBytes is a zero-alloc wrapper around strconv.ParseUint.
func parseUintBytes(b []byte, base int, bitSize int) (i uint64, err error) {
s := unsafeBytesToString(b)
return strconv.ParseUint(s, base, bitSize)
}
// parseFloatBytes is a zero-alloc wrapper around strconv.ParseFloat.
func parseFloatBytes(b []byte, bitSize int) (float64, error) {
s := unsafeBytesToString(b)
return strconv.ParseFloat(s, bitSize)
}
// parseBoolBytes is a zero-alloc wrapper around strconv.ParseBool.
func parseBoolBytes(b []byte) (bool, error) {
return strconv.ParseBool(unsafeBytesToString(b))
}
func stringFieldUnescape(b []byte) string {
if bytes.ContainsAny(b, stringFieldEscapes) {
return stringFieldUnescaper.Replace(unsafeBytesToString(b))
}
return string(b)
}
+128
View File
@@ -0,0 +1,128 @@
package protocol
import (
"bytes"
"errors"
"strconv"
"time"
)
// MetricHandler implements the Handler interface and produces Metric.
type MetricHandler struct {
timePrecision time.Duration
timeFunc TimeFunc
metric MutableMetric
}
func NewMetricHandler() *MetricHandler {
return &MetricHandler{
timePrecision: time.Nanosecond,
timeFunc: time.Now,
}
}
func (h *MetricHandler) SetTimePrecision(p time.Duration) {
h.timePrecision = p
// When the timestamp is omitted from the metric, the timestamp
// comes from the server clock, truncated to the nearest unit of
// measurement provided in precision.
//
// When a timestamp is provided in the metric, precsision is
// overloaded to hold the unit of measurement of the timestamp.
}
func (h *MetricHandler) SetTimeFunc(f TimeFunc) {
h.timeFunc = f
}
func (h *MetricHandler) Metric() (Metric, error) {
if h.metric.Time().IsZero() {
h.metric.SetTime(h.timeFunc().Truncate(h.timePrecision))
}
return h.metric, nil
}
func (h *MetricHandler) SetMeasurement(name []byte) error {
var err error
h.metric, err = New(nameUnescape(name),
nil, nil, time.Time{})
return err
}
func (h *MetricHandler) AddTag(key []byte, value []byte) error {
tk := unescape(key)
tv := unescape(value)
h.metric.AddTag(tk, tv)
return nil
}
func (h *MetricHandler) AddInt(key []byte, value []byte) error {
fk := unescape(key)
fv, err := parseIntBytes(bytes.TrimSuffix(value, []byte("i")), 10, 64)
if err != nil {
if numerr, ok := err.(*strconv.NumError); ok {
return numerr.Err
}
return err
}
h.metric.AddField(fk, fv)
return nil
}
func (h *MetricHandler) AddUint(key []byte, value []byte) error {
fk := unescape(key)
fv, err := parseUintBytes(bytes.TrimSuffix(value, []byte("u")), 10, 64)
if err != nil {
if numerr, ok := err.(*strconv.NumError); ok {
return numerr.Err
}
return err
}
h.metric.AddField(fk, fv)
return nil
}
func (h *MetricHandler) AddFloat(key []byte, value []byte) error {
fk := unescape(key)
fv, err := parseFloatBytes(value, 64)
if err != nil {
if numerr, ok := err.(*strconv.NumError); ok {
return numerr.Err
}
return err
}
h.metric.AddField(fk, fv)
return nil
}
func (h *MetricHandler) AddString(key []byte, value []byte) error {
fk := unescape(key)
fv := stringFieldUnescape(value)
h.metric.AddField(fk, fv)
return nil
}
func (h *MetricHandler) AddBool(key []byte, value []byte) error {
fk := unescape(key)
fv, err := parseBoolBytes(value)
if err != nil {
return errors.New("unparseable bool")
}
h.metric.AddField(fk, fv)
return nil
}
func (h *MetricHandler) SetTimestamp(tm []byte) error {
v, err := parseIntBytes(tm, 10, 64)
if err != nil {
if numerr, ok := err.(*strconv.NumError); ok {
return numerr.Err
}
return err
}
//time precision is overloaded to mean time unit here
ns := v * int64(h.timePrecision)
h.metric.SetTime(time.Unix(0, ns))
return nil
}
File diff suppressed because it is too large Load Diff
+549
View File
@@ -0,0 +1,549 @@
package protocol
import (
"errors"
"io"
)
var (
ErrNameParse = errors.New("expected measurement name")
ErrFieldParse = errors.New("expected field")
ErrTagParse = errors.New("expected tag")
ErrTimestampParse = errors.New("expected timestamp")
ErrParse = errors.New("parse error")
EOF = errors.New("EOF")
)
%%{
machine LineProtocol;
action begin {
m.pb = m.p
}
action name_error {
err = ErrNameParse
fhold;
fnext discard_line;
fbreak;
}
action field_error {
err = ErrFieldParse
fhold;
fnext discard_line;
fbreak;
}
action tagset_error {
err = ErrTagParse
fhold;
fnext discard_line;
fbreak;
}
action timestamp_error {
err = ErrTimestampParse
fhold;
fnext discard_line;
fbreak;
}
action parse_error {
err = ErrParse
fhold;
fnext discard_line;
fbreak;
}
action align_error {
err = ErrParse
fnext discard_line;
fbreak;
}
action hold_recover {
fhold;
fgoto main;
}
action goto_align {
fgoto align;
}
action begin_metric {
m.beginMetric = true
}
action name {
err = m.handler.SetMeasurement(m.text())
if err != nil {
fhold;
fnext discard_line;
fbreak;
}
}
action tagkey {
m.key = m.text()
}
action tagvalue {
err = m.handler.AddTag(m.key, m.text())
if err != nil {
fhold;
fnext discard_line;
fbreak;
}
}
action fieldkey {
m.key = m.text()
}
action integer {
err = m.handler.AddInt(m.key, m.text())
if err != nil {
fhold;
fnext discard_line;
fbreak;
}
}
action unsigned {
err = m.handler.AddUint(m.key, m.text())
if err != nil {
fhold;
fnext discard_line;
fbreak;
}
}
action float {
err = m.handler.AddFloat(m.key, m.text())
if err != nil {
fhold;
fnext discard_line;
fbreak;
}
}
action bool {
err = m.handler.AddBool(m.key, m.text())
if err != nil {
fhold;
fnext discard_line;
fbreak;
}
}
action string {
err = m.handler.AddString(m.key, m.text())
if err != nil {
fhold;
fnext discard_line;
fbreak;
}
}
action timestamp {
err = m.handler.SetTimestamp(m.text())
if err != nil {
fhold;
fnext discard_line;
fbreak;
}
}
action incr_newline {
m.lineno++
m.sol = m.p
m.sol++ // next char will be the first column in the line
}
action eol {
m.finishMetric = true
fnext align;
fbreak;
}
action finish_metric {
m.finishMetric = true
}
ws =
[\t\v\f ];
newline =
'\r'? '\n' >incr_newline;
non_zero_digit =
[1-9];
integer =
'-'? ( digit | ( non_zero_digit digit* ) );
unsigned =
( digit | ( non_zero_digit digit* ) );
number =
'-'? (digit+ ('.' digit*)? | '.' digit+);
scientific =
number 'e'i ["\-+"]? digit+;
timestamp =
('-'? digit{1,19}) >begin %timestamp;
fieldkeychar =
[^\t\n\f\r ,=\\] | ( '\\' [^\t\n\f\r] );
fieldkey =
fieldkeychar+ >begin %fieldkey;
fieldfloat =
(scientific | number) >begin %float;
fieldinteger =
(integer 'i') >begin %integer;
fieldunsigned =
(unsigned 'u') >begin %unsigned;
false =
"false" | "FALSE" | "False" | "F" | "f";
true =
"true" | "TRUE" | "True" | "T" | "t";
fieldbool =
(true | false) >begin %bool;
fieldstringchar =
[^\f\r\n\\"] | '\\' [\\"] | newline;
fieldstring =
fieldstringchar* >begin %string;
fieldstringquoted =
'"' fieldstring '"';
fieldvalue = fieldinteger | fieldunsigned | fieldfloat | fieldstringquoted | fieldbool;
field =
fieldkey '=' fieldvalue;
fieldset =
field ( ',' field )*;
tagchar =
[^\t\n\f\r ,=\\] | ( '\\' [^\t\n\f\r\\] ) | '\\\\' %to{ fhold; };
tagkey =
tagchar+ >begin %tagkey;
tagvalue =
tagchar+ >begin %eof(tagvalue) %tagvalue;
tagset =
((',' tagkey '=' tagvalue) $err(tagset_error))*;
measurement_chars =
[^\t\n\f\r ,\\] | ( '\\' [^\t\n\f\r] );
measurement_start =
measurement_chars - '#';
measurement =
(measurement_start measurement_chars*) >begin %eof(name) %name;
eol_break =
newline %to(eol)
;
metric =
measurement >err(name_error)
tagset
ws+ fieldset $err(field_error)
(ws+ timestamp)? $err(timestamp_error)
;
line_with_term =
ws* metric ws* eol_break
;
line_without_term =
ws* metric ws*
;
main :=
(line_with_term*
(line_with_term | line_without_term?)
) >begin_metric %eof(finish_metric)
;
# The discard_line machine discards the current line. Useful for recovering
# on the next line when an error occurs.
discard_line :=
(any -- newline)* newline @goto_align;
commentline =
ws* '#' (any -- newline)* newline;
emptyline =
ws* newline;
# The align machine scans forward to the start of the next line. This machine
# is used to skip over whitespace and comments, keeping this logic out of the
# main machine.
#
# Skip valid lines that don't contain line protocol, any other data will move
# control to the main parser via the err action.
align :=
(emptyline | commentline | ws+)* %err(hold_recover);
# Series is a machine for matching measurement+tagset
series :=
(measurement >err(name_error) tagset eol_break?)
>begin_metric
;
}%%
%% write data;
type Handler interface {
SetMeasurement(name []byte) error
AddTag(key []byte, value []byte) error
AddInt(key []byte, value []byte) error
AddUint(key []byte, value []byte) error
AddFloat(key []byte, value []byte) error
AddString(key []byte, value []byte) error
AddBool(key []byte, value []byte) error
SetTimestamp(tm []byte) error
}
type machine struct {
data []byte
cs int
p, pe, eof int
pb int
lineno int
sol int
handler Handler
initState int
key []byte
beginMetric bool
finishMetric bool
}
func NewMachine(handler Handler) *machine {
m := &machine{
handler: handler,
initState: LineProtocol_en_align,
}
%% access m.;
%% variable p m.p;
%% variable cs m.cs;
%% variable pe m.pe;
%% variable eof m.eof;
%% variable data m.data;
%% write init;
return m
}
func NewSeriesMachine(handler Handler) *machine {
m := &machine{
handler: handler,
initState: LineProtocol_en_series,
}
%% access m.;
%% variable p m.p;
%% variable pe m.pe;
%% variable eof m.eof;
%% variable data m.data;
%% write init;
return m
}
func (m *machine) SetData(data []byte) {
m.data = data
m.p = 0
m.pb = 0
m.lineno = 1
m.sol = 0
m.pe = len(data)
m.eof = len(data)
m.key = nil
m.beginMetric = false
m.finishMetric = false
%% write init;
m.cs = m.initState
}
// Next parses the next metric line and returns nil if it was successfully
// processed. If the line contains a syntax error an error is returned,
// otherwise if the end of file is reached before finding a metric line then
// EOF is returned.
func (m *machine) Next() error {
if m.p == m.pe && m.pe == m.eof {
return EOF
}
m.key = nil
m.beginMetric = false
m.finishMetric = false
return m.exec()
}
func (m *machine) exec() error {
var err error
%% write exec;
if err != nil {
return err
}
// This would indicate an error in the machine that was reported with a
// more specific error. We return a generic error but this should
// possibly be a panic.
if m.cs == %%{ write error; }%% {
m.cs = LineProtocol_en_discard_line
return ErrParse
}
// If we haven't found a metric line yet and we reached the EOF, report it
// now. This happens when the data ends with a comment or whitespace.
//
// Otherwise we have successfully parsed a metric line, so if we are at
// the EOF we will report it the next call.
if !m.beginMetric && m.p == m.pe && m.pe == m.eof {
return EOF
}
return nil
}
// Position returns the current byte offset into the data.
func (m *machine) Position() int {
return m.p
}
// LineOffset returns the byte offset of the current line.
func (m *machine) LineOffset() int {
return m.sol
}
// LineNumber returns the current line number. Lines are counted based on the
// regular expression `\r?\n`.
func (m *machine) LineNumber() int {
return m.lineno
}
// Column returns the current column.
func (m *machine) Column() int {
lineOffset := m.p - m.sol
return lineOffset + 1
}
func (m *machine) text() []byte {
return m.data[m.pb:m.p]
}
type streamMachine struct {
machine *machine
reader io.Reader
}
func NewStreamMachine(r io.Reader, handler Handler) *streamMachine {
m := &streamMachine{
machine: NewMachine(handler),
reader: r,
}
m.machine.SetData(make([]byte, 1024))
m.machine.pe = 0
m.machine.eof = -1
return m
}
func (m *streamMachine) Next() error {
// Check if we are already at EOF, this should only happen if called again
// after already returning EOF.
if m.machine.p == m.machine.pe && m.machine.pe == m.machine.eof {
return EOF
}
copy(m.machine.data, m.machine.data[m.machine.p:])
m.machine.pe = m.machine.pe - m.machine.p
m.machine.sol = m.machine.sol - m.machine.p
m.machine.pb = 0
m.machine.p = 0
m.machine.eof = -1
m.machine.key = nil
m.machine.beginMetric = false
m.machine.finishMetric = false
for {
// Expand the buffer if it is full
if m.machine.pe == len(m.machine.data) {
expanded := make([]byte, 2 * len(m.machine.data))
copy(expanded, m.machine.data)
m.machine.data = expanded
}
n, err := m.reader.Read(m.machine.data[m.machine.pe:])
if n == 0 && err == io.EOF {
m.machine.eof = m.machine.pe
} else if err != nil && err != io.EOF {
return err
}
m.machine.pe += n
err = m.machine.exec()
if err != nil {
return err
}
// If we have successfully parsed a full metric line break out
if m.machine.finishMetric {
break
}
}
return nil
}
// Position returns the current byte offset into the data.
func (m *streamMachine) Position() int {
return m.machine.Position()
}
// LineOffset returns the byte offset of the current line.
func (m *streamMachine) LineOffset() int {
return m.machine.LineOffset()
}
// LineNumber returns the current line number. Lines are counted based on the
// regular expression `\r?\n`.
func (m *streamMachine) LineNumber() int {
return m.machine.LineNumber()
}
// Column returns the current column.
func (m *streamMachine) Column() int {
return m.machine.Column()
}
// LineText returns the text of the current line that has been parsed so far.
func (m *streamMachine) LineText() string {
return string(m.machine.data[0:m.machine.p])
}
+428
View File
@@ -0,0 +1,428 @@
package protocol
import (
"fmt"
"hash/fnv"
"sort"
"time"
)
// Tag holds the keys and values for a bunch of Tag k/v pairs.
type Tag struct {
Key string
Value string
}
// Field holds the keys and values for a bunch of Metric Field k/v pairs where Value can be a uint64, int64, int, float32, float64, string, or bool.
type Field struct {
Key string
Value interface{}
}
// Metric is the interface for marshaling, if you implement this interface you can be marshalled into the line protocol. Woot!
type Metric interface {
Time() time.Time
Name() string
TagList() []*Tag
FieldList() []*Field
}
// MutableMetric represents a metric that can be be modified.
type MutableMetric interface {
Metric
SetTime(time.Time)
AddTag(key, value string)
AddField(key string, value interface{})
}
// FieldSortOrder is a type for controlling if Fields are sorted
type FieldSortOrder int
const (
// NoSortFields tells the Decoder to not sort the fields.
NoSortFields FieldSortOrder = iota
// SortFields tells the Decoder to sort the fields.
SortFields
)
// FieldTypeSupport is a type for the parser to understand its type support.
type FieldTypeSupport int
const (
// UintSupport means the parser understands uint64s and can store them without having to convert to int64.
UintSupport FieldTypeSupport = 1 << iota
)
// MetricError is an error causing a metric to be unserializable.
type MetricError struct {
s string
}
func (e MetricError) Error() string {
return e.s
}
// FieldError is an error causing a field to be unserializable.
type FieldError struct {
s string
}
func (e FieldError) Error() string {
return e.s
}
var (
// ErrNeedMoreSpace tells us that the Decoder's io.Reader is full.
ErrNeedMoreSpace = &MetricError{"need more space"}
// ErrInvalidName tells us that the chosen name is invalid.
ErrInvalidName = &MetricError{"invalid name"}
// ErrNoFields tells us that there were no serializable fields in the line/metric.
ErrNoFields = &MetricError{"no serializable fields"}
)
type metric struct {
name string
tags []*Tag
fields []*Field
tm time.Time
}
// New creates a new metric via maps.
func New(
name string,
tags map[string]string,
fields map[string]interface{},
tm time.Time,
) (MutableMetric, error) {
m := &metric{
name: name,
tags: nil,
fields: nil,
tm: tm,
}
if len(tags) > 0 {
m.tags = make([]*Tag, 0, len(tags))
for k, v := range tags {
m.tags = append(m.tags,
&Tag{Key: k, Value: v})
}
sort.Slice(m.tags, func(i, j int) bool { return m.tags[i].Key < m.tags[j].Key })
}
if len(fields) > 0 {
m.fields = make([]*Field, 0, len(fields))
for k, v := range fields {
v := convertField(v)
if v == nil {
continue
}
m.AddField(k, v)
}
}
return m, nil
}
// FromMetric returns a deep copy of the metric with any tracking information
// removed.
func FromMetric(other Metric) Metric {
m := &metric{
name: other.Name(),
tags: make([]*Tag, len(other.TagList())),
fields: make([]*Field, len(other.FieldList())),
tm: other.Time(),
}
for i, tag := range other.TagList() {
m.tags[i] = &Tag{Key: tag.Key, Value: tag.Value}
}
for i, field := range other.FieldList() {
m.fields[i] = &Field{Key: field.Key, Value: field.Value}
}
return m
}
func (m *metric) String() string {
return fmt.Sprintf("%s %v %v %d", m.name, m.Tags(), m.Fields(), m.tm.UnixNano())
}
func (m *metric) Name() string {
return m.name
}
func (m *metric) Tags() map[string]string {
tags := make(map[string]string, len(m.tags))
for _, tag := range m.tags {
tags[tag.Key] = tag.Value
}
return tags
}
func (m *metric) TagList() []*Tag {
return m.tags
}
func (m *metric) Fields() map[string]interface{} {
fields := make(map[string]interface{}, len(m.fields))
for _, field := range m.fields {
fields[field.Key] = field.Value
}
return fields
}
func (m *metric) FieldList() []*Field {
return m.fields
}
func (m *metric) Time() time.Time {
return m.tm
}
func (m *metric) SetName(name string) {
m.name = name
}
func (m *metric) AddPrefix(prefix string) {
m.name = prefix + m.name
}
func (m *metric) AddSuffix(suffix string) {
m.name = m.name + suffix
}
func (m *metric) AddTag(key, value string) {
for i, tag := range m.tags {
if key > tag.Key {
continue
}
if key == tag.Key {
tag.Value = value
return
}
m.tags = append(m.tags, nil)
copy(m.tags[i+1:], m.tags[i:])
m.tags[i] = &Tag{Key: key, Value: value}
return
}
m.tags = append(m.tags, &Tag{Key: key, Value: value})
}
func (m *metric) HasTag(key string) bool {
for _, tag := range m.tags {
if tag.Key == key {
return true
}
}
return false
}
func (m *metric) GetTag(key string) (string, bool) {
for _, tag := range m.tags {
if tag.Key == key {
return tag.Value, true
}
}
return "", false
}
func (m *metric) RemoveTag(key string) {
for i, tag := range m.tags {
if tag.Key == key {
copy(m.tags[i:], m.tags[i+1:])
m.tags[len(m.tags)-1] = nil
m.tags = m.tags[:len(m.tags)-1]
return
}
}
}
func (m *metric) AddField(key string, value interface{}) {
for i, field := range m.fields {
if key == field.Key {
m.fields[i] = &Field{Key: key, Value: convertField(value)}
return
}
}
m.fields = append(m.fields, &Field{Key: key, Value: convertField(value)})
}
func (m *metric) HasField(key string) bool {
for _, field := range m.fields {
if field.Key == key {
return true
}
}
return false
}
func (m *metric) GetField(key string) (interface{}, bool) {
for _, field := range m.fields {
if field.Key == key {
return field.Value, true
}
}
return nil, false
}
func (m *metric) RemoveField(key string) {
for i, field := range m.fields {
if field.Key == key {
copy(m.fields[i:], m.fields[i+1:])
m.fields[len(m.fields)-1] = nil
m.fields = m.fields[:len(m.fields)-1]
return
}
}
}
func (m *metric) SetTime(t time.Time) {
m.tm = t
}
func (m *metric) Copy() Metric {
m2 := &metric{
name: m.name,
tags: make([]*Tag, len(m.tags)),
fields: make([]*Field, len(m.fields)),
tm: m.tm,
}
for i, tag := range m.tags {
m2.tags[i] = &Tag{Key: tag.Key, Value: tag.Value}
}
for i, field := range m.fields {
m2.fields[i] = &Field{Key: field.Key, Value: field.Value}
}
return m2
}
func (m *metric) HashID() uint64 {
h := fnv.New64a()
h.Write([]byte(m.name))
h.Write([]byte("\n"))
for _, tag := range m.tags {
h.Write([]byte(tag.Key))
h.Write([]byte("\n"))
h.Write([]byte(tag.Value))
h.Write([]byte("\n"))
}
return h.Sum64()
}
func (m *metric) Accept() {
}
func (m *metric) Reject() {
}
func (m *metric) Drop() {
}
// Convert field to a supported type or nil if unconvertible
func convertField(v interface{}) interface{} {
switch v := v.(type) {
case float64:
return v
case int64:
return v
case string:
return v
case bool:
return v
case int:
return int64(v)
case uint:
return uint64(v)
case uint64:
return uint64(v)
case []byte:
return string(v)
case int32:
return int64(v)
case int16:
return int64(v)
case int8:
return int64(v)
case uint32:
return uint64(v)
case uint16:
return uint64(v)
case uint8:
return uint64(v)
case float32:
return float64(v)
case *float64:
if v != nil {
return *v
}
case *int64:
if v != nil {
return *v
}
case *string:
if v != nil {
return *v
}
case *bool:
if v != nil {
return *v
}
case *int:
if v != nil {
return int64(*v)
}
case *uint:
if v != nil {
return uint64(*v)
}
case *uint64:
if v != nil {
return uint64(*v)
}
case *[]byte:
if v != nil {
return string(*v)
}
case *int32:
if v != nil {
return int64(*v)
}
case *int16:
if v != nil {
return int64(*v)
}
case *int8:
if v != nil {
return int64(*v)
}
case *uint32:
if v != nil {
return uint64(*v)
}
case *uint16:
if v != nil {
return uint64(*v)
}
case *uint8:
if v != nil {
return uint64(*v)
}
case *float32:
if v != nil {
return float64(*v)
}
default:
return nil
}
return nil
}
+192
View File
@@ -0,0 +1,192 @@
package protocol
import (
"fmt"
"io"
"strings"
"sync"
"time"
)
const (
maxErrorBufferSize = 1024
)
// TimeFunc is used to override the default time for a metric
// with no specified timestamp.
type TimeFunc func() time.Time
// ParseError indicates a error in the parsing of the text.
type ParseError struct {
Offset int
LineOffset int
LineNumber int
Column int
msg string
buf string
}
func (e *ParseError) Error() string {
buffer := e.buf[e.LineOffset:]
eol := strings.IndexAny(buffer, "\r\n")
if eol >= 0 {
buffer = buffer[:eol]
}
if len(buffer) > maxErrorBufferSize {
buffer = buffer[:maxErrorBufferSize] + "..."
}
return fmt.Sprintf("metric parse error: %s at %d:%d: %q", e.msg, e.LineNumber, e.Column, buffer)
}
// Parser is an InfluxDB Line Protocol parser that implements the
// parsers.Parser interface.
type Parser struct {
DefaultTags map[string]string
sync.Mutex
*machine
handler *MetricHandler
}
// NewParser returns a Parser than accepts line protocol
func NewParser(handler *MetricHandler) *Parser {
return &Parser{
machine: NewMachine(handler),
handler: handler,
}
}
// NewSeriesParser returns a Parser than accepts a measurement and tagset
func NewSeriesParser(handler *MetricHandler) *Parser {
return &Parser{
machine: NewSeriesMachine(handler),
handler: handler,
}
}
// SetTimeFunc allows default times to be set when no time is specified
// for a metric in line-protocol.
func (p *Parser) SetTimeFunc(f TimeFunc) {
p.handler.SetTimeFunc(f)
}
// Parse interprets line-protocol bytes as many metrics.
func (p *Parser) Parse(input []byte) ([]Metric, error) {
p.Lock()
defer p.Unlock()
metrics := make([]Metric, 0)
p.machine.SetData(input)
for {
err := p.machine.Next()
if err == EOF {
break
}
if err != nil {
return nil, &ParseError{
Offset: p.machine.Position(),
LineOffset: p.machine.LineOffset(),
LineNumber: p.machine.LineNumber(),
Column: p.machine.Column(),
msg: err.Error(),
buf: string(input),
}
}
metric, err := p.handler.Metric()
if err != nil {
return nil, err
}
if metric == nil {
continue
}
metrics = append(metrics, metric)
}
return metrics, nil
}
// StreamParser is an InfluxDB Line Protocol parser. It is not safe for
// concurrent use in multiple goroutines.
type StreamParser struct {
machine *streamMachine
handler *MetricHandler
}
// NewStreamParser parses from a reader and iterates the machine
// metric by metric. Not safe for concurrent use in multiple goroutines.
func NewStreamParser(r io.Reader) *StreamParser {
handler := NewMetricHandler()
return &StreamParser{
machine: NewStreamMachine(r, handler),
handler: handler,
}
}
// SetTimeFunc changes the function used to determine the time of metrics
// without a timestamp. The default TimeFunc is time.Now. Useful mostly for
// testing, or perhaps if you want all metrics to have the same timestamp.
func (p *StreamParser) SetTimeFunc(f TimeFunc) {
p.handler.SetTimeFunc(f)
}
// SetTimePrecision specifies units for the time stamp.
func (p *StreamParser) SetTimePrecision(u time.Duration) {
p.handler.SetTimePrecision(u)
}
// Next parses the next item from the stream. You can repeat calls to this
// function until it returns EOF.
func (p *StreamParser) Next() (Metric, error) {
err := p.machine.Next()
if err == EOF {
return nil, EOF
}
if err != nil {
return nil, &ParseError{
Offset: p.machine.Position(),
LineOffset: p.machine.LineOffset(),
LineNumber: p.machine.LineNumber(),
Column: p.machine.Column(),
msg: err.Error(),
buf: p.machine.LineText(),
}
}
metric, err := p.handler.Metric()
if err != nil {
return nil, err
}
return metric, nil
}
// Position returns the current byte offset into the data.
func (p *StreamParser) Position() int {
return p.machine.Position()
}
// LineOffset returns the byte offset of the current line.
func (p *StreamParser) LineOffset() int {
return p.machine.LineOffset()
}
// LineNumber returns the current line number. Lines are counted based on the
// regular expression `\r?\n`.
func (p *StreamParser) LineNumber() int {
return p.machine.LineNumber()
}
// Column returns the current column.
func (p *StreamParser) Column() int {
return p.machine.Column()
}
// LineText returns the text of the current line that has been parsed so far.
func (p *StreamParser) LineText() string {
return p.machine.LineText()
}
+130
View File
@@ -0,0 +1,130 @@
package protocol
import (
"fmt"
"time"
)
// Write writes out data to a line protocol encoder. Note: it does no sorting. It assumes you have done your own sorting for tagValues
func (e *Encoder) Write(name []byte, ts time.Time, tagKeys, tagVals, fieldKeys [][]byte, fieldVals []interface{}) (int, error) {
e.header = e.header[:0]
if len(name) == 0 || name[len(name)-1] == byte('\\') {
return 0, ErrInvalidName
}
nameEscapeBytes(&e.header, name)
for i := range tagKeys {
// Some keys and values are not encodeable as line protocol, such as
// those with a trailing '\' or empty strings.
if len(tagKeys[i]) == 0 || len(tagVals[i]) == 0 || tagKeys[i][len(tagKeys[i])-1] == byte('\\') {
if e.failOnFieldError {
return 0, fmt.Errorf("invalid field: key \"%s\", val \"%s\"", tagKeys[i], tagVals[i])
}
continue
}
e.header = append(e.header, byte(','))
escapeBytes(&e.header, tagKeys[i])
e.header = append(e.header, byte('='))
escapeBytes(&e.header, tagVals[i])
}
e.header = append(e.header, byte(' '))
e.buildFooter(ts)
i := 0
totalWritten := 0
pairsLen := 0
firstField := true
for i := range fieldKeys {
e.pair = e.pair[:0]
key := fieldKeys[i]
if len(key) == 0 || key[len(key)-1] == byte('\\') {
if e.failOnFieldError {
return 0, &FieldError{"invalid field key"}
}
continue
}
escapeBytes(&e.pair, key)
// Some keys are not encodeable as line protocol, such as those with a
// trailing '\' or empty strings.
e.pair = append(e.pair, byte('='))
err := e.buildFieldVal(fieldVals[i])
if err != nil {
if e.failOnFieldError {
return 0, err
}
continue
}
bytesNeeded := len(e.header) + pairsLen + len(e.pair) + len(e.footer)
// Additional length needed for field separator `,`
if !firstField {
bytesNeeded++
}
if e.maxLineBytes > 0 && bytesNeeded > e.maxLineBytes {
// Need at least one field per line
if firstField {
return 0, ErrNeedMoreSpace
}
i, err = e.w.Write(e.footer)
if err != nil {
return 0, err
}
totalWritten += i
bytesNeeded = len(e.header) + len(e.pair) + len(e.footer)
if e.maxLineBytes > 0 && bytesNeeded > e.maxLineBytes {
return 0, ErrNeedMoreSpace
}
i, err = e.w.Write(e.header)
if err != nil {
return 0, err
}
totalWritten += i
i, err = e.w.Write(e.pair)
if err != nil {
return 0, err
}
totalWritten += i
pairsLen += len(e.pair)
firstField = false
continue
}
if firstField {
i, err = e.w.Write(e.header)
if err != nil {
return 0, err
}
totalWritten += i
} else {
i, err = e.w.Write(comma)
if err != nil {
return 0, err
}
totalWritten += i
}
e.w.Write(e.pair)
pairsLen += len(e.pair)
firstField = false
}
if firstField {
return 0, ErrNoFields
}
i, err := e.w.Write(e.footer)
if err != nil {
return 0, err
}
totalWritten += i
return totalWritten, nil
}
+191
View File
@@ -0,0 +1,191 @@
// Copyright 2012 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:generate go run gen.go
// Package publicsuffix provides a public suffix list based on data from
// https://publicsuffix.org/
//
// A public suffix is one under which Internet users can directly register
// names. It is related to, but different from, a TLD (top level domain).
//
// "com" is a TLD (top level domain). Top level means it has no dots.
//
// "com" is also a public suffix. Amazon and Google have registered different
// siblings under that domain: "amazon.com" and "google.com".
//
// "au" is another TLD, again because it has no dots. But it's not "amazon.au".
// Instead, it's "amazon.com.au".
//
// "com.au" isn't an actual TLD, because it's not at the top level (it has
// dots). But it is an eTLD (effective TLD), because that's the branching point
// for domain name registrars.
//
// Another name for "an eTLD" is "a public suffix". Often, what's more of
// interest is the eTLD+1, or one more label than the public suffix. For
// example, browsers partition read/write access to HTTP cookies according to
// the eTLD+1. Web pages served from "amazon.com.au" can't read cookies from
// "google.com.au", but web pages served from "maps.google.com" can share
// cookies from "www.google.com", so you don't have to sign into Google Maps
// separately from signing into Google Web Search. Note that all four of those
// domains have 3 labels and 2 dots. The first two domains are each an eTLD+1,
// the last two are not (but share the same eTLD+1: "google.com").
//
// All of these domains have the same eTLD+1:
// - "www.books.amazon.co.uk"
// - "books.amazon.co.uk"
// - "amazon.co.uk"
//
// Specifically, the eTLD+1 is "amazon.co.uk", because the eTLD is "co.uk".
//
// There is no closed form algorithm to calculate the eTLD of a domain.
// Instead, the calculation is data driven. This package provides a
// pre-compiled snapshot of Mozilla's PSL (Public Suffix List) data at
// https://publicsuffix.org/
package publicsuffix // import "golang.org/x/net/publicsuffix"
// TODO: specify case sensitivity and leading/trailing dot behavior for
// func PublicSuffix and func EffectiveTLDPlusOne.
import (
"fmt"
"net/http/cookiejar"
"strings"
)
// List implements the cookiejar.PublicSuffixList interface by calling the
// PublicSuffix function.
var List cookiejar.PublicSuffixList = list{}
type list struct{}
func (list) PublicSuffix(domain string) string {
ps, _ := PublicSuffix(domain)
return ps
}
func (list) String() string {
return version
}
// PublicSuffix returns the public suffix of the domain using a copy of the
// publicsuffix.org database compiled into the library.
//
// icann is whether the public suffix is managed by the Internet Corporation
// for Assigned Names and Numbers. If not, the public suffix is either a
// privately managed domain (and in practice, not a top level domain) or an
// unmanaged top level domain (and not explicitly mentioned in the
// publicsuffix.org list). For example, "foo.org" and "foo.co.uk" are ICANN
// domains, "foo.dyndns.org" and "foo.blogspot.co.uk" are private domains and
// "cromulent" is an unmanaged top level domain.
//
// Use cases for distinguishing ICANN domains like "foo.com" from private
// domains like "foo.appspot.com" can be found at
// https://wiki.mozilla.org/Public_Suffix_List/Use_Cases
func PublicSuffix(domain string) (publicSuffix string, icann bool) {
lo, hi := uint32(0), uint32(numTLD)
s, suffix, icannNode, wildcard := domain, len(domain), false, false
loop:
for {
dot := strings.LastIndex(s, ".")
if wildcard {
icann = icannNode
suffix = 1 + dot
}
if lo == hi {
break
}
f := find(s[1+dot:], lo, hi)
if f == notFound {
break
}
u := uint32(nodeValue(f) >> (nodesBitsTextOffset + nodesBitsTextLength))
icannNode = u&(1<<nodesBitsICANN-1) != 0
u >>= nodesBitsICANN
u = children[u&(1<<nodesBitsChildren-1)]
lo = u & (1<<childrenBitsLo - 1)
u >>= childrenBitsLo
hi = u & (1<<childrenBitsHi - 1)
u >>= childrenBitsHi
switch u & (1<<childrenBitsNodeType - 1) {
case nodeTypeNormal:
suffix = 1 + dot
case nodeTypeException:
suffix = 1 + len(s)
break loop
}
u >>= childrenBitsNodeType
wildcard = u&(1<<childrenBitsWildcard-1) != 0
if !wildcard {
icann = icannNode
}
if dot == -1 {
break
}
s = s[:dot]
}
if suffix == len(domain) {
// If no rules match, the prevailing rule is "*".
return domain[1+strings.LastIndex(domain, "."):], icann
}
return domain[suffix:], icann
}
const notFound uint32 = 1<<32 - 1
// find returns the index of the node in the range [lo, hi) whose label equals
// label, or notFound if there is no such node. The range is assumed to be in
// strictly increasing node label order.
func find(label string, lo, hi uint32) uint32 {
for lo < hi {
mid := lo + (hi-lo)/2
s := nodeLabel(mid)
if s < label {
lo = mid + 1
} else if s == label {
return mid
} else {
hi = mid
}
}
return notFound
}
func nodeValue(i uint32) uint64 {
off := uint64(i * (nodesBits / 8))
return uint64(nodes[off])<<32 |
uint64(nodes[off+1])<<24 |
uint64(nodes[off+2])<<16 |
uint64(nodes[off+3])<<8 |
uint64(nodes[off+4])
}
// nodeLabel returns the label for the i'th node.
func nodeLabel(i uint32) string {
x := nodeValue(i)
length := x & (1<<nodesBitsTextLength - 1)
x >>= nodesBitsTextLength
offset := x & (1<<nodesBitsTextOffset - 1)
return text[offset : offset+length]
}
// EffectiveTLDPlusOne returns the effective top level domain plus one more
// label. For example, the eTLD+1 for "foo.bar.golang.org" is "golang.org".
func EffectiveTLDPlusOne(domain string) (string, error) {
if strings.HasPrefix(domain, ".") || strings.HasSuffix(domain, ".") || strings.Contains(domain, "..") {
return "", fmt.Errorf("publicsuffix: empty label in domain %q", domain)
}
suffix, _ := PublicSuffix(domain)
if len(domain) <= len(suffix) {
return "", fmt.Errorf("publicsuffix: cannot derive eTLD+1 for domain %q", domain)
}
i := len(domain) - len(suffix) - 1
if domain[i] != '.' {
return "", fmt.Errorf("publicsuffix: invalid public suffix %q for domain %q", suffix, domain)
}
return domain[1+strings.LastIndex(domain[:i], "."):], nil
}
+10586
View File
File diff suppressed because it is too large Load Diff
+21 -5
View File
@@ -37,6 +37,10 @@ github.com/containerd/continuity/pathdriver
# github.com/davecgh/go-spew v1.1.1
## explicit
github.com/davecgh/go-spew/spew
# github.com/deepmap/oapi-codegen v1.8.2
## explicit; go 1.14
github.com/deepmap/oapi-codegen/pkg/runtime
github.com/deepmap/oapi-codegen/pkg/types
# github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f
## explicit
github.com/dgryski/go-rendezvous
@@ -266,11 +270,22 @@ github.com/imdario/mergo
# github.com/inconshreveable/mousetrap v1.0.1
## explicit; go 1.18
github.com/inconshreveable/mousetrap
# github.com/influxdata/influxdb v1.10.0
## explicit; go 1.18
github.com/influxdata/influxdb/client/v2
github.com/influxdata/influxdb/models
github.com/influxdata/influxdb/pkg/escape
# github.com/influxdata/influxdb-client-go/v2 v2.12.2
## explicit; go 1.17
github.com/influxdata/influxdb-client-go/v2
github.com/influxdata/influxdb-client-go/v2/api
github.com/influxdata/influxdb-client-go/v2/api/http
github.com/influxdata/influxdb-client-go/v2/api/query
github.com/influxdata/influxdb-client-go/v2/api/write
github.com/influxdata/influxdb-client-go/v2/domain
github.com/influxdata/influxdb-client-go/v2/internal/gzip
github.com/influxdata/influxdb-client-go/v2/internal/http
github.com/influxdata/influxdb-client-go/v2/internal/log
github.com/influxdata/influxdb-client-go/v2/internal/write
github.com/influxdata/influxdb-client-go/v2/log
# github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839
## explicit; go 1.13
github.com/influxdata/line-protocol
# github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa
## explicit; go 1.12
github.com/jackc/pgerrcode
@@ -680,6 +695,7 @@ golang.org/x/net/internal/timeseries
golang.org/x/net/ipv4
golang.org/x/net/ipv6
golang.org/x/net/proxy
golang.org/x/net/publicsuffix
golang.org/x/net/trace
# golang.org/x/sync v0.1.0
## explicit