youtubebeat/vendor/github.com/elastic/beats/libbeat/kibana/client.go

303 lines
8 KiB
Go

// Licensed to Elasticsearch B.V. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Elasticsearch B.V. licenses this file to you 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 kibana
import (
"bytes"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"path"
"strings"
"github.com/pkg/errors"
"github.com/elastic/beats/libbeat/common"
"github.com/elastic/beats/libbeat/common/transport/tlscommon"
"github.com/elastic/beats/libbeat/logp"
"github.com/elastic/beats/libbeat/outputs/transport"
)
type Connection struct {
URL string
Username string
Password string
Headers map[string]string
http *http.Client
version string
}
type Client struct {
Connection
}
func addToURL(_url, _path string, params url.Values) string {
if len(params) == 0 {
return _url + _path
}
return strings.Join([]string{_url, _path, "?", params.Encode()}, "")
}
func extractError(result []byte) error {
var kibanaResult struct {
Objects []struct {
Error struct {
Message string
}
}
}
if err := json.Unmarshal(result, &kibanaResult); err != nil {
return errors.Wrap(err, "parsing kibana response")
}
for _, o := range kibanaResult.Objects {
if o.Error.Message != "" {
return errors.New(kibanaResult.Objects[0].Error.Message)
}
}
return nil
}
// NewKibanaClient builds and returns a new Kibana client
func NewKibanaClient(cfg *common.Config) (*Client, error) {
config := defaultClientConfig
if err := cfg.Unpack(&config); err != nil {
return nil, err
}
return NewClientWithConfig(&config)
}
// NewClientWithConfig creates and returns a kibana client using the given config
func NewClientWithConfig(config *ClientConfig) (*Client, error) {
p := config.Path
if config.SpaceID != "" {
p = path.Join(p, "s", config.SpaceID)
}
kibanaURL, err := common.MakeURL(config.Protocol, p, config.Host, 5601)
if err != nil {
return nil, fmt.Errorf("invalid Kibana host: %v", err)
}
u, err := url.Parse(kibanaURL)
if err != nil {
return nil, fmt.Errorf("failed to parse the Kibana URL: %v", err)
}
username := config.Username
password := config.Password
if u.User != nil {
username = u.User.Username()
password, _ = u.User.Password()
u.User = nil
// Re-write URL without credentials.
kibanaURL = u.String()
}
logp.Info("Kibana url: %s", kibanaURL)
var dialer, tlsDialer transport.Dialer
tlsConfig, err := tlscommon.LoadTLSConfig(config.TLS)
if err != nil {
return nil, fmt.Errorf("fail to load the TLS config: %v", err)
}
dialer = transport.NetDialer(config.Timeout)
tlsDialer, err = transport.TLSDialer(dialer, tlsConfig, config.Timeout)
if err != nil {
return nil, err
}
client := &Client{
Connection: Connection{
URL: kibanaURL,
Username: username,
Password: password,
http: &http.Client{
Transport: &http.Transport{
Dial: dialer.Dial,
DialTLS: tlsDialer.Dial,
},
Timeout: config.Timeout,
},
},
}
if !config.IgnoreVersion {
if err = client.SetVersion(); err != nil {
return nil, fmt.Errorf("fail to get the Kibana version: %v", err)
}
}
return client, nil
}
func (conn *Connection) Request(method, extraPath string,
params url.Values, headers http.Header, body io.Reader) (int, []byte, error) {
reqURL := addToURL(conn.URL, extraPath, params)
req, err := http.NewRequest(method, reqURL, body)
if err != nil {
return 0, nil, fmt.Errorf("fail to create the HTTP %s request: %v", method, err)
}
if conn.Username != "" || conn.Password != "" {
req.SetBasicAuth(conn.Username, conn.Password)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Add("Accept", "application/json")
for header, values := range headers {
for _, value := range values {
req.Header.Add(header, value)
}
}
if method != "GET" {
req.Header.Set("kbn-version", conn.version)
}
resp, err := conn.http.Do(req)
if err != nil {
return 0, nil, fmt.Errorf("fail to execute the HTTP %s request: %v", method, err)
}
defer resp.Body.Close()
var retError error
if resp.StatusCode >= 300 {
retError = fmt.Errorf("%v", resp.Status)
}
result, err := ioutil.ReadAll(resp.Body)
if err != nil {
return 0, nil, fmt.Errorf("fail to read response %s", err)
}
retError = extractError(result)
return resp.StatusCode, result, retError
}
func (client *Client) SetVersion() error {
type kibanaVersionResponse struct {
Name string `json:"name"`
Version struct {
Number string `json:"number"`
Snapshot bool `json:"build_snapshot"`
} `json:"version"`
}
type kibanaVersionResponse5x struct {
Name string `json:"name"`
Version string `json:"version"`
}
code, result, err := client.Connection.Request("GET", "/api/status", nil, nil, nil)
if err != nil || code >= 400 {
return fmt.Errorf("HTTP GET request to /api/status fails: %v. Response: %s.",
err, truncateString(result))
}
var kibanaVersion kibanaVersionResponse
var kibanaVersion5x kibanaVersionResponse5x
err = json.Unmarshal(result, &kibanaVersion)
if err != nil {
// The response returned by /api/status is different in Kibana 5.x than in Kibana 6.x
err5x := json.Unmarshal(result, &kibanaVersion5x)
if err5x != nil {
return fmt.Errorf("fail to unmarshal the response from GET %s/api/status. Response: %s. Kibana 5.x status api returns: %v. Kibana 6.x status api returns: %v",
client.Connection.URL, truncateString(result), err5x, err)
}
client.version = kibanaVersion5x.Version
} else {
client.version = kibanaVersion.Version.Number
if kibanaVersion.Version.Snapshot {
// needed for the tests
client.version = client.version + "-SNAPSHOT"
}
}
return nil
}
func (client *Client) GetVersion() string { return client.version }
func (client *Client) ImportJSON(url string, params url.Values, jsonBody map[string]interface{}) error {
body, err := json.Marshal(jsonBody)
if err != nil {
logp.Err("Failed to json encode body (%v): %#v", err, jsonBody)
return fmt.Errorf("fail to marshal the json content: %v", err)
}
statusCode, response, err := client.Connection.Request("POST", url, params, nil, bytes.NewBuffer(body))
if err != nil {
return fmt.Errorf("%v. Response: %s", err, truncateString(response))
}
if statusCode >= 300 {
return fmt.Errorf("returned %d to import file: %v. Response: %s", statusCode, err, response)
}
return nil
}
func (client *Client) Close() error { return nil }
// GetDashboard returns the dashboard with the given id with the index pattern removed
func (client *Client) GetDashboard(id string) (common.MapStr, error) {
params := url.Values{}
params.Add("dashboard", id)
_, response, err := client.Request("GET", "/api/kibana/dashboards/export", params, nil, nil)
if err != nil {
return nil, fmt.Errorf("error exporting dashboard: %+v", err)
}
result, err := RemoveIndexPattern(response)
if err != nil {
return nil, fmt.Errorf("error removing index pattern: %+v", err)
}
return result, nil
}
// truncateString returns a truncated string if the length is greater than 250
// runes. If the string is truncated "... (truncated)" is appended. Newlines are
// replaced by spaces in the returned string.
//
// This function is useful for logging raw HTTP responses with errors when those
// responses can be very large (such as an HTML page with CSS content).
func truncateString(b []byte) string {
const maxLength = 250
runes := bytes.Runes(b)
if len(runes) > maxLength {
runes = append(runes[:maxLength], []rune("... (truncated)")...)
}
return strings.Replace(string(runes), "\n", " ", -1)
}