@ -2,40 +2,39 @@ package loki
import (
"context"
"crypto/tls"
"encoding/json"
"fmt"
"net/http"
"regexp"
"strings"
"time"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/backend/datasource"
"github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt"
"github.com/grafana/grafana-plugin-sdk-go/data"
"github.com/grafana/grafana/pkg/infra/httpclient"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/tsdb/interval"
"github.com/grafana/grafana/pkg/plugins/backendplugin"
"github.com/grafana/grafana/pkg/plugins/backendplugin/coreplugin"
"github.com/grafana/grafana/pkg/registry"
"github.com/grafana/grafana/pkg/tsdb"
"github.com/grafana/loki/pkg/logcli/client"
"github.com/grafana/loki/pkg/loghttp"
"github.com/grafana/loki/pkg/logproto"
"github.com/opentracing/opentracing-go"
"github.com/prometheus/common/config"
"github.com/prometheus/common/model"
)
type LokiExecutor struct {
intervalCalculator interval . Calculator
httpClientProvider httpclient . Provider
}
type Service struct {
intervalCalculator tsdb . Calculator
im instancemgmt . InstanceManager
// nolint:staticcheck // plugins.DataPlugin deprecated
func New ( httpClientProvider httpclient . Provider ) func ( dsInfo * models . DataSource ) ( plugins . DataPlugin , error ) {
// nolint:staticcheck // plugins.DataPlugin deprecated
return func ( dsInfo * models . DataSource ) ( plugins . DataPlugin , error ) {
return & LokiExecutor {
intervalCalculator : interval . NewCalculator ( interval . CalculatorOptions { MinInterval : time . Second * 1 } ) ,
httpClientProvider : httpClientProvider ,
} , nil
}
HTTPClientProvider httpclient . Provider ` inject:"" `
BackendPluginManager backendplugin . Manager ` inject:"" `
}
var (
@ -43,39 +42,103 @@ var (
legendFormat = regexp . MustCompile ( ` \ { \ { \s*(.+?)\s*\}\} ` )
)
// DataQuery executes a Loki query.
//nolint: staticcheck // plugins.DataPlugin deprecated
func ( e * LokiExecutor ) DataQuery ( ctx context . Context , dsInfo * models . DataSource ,
queryContext plugins . DataQuery ) ( plugins . DataResponse , error ) {
result := plugins . DataResponse {
Results : map [ string ] plugins . DataQueryResult { } ,
type datasourceInfo struct {
HTTPClient * http . Client
URL string
TLSClientConfig * tls . Config
BasicAuthUser string
BasicAuthPassword string
TimeInterval string ` json:"timeInterval" `
}
type ResponseModel struct {
Expr string ` json:"expr" `
LegendFormat string ` json:"legendFormat" `
Interval string ` json:"interval" `
IntervalMS int ` json:"intervalMS" `
}
func init ( ) {
registry . Register ( & registry . Descriptor {
Name : "LokiService" ,
InitPriority : registry . Low ,
Instance : & Service { } ,
} )
}
func ( s * Service ) Init ( ) error {
s . im = datasource . NewInstanceManager ( newInstanceSettings ( s . HTTPClientProvider ) )
s . intervalCalculator = tsdb . NewCalculator ( )
factory := coreplugin . New ( backend . ServeOpts {
QueryDataHandler : s ,
} )
if err := s . BackendPluginManager . RegisterAndStart ( context . Background ( ) , "loki" , factory ) ; err != nil {
plog . Error ( "Failed to register plugin" , "error" , err )
}
tlsConfig , err := dsInfo . GetTLSConfig ( e . httpClientProvider )
if err != nil {
return plugins . DataResponse { } , err
return nil
}
func newInstanceSettings ( httpClientProvider httpclient . Provider ) datasource . InstanceFactoryFunc {
return func ( settings backend . DataSourceInstanceSettings ) ( instancemgmt . Instance , error ) {
opts , err := settings . HTTPClientOptions ( )
if err != nil {
return nil , err
}
client , err := httpClientProvider . New ( opts )
if err != nil {
return nil , err
}
tlsClientConfig , err := httpClientProvider . GetTLSConfig ( opts )
if err != nil {
return nil , err
}
jsonData := datasourceInfo { }
err = json . Unmarshal ( settings . JSONData , & jsonData )
if err != nil {
return nil , fmt . Errorf ( "error reading settings: %w" , err )
}
model := & datasourceInfo {
HTTPClient : client ,
URL : settings . URL ,
TLSClientConfig : tlsClientConfig ,
TimeInterval : jsonData . TimeInterval ,
BasicAuthUser : settings . BasicAuthUser ,
BasicAuthPassword : settings . DecryptedSecureJSONData [ "basicAuthPassword" ] ,
}
return model , nil
}
}
transport , err := dsInfo . GetHTTPTransport ( e . httpClientProvider )
func ( s * Service ) QueryData ( ctx context . Context , req * backend . QueryDataRequest ) ( * backend . QueryDataResponse , error ) {
result := backend . NewQueryDataResponse ( )
queryRes := backend . DataResponse { }
dsInfo , err := s . getDSInfo ( req . PluginContext )
if err != nil {
return plugins . DataResponse { } , err
return result , err
}
client := & client . DefaultClient {
Address : dsInfo . Url ,
Address : dsInfo . URL ,
Username : dsInfo . BasicAuthUser ,
Password : dsInfo . DecryptedBasicAuthPassword ( ) ,
Password : dsInfo . BasicAuthPassword ,
TLSConfig : config . TLSConfig {
InsecureSkipVerify : tlsConfig . InsecureSkipVerify ,
InsecureSkipVerify : dsInfo . TLSClient Config. InsecureSkipVerify ,
} ,
Tripperware : func ( t http . RoundTripper ) http . RoundTripper {
return transport
return dsInfo . HTTPClien t. T ransport
} ,
}
queries , err := e . parseQuery ( dsInfo , queryContext )
queries , err := s . parseQuery ( dsInfo , req )
if err != nil {
return plugins . DataResponse { } , err
return result , err
}
for _ , query := range queries {
@ -93,16 +156,16 @@ func (e *LokiExecutor) DataQuery(ctx context.Context, dsInfo *models.DataSource,
value , err := client . QueryRange ( query . Expr , limit , query . Start , query . End , logproto . BACKWARD , query . Step , interval , false )
if err != nil {
return plugins . DataResponse { } , err
return result , err
}
queryResult , err := parseResponse ( value , query )
frames , err := parseResponse ( value , query )
if err != nil {
return plugins . DataResponse { } , err
return result , err
}
result . Results [ query . RefID ] = queryResult
queryRes . Frames = frames
result . Responses [ query . RefID ] = queryRes
}
return result , nil
}
@ -125,59 +188,50 @@ func formatLegend(metric model.Metric, query *lokiQuery) string {
return string ( result )
}
func ( e * LokiExecutor ) parseQuery ( dsInfo * models . DataSource , queryContext plugins . DataQuery ) ( [ ] * lokiQuery , error ) {
func ( s * Service ) parseQuery ( dsInfo * datasourceInfo , queryContext * backend . QueryDataRequest ) ( [ ] * lokiQuery , error ) {
qs := [ ] * lokiQuery { }
for _ , queryModel := range queryContext . Queries {
expr , err := queryModel . Model . Get ( "expr" ) . String ( )
if err != nil {
return nil , fmt . Errorf ( "failed to parse Expr: %v" , err )
}
format := queryModel . Model . Get ( "legendFormat" ) . MustString ( "" )
start , err := queryContext . TimeRange . ParseFrom ( )
for _ , query := range queryContext . Queries {
model := & ResponseModel { }
err := json . Unmarshal ( query . JSON , model )
if err != nil {
return nil , fmt . Errorf ( "failed to parse From: %v" , err )
return nil , err
}
end , err := queryContext . TimeRange . ParseTo ( )
if err != nil {
return nil , fmt . Errorf ( "failed to parse To: %v" , err )
}
start := query . TimeRange . From
end := query . TimeRange . To
dsInterval , err := interval . GetIntervalFrom ( dsInfo , queryModel . Model , time . Second )
dsInterval , err := tsdb . GetIntervalFrom ( dsInfo . TimeInterval , model . Interval , int64 ( model . IntervalMS ) , time . Second )
if err != nil {
return nil , fmt . Errorf ( "failed to parse Interval: %v" , err )
}
interval , err := e . intervalCalculator . Calculate ( * queryContext . TimeRange , dsInterval , "min" )
interval , err := s . intervalCalculator . Calculate ( query . TimeRange , dsInterval , tsdb . Min )
if err != nil {
return nil , err
}
step := time . Duration ( int64 ( interval . Value ) )
qs = append ( qs , & lokiQuery {
Expr : expr ,
Expr : mod el . E xpr,
Step : step ,
LegendFormat : f ormat,
LegendFormat : model . LegendF ormat,
Start : start ,
End : end ,
RefID : queryModel . RefID ,
RefID : query . RefID ,
} )
}
return qs , nil
}
//nolint: staticcheck // plugins.DataPlugin deprecated
func parseResponse ( value * loghttp . QueryResponse , query * lokiQuery ) ( plugins . DataQueryResult , error ) {
var queryRes plugins . DataQueryResult
func parseResponse ( value * loghttp . QueryResponse , query * lokiQuery ) ( data . Frames , error ) {
frames := data . Frames { }
//We are currently processing only matrix results (for alerting)
matrix , ok := value . Data . Result . ( loghttp . Matrix )
if ! ok {
return queryR es, fmt . Errorf ( "unsupported result format: %q" , value . Data . ResultType )
return fram es, fmt . Errorf ( "unsupported result format: %q" , value . Data . ResultType )
}
for _ , v := range matrix {
@ -199,7 +253,20 @@ func parseResponse(value *loghttp.QueryResponse, query *lokiQuery) (plugins.Data
data . NewField ( "time" , nil , timeVector ) ,
data . NewField ( "value" , tags , values ) . SetConfig ( & data . FieldConfig { DisplayNameFromDS : name } ) ) )
}
queryRes . Dataframes = plugins . NewDecodedDataFrames ( frames )
return queryRes , nil
return frames , nil
}
func ( s * Service ) getDSInfo ( pluginCtx backend . PluginContext ) ( * datasourceInfo , error ) {
i , err := s . im . Get ( pluginCtx )
if err != nil {
return nil , err
}
instance , ok := i . ( * datasourceInfo )
if ! ok {
return nil , fmt . Errorf ( "failed to cast datsource info" )
}
return instance , nil
}