2018-08-29 23:15:39 +00:00
|
|
|
package http
|
|
|
|
|
|
|
|
import (
|
|
|
|
"compress/gzip"
|
|
|
|
"context"
|
|
|
|
"fmt"
|
2018-10-10 17:42:31 +00:00
|
|
|
"io"
|
2018-09-28 17:17:40 +00:00
|
|
|
"io/ioutil"
|
2018-08-29 23:15:39 +00:00
|
|
|
"net/http"
|
2018-10-26 02:23:50 +00:00
|
|
|
"time"
|
2018-08-29 23:15:39 +00:00
|
|
|
|
|
|
|
"github.com/influxdata/platform"
|
|
|
|
pcontext "github.com/influxdata/platform/context"
|
|
|
|
"github.com/influxdata/platform/kit/errors"
|
2018-09-28 17:17:40 +00:00
|
|
|
"github.com/influxdata/platform/models"
|
2018-10-05 11:43:56 +00:00
|
|
|
"github.com/influxdata/platform/storage"
|
2018-09-28 17:17:40 +00:00
|
|
|
"github.com/influxdata/platform/tsdb"
|
2018-08-29 23:15:39 +00:00
|
|
|
"github.com/julienschmidt/httprouter"
|
|
|
|
"go.uber.org/zap"
|
|
|
|
)
|
|
|
|
|
2018-09-04 21:08:00 +00:00
|
|
|
// WriteHandler receives line protocol and sends to a publish function.
|
2018-08-29 23:15:39 +00:00
|
|
|
type WriteHandler struct {
|
|
|
|
*httprouter.Router
|
|
|
|
|
|
|
|
Logger *zap.Logger
|
|
|
|
|
2018-11-20 18:56:58 +00:00
|
|
|
BucketService platform.BucketService
|
|
|
|
OrganizationService platform.OrganizationService
|
2018-08-29 23:15:39 +00:00
|
|
|
|
2018-10-05 11:43:56 +00:00
|
|
|
PointsWriter storage.PointsWriter
|
2018-08-29 23:15:39 +00:00
|
|
|
}
|
|
|
|
|
2018-10-10 17:42:31 +00:00
|
|
|
const (
|
2018-12-21 16:27:10 +00:00
|
|
|
writePath = "/api/v2/write"
|
|
|
|
errInvalidGzipHeader = "gzipped HTTP body contains an invalid header"
|
|
|
|
errInvalidPrecision = "invalid precision; valid precision units are ns, us, ms, and s"
|
2018-10-10 17:42:31 +00:00
|
|
|
)
|
|
|
|
|
2018-09-26 08:49:19 +00:00
|
|
|
// NewWriteHandler creates a new handler at /api/v2/write to receive line protocol.
|
2018-10-05 11:43:56 +00:00
|
|
|
func NewWriteHandler(writer storage.PointsWriter) *WriteHandler {
|
2018-08-29 23:15:39 +00:00
|
|
|
h := &WriteHandler{
|
2018-12-15 15:33:54 +00:00
|
|
|
Router: NewRouter(),
|
2018-10-08 17:18:06 +00:00
|
|
|
Logger: zap.NewNop(),
|
2018-10-05 11:43:56 +00:00
|
|
|
PointsWriter: writer,
|
2018-08-29 23:15:39 +00:00
|
|
|
}
|
|
|
|
|
2018-10-10 17:42:31 +00:00
|
|
|
h.HandlerFunc("POST", writePath, h.handleWrite)
|
2018-08-29 23:15:39 +00:00
|
|
|
return h
|
|
|
|
}
|
|
|
|
|
|
|
|
func (h *WriteHandler) handleWrite(w http.ResponseWriter, r *http.Request) {
|
|
|
|
ctx := r.Context()
|
|
|
|
defer r.Body.Close()
|
|
|
|
|
|
|
|
in := r.Body
|
|
|
|
if r.Header.Get("Content-Encoding") == "gzip" {
|
|
|
|
var err error
|
|
|
|
in, err = gzip.NewReader(r.Body)
|
|
|
|
if err != nil {
|
2018-12-21 16:27:10 +00:00
|
|
|
EncodeError(ctx, &platform.Error{
|
|
|
|
Code: platform.EInvalid,
|
|
|
|
Op: "http/handleWrite",
|
|
|
|
Msg: errInvalidGzipHeader,
|
|
|
|
Err: err,
|
|
|
|
}, w)
|
2018-08-29 23:15:39 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
defer in.Close()
|
|
|
|
}
|
|
|
|
|
2018-10-03 19:13:27 +00:00
|
|
|
a, err := pcontext.GetAuthorizer(ctx)
|
2018-08-29 23:15:39 +00:00
|
|
|
if err != nil {
|
|
|
|
EncodeError(ctx, err, w)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2018-10-26 02:23:50 +00:00
|
|
|
req, err := decodeWriteRequest(ctx, r)
|
2018-08-29 23:15:39 +00:00
|
|
|
if err != nil {
|
|
|
|
EncodeError(ctx, err, w)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
logger := h.Logger.With(zap.String("org", req.Org), zap.String("bucket", req.Bucket))
|
|
|
|
|
|
|
|
var org *platform.Organization
|
|
|
|
if id, err := platform.IDFromString(req.Org); err == nil {
|
|
|
|
// Decoded ID successfully. Make sure it's a real org.
|
2018-09-05 22:53:57 +00:00
|
|
|
o, err := h.OrganizationService.FindOrganizationByID(ctx, *id)
|
|
|
|
if err == nil {
|
2018-08-29 23:15:39 +00:00
|
|
|
org = o
|
2018-12-21 16:27:10 +00:00
|
|
|
} else if platform.ErrorCode(err) != platform.ENotFound {
|
2018-09-05 22:53:57 +00:00
|
|
|
EncodeError(ctx, err, w)
|
|
|
|
return
|
2018-08-29 23:15:39 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
if org == nil {
|
|
|
|
o, err := h.OrganizationService.FindOrganization(ctx, platform.OrganizationFilter{Name: &req.Org})
|
|
|
|
if err != nil {
|
|
|
|
logger.Info("Failed to find organization", zap.Error(err))
|
|
|
|
EncodeError(ctx, fmt.Errorf("organization %q not found", req.Org), w)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
org = o
|
|
|
|
}
|
|
|
|
|
|
|
|
var bucket *platform.Bucket
|
|
|
|
if id, err := platform.IDFromString(req.Bucket); err == nil {
|
|
|
|
// Decoded ID successfully. Make sure it's a real bucket.
|
2018-09-05 22:53:57 +00:00
|
|
|
b, err := h.BucketService.FindBucket(ctx, platform.BucketFilter{
|
2018-08-29 23:15:39 +00:00
|
|
|
OrganizationID: &org.ID,
|
|
|
|
ID: id,
|
2018-09-05 22:53:57 +00:00
|
|
|
})
|
|
|
|
if err == nil {
|
2018-08-29 23:15:39 +00:00
|
|
|
bucket = b
|
2018-12-21 16:27:10 +00:00
|
|
|
} else if platform.ErrorCode(err) != platform.ENotFound {
|
2018-09-05 22:53:57 +00:00
|
|
|
EncodeError(ctx, err, w)
|
|
|
|
return
|
2018-08-29 23:15:39 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if bucket == nil {
|
|
|
|
b, err := h.BucketService.FindBucket(ctx, platform.BucketFilter{
|
|
|
|
OrganizationID: &org.ID,
|
|
|
|
Name: &req.Bucket,
|
|
|
|
})
|
|
|
|
if err != nil {
|
2018-12-21 16:27:10 +00:00
|
|
|
EncodeError(ctx, &platform.Error{
|
|
|
|
Code: platform.ENotFound,
|
|
|
|
Op: "http/handleWrite",
|
|
|
|
Err: err,
|
|
|
|
Msg: fmt.Sprintf("bucket %q not found", req.Bucket),
|
|
|
|
}, w)
|
2018-08-29 23:15:39 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
bucket = b
|
|
|
|
}
|
|
|
|
|
2018-12-28 23:02:19 +00:00
|
|
|
p, err := platform.NewPermissionAtID(bucket.ID, platform.WriteAction, platform.BucketsResource)
|
|
|
|
if err != nil {
|
|
|
|
EncodeError(ctx, fmt.Errorf("could not create permission for bucket: %v", err), w)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if !a.Allowed(*p) {
|
2018-08-29 23:15:39 +00:00
|
|
|
EncodeError(ctx, errors.Forbiddenf("insufficient permissions for write"), w)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2018-09-28 17:17:40 +00:00
|
|
|
// TODO(jeff): we should be publishing with the org and bucket instead of
|
|
|
|
// parsing, rewriting, and publishing, but the interface isn't quite there yet.
|
|
|
|
// be sure to remove this when it is there!
|
2018-10-08 17:18:06 +00:00
|
|
|
data, err := ioutil.ReadAll(in)
|
|
|
|
if err != nil {
|
|
|
|
logger.Info("Error reading body", zap.Error(err))
|
|
|
|
EncodeError(ctx, err, w)
|
|
|
|
return
|
|
|
|
}
|
2018-09-28 17:17:40 +00:00
|
|
|
|
2018-10-26 02:23:50 +00:00
|
|
|
points, err := models.ParsePointsWithPrecision(data, time.Now(), req.Precision)
|
2018-10-08 17:18:06 +00:00
|
|
|
if err != nil {
|
|
|
|
logger.Info("Error parsing points", zap.Error(err))
|
|
|
|
EncodeError(ctx, err, w)
|
|
|
|
return
|
|
|
|
}
|
2018-09-28 17:17:40 +00:00
|
|
|
|
2018-10-08 17:18:06 +00:00
|
|
|
exploded, err := tsdb.ExplodePoints(org.ID, bucket.ID, points)
|
|
|
|
if err != nil {
|
|
|
|
logger.Info("Error exploding points", zap.Error(err))
|
|
|
|
EncodeError(ctx, err, w)
|
|
|
|
return
|
|
|
|
}
|
2018-09-28 17:17:40 +00:00
|
|
|
|
2018-10-08 17:18:06 +00:00
|
|
|
if err := h.PointsWriter.WritePoints(exploded); err != nil {
|
|
|
|
EncodeError(ctx, errors.BadRequestError(err.Error()), w)
|
|
|
|
return
|
|
|
|
}
|
2018-08-29 23:15:39 +00:00
|
|
|
|
|
|
|
w.WriteHeader(http.StatusNoContent)
|
|
|
|
}
|
|
|
|
|
2018-10-26 02:23:50 +00:00
|
|
|
func decodeWriteRequest(ctx context.Context, r *http.Request) (*postWriteRequest, error) {
|
2018-08-29 23:15:39 +00:00
|
|
|
qp := r.URL.Query()
|
2018-10-26 02:23:50 +00:00
|
|
|
p := qp.Get("precision")
|
|
|
|
if p == "" {
|
|
|
|
p = "ns"
|
|
|
|
}
|
2018-08-29 23:15:39 +00:00
|
|
|
|
2018-10-26 02:23:50 +00:00
|
|
|
if !models.ValidPrecision(p) {
|
2018-12-21 16:27:10 +00:00
|
|
|
return nil, &platform.Error{
|
|
|
|
Code: platform.EInvalid,
|
|
|
|
Op: "http/decodeWriteRequest",
|
|
|
|
Msg: errInvalidPrecision,
|
|
|
|
}
|
2018-08-29 23:15:39 +00:00
|
|
|
}
|
2018-10-26 02:23:50 +00:00
|
|
|
|
|
|
|
return &postWriteRequest{
|
|
|
|
Bucket: qp.Get("bucket"),
|
|
|
|
Org: qp.Get("org"),
|
|
|
|
Precision: p,
|
|
|
|
}, nil
|
2018-08-29 23:15:39 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
type postWriteRequest struct {
|
2018-10-26 02:23:50 +00:00
|
|
|
Org string
|
|
|
|
Bucket string
|
|
|
|
Precision string
|
2018-08-29 23:15:39 +00:00
|
|
|
}
|
2018-10-10 17:42:31 +00:00
|
|
|
|
|
|
|
// WriteService sends data over HTTP to influxdb via line protocol.
|
|
|
|
type WriteService struct {
|
|
|
|
Addr string
|
|
|
|
Token string
|
2018-10-26 02:23:50 +00:00
|
|
|
Precision string
|
2018-10-10 17:42:31 +00:00
|
|
|
InsecureSkipVerify bool
|
|
|
|
}
|
|
|
|
|
|
|
|
var _ platform.WriteService = (*WriteService)(nil)
|
|
|
|
|
2018-10-12 02:42:42 +00:00
|
|
|
func (s *WriteService) Write(ctx context.Context, orgID, bucketID platform.ID, r io.Reader) error {
|
2018-10-26 02:23:50 +00:00
|
|
|
precision := s.Precision
|
|
|
|
if precision == "" {
|
|
|
|
precision = "ns"
|
|
|
|
}
|
|
|
|
|
|
|
|
if !models.ValidPrecision(precision) {
|
2018-12-21 16:27:10 +00:00
|
|
|
return &platform.Error{
|
|
|
|
Code: platform.EInvalid,
|
|
|
|
Op: "http/Write",
|
|
|
|
Msg: errInvalidPrecision,
|
|
|
|
}
|
2018-10-26 02:23:50 +00:00
|
|
|
}
|
|
|
|
|
2018-10-10 17:42:31 +00:00
|
|
|
u, err := newURL(s.Addr, writePath)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
r, err = compressWithGzip(r)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
req, err := http.NewRequest("POST", u.String(), r)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
req.Header.Set("Content-Type", "text/plain; charset=utf-8")
|
|
|
|
req.Header.Set("Content-Encoding", "gzip")
|
|
|
|
SetToken(s.Token, req)
|
|
|
|
|
2018-10-12 02:42:42 +00:00
|
|
|
org, err := orgID.Encode()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
bucket, err := bucketID.Encode()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2018-10-10 17:42:31 +00:00
|
|
|
params := req.URL.Query()
|
2018-10-12 02:42:42 +00:00
|
|
|
params.Set("org", string(org))
|
|
|
|
params.Set("bucket", string(bucket))
|
2018-10-26 02:23:50 +00:00
|
|
|
params.Set("precision", string(precision))
|
2018-10-10 17:42:31 +00:00
|
|
|
req.URL.RawQuery = params.Encode()
|
|
|
|
|
|
|
|
hc := newClient(u.Scheme, s.InsecureSkipVerify)
|
|
|
|
|
|
|
|
resp, err := hc.Do(req)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2018-12-21 16:27:10 +00:00
|
|
|
return CheckError(resp, true)
|
2018-10-10 17:42:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func compressWithGzip(data io.Reader) (io.Reader, error) {
|
|
|
|
pr, pw := io.Pipe()
|
|
|
|
gw := gzip.NewWriter(pw)
|
|
|
|
var err error
|
|
|
|
|
|
|
|
go func() {
|
|
|
|
_, err = io.Copy(gw, data)
|
|
|
|
gw.Close()
|
|
|
|
pw.Close()
|
|
|
|
}()
|
|
|
|
|
|
|
|
return pr, err
|
|
|
|
}
|