github.com/qxnw/lib4go@v0.0.0-20180426074627-c80c7e84b925/influxdb/v2/client.go (about)

     1  // Package client (v2) is the current official Go client for InfluxDB.
     2  package client // import "github.com/influxdata/influxdb/client/v2"
     3  
     4  import (
     5  	"bytes"
     6  	"crypto/tls"
     7  	"encoding/json"
     8  	"errors"
     9  	"fmt"
    10  	"io"
    11  	"io/ioutil"
    12  	"net/http"
    13  	"net/url"
    14  	"strconv"
    15  	"strings"
    16  	"time"
    17  
    18  	"github.com/qxnw/influxdb/models"
    19  )
    20  
    21  // HTTPConfig is the config data needed to create an HTTP Client.
    22  type HTTPConfig struct {
    23  	// Addr should be of the form "http://host:port"
    24  	// or "http://[ipv6-host%zone]:port".
    25  	Addr string
    26  
    27  	// Username is the influxdb username, optional.
    28  	Username string
    29  
    30  	// Password is the influxdb password, optional.
    31  	Password string
    32  
    33  	// UserAgent is the http User Agent, defaults to "InfluxDBClient".
    34  	UserAgent string
    35  
    36  	// Timeout for influxdb writes, defaults to no timeout.
    37  	Timeout time.Duration
    38  
    39  	// InsecureSkipVerify gets passed to the http client, if true, it will
    40  	// skip https certificate verification. Defaults to false.
    41  	InsecureSkipVerify bool
    42  
    43  	// TLSConfig allows the user to set their own TLS config for the HTTP
    44  	// Client. If set, this option overrides InsecureSkipVerify.
    45  	TLSConfig *tls.Config
    46  }
    47  
    48  // BatchPointsConfig is the config data needed to create an instance of the BatchPoints struct.
    49  type BatchPointsConfig struct {
    50  	// Precision is the write precision of the points, defaults to "ns".
    51  	Precision string
    52  
    53  	// Database is the database to write points to.
    54  	Database string
    55  
    56  	// RetentionPolicy is the retention policy of the points.
    57  	RetentionPolicy string
    58  
    59  	// Write consistency is the number of servers required to confirm write.
    60  	WriteConsistency string
    61  }
    62  
    63  // Client is a client interface for writing & querying the database.
    64  type Client interface {
    65  	// Ping checks that status of cluster, and will always return 0 time and no
    66  	// error for UDP clients.
    67  	Ping(timeout time.Duration) (time.Duration, string, error)
    68  
    69  	// Write takes a BatchPoints object and writes all Points to InfluxDB.
    70  	Write(bp BatchPoints) error
    71  
    72  	// Query makes an InfluxDB Query on the database. This will fail if using
    73  	// the UDP client.
    74  	Query(q Query) (*Response, error)
    75  
    76  	// Close releases any resources a Client may be using.
    77  	Close() error
    78  }
    79  
    80  // NewHTTPClient returns a new Client from the provided config.
    81  // Client is safe for concurrent use by multiple goroutines.
    82  func NewHTTPClient(conf HTTPConfig) (Client, error) {
    83  	if conf.UserAgent == "" {
    84  		conf.UserAgent = "InfluxDBClient"
    85  	}
    86  
    87  	u, err := url.Parse(conf.Addr)
    88  	if err != nil {
    89  		return nil, err
    90  	} else if u.Scheme != "http" && u.Scheme != "https" {
    91  		m := fmt.Sprintf("Unsupported protocol scheme: %s, your address"+
    92  			" must start with http:// or https://", u.Scheme)
    93  		return nil, errors.New(m)
    94  	}
    95  
    96  	tr := &http.Transport{
    97  		TLSClientConfig: &tls.Config{
    98  			InsecureSkipVerify: conf.InsecureSkipVerify,
    99  		},
   100  	}
   101  	if conf.TLSConfig != nil {
   102  		tr.TLSClientConfig = conf.TLSConfig
   103  	}
   104  	return &client{
   105  		url:       *u,
   106  		username:  conf.Username,
   107  		password:  conf.Password,
   108  		useragent: conf.UserAgent,
   109  		httpClient: &http.Client{
   110  			Timeout:   conf.Timeout,
   111  			Transport: tr,
   112  		},
   113  		transport: tr,
   114  	}, nil
   115  }
   116  
   117  // Ping will check to see if the server is up with an optional timeout on waiting for leader.
   118  // Ping returns how long the request took, the version of the server it connected to, and an error if one occurred.
   119  func (c *client) Ping(timeout time.Duration) (time.Duration, string, error) {
   120  	now := time.Now()
   121  	u := c.url
   122  	u.Path = "ping"
   123  
   124  	req, err := http.NewRequest("GET", u.String(), nil)
   125  	if err != nil {
   126  		return 0, "", err
   127  	}
   128  
   129  	req.Header.Set("User-Agent", c.useragent)
   130  
   131  	if c.username != "" {
   132  		req.SetBasicAuth(c.username, c.password)
   133  	}
   134  
   135  	if timeout > 0 {
   136  		params := req.URL.Query()
   137  		params.Set("wait_for_leader", fmt.Sprintf("%.0fs", timeout.Seconds()))
   138  		req.URL.RawQuery = params.Encode()
   139  	}
   140  
   141  	resp, err := c.httpClient.Do(req)
   142  	if err != nil {
   143  		return 0, "", err
   144  	}
   145  	defer resp.Body.Close()
   146  
   147  	body, err := ioutil.ReadAll(resp.Body)
   148  	if err != nil {
   149  		return 0, "", err
   150  	}
   151  
   152  	if resp.StatusCode != http.StatusNoContent {
   153  		var err = fmt.Errorf(string(body))
   154  		return 0, "", err
   155  	}
   156  
   157  	version := resp.Header.Get("X-Influxdb-Version")
   158  	return time.Since(now), version, nil
   159  }
   160  
   161  // Close releases the client's resources.
   162  func (c *client) Close() error {
   163  	c.transport.CloseIdleConnections()
   164  	return nil
   165  }
   166  
   167  // client is safe for concurrent use as the fields are all read-only
   168  // once the client is instantiated.
   169  type client struct {
   170  	// N.B - if url.UserInfo is accessed in future modifications to the
   171  	// methods on client, you will need to syncronise access to url.
   172  	url        url.URL
   173  	username   string
   174  	password   string
   175  	useragent  string
   176  	httpClient *http.Client
   177  	transport  *http.Transport
   178  }
   179  
   180  // BatchPoints is an interface into a batched grouping of points to write into
   181  // InfluxDB together. BatchPoints is NOT thread-safe, you must create a separate
   182  // batch for each goroutine.
   183  type BatchPoints interface {
   184  	// AddPoint adds the given point to the Batch of points.
   185  	AddPoint(p *Point)
   186  	// AddPoints adds the given points to the Batch of points.
   187  	AddPoints(ps []*Point)
   188  	// Points lists the points in the Batch.
   189  	Points() []*Point
   190  
   191  	// Precision returns the currently set precision of this Batch.
   192  	Precision() string
   193  	// SetPrecision sets the precision of this batch.
   194  	SetPrecision(s string) error
   195  
   196  	// Database returns the currently set database of this Batch.
   197  	Database() string
   198  	// SetDatabase sets the database of this Batch.
   199  	SetDatabase(s string)
   200  
   201  	// WriteConsistency returns the currently set write consistency of this Batch.
   202  	WriteConsistency() string
   203  	// SetWriteConsistency sets the write consistency of this Batch.
   204  	SetWriteConsistency(s string)
   205  
   206  	// RetentionPolicy returns the currently set retention policy of this Batch.
   207  	RetentionPolicy() string
   208  	// SetRetentionPolicy sets the retention policy of this Batch.
   209  	SetRetentionPolicy(s string)
   210  }
   211  
   212  // NewBatchPoints returns a BatchPoints interface based on the given config.
   213  func NewBatchPoints(conf BatchPointsConfig) (BatchPoints, error) {
   214  	if conf.Precision == "" {
   215  		conf.Precision = "ns"
   216  	}
   217  	if _, err := time.ParseDuration("1" + conf.Precision); err != nil {
   218  		return nil, err
   219  	}
   220  	bp := &batchpoints{
   221  		database:         conf.Database,
   222  		precision:        conf.Precision,
   223  		retentionPolicy:  conf.RetentionPolicy,
   224  		writeConsistency: conf.WriteConsistency,
   225  	}
   226  	return bp, nil
   227  }
   228  
   229  type batchpoints struct {
   230  	points           []*Point
   231  	database         string
   232  	precision        string
   233  	retentionPolicy  string
   234  	writeConsistency string
   235  }
   236  
   237  func (bp *batchpoints) AddPoint(p *Point) {
   238  	bp.points = append(bp.points, p)
   239  }
   240  
   241  func (bp *batchpoints) AddPoints(ps []*Point) {
   242  	bp.points = append(bp.points, ps...)
   243  }
   244  
   245  func (bp *batchpoints) Points() []*Point {
   246  	return bp.points
   247  }
   248  
   249  func (bp *batchpoints) Precision() string {
   250  	return bp.precision
   251  }
   252  
   253  func (bp *batchpoints) Database() string {
   254  	return bp.database
   255  }
   256  
   257  func (bp *batchpoints) WriteConsistency() string {
   258  	return bp.writeConsistency
   259  }
   260  
   261  func (bp *batchpoints) RetentionPolicy() string {
   262  	return bp.retentionPolicy
   263  }
   264  
   265  func (bp *batchpoints) SetPrecision(p string) error {
   266  	if _, err := time.ParseDuration("1" + p); err != nil {
   267  		return err
   268  	}
   269  	bp.precision = p
   270  	return nil
   271  }
   272  
   273  func (bp *batchpoints) SetDatabase(db string) {
   274  	bp.database = db
   275  }
   276  
   277  func (bp *batchpoints) SetWriteConsistency(wc string) {
   278  	bp.writeConsistency = wc
   279  }
   280  
   281  func (bp *batchpoints) SetRetentionPolicy(rp string) {
   282  	bp.retentionPolicy = rp
   283  }
   284  
   285  // Point represents a single data point.
   286  type Point struct {
   287  	pt models.Point
   288  }
   289  
   290  // NewPoint returns a point with the given timestamp. If a timestamp is not
   291  // given, then data is sent to the database without a timestamp, in which case
   292  // the server will assign local time upon reception. NOTE: it is recommended to
   293  // send data with a timestamp.
   294  func NewPoint(
   295  	name string,
   296  	tags map[string]string,
   297  	fields map[string]interface{},
   298  	t ...time.Time,
   299  ) (*Point, error) {
   300  	var T time.Time
   301  	if len(t) > 0 {
   302  		T = t[0]
   303  	}
   304  
   305  	pt, err := models.NewPoint(name, models.NewTags(tags), fields, T)
   306  	if err != nil {
   307  		return nil, err
   308  	}
   309  	return &Point{
   310  		pt: pt,
   311  	}, nil
   312  }
   313  
   314  // String returns a line-protocol string of the Point.
   315  func (p *Point) String() string {
   316  	return p.pt.String()
   317  }
   318  
   319  // PrecisionString returns a line-protocol string of the Point,
   320  // with the timestamp formatted for the given precision.
   321  func (p *Point) PrecisionString(precison string) string {
   322  	return p.pt.PrecisionString(precison)
   323  }
   324  
   325  // Name returns the measurement name of the point.
   326  func (p *Point) Name() string {
   327  	return p.pt.Name()
   328  }
   329  
   330  // Tags returns the tags associated with the point.
   331  func (p *Point) Tags() map[string]string {
   332  	return p.pt.Tags().Map()
   333  }
   334  
   335  // Time return the timestamp for the point.
   336  func (p *Point) Time() time.Time {
   337  	return p.pt.Time()
   338  }
   339  
   340  // UnixNano returns timestamp of the point in nanoseconds since Unix epoch.
   341  func (p *Point) UnixNano() int64 {
   342  	return p.pt.UnixNano()
   343  }
   344  
   345  // Fields returns the fields for the point.
   346  func (p *Point) Fields() (map[string]interface{}, error) {
   347  	return p.pt.Fields()
   348  }
   349  
   350  // NewPointFrom returns a point from the provided models.Point.
   351  func NewPointFrom(pt models.Point) *Point {
   352  	return &Point{pt: pt}
   353  }
   354  
   355  func (c *client) Write(bp BatchPoints) error {
   356  	var b bytes.Buffer
   357  
   358  	for _, p := range bp.Points() {
   359  		if _, err := b.WriteString(p.pt.PrecisionString(bp.Precision())); err != nil {
   360  			return err
   361  		}
   362  
   363  		if err := b.WriteByte('\n'); err != nil {
   364  			return err
   365  		}
   366  	}
   367  
   368  	u := c.url
   369  	u.Path = "write"
   370  	req, err := http.NewRequest("POST", u.String(), &b)
   371  	if err != nil {
   372  		return err
   373  	}
   374  	req.Header.Set("Content-Type", "")
   375  	req.Header.Set("User-Agent", c.useragent)
   376  	if c.username != "" {
   377  		req.SetBasicAuth(c.username, c.password)
   378  	}
   379  
   380  	params := req.URL.Query()
   381  	params.Set("db", bp.Database())
   382  	params.Set("rp", bp.RetentionPolicy())
   383  	params.Set("precision", bp.Precision())
   384  	params.Set("consistency", bp.WriteConsistency())
   385  	req.URL.RawQuery = params.Encode()
   386  
   387  	resp, err := c.httpClient.Do(req)
   388  	if err != nil {
   389  		return err
   390  	}
   391  	defer resp.Body.Close()
   392  
   393  	body, err := ioutil.ReadAll(resp.Body)
   394  	if err != nil {
   395  		return err
   396  	}
   397  
   398  	if resp.StatusCode != http.StatusNoContent && resp.StatusCode != http.StatusOK {
   399  		var err = fmt.Errorf(string(body))
   400  		return err
   401  	}
   402  
   403  	return nil
   404  }
   405  
   406  // Query defines a query to send to the server.
   407  type Query struct {
   408  	Command    string
   409  	Database   string
   410  	Precision  string
   411  	Chunked    bool
   412  	ChunkSize  int
   413  	Parameters map[string]interface{}
   414  }
   415  
   416  // NewQuery returns a query object.
   417  // The database and precision arguments can be empty strings if they are not needed for the query.
   418  func NewQuery(command, database, precision string) Query {
   419  	return Query{
   420  		Command:    command,
   421  		Database:   database,
   422  		Precision:  precision,
   423  		Parameters: make(map[string]interface{}),
   424  	}
   425  }
   426  
   427  // NewQueryWithParameters returns a query object.
   428  // The database and precision arguments can be empty strings if they are not needed for the query.
   429  // parameters is a map of the parameter names used in the command to their values.
   430  func NewQueryWithParameters(command, database, precision string, parameters map[string]interface{}) Query {
   431  	return Query{
   432  		Command:    command,
   433  		Database:   database,
   434  		Precision:  precision,
   435  		Parameters: parameters,
   436  	}
   437  }
   438  
   439  // Response represents a list of statement results.
   440  type Response struct {
   441  	Results []Result
   442  	Err     string `json:"error,omitempty"`
   443  }
   444  
   445  // Error returns the first error from any statement.
   446  // It returns nil if no errors occurred on any statements.
   447  func (r *Response) Error() error {
   448  	if r.Err != "" {
   449  		return fmt.Errorf(r.Err)
   450  	}
   451  	for _, result := range r.Results {
   452  		if result.Err != "" {
   453  			return fmt.Errorf(result.Err)
   454  		}
   455  	}
   456  	return nil
   457  }
   458  
   459  // Message represents a user message.
   460  type Message struct {
   461  	Level string
   462  	Text  string
   463  }
   464  
   465  // Result represents a resultset returned from a single statement.
   466  type Result struct {
   467  	Series   []models.Row
   468  	Messages []*Message
   469  	Err      string `json:"error,omitempty"`
   470  }
   471  
   472  // Query sends a command to the server and returns the Response.
   473  func (c *client) Query(q Query) (*Response, error) {
   474  	u := c.url
   475  	u.Path = "query"
   476  
   477  	jsonParameters, err := json.Marshal(q.Parameters)
   478  
   479  	if err != nil {
   480  		return nil, err
   481  	}
   482  
   483  	req, err := http.NewRequest("POST", u.String(), nil)
   484  	if err != nil {
   485  		return nil, err
   486  	}
   487  
   488  	req.Header.Set("Content-Type", "")
   489  	req.Header.Set("User-Agent", c.useragent)
   490  
   491  	if c.username != "" {
   492  		req.SetBasicAuth(c.username, c.password)
   493  	}
   494  
   495  	params := req.URL.Query()
   496  	params.Set("q", q.Command)
   497  	params.Set("db", q.Database)
   498  	params.Set("params", string(jsonParameters))
   499  	if q.Chunked {
   500  		params.Set("chunked", "true")
   501  		if q.ChunkSize > 0 {
   502  			params.Set("chunk_size", strconv.Itoa(q.ChunkSize))
   503  		}
   504  	}
   505  
   506  	if q.Precision != "" {
   507  		params.Set("epoch", q.Precision)
   508  	}
   509  	req.URL.RawQuery = params.Encode()
   510  
   511  	resp, err := c.httpClient.Do(req)
   512  	if err != nil {
   513  		return nil, err
   514  	}
   515  	defer resp.Body.Close()
   516  
   517  	var response Response
   518  	if q.Chunked {
   519  		cr := NewChunkedResponse(resp.Body)
   520  		for {
   521  			r, err := cr.NextResponse()
   522  			if err != nil {
   523  				// If we got an error while decoding the response, send that back.
   524  				return nil, err
   525  			}
   526  
   527  			if r == nil {
   528  				break
   529  			}
   530  
   531  			response.Results = append(response.Results, r.Results...)
   532  			if r.Err != "" {
   533  				response.Err = r.Err
   534  				break
   535  			}
   536  		}
   537  	} else {
   538  		dec := json.NewDecoder(resp.Body)
   539  		dec.UseNumber()
   540  		decErr := dec.Decode(&response)
   541  
   542  		// ignore this error if we got an invalid status code
   543  		if decErr != nil && decErr.Error() == "EOF" && resp.StatusCode != http.StatusOK {
   544  			decErr = nil
   545  		}
   546  		// If we got a valid decode error, send that back
   547  		if decErr != nil {
   548  			return nil, fmt.Errorf("unable to decode json: received status code %d err: %s", resp.StatusCode, decErr)
   549  		}
   550  	}
   551  	// If we don't have an error in our json response, and didn't get statusOK
   552  	// then send back an error
   553  	if resp.StatusCode != http.StatusOK && response.Error() == nil {
   554  		return &response, fmt.Errorf("received status code %d from server",
   555  			resp.StatusCode)
   556  	}
   557  	return &response, nil
   558  }
   559  
   560  // duplexReader reads responses and writes it to another writer while
   561  // satisfying the reader interface.
   562  type duplexReader struct {
   563  	r io.Reader
   564  	w io.Writer
   565  }
   566  
   567  func (r *duplexReader) Read(p []byte) (n int, err error) {
   568  	n, err = r.r.Read(p)
   569  	if err == nil {
   570  		r.w.Write(p[:n])
   571  	}
   572  	return n, err
   573  }
   574  
   575  // ChunkedResponse represents a response from the server that
   576  // uses chunking to stream the output.
   577  type ChunkedResponse struct {
   578  	dec    *json.Decoder
   579  	duplex *duplexReader
   580  	buf    bytes.Buffer
   581  }
   582  
   583  // NewChunkedResponse reads a stream and produces responses from the stream.
   584  func NewChunkedResponse(r io.Reader) *ChunkedResponse {
   585  	resp := &ChunkedResponse{}
   586  	resp.duplex = &duplexReader{r: r, w: &resp.buf}
   587  	resp.dec = json.NewDecoder(resp.duplex)
   588  	resp.dec.UseNumber()
   589  	return resp
   590  }
   591  
   592  // NextResponse reads the next line of the stream and returns a response.
   593  func (r *ChunkedResponse) NextResponse() (*Response, error) {
   594  	var response Response
   595  
   596  	if err := r.dec.Decode(&response); err != nil {
   597  		if err == io.EOF {
   598  			return nil, nil
   599  		}
   600  		// A decoding error happened. This probably means the server crashed
   601  		// and sent a last-ditch error message to us. Ensure we have read the
   602  		// entirety of the connection to get any remaining error text.
   603  		io.Copy(ioutil.Discard, r.duplex)
   604  		return nil, errors.New(strings.TrimSpace(r.buf.String()))
   605  	}
   606  
   607  	r.buf.Reset()
   608  	return &response, nil
   609  }