2018-03-23 10:50:16 -05:00
package elasticsearch
import (
2023-05-03 11:09:18 -05:00
"bytes"
2018-03-23 10:50:16 -05:00
"context"
2021-07-15 09:45:59 -05:00
"encoding/json"
"errors"
2018-03-23 10:50:16 -05:00
"fmt"
2023-05-03 11:09:18 -05:00
"io"
"net/http"
"net/url"
"path"
2022-04-11 03:29:49 -05:00
"strconv"
2023-05-05 04:35:30 -05:00
"strings"
2023-09-07 06:54:31 -05:00
"time"
2018-05-23 07:36:41 -05:00
2021-07-15 09:45:59 -05:00
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/backend/datasource"
2024-07-19 01:01:41 -05:00
"github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
2021-07-15 09:45:59 -05:00
"github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt"
2024-07-19 01:51:18 -05:00
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
2024-02-16 09:28:46 -06:00
exp "github.com/grafana/grafana-plugin-sdk-go/experimental/errorsource"
2023-11-06 04:36:39 -06:00
exphttpclient "github.com/grafana/grafana-plugin-sdk-go/experimental/errorsource/httpclient"
2022-11-02 09:03:50 -05:00
2019-10-02 06:59:05 -05:00
es "github.com/grafana/grafana/pkg/tsdb/elasticsearch/client"
2018-03-23 10:50:16 -05:00
)
2024-03-18 12:01:33 -05:00
const (
// headerFromExpression is used by data sources to identify expression queries
headerFromExpression = "X-Grafana-From-Expr"
2024-05-13 08:39:37 -05:00
// headerFromAlert is used by data sources to identify alert queries
2024-03-18 12:01:33 -05:00
headerFromAlert = "FromAlert"
2024-05-13 08:39:37 -05:00
// this is the default value for the maxConcurrentShardRequests setting - it should be in sync with the default value in the datasource config settings
defaultMaxConcurrentShardRequests = int64 ( 5 )
2024-03-18 12:01:33 -05:00
)
2021-07-15 09:45:59 -05:00
type Service struct {
2024-07-19 01:01:41 -05:00
im instancemgmt . InstanceManager
2024-07-19 01:51:18 -05:00
logger log . Logger
2018-03-23 10:50:16 -05:00
}
2024-07-19 02:26:10 -05:00
func ProvideService ( httpClientProvider * httpclient . Provider ) * Service {
2021-07-15 09:45:59 -05:00
return & Service {
2024-07-19 01:01:41 -05:00
im : datasource . NewInstanceManager ( newInstanceSettings ( httpClientProvider ) ) ,
2024-07-19 01:51:18 -05:00
logger : backend . NewLoggerWith ( "logger" , "tsdb.elasticsearch" ) ,
2018-03-26 06:48:57 -05:00
}
2021-07-15 09:45:59 -05:00
}
2018-03-26 06:48:57 -05:00
2021-07-15 09:45:59 -05:00
func ( s * Service ) QueryData ( ctx context . Context , req * backend . QueryDataRequest ) ( * backend . QueryDataResponse , error ) {
2023-05-24 03:19:34 -05:00
dsInfo , err := s . getDSInfo ( ctx , req . PluginContext )
2024-03-18 12:01:33 -05:00
_ , fromAlert := req . Headers [ headerFromAlert ]
2024-07-19 01:51:18 -05:00
logger := s . logger . FromContext ( ctx ) . With ( "fromAlert" , fromAlert )
2023-09-07 06:54:16 -05:00
2018-03-23 10:50:16 -05:00
if err != nil {
2023-09-07 06:54:16 -05:00
logger . Error ( "Failed to get data source info" , "error" , err )
2021-07-15 09:45:59 -05:00
return & backend . QueryDataResponse { } , err
2018-03-23 10:50:16 -05:00
}
2024-07-19 02:26:10 -05:00
return queryData ( ctx , req , dsInfo , logger )
2022-11-28 07:59:57 -06:00
}
// separate function to allow testing the whole transformation and query flow
2024-07-19 02:26:10 -05:00
func queryData ( ctx context . Context , req * backend . QueryDataRequest , dsInfo * es . DatasourceInfo , logger log . Logger ) ( * backend . QueryDataResponse , error ) {
2024-03-18 12:01:33 -05:00
if len ( req . Queries ) == 0 {
2022-05-05 09:16:34 -05:00
return & backend . QueryDataResponse { } , fmt . Errorf ( "query contains no queries" )
}
2024-07-19 02:26:10 -05:00
client , err := es . NewClient ( ctx , dsInfo , logger )
2021-07-15 09:45:59 -05:00
if err != nil {
return & backend . QueryDataResponse { } , err
2019-06-25 01:52:17 -05:00
}
2024-07-19 02:26:10 -05:00
query := newElasticsearchDataQuery ( ctx , client , req , logger )
2018-05-23 08:09:58 -05:00
return query . execute ( )
2018-03-23 10:50:16 -05:00
}
2021-07-15 09:45:59 -05:00
2024-07-19 01:01:41 -05:00
func newInstanceSettings ( httpClientProvider * httpclient . Provider ) datasource . InstanceFactoryFunc {
2023-10-16 09:40:04 -05:00
return func ( ctx context . Context , settings backend . DataSourceInstanceSettings ) ( instancemgmt . Instance , error ) {
2023-08-30 10:46:47 -05:00
jsonData := map [ string ] any { }
2021-07-15 09:45:59 -05:00
err := json . Unmarshal ( settings . JSONData , & jsonData )
if err != nil {
return nil , fmt . Errorf ( "error reading settings: %w" , err )
}
2023-10-16 09:40:04 -05:00
httpCliOpts , err := settings . HTTPClientOptions ( ctx )
2021-07-15 09:45:59 -05:00
if err != nil {
return nil , fmt . Errorf ( "error getting http options: %w" , err )
}
2021-09-27 07:32:19 -05:00
// Set SigV4 service namespace
if httpCliOpts . SigV4 != nil {
httpCliOpts . SigV4 . Service = "es"
}
2024-02-06 00:11:54 -06:00
// set the default middlewars from the httpClientProvider
2024-07-19 01:01:41 -05:00
httpCliOpts . Middlewares = httpClientProvider . Opts . Middlewares
2023-11-06 04:36:39 -06:00
// enable experimental http client to support errors with source
httpCli , err := exphttpclient . New ( httpCliOpts )
2023-01-23 09:43:55 -06:00
if err != nil {
return nil , err
}
2023-03-28 09:04:56 -05:00
// we used to have a field named `esVersion`, please do not use this name in the future.
2021-07-15 09:45:59 -05:00
timeField , ok := jsonData [ "timeField" ] . ( string )
if ! ok {
2024-09-27 14:40:35 -05:00
return nil , exp . DownstreamError ( errors . New ( "timeField cannot be cast to string" ) , false )
2021-07-15 09:45:59 -05:00
}
if timeField == "" {
2024-09-27 14:40:35 -05:00
return nil , exp . DownstreamError ( errors . New ( "elasticsearch time field name is required" ) , false )
2021-07-15 09:45:59 -05:00
}
2023-03-01 04:50:56 -06:00
logLevelField , ok := jsonData [ "logLevelField" ] . ( string )
if ! ok {
logLevelField = ""
}
logMessageField , ok := jsonData [ "logMessageField" ] . ( string )
if ! ok {
logMessageField = ""
}
2021-07-15 09:45:59 -05:00
interval , ok := jsonData [ "interval" ] . ( string )
if ! ok {
interval = ""
}
2023-04-19 03:30:09 -05:00
index , ok := jsonData [ "index" ] . ( string )
if ! ok {
index = ""
}
if index == "" {
index = settings . Database
}
2024-05-13 08:39:37 -05:00
var maxConcurrentShardRequests int64
2022-04-11 03:29:49 -05:00
switch v := jsonData [ "maxConcurrentShardRequests" ] . ( type ) {
2024-05-13 08:39:37 -05:00
// unmarshalling from JSON will return float64 for numbers, so we need to handle that and convert to int64
2022-04-11 03:29:49 -05:00
case float64 :
2024-05-13 08:39:37 -05:00
maxConcurrentShardRequests = int64 ( v )
2022-04-11 03:29:49 -05:00
case string :
2024-05-13 08:39:37 -05:00
maxConcurrentShardRequests , err = strconv . ParseInt ( v , 10 , 64 )
2022-04-11 03:29:49 -05:00
if err != nil {
2024-05-13 08:39:37 -05:00
maxConcurrentShardRequests = defaultMaxConcurrentShardRequests
2022-04-11 03:29:49 -05:00
}
default :
2024-05-13 08:39:37 -05:00
maxConcurrentShardRequests = defaultMaxConcurrentShardRequests
}
if maxConcurrentShardRequests <= 0 {
maxConcurrentShardRequests = defaultMaxConcurrentShardRequests
2021-07-15 09:45:59 -05:00
}
includeFrozen , ok := jsonData [ "includeFrozen" ] . ( bool )
if ! ok {
includeFrozen = false
}
2023-03-01 04:50:56 -06:00
configuredFields := es . ConfiguredFields {
TimeField : timeField ,
LogLevelField : logLevelField ,
LogMessageField : logMessageField ,
}
2021-07-15 09:45:59 -05:00
model := es . DatasourceInfo {
ID : settings . ID ,
URL : settings . URL ,
2022-09-26 07:27:46 -05:00
HTTPClient : httpCli ,
2023-04-19 03:30:09 -05:00
Database : index ,
2024-05-13 08:39:37 -05:00
MaxConcurrentShardRequests : maxConcurrentShardRequests ,
2023-03-01 04:50:56 -06:00
ConfiguredFields : configuredFields ,
2021-07-15 09:45:59 -05:00
Interval : interval ,
IncludeFrozen : includeFrozen ,
}
return model , nil
}
}
2023-05-24 03:19:34 -05:00
func ( s * Service ) getDSInfo ( ctx context . Context , pluginCtx backend . PluginContext ) ( * es . DatasourceInfo , error ) {
i , err := s . im . Get ( ctx , pluginCtx )
2021-07-15 09:45:59 -05:00
if err != nil {
return nil , err
}
instance := i . ( es . DatasourceInfo )
return & instance , nil
}
2023-05-03 11:09:18 -05:00
func ( s * Service ) CallResource ( ctx context . Context , req * backend . CallResourceRequest , sender backend . CallResourceResponseSender ) error {
2024-07-19 01:51:18 -05:00
logger := s . logger . FromContext ( ctx )
2023-05-03 11:09:18 -05:00
// allowed paths for resource calls:
// - empty string for fetching db version
2024-01-23 05:41:13 -06:00
// - /_mapping for fetching index mapping, e.g. requests going to `index/_mapping`
2023-05-06 03:00:43 -05:00
// - _msearch for executing getTerms queries
2024-01-23 05:41:13 -06:00
// - _mapping for fetching "root" index mappings
if req . Path != "" && ! strings . HasSuffix ( req . Path , "/_mapping" ) && req . Path != "_msearch" && req . Path != "_mapping" {
2023-09-07 06:54:31 -05:00
logger . Error ( "Invalid resource path" , "path" , req . Path )
2023-05-03 11:09:18 -05:00
return fmt . Errorf ( "invalid resource URL: %s" , req . Path )
}
2023-05-24 03:19:34 -05:00
ds , err := s . getDSInfo ( ctx , req . PluginContext )
2023-05-03 11:09:18 -05:00
if err != nil {
2023-09-07 06:54:31 -05:00
logger . Error ( "Failed to get data source info" , "error" , err )
2023-05-03 11:09:18 -05:00
return err
}
2024-02-13 06:44:08 -06:00
esUrl , err := createElasticsearchURL ( req , ds )
2023-05-03 11:09:18 -05:00
if err != nil {
2024-02-13 06:44:08 -06:00
logger . Error ( "Failed to create request url" , "error" , err , "url" , ds . URL , "path" , req . Path )
2023-05-03 11:09:18 -05:00
}
2024-06-05 08:54:04 -05:00
request , err := http . NewRequestWithContext ( ctx , req . Method , esUrl , bytes . NewBuffer ( req . Body ) )
2023-05-03 11:09:18 -05:00
if err != nil {
2024-06-05 08:54:04 -05:00
logger . Error ( "Failed to create request" , "error" , err , "url" , esUrl )
2023-05-03 11:09:18 -05:00
return err
}
2023-09-07 06:54:31 -05:00
logger . Debug ( "Sending request to Elasticsearch" , "resourcePath" , req . Path )
start := time . Now ( )
2023-05-03 11:09:18 -05:00
response , err := ds . HTTPClient . Do ( request )
if err != nil {
2023-09-07 06:54:31 -05:00
status := "error"
if errors . Is ( err , context . Canceled ) {
status = "cancelled"
}
2023-09-07 11:15:24 -05:00
lp := [ ] any { "error" , err , "status" , status , "duration" , time . Since ( start ) , "stage" , es . StageDatabaseRequest , "resourcePath" , req . Path }
2024-02-16 09:28:46 -06:00
sourceErr := exp . Error { }
if errors . As ( err , & sourceErr ) {
lp = append ( lp , "statusSource" , sourceErr . Source ( ) )
}
2023-09-07 11:15:24 -05:00
if response != nil {
lp = append ( lp , "statusCode" , response . StatusCode )
}
logger . Error ( "Error received from Elasticsearch" , lp ... )
2023-05-03 11:09:18 -05:00
return err
}
2023-09-07 11:15:24 -05:00
logger . Info ( "Response received from Elasticsearch" , "statusCode" , response . StatusCode , "status" , "ok" , "duration" , time . Since ( start ) , "stage" , es . StageDatabaseRequest , "contentLength" , response . Header . Get ( "Content-Length" ) , "resourcePath" , req . Path )
2023-05-03 11:09:18 -05:00
defer func ( ) {
if err := response . Body . Close ( ) ; err != nil {
2023-09-07 06:54:16 -05:00
logger . Warn ( "Failed to close response body" , "error" , err )
2023-05-03 11:09:18 -05:00
}
} ( )
body , err := io . ReadAll ( response . Body )
if err != nil {
2023-09-07 06:54:31 -05:00
logger . Error ( "Error reading response body bytes" , "error" , err )
2023-05-03 11:09:18 -05:00
return err
}
responseHeaders := map [ string ] [ ] string {
"content-type" : { "application/json" } ,
}
if response . Header . Get ( "Content-Encoding" ) != "" {
responseHeaders [ "content-encoding" ] = [ ] string { response . Header . Get ( "Content-Encoding" ) }
}
return sender . Send ( & backend . CallResourceResponse {
Status : response . StatusCode ,
Headers : responseHeaders ,
Body : body ,
} )
}
2024-02-13 06:44:08 -06:00
2024-06-05 08:54:04 -05:00
func createElasticsearchURL ( req * backend . CallResourceRequest , ds * es . DatasourceInfo ) ( string , error ) {
2024-02-13 06:44:08 -06:00
esUrl , err := url . Parse ( ds . URL )
if err != nil {
2024-06-05 08:54:04 -05:00
return "" , fmt . Errorf ( "failed to parse data source URL: %s, error: %w" , ds . URL , err )
2024-02-13 06:44:08 -06:00
}
esUrl . Path = path . Join ( esUrl . Path , req . Path )
2024-06-05 08:54:04 -05:00
esUrlString := esUrl . String ( )
// If the request path is empty and the URL does not end with a slash, add a slash to the URL.
// This ensures that for version checks executed to the root URL, the URL ends with a slash.
// This is helpful, for example, for load balancers that expect URLs to match the pattern /.*.
if req . Path == "" && esUrlString [ len ( esUrlString ) - 1 : ] != "/" {
return esUrl . String ( ) + "/" , nil
}
return esUrlString , nil
2024-02-13 06:44:08 -06:00
}