304 lines
8 KiB
Go
304 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)
|
||
|
}
|