Documentation ¶
Overview ¶
Package grafana_json_server provides a Go implementation of the JSON API Grafana Datasource, which provides a way of sending JSON-formatted data into Grafana dataframes.
Creating a JSON API server for a metric ¶
A metric is the source of data to be sent to Grafana. In its simplest form, running a JSON API server for a metric requires creating a server for that metric and starting an HTTP listener:
s := grafana_json_server.NewServer( grafana_json_server.WithHandler("metric1", query), ) _ = http.ListenAndServe(":8080", s)
This starts a JSON API server for a single metric, called 'metric1' and a query, which implements the Query interface and will generate the data for the metric.
To provide more configuration options for the metric, use WithMetric instead:
metric := grafana_json_server.Metric{ Label: "My first metric", Value: "metric1", } s := grafana_json_server.NewServer( grafana_json_server.WithMetric(metric, query, nil), ) _ = http.ListenAndServe(":8080", s)
Writing query functions ¶
The query function produces the data to be sent to Grafana. Queries can be of one of two types:
- time series queries return values as a list of timestamp/value tuples.
- table queries return data organized in columns and rows. Each column needs to have the same number of rows
Time series queries can therefore only return a single set of values. If your query involves returning multiple sets of data, use table queries instead.
Writing time series queries ¶
A time series query returns a TimeSeriesResponse:
func Query(_ context.Context, target string, req grafanaJSONServer.QueryRequest) (grafanaJSONServer.QueryResponse, error) { return grafanaJSONServer.TimeSeriesResponse{ Target: target, DataPoints: []grafanaJSONServer.DataPoint{ {Timestamp: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC), Value: 100}, {Timestamp: time.Date(2020, 1, 1, 0, 1, 0, 0, time.UTC), Value: 101}, {Timestamp: time.Date(2020, 1, 1, 0, 2, 0, 0, time.UTC), Value: 103}, }, }, nil }
Writing table queries ¶
A table query returns a TableResponse:
func Query(_ context.Context, _ string, _ grafanaJSONServer.QueryRequest) (grafanaJSONServer.QueryResponse, error) { return grafanaJSONServer.TableResponse{ Columns: []grafanaJSONServer.Column{ {Text: "Time", Data: grafanaJSONServer.TimeColumn{ time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC), time.Date(2020, 1, 1, 0, 1, 0, 0, time.UTC)}}, {Text: "Label", Data: grafanaJSONServer.StringColumn{"foo", "bar"}}, {Text: "Series A", Data: grafanaJSONServer.NumberColumn{42, 43}}, {Text: "Series B", Data: grafanaJSONServer.NumberColumn{64.5, 100.0}}, }, }, nil }
Note that the table must be 'complete', i.e. each column should have the same number of entries.
Metric Payload Options ¶
The JSON API Grafana Datasource allows each metric to have a number of user-selectable options. In the Grafana Edit panel, these are shown in the Payload section of the metric.
To add payload options to the metric, configure these when creating the metric:
metric := grafanaJSONServer.Metric{ Label: "my advanced metric", Value: "metric1", Payloads: []grafanaJSONServer.MetricPayload{ { Label: "Option", Name: "option", Type: "multi-select", Width: 40, Options: []grafanaJSONServer.MetricPayloadOption{ {Label: "Option 1", Value: "option1"}, {Label: "Option 2", Value: "option2"}, }, }, }, }
The above example configures a new metric, with one payload option, called "option". The option allows multiple values to be selected. Two possible options are configured: option1 and option2.
When performing the query, the query function can check which options are selected by reading the request's payload:
var payload struct { Option []string } _ = req.GetPayload(target, &payload)
The payload Option field will contain all selected options, i.e. option1, option2. If no options are selected, Option will be an empty slice.
Note: the payload structure must match the metric's payload definition. Otherwise GetPayload returns an error. With the above metric definition, the following will fail:
var payload struct { Option string } _ = req.GetPayload(target, &payload)
Since Option is a multi-select option, a slice is expected.
Dynamic Metric Payload Options ¶
In the previous section, we configured a metric with hard-coded options. If we want to have dynamic options, we use the MetricPayloadOption function when creating the metric. Possible use cases for this could be if runtime conditions determine which options you want to present, or if you want to determine valid options based on what other options are selected.
metric := grafanaJSONServer.Metric{ Label: "my advanced metric", Value: "metric1", Payloads: []grafanaJSONServer.MetricPayload{ {Label: "Option 1", Name: "option1", Type: "select", Width: 40, Options: []grafanaJSONServer.MetricPayloadOption{ {Label: "Mode 1", Value: "mode1"}, {Label: "Mode 2", Value: "mode2"}, }}, {Label: "Option 2", Name: "option2", Type: "select", Width: 40}, }, } s := grafanaJSONServer.NewServer(grafanaJSONServer.WithMetric(metric, metricOptionsDynamicQuery, metricPayloadOptionsFunc))
This creates a single metric, metric1. The metric has two payload options, option1 and option2. The former has two hardcoded options. The latter has no Options configured. This will cause the data source plugin to call metricPayloadOptionsFunc to determine which options to present.
The following is a basic example of such a MetricPayloadOption function:
func Query(req grafanaJSONServer.MetricPayloadOptionsRequest) ([]grafanaJSONServer.MetricPayloadOption, error) { var payload struct { Option1 string Option2 string } if err := req.GetPayload(&payload); err != nil { return nil, err } // payload will now contain all selected options across all metric's payloads // req.Name tells us for which metric the function was called return []grafanaJSONServer.MetricPayloadOption{ {Label: "Value 1", Value: "value1"}, {Label: "Value 2", Value: "value2"}, }, nil }
Variables ¶
The JSON API datasource supports dashboard variable values to be retrieved from an JSON API server. To configure this, add a dashboard variable with the variable type set to "Query" and the data source to your JSON API server.
In the server, create the server with the WithVariable option:
s := grafanaJSONServer.NewServer( grafanaJSONServer.WithVariable("query0", variableFunc), ) _ = http.ListenAndServe(":8080", s)
In the example, "query0" is the name specified in the Query section of the variable definition.
This causes Grafana to call the variableFunc whenever the variable is refreshed. This function returns all possible values for the variable:
func variableFunc(_ grafanaJSONServer.VariableRequest) ([]grafanaJSONServer.Variable, error) { return []grafanaJSONServer.Variable{ {Text: "Value 1", Value: "value1"}, {Text: "Value 2", Value: "value2"}, }, nil }
A Query function can read the value of each variables by examining the ScopedVars in the QueryRequest:
var req grafanaJSONServer.QueryRequest var scopedVars struct { Var1 grafanaJSONServer.ScopedVar[string] Query0 grafanaJSONServer.ScopedVar[[]string] } _ = req.GetScopedVars(&scopedVars))
Note: in non-Raw JSON mode, GrafanaJSONDatasource stores the Query name in a json payload, with a "target" field holding the Query name.
In Raw JSON mode, a user can specify any JSON structure as payload. To route the request to the correct VariableFunc, add a "target" field with the relevant name. If no "target" field is found, the request is routed to a VariableFunc with a blank ("") target:
- Query(non-raw): "foo" will route the request to "foo"
- Query(raw): "{ "target": "bar" }" will route the request to "bar"
- Query(raw): "{ "args": "some-args" }" will route the request to ""
See the Variable example for more.
Example (BasicTableQuery) ¶
package main import ( "context" grafanaJSONServer "github.com/clambin/grafana-json-server" "net/http" "time" ) func main() { s := grafanaJSONServer.NewServer(grafanaJSONServer.WithHandler("metric1", grafanaJSONServer.HandlerFunc(basicTableQueryFunc))) _ = http.ListenAndServe(":8080", s) } func basicTableQueryFunc(_ context.Context, _ string, _ grafanaJSONServer.QueryRequest) (grafanaJSONServer.QueryResponse, error) { return grafanaJSONServer.TableResponse{ Columns: []grafanaJSONServer.Column{ {Text: "Time", Data: grafanaJSONServer.TimeColumn{ time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC), time.Date(2020, 1, 1, 0, 1, 0, 0, time.UTC)}, }, {Text: "Label", Data: grafanaJSONServer.StringColumn{"foo", "bar"}}, {Text: "Series A", Data: grafanaJSONServer.NumberColumn{42, 43}}, {Text: "Series B", Data: grafanaJSONServer.NumberColumn{64.5, 100.0}}, }, }, nil }
Output:
Example (BasicTimeSeriesQuery) ¶
package main import ( "context" grafanaJSONServer "github.com/clambin/grafana-json-server" "net/http" "time" ) func main() { s := grafanaJSONServer.NewServer(grafanaJSONServer.WithHandler("metric1", grafanaJSONServer.HandlerFunc(basicTimeSeriesQueryFunc))) _ = http.ListenAndServe(":8080", s) } func basicTimeSeriesQueryFunc(_ context.Context, target string, _ grafanaJSONServer.QueryRequest) (grafanaJSONServer.QueryResponse, error) { return grafanaJSONServer.TimeSeriesResponse{ Target: target, DataPoints: []grafanaJSONServer.DataPoint{ {Timestamp: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC), Value: 100}, {Timestamp: time.Date(2020, 1, 1, 0, 1, 0, 0, time.UTC), Value: 101}, {Timestamp: time.Date(2020, 1, 1, 0, 2, 0, 0, time.UTC), Value: 103}, }, }, nil }
Output:
Example (MetricOptions) ¶
package main import ( "context" grafanaJSONServer "github.com/clambin/grafana-json-server" "net/http" "time" ) func main() { metric := grafanaJSONServer.Metric{ Label: "my advanced metric", Value: "metric1", Payloads: []grafanaJSONServer.MetricPayload{ { Label: "Option", Name: "option", Type: "multi-select", Width: 40, Options: []grafanaJSONServer.MetricPayloadOption{ {Label: "Option 1", Value: "option1"}, {Label: "Option 2", Value: "option2"}, }, }, }, } s := grafanaJSONServer.NewServer(grafanaJSONServer.WithMetric(metric, grafanaJSONServer.HandlerFunc(metricOptionsQueryFunc), nil)) _ = http.ListenAndServe(":8080", s) } func metricOptionsQueryFunc(_ context.Context, target string, req grafanaJSONServer.QueryRequest) (grafanaJSONServer.QueryResponse, error) { var payload struct { Option []string } if err := req.GetPayload(target, &payload); err != nil { return nil, err } // payload Option will now contain all selected options, i.e. option1, option2. If no options are selected, Option will be an empty slice. return grafanaJSONServer.TimeSeriesResponse{ Target: target, DataPoints: []grafanaJSONServer.DataPoint{ {Timestamp: time.Now(), Value: 1.0}, }, }, nil }
Output:
Example (MetricOptionsDynamic) ¶
package main import ( "context" grafanaJSONServer "github.com/clambin/grafana-json-server" "net/http" "time" ) func main() { metric := grafanaJSONServer.Metric{ Label: "my advanced metric", Value: "metric1", Payloads: []grafanaJSONServer.MetricPayload{ {Label: "Option 1", Name: "option1", Type: "select", Width: 40, Options: []grafanaJSONServer.MetricPayloadOption{ {Label: "Mode 1", Value: "mode1"}, {Label: "Mode 2", Value: "mode2"}, }}, {Label: "Option 2", Name: "option2", Type: "select", Width: 40}, }, } s := grafanaJSONServer.NewServer(grafanaJSONServer.WithMetric(metric, grafanaJSONServer.HandlerFunc(metricOptionsDynamicQueryFunc), metricPayloadOptionsFunc)) _ = http.ListenAndServe(":8080", s) } func metricPayloadOptionsFunc(req grafanaJSONServer.MetricPayloadOptionsRequest) ([]grafanaJSONServer.MetricPayloadOption, error) { var payload struct { Option1 string Option2 string } if err := req.GetPayload(&payload); err != nil { return nil, err } // payload will now contain all selected options across all metric's payloads // req.Name tells us for metric payload the function was called return []grafanaJSONServer.MetricPayloadOption{ {Label: "Value 1", Value: "value1"}, {Label: "Value 2", Value: "value2"}, }, nil } func metricOptionsDynamicQueryFunc(_ context.Context, target string, req grafanaJSONServer.QueryRequest) (grafanaJSONServer.QueryResponse, error) { var payload struct { Option1 string Option2 string } if err := req.GetPayload(target, &payload); err != nil { return nil, err } // payload will now contain all selected options across all metric's payloads return grafanaJSONServer.TimeSeriesResponse{ Target: target, DataPoints: []grafanaJSONServer.DataPoint{ {Timestamp: time.Now(), Value: 1.0}, }, }, nil }
Output:
Example (Variable) ¶
package main import ( grafanaJSONServer "github.com/clambin/grafana-json-server" "net/http" ) func main() { s := grafanaJSONServer.NewServer( grafanaJSONServer.WithVariable("query0", variableFunc), // this will be called if the payload contains "target": "query0" grafanaJSONServer.WithVariable("", variableFunc), // this will be called if the payload contains no "target" ) _ = http.ListenAndServe(":8080", s) } func variableFunc(_ grafanaJSONServer.VariableRequest) ([]grafanaJSONServer.Variable, error) { return []grafanaJSONServer.Variable{ {Text: "Value 1", Value: "value1"}, {Text: "Value 2", Value: "value2"}, }, nil }
Output:
Index ¶
- type Column
- type DataPoint
- type Handler
- type HandlerFunc
- type Metric
- type MetricPayload
- type MetricPayloadOption
- type MetricPayloadOptionFunc
- type MetricPayloadOptionsRequest
- type NumberColumn
- type Option
- func WithHTTPHandler(method, path string, handler http.Handler) Option
- func WithHandler(target string, handler Handler) Option
- func WithLogger(l *slog.Logger) Option
- func WithMetric(m Metric, handler Handler, payloadOption MetricPayloadOptionFunc) Option
- func WithPrometheusQueryMetrics(metrics PrometheusQueryMetrics) Option
- func WithVariable(name string, v VariableFunc) Option
- type PrometheusQueryMetrics
- type QueryRequest
- type QueryRequestTarget
- type QueryResponse
- type Range
- type RawRange
- type ScopedVar
- type Server
- type StringColumn
- type TableResponse
- type TimeColumn
- type TimeSeriesResponse
- type Variable
- type VariableFunc
- type VariableRequest
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Column ¶
Column is a column returned by a table query. Text holds the column's header, Data holds the slice of values and should be a TimeColumn, a StringColumn or a NumberColumn.
type DataPoint ¶
DataPoint contains one entry of a TimeSeriesResponse.
func (DataPoint) MarshalJSON ¶
MarshalJSON converts a DataPoint to JSON.
type Handler ¶ added in v0.3.0
type Handler interface {
Query(ctx context.Context, target string, request QueryRequest) (QueryResponse, error)
}
A Handler responds to a query request from the JSON API datasource.
type HandlerFunc ¶ added in v0.3.0
type HandlerFunc func(ctx context.Context, target string, request QueryRequest) (QueryResponse, error)
The HandlerFunc type is an adapter to allow the use of an ordinary function as Handler handlers. If f is a function with the appropriate signature, then HandlerFunc(f) is a Handler that calls f.
func (HandlerFunc) Query ¶ added in v0.3.0
func (qf HandlerFunc) Query(ctx context.Context, target string, request QueryRequest) (QueryResponse, error)
Query calls f(ctx, target, request)
type Metric ¶
type Metric struct { // Label is the name of the metric, as shown on the screen. Label string `json:"label,omitempty"` // Value is the internal name of the metric, used in API calls. Value string `json:"value"` // Payloads configures one or more payload options for the metric. Payloads []MetricPayload `json:"payloads"` }
A Metric represents one data source offered by a JSON API server.
type MetricPayload ¶
type MetricPayload struct { // Label is the name of the option, as shown on the screen. Label string `json:"label,omitempty"` // Name is the internal name of the metric, used in API calls. Name string `json:"name"` // Type specifies what kind of option should be provided: // If the value is select, the UI of the payload is a radio box. // If the value is multi-select, the UI of the payload is a multi selection box. // If the value is input, the UI of the payload is an input box. // If the value is textarea, the UI of the payload is a multiline input box. // The default is input. Type string `json:"type"` // Placeholder specifies the input box / selection box prompt information. Placeholder string `json:"placeholder,omitempty"` // ReloadMetric specifies whether to overload the metrics API after modifying the value of the payload. ReloadMetric bool `json:"reloadMetric,omitempty"` // Width specifies the width of the input / selection box width to a multiple of 8px. Width int `json:"width,omitempty"` // Options lists of the configuration of the options list, if the payload type is select / multi-select. // If Options is nil, and the type is select / multi-select, Grafana JSON API datasource will call // the MetricPayloadOptionFunc provided to WithMetric to populate the options list. Options []MetricPayloadOption `json:"options,omitempty"` }
A MetricPayload configures a payload options for a metric.
If the metric is of Type "select", or "multi-select", Options should contain all possible values for the metric. Alternatively, it may be nil, in which case you should pass a MetricPayloadOptionFunc when calling WithMetric.
type MetricPayloadOption ¶
MetricPayloadOption contains one option of a MetricPayload. Label is the value to display on the screen, while Value is the internal name used in API calls.
type MetricPayloadOptionFunc ¶
type MetricPayloadOptionFunc func(MetricPayloadOptionsRequest) ([]MetricPayloadOption, error)
MetricPayloadOptionFunc is the function signature of the metric payload option function, provided to WithMetric. It is called when a metric has a payload with a nil Options field and returns the possible options for the requested Metric payload.
type MetricPayloadOptionsRequest ¶
type MetricPayloadOptionsRequest struct { Metric string `json:"metric"` Name string `json:"name"` Payload json.RawMessage `json:"payload"` }
MetricPayloadOptionsRequest is the request provided to MetricPayloadOptionFunc.
func (MetricPayloadOptionsRequest) GetPayload ¶
func (r MetricPayloadOptionsRequest) GetPayload(payload any) error
GetPayload unmarshals the json object from the MetricPayloadOptionsRequest in the provided payload.
type NumberColumn ¶
type NumberColumn []float64
NumberColumn holds a slice of float64 values (one per row).
type Option ¶
type Option func(*Server)
Option configures a Server.
func WithHTTPHandler ¶ added in v0.3.0
WithHTTPHandler adds a http.Handler to its http router.
func WithHandler ¶ added in v0.3.0
WithHandler is a convenience function to create a simple metric (i.e. one without any payload options).
func WithLogger ¶
WithLogger sets the slog Logger. The default is slog.Default().
func WithMetric ¶
func WithMetric(m Metric, handler Handler, payloadOption MetricPayloadOptionFunc) Option
WithMetric adds a new metric to the server. See Metric for more configuration options for a metric.
func WithPrometheusQueryMetrics ¶ added in v0.2.0
func WithPrometheusQueryMetrics(metrics PrometheusQueryMetrics) Option
WithPrometheusQueryMetrics adds Prometheus metrics to the server's Queries. The caller must register the metrics with the Prometheus registry.
See NewDefaultPrometheusQueryMetrics for the default implementation of Prometheus metrics.
func WithVariable ¶
func WithVariable(name string, v VariableFunc) Option
WithVariable adds a new dashboard variable to the server.
type PrometheusQueryMetrics ¶ added in v0.7.0
type PrometheusQueryMetrics interface { Measure(target string, duration time.Duration, err error) prometheus.Collector }
func NewDefaultPrometheusQueryMetrics ¶ added in v0.7.0
func NewDefaultPrometheusQueryMetrics(namespace, subsystem, application string) PrometheusQueryMetrics
NewDefaultPrometheusQueryMetrics returns the default PrometheusQueryMetrics implementation. It created two Prometheus metrics:
- json_query_duration_seconds records the duration of each query
- json_query_error_count counts the total number of errors executing a query
If namespace and/or subsystem are not blank, they are prepended to the metric name. Application is added as a label "application". The query target is added as a label "target".
type QueryRequest ¶
type QueryRequest struct { App string `json:"app"` Timezone string `json:"timezone"` StartTime int64 `json:"startTime"` Interval string `json:"interval"` IntervalMs int `json:"intervalMs"` PanelID any `json:"panelId"` Targets []QueryRequestTarget `json:"targets"` Range Range `json:"range"` RequestID string `json:"requestId"` RangeRaw RawRange `json:"rangeRaw"` ScopedVars json.RawMessage `json:"scopedVars"` MaxDataPoints int `json:"maxDataPoints"` LiveStreaming bool `json:"liveStreaming"` AdhocFilters []interface{} `json:"adhocFilters"` }
The QueryRequest structure is the query request from Grafana to the data source.
func (QueryRequest) GetPayload ¶
func (r QueryRequest) GetPayload(target string, payload any) error
GetPayload unmarshals the target's raw payload into a provided payload.
func (QueryRequest) GetScopedVars ¶
func (r QueryRequest) GetScopedVars(vars any) error
GetScopedVars unmarshals all variables in a QueryRequest into a Go structure. The vars variable should be struct of ScopedVar structs, matching the type of the variable. E.g. a multi-select variable should be represented by a ScopedVar[[]string].
type QueryRequestTarget ¶
type QueryRequestTarget struct { RefID string `json:"refId"` Datasource struct { Type string `json:"type"` UID string `json:"uid"` } `json:"datasource"` EditorMode string `json:"editorMode"` Payload json.RawMessage `json:"payload"` Target string `json:"target"` Key string `json:"key"` Type string `json:"type"` // TODO: is this really present? }
QueryRequestTarget is one target in the QueryRequest structure. The main interesting fields are the Target, which is the Metric's name, and the Payload, which contains all selection options in any payload options. Use GetPayload to unmarshal the Payload into a Go structure.
type QueryResponse ¶
QueryResponse is the output of the query function. Both TimeSeriesResponse and TableResponse implement this interface.
type Range ¶
type Range struct { From time.Time `json:"from"` To time.Time `json:"to"` Raw RawRange `json:"raw"` }
Range is the time range of the QueryRequest.
type ScopedVar ¶
A ScopedVar holds the value of a dashboard variable and is sent to the server as part of the QueryRequest.
type Server ¶
type Server struct { chi.Router // contains filtered or unexported fields }
The Server structure implements a JSON API server compatible with the JSON API Grafana datasource.
type StringColumn ¶
type StringColumn []string
StringColumn holds a slice of string values (one per row).
type TableResponse ¶
type TableResponse struct {
Columns []Column
}
TableResponse is returned by a table query, i.e. a slice of Column structures.
func (TableResponse) MarshalJSON ¶
func (t TableResponse) MarshalJSON() (output []byte, err error)
MarshalJSON converts a TableResponse to JSON.
type TimeColumn ¶
TimeColumn holds a slice of time.Time values (one per row).
type TimeSeriesResponse ¶
type TimeSeriesResponse struct { Target string `json:"target"` DataPoints []DataPoint `json:"datapoints"` }
TimeSeriesResponse is the response to a query as a time series. Target should match the Target of the received request.
func (TimeSeriesResponse) MarshalJSON ¶
func (r TimeSeriesResponse) MarshalJSON() ([]byte, error)
MarshalJSON converts a TimeSeriesResponse to JSON.
type Variable ¶
Variable is one possible value for a dashboard value. Text is the name to be displayed on the screen. Value will be used in API calls.
type VariableFunc ¶
type VariableFunc func(VariableRequest) ([]Variable, error)
VariableFunc is the function signature of function provided to WithVariable. It returns a list of possible values for a dashboard variable.
type VariableRequest ¶
type VariableRequest struct { Payload json.RawMessage `json:"payload"` Range Range `json:"range"` Target string }
VariableRequest is the request sent to VariableFunc.
Payload and Target are determined by the Grafana definition of the variable:
- if Raw JSON is off, Payload contains a JSON object with a single field "target", as set in the Variable's Query field.
- if Raw JSON is on, Payload contains the JSON object set in the Variable's Query field.
In both cases, if the Payload contains a field "target", its value is stored in Target. If no "target" exists, Target is blank. No error is raised.
func (*VariableRequest) UnmarshalJSON ¶ added in v0.6.0
func (v *VariableRequest) UnmarshalJSON(bytes []byte) error