Expose GrafanaManagedAlert properties

This commit is contained in:
Sofia Papagiannaki committed 2021-02-18 17:35:56 +02:00
1 parent fd5fd392b2
commit eb26bb8842
647 files changed
+131406 -5

No files matched your search

+1 -1
View File
@@ -14,7 +14,7 @@ require (
golang.org/x/net v0.0.0-20210119194325-5f4716e94777 // indirect
golang.org/x/oauth2 v0.0.0-20210210192628-66670185b0cd // indirect
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
)
// **WARNING** below are cortex replace directives, copied for compatibility
+2
View File
@@ -633,6 +633,7 @@ github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad
github.com/gosimple/slug v1.9.0 h1:r5vDcYrFz9BmfIAMC829un9hq7hKM4cHUrsv36LbEqs=
github.com/gosimple/slug v1.9.0/go.mod h1:AMZ+sOVe65uByN3kgEyf9WEBKBCSS+dJjMX9x4vDJbg=
github.com/grafana/gocql v0.0.0-20200605141915-ba5dc39ece85/go.mod h1:crI9WX6p0IhrqB+DqIUHulRW853PaNFf7o4UprV//3I=
github.com/grafana/grafana v1.9.1 h1:4ykSOyQ+qImmZmILEgqsokVnWmvR8/A0zt7jN2RAQxM=
github.com/grafana/grafana v1.9.2-0.20210217182004-6c4be29655a6 h1:qb6jgR/WKFRaISkkAbXU5YaZcp5oeElRspSzGNZBJZU=
github.com/grafana/grafana v1.9.2-0.20210217182004-6c4be29655a6/go.mod h1:krMdqLnTBCyxzi+jRQ2LVlWngfWg2pihNlPD+VzzBww=
github.com/grafana/grafana-aws-sdk v0.1.0 h1:25TSkS57lvVen3nTCNMu5SdI8ju5Z0f7cvCsukhlqJc=
@@ -1770,6 +1771,7 @@ golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8T
gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo=
gonum.org/v1/gonum v0.0.0-20181121035319-3f7ecaa7e8ca/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo=
gonum.org/v1/gonum v0.6.0/go.mod h1:9mxDZsDKxgMAuccQkewq682L+0eCu4dCN2yonUJTCLU=
gonum.org/v1/gonum v0.8.2 h1:CCXrcPKiGGotvnN6jfUsKk4rRqm7q09/YbKb5xCEvtM=
gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0=
gonum.org/v1/netlib v0.0.0-20181029234149-ec6d1f5cefe6/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=
gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=
+45 -2
View File
@@ -1,9 +1,9 @@
package api
import (
"github.com/grafana/grafana/pkg/services/ngalert/eval"
"github.com/prometheus/common/model"
"github.com/prometheus/prometheus/pkg/rulefmt"
"gopkg.in/yaml.v3"
)
// swagger:route Get /api/v1/rules RulerConfig RouteGetRulesConfig
@@ -99,5 +99,48 @@ type RuleGroupConfig struct {
type ExtendedRuleNode struct {
rulefmt.RuleNode
GrafanaManagedAlert yaml.Node `yaml:"alert,omitempty"`
//GrafanaManagedAlert yaml.Node `yaml:"grafana_alert,omitempty"`
GrafanaManagedAlert ExtendedUpsertAlertDefinitionCommand `yaml:"grafana_alert,omitempty"`
}
// UpsertAlertDefinitionCommand is copy of the unexported struct:
https://github.com/grafana/grafana/blob/debb82e12417e82a0e2bd09e1a450065f884c1bc/pkg/services/ngalert/models.go#L85
type UpsertAlertDefinitionCommand struct {
Title string `json:"title"`
OrgID int64 `json:"org_id"`
Condition string `json:"condition"`
Data []eval.AlertQuery `json:"data"`
IntervalSeconds *int64 `json:"intervalSeconds"`
// UID exists is set only for existing definitins
UID string `json:"uid"`
Result *ngalert.AlertDefinition `json:"-"`
}
// swagger:enum NoDataState
type NoDataState string
const (
Alerting NoDataState = "Alerting"
NoData NoDataState = "NoData"
KeepLastState NoDataState = "KeepLastState"
OK NoDataState = "OK"
)
// swagger:enum ExecutionErrorState
type ExecutionErrorState string
const (
AlertingErrState ExecutionErrorState = "Alerting"
KeepLastStateErrState ExecutionErrorState = "KeepLastState"
)
// ExtendedUpsertAlertDefinitionCommand extends UpsertAlertDefinitionCommand
// with properties of grafana dashboard alerts
// swagger:model
type ExtendedUpsertAlertDefinitionCommand struct {
UpsertAlertDefinitionCommand
NoDataState NoDataState `json:"no_data_state"`
ExecutionErrorState ExecutionErrorState `json:"exec_err_state"`
Settings map[string]interface{} `json:"settings"`
}
+138 -2
View File
@@ -495,6 +495,31 @@
},
"x-go-package": "github.com/grafana/grafana/pkg/models"
},
"AlertQuery": {
"type": "object",
"title": "AlertQuery represents a single query associated with an alert definition.",
"properties": {
"model": {
"description": "JSON is the raw JSON query and includes the above properties as well as custom properties.",
"type": "object",
"x-go-name": "Model"
},
"queryType": {
"description": "QueryType is an optional identifier for the type of query.\nIt can be used to distinguish different types of queries.",
"type": "string",
"x-go-name": "QueryType"
},
"refId": {
"description": "RefID is the unique identifier of the query, set by the frontend call.",
"type": "string",
"x-go-name": "RefID"
},
"relativeTimeRange": {
"$ref": "#/definitions/RelativeTimeRange"
}
},
"x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/eval"
},
"AlertResponse": {
"type": "object",
"properties": {
@@ -802,9 +827,9 @@
"x-go-package": "github.com/grafana/grafana/pkg/models"
},
"Duration": {
"description": "A Duration represents the elapsed time between two instants\nas an int64 nanosecond count. The representation limits the\nlargest representable duration to approximately 290 years.",
"type": "integer",
"format": "int64",
"title": "Duration is a type used for marshalling durations.",
"$ref": "#/definitions/Duration"
},
"EmailConfig": {
@@ -906,7 +931,7 @@
"$ref": "#/definitions/Duration"
},
"GrafanaManagedAlert": {
"$ref": "#/definitions/Node"
"$ref": "#/definitions/ExtendedUpsertAlertDefinitionCommand"
},
"Labels": {
"type": "object",
@@ -920,6 +945,68 @@
},
"x-go-package": "github.com/grafana/alerting-api/pkg/api"
},
"ExtendedUpsertAlertDefinitionCommand": {
"description": "ExtendedUpsertAlertDefinitionCommand extends UpsertAlertDefinitionCommand\nwith properties of grafana dashboard alerts",
"type": "object",
"properties": {
"condition": {
"type": "string",
"x-go-name": "Condition"
},
"data": {
"type": "array",
"items": {
"$ref": "#/definitions/AlertQuery"
},
"x-go-name": "Data"
},
"exec_err_state": {
"type": "string",
"enum": [
"Alerting",
"KeepLastState"
],
"x-go-name": "ExecutionErrorState"
},
"intervalSeconds": {
"type": "integer",
"format": "int64",
"x-go-name": "IntervalSeconds"
},
"no_data_state": {
"type": "string",
"enum": [
"Alerting",
"NoData",
"KeepLastState",
"OK"
],
"x-go-name": "NoDataState"
},
"org_id": {
"type": "integer",
"format": "int64",
"x-go-name": "OrgID"
},
"settings": {
"type": "object",
"additionalProperties": {
"type": "object"
},
"x-go-name": "Settings"
},
"title": {
"type": "string",
"x-go-name": "Title"
},
"uid": {
"description": "UID exists is set only for existing definitins",
"type": "string",
"x-go-name": "UID"
}
},
"x-go-package": "github.com/grafana/alerting-api/pkg/api"
},
"GlobalConfig": {
"description": "GlobalConfig defines configuration parameters that are valid globally\nunless overwritten.",
"type": "object",
@@ -1529,6 +1616,19 @@
"title": "Regexp is the representation of a compiled regular expression.",
"x-go-package": "regexp"
},
"RelativeTimeRange": {
"description": "RelativeTimeRange is the per query start and end time\nfor requests.",
"type": "object",
"properties": {
"from": {
"$ref": "#/definitions/Duration"
},
"to": {
"$ref": "#/definitions/Duration"
}
},
"x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/eval"
},
"Route": {
"type": "object",
"title": "A Route is a node that contains definitions of how to handle alerts.",
@@ -2081,6 +2181,42 @@
"type": "object",
"x-go-package": "github.com/grafana/alerting-api/pkg/models"
},
"UpsertAlertDefinitionCommand": {
"type": "object",
"properties": {
"condition": {
"type": "string",
"x-go-name": "Condition"
},
"data": {
"type": "array",
"items": {
"$ref": "#/definitions/AlertQuery"
},
"x-go-name": "Data"
},
"intervalSeconds": {
"type": "integer",
"format": "int64",
"x-go-name": "IntervalSeconds"
},
"org_id": {
"type": "integer",
"format": "int64",
"x-go-name": "OrgID"
},
"title": {
"type": "string",
"x-go-name": "Title"
},
"uid": {
"description": "UID exists is set only for existing definitins",
"type": "string",
"x-go-name": "UID"
}
},
"x-go-package": "github.com/grafana/alerting-api/pkg/api"
},
"Userinfo": {
"description": "The Userinfo type is an immutable encapsulation of username and\npassword details for a URL. An existing Userinfo value is guaranteed\nto have a username set (potentially empty, as allowed by RFC 2396),\nand optionally a password.",
"type": "object",
+20
View File
@@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright (c) 2013 Fatih Arslan
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+173
View File
@@ -0,0 +1,173 @@
# color [![](https://github.com/fatih/color/workflows/build/badge.svg)](https://github.com/fatih/color/actions) [![PkgGoDev](https://pkg.go.dev/badge/github.com/fatih/color)](https://pkg.go.dev/github.com/fatih/color)
Color lets you use colorized outputs in terms of [ANSI Escape
Codes](http://en.wikipedia.org/wiki/ANSI_escape_code#Colors) in Go (Golang). It
has support for Windows too! The API can be used in several ways, pick one that
suits you.
![Color](https://user-images.githubusercontent.com/438920/96832689-03b3e000-13f4-11eb-9803-46f4c4de3406.jpg)
## Install
```bash
go get github.com/fatih/color
```
## Examples
### Standard colors
```go
// Print with default helper functions
color.Cyan("Prints text in cyan.")
// A newline will be appended automatically
color.Blue("Prints %s in blue.", "text")
// These are using the default foreground colors
color.Red("We have red")
color.Magenta("And many others ..")
```
### Mix and reuse colors
```go
// Create a new color object
c := color.New(color.FgCyan).Add(color.Underline)
c.Println("Prints cyan text with an underline.")
// Or just add them to New()
d := color.New(color.FgCyan, color.Bold)
d.Printf("This prints bold cyan %s\n", "too!.")
// Mix up foreground and background colors, create new mixes!
red := color.New(color.FgRed)
boldRed := red.Add(color.Bold)
boldRed.Println("This will print text in bold red.")
whiteBackground := red.Add(color.BgWhite)
whiteBackground.Println("Red text with white background.")
```
### Use your own output (io.Writer)
```go
// Use your own io.Writer output
color.New(color.FgBlue).Fprintln(myWriter, "blue color!")
blue := color.New(color.FgBlue)
blue.Fprint(writer, "This will print text in blue.")
```
### Custom print functions (PrintFunc)
```go
// Create a custom print function for convenience
red := color.New(color.FgRed).PrintfFunc()
red("Warning")
red("Error: %s", err)
// Mix up multiple attributes
notice := color.New(color.Bold, color.FgGreen).PrintlnFunc()
notice("Don't forget this...")
```
### Custom fprint functions (FprintFunc)
```go
blue := color.New(FgBlue).FprintfFunc()
blue(myWriter, "important notice: %s", stars)
// Mix up with multiple attributes
success := color.New(color.Bold, color.FgGreen).FprintlnFunc()
success(myWriter, "Don't forget this...")
```
### Insert into noncolor strings (SprintFunc)
```go
// Create SprintXxx functions to mix strings with other non-colorized strings:
yellow := color.New(color.FgYellow).SprintFunc()
red := color.New(color.FgRed).SprintFunc()
fmt.Printf("This is a %s and this is %s.\n", yellow("warning"), red("error"))
info := color.New(color.FgWhite, color.BgGreen).SprintFunc()
fmt.Printf("This %s rocks!\n", info("package"))
// Use helper functions
fmt.Println("This", color.RedString("warning"), "should be not neglected.")
fmt.Printf("%v %v\n", color.GreenString("Info:"), "an important message.")
// Windows supported too! Just don't forget to change the output to color.Output
fmt.Fprintf(color.Output, "Windows support: %s", color.GreenString("PASS"))
```
### Plug into existing code
```go
// Use handy standard colors
color.Set(color.FgYellow)
fmt.Println("Existing text will now be in yellow")
fmt.Printf("This one %s\n", "too")
color.Unset() // Don't forget to unset
// You can mix up parameters
color.Set(color.FgMagenta, color.Bold)
defer color.Unset() // Use it in your function
fmt.Println("All text will now be bold magenta.")
```
### Disable/Enable color
There might be a case where you want to explicitly disable/enable color output. the
`go-isatty` package will automatically disable color output for non-tty output streams
(for example if the output were piped directly to `less`)
`Color` has support to disable/enable colors both globally and for single color
definitions. For example suppose you have a CLI app and a `--no-color` bool flag. You
can easily disable the color output with:
```go
var flagNoColor = flag.Bool("no-color", false, "Disable color output")
if *flagNoColor {
color.NoColor = true // disables colorized output
}
```
It also has support for single color definitions (local). You can
disable/enable color output on the fly:
```go
c := color.New(color.FgCyan)
c.Println("Prints cyan text")
c.DisableColor()
c.Println("This is printed without any color")
c.EnableColor()
c.Println("This prints again cyan...")
```
## Todo
* Save/Return previous values
* Evaluate fmt.Formatter interface
## Credits
* [Fatih Arslan](https://github.com/fatih)
* Windows support via @mattn: [colorable](https://github.com/mattn/go-colorable)
## License
The MIT License (MIT) - see [`LICENSE.md`](https://github.com/fatih/color/blob/master/LICENSE.md) for more details
+603
View File
@@ -0,0 +1,603 @@
package color
import (
"fmt"
"io"
"os"
"strconv"
"strings"
"sync"
"github.com/mattn/go-colorable"
"github.com/mattn/go-isatty"
)
var (
// NoColor defines if the output is colorized or not. It's dynamically set to
// false or true based on the stdout's file descriptor referring to a terminal
// or not. This is a global option and affects all colors. For more control
// over each color block use the methods DisableColor() individually.
NoColor = os.Getenv("TERM") == "dumb" ||
(!isatty.IsTerminal(os.Stdout.Fd()) && !isatty.IsCygwinTerminal(os.Stdout.Fd()))
// Output defines the standard output of the print functions. By default
// os.Stdout is used.
Output = colorable.NewColorableStdout()
// Error defines a color supporting writer for os.Stderr.
Error = colorable.NewColorableStderr()
// colorsCache is used to reduce the count of created Color objects and
// allows to reuse already created objects with required Attribute.
colorsCache = make(map[Attribute]*Color)
colorsCacheMu sync.Mutex // protects colorsCache
)
// Color defines a custom color object which is defined by SGR parameters.
type Color struct {
params []Attribute
noColor *bool
}
// Attribute defines a single SGR Code
type Attribute int
const escape = "\x1b"
// Base attributes
const (
Reset Attribute = iota
Bold
Faint
Italic
Underline
BlinkSlow
BlinkRapid
ReverseVideo
Concealed
CrossedOut
)
// Foreground text colors
const (
FgBlack Attribute = iota + 30
FgRed
FgGreen
FgYellow
FgBlue
FgMagenta
FgCyan
FgWhite
)
// Foreground Hi-Intensity text colors
const (
FgHiBlack Attribute = iota + 90
FgHiRed
FgHiGreen
FgHiYellow
FgHiBlue
FgHiMagenta
FgHiCyan
FgHiWhite
)
// Background text colors
const (
BgBlack Attribute = iota + 40
BgRed
BgGreen
BgYellow
BgBlue
BgMagenta
BgCyan
BgWhite
)
// Background Hi-Intensity text colors
const (
BgHiBlack Attribute = iota + 100
BgHiRed
BgHiGreen
BgHiYellow
BgHiBlue
BgHiMagenta
BgHiCyan
BgHiWhite
)
// New returns a newly created color object.
func New(value ...Attribute) *Color {
c := &Color{params: make([]Attribute, 0)}
c.Add(value...)
return c
}
// Set sets the given parameters immediately. It will change the color of
// output with the given SGR parameters until color.Unset() is called.
func Set(p ...Attribute) *Color {
c := New(p...)
c.Set()
return c
}
// Unset resets all escape attributes and clears the output. Usually should
// be called after Set().
func Unset() {
if NoColor {
return
}
fmt.Fprintf(Output, "%s[%dm", escape, Reset)
}
// Set sets the SGR sequence.
func (c *Color) Set() *Color {
if c.isNoColorSet() {
return c
}
fmt.Fprintf(Output, c.format())
return c
}
func (c *Color) unset() {
if c.isNoColorSet() {
return
}
Unset()
}
func (c *Color) setWriter(w io.Writer) *Color {
if c.isNoColorSet() {
return c
}
fmt.Fprintf(w, c.format())
return c
}
func (c *Color) unsetWriter(w io.Writer) {
if c.isNoColorSet() {
return
}
if NoColor {
return
}
fmt.Fprintf(w, "%s[%dm", escape, Reset)
}
// Add is used to chain SGR parameters. Use as many as parameters to combine
// and create custom color objects. Example: Add(color.FgRed, color.Underline).
func (c *Color) Add(value ...Attribute) *Color {
c.params = append(c.params, value...)
return c
}
func (c *Color) prepend(value Attribute) {
c.params = append(c.params, 0)
copy(c.params[1:], c.params[0:])
c.params[0] = value
}
// Fprint formats using the default formats for its operands and writes to w.
// Spaces are added between operands when neither is a string.
// It returns the number of bytes written and any write error encountered.
// On Windows, users should wrap w with colorable.NewColorable() if w is of
// type *os.File.
func (c *Color) Fprint(w io.Writer, a ...interface{}) (n int, err error) {
c.setWriter(w)
defer c.unsetWriter(w)
return fmt.Fprint(w, a...)
}
// Print formats using the default formats for its operands and writes to
// standard output. Spaces are added between operands when neither is a
// string. It returns the number of bytes written and any write error
// encountered. This is the standard fmt.Print() method wrapped with the given
// color.
func (c *Color) Print(a ...interface{}) (n int, err error) {
c.Set()
defer c.unset()
return fmt.Fprint(Output, a...)
}
// Fprintf formats according to a format specifier and writes to w.
// It returns the number of bytes written and any write error encountered.
// On Windows, users should wrap w with colorable.NewColorable() if w is of
// type *os.File.
func (c *Color) Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) {
c.setWriter(w)
defer c.unsetWriter(w)
return fmt.Fprintf(w, format, a...)
}
// Printf formats according to a format specifier and writes to standard output.
// It returns the number of bytes written and any write error encountered.
// This is the standard fmt.Printf() method wrapped with the given color.
func (c *Color) Printf(format string, a ...interface{}) (n int, err error) {
c.Set()
defer c.unset()
return fmt.Fprintf(Output, format, a...)
}
// Fprintln formats using the default formats for its operands and writes to w.
// Spaces are always added between operands and a newline is appended.
// On Windows, users should wrap w with colorable.NewColorable() if w is of
// type *os.File.
func (c *Color) Fprintln(w io.Writer, a ...interface{}) (n int, err error) {
c.setWriter(w)
defer c.unsetWriter(w)
return fmt.Fprintln(w, a...)
}
// Println formats using the default formats for its operands and writes to
// standard output. Spaces are always added between operands and a newline is
// appended. It returns the number of bytes written and any write error
// encountered. This is the standard fmt.Print() method wrapped with the given
// color.
func (c *Color) Println(a ...interface{}) (n int, err error) {
c.Set()
defer c.unset()
return fmt.Fprintln(Output, a...)
}
// Sprint is just like Print, but returns a string instead of printing it.
func (c *Color) Sprint(a ...interface{}) string {
return c.wrap(fmt.Sprint(a...))
}
// Sprintln is just like Println, but returns a string instead of printing it.
func (c *Color) Sprintln(a ...interface{}) string {
return c.wrap(fmt.Sprintln(a...))
}
// Sprintf is just like Printf, but returns a string instead of printing it.
func (c *Color) Sprintf(format string, a ...interface{}) string {
return c.wrap(fmt.Sprintf(format, a...))
}
// FprintFunc returns a new function that prints the passed arguments as
// colorized with color.Fprint().
func (c *Color) FprintFunc() func(w io.Writer, a ...interface{}) {
return func(w io.Writer, a ...interface{}) {
c.Fprint(w, a...)
}
}
// PrintFunc returns a new function that prints the passed arguments as
// colorized with color.Print().
func (c *Color) PrintFunc() func(a ...interface{}) {
return func(a ...interface{}) {
c.Print(a...)
}
}
// FprintfFunc returns a new function that prints the passed arguments as
// colorized with color.Fprintf().
func (c *Color) FprintfFunc() func(w io.Writer, format string, a ...interface{}) {
return func(w io.Writer, format string, a ...interface{}) {
c.Fprintf(w, format, a...)
}
}
// PrintfFunc returns a new function that prints the passed arguments as
// colorized with color.Printf().
func (c *Color) PrintfFunc() func(format string, a ...interface{}) {
return func(format string, a ...interface{}) {
c.Printf(format, a...)
}
}
// FprintlnFunc returns a new function that prints the passed arguments as
// colorized with color.Fprintln().
func (c *Color) FprintlnFunc() func(w io.Writer, a ...interface{}) {
return func(w io.Writer, a ...interface{}) {
c.Fprintln(w, a...)
}
}
// PrintlnFunc returns a new function that prints the passed arguments as
// colorized with color.Println().
func (c *Color) PrintlnFunc() func(a ...interface{}) {
return func(a ...interface{}) {
c.Println(a...)
}
}
// SprintFunc returns a new function that returns colorized strings for the
// given arguments with fmt.Sprint(). Useful to put into or mix into other
// string. Windows users should use this in conjunction with color.Output, example:
//
// put := New(FgYellow).SprintFunc()
// fmt.Fprintf(color.Output, "This is a %s", put("warning"))
func (c *Color) SprintFunc() func(a ...interface{}) string {
return func(a ...interface{}) string {
return c.wrap(fmt.Sprint(a...))
}
}
// SprintfFunc returns a new function that returns colorized strings for the
// given arguments with fmt.Sprintf(). Useful to put into or mix into other
// string. Windows users should use this in conjunction with color.Output.
func (c *Color) SprintfFunc() func(format string, a ...interface{}) string {
return func(format string, a ...interface{}) string {
return c.wrap(fmt.Sprintf(format, a...))
}
}
// SprintlnFunc returns a new function that returns colorized strings for the
// given arguments with fmt.Sprintln(). Useful to put into or mix into other
// string. Windows users should use this in conjunction with color.Output.
func (c *Color) SprintlnFunc() func(a ...interface{}) string {
return func(a ...interface{}) string {
return c.wrap(fmt.Sprintln(a...))
}
}
// sequence returns a formatted SGR sequence to be plugged into a "\x1b[...m"
// an example output might be: "1;36" -> bold cyan
func (c *Color) sequence() string {
format := make([]string, len(c.params))
for i, v := range c.params {
format[i] = strconv.Itoa(int(v))
}
return strings.Join(format, ";")
}
// wrap wraps the s string with the colors attributes. The string is ready to
// be printed.
func (c *Color) wrap(s string) string {
if c.isNoColorSet() {
return s
}
return c.format() + s + c.unformat()
}
func (c *Color) format() string {
return fmt.Sprintf("%s[%sm", escape, c.sequence())
}
func (c *Color) unformat() string {
return fmt.Sprintf("%s[%dm", escape, Reset)
}
// DisableColor disables the color output. Useful to not change any existing
// code and still being able to output. Can be used for flags like
// "--no-color". To enable back use EnableColor() method.
func (c *Color) DisableColor() {
c.noColor = boolPtr(true)
}
// EnableColor enables the color output. Use it in conjunction with
// DisableColor(). Otherwise this method has no side effects.
func (c *Color) EnableColor() {
c.noColor = boolPtr(false)
}
func (c *Color) isNoColorSet() bool {
// check first if we have user setted action
if c.noColor != nil {
return *c.noColor
}
// if not return the global option, which is disabled by default
return NoColor
}
// Equals returns a boolean value indicating whether two colors are equal.
func (c *Color) Equals(c2 *Color) bool {
if len(c.params) != len(c2.params) {
return false
}
for _, attr := range c.params {
if !c2.attrExists(attr) {
return false
}
}
return true
}
func (c *Color) attrExists(a Attribute) bool {
for _, attr := range c.params {
if attr == a {
return true
}
}
return false
}
func boolPtr(v bool) *bool {
return &v
}
func getCachedColor(p Attribute) *Color {
colorsCacheMu.Lock()
defer colorsCacheMu.Unlock()
c, ok := colorsCache[p]
if !ok {
c = New(p)
colorsCache[p] = c
}
return c
}
func colorPrint(format string, p Attribute, a ...interface{}) {
c := getCachedColor(p)
if !strings.HasSuffix(format, "\n") {
format += "\n"
}
if len(a) == 0 {
c.Print(format)
} else {
c.Printf(format, a...)
}
}
func colorString(format string, p Attribute, a ...interface{}) string {
c := getCachedColor(p)
if len(a) == 0 {
return c.SprintFunc()(format)
}
return c.SprintfFunc()(format, a...)
}
// Black is a convenient helper function to print with black foreground. A
// newline is appended to format by default.
func Black(format string, a ...interface{}) { colorPrint(format, FgBlack, a...) }
// Red is a convenient helper function to print with red foreground. A
// newline is appended to format by default.
func Red(format string, a ...interface{}) { colorPrint(format, FgRed, a...) }
// Green is a convenient helper function to print with green foreground. A
// newline is appended to format by default.
func Green(format string, a ...interface{}) { colorPrint(format, FgGreen, a...) }
// Yellow is a convenient helper function to print with yellow foreground.
// A newline is appended to format by default.
func Yellow(format string, a ...interface{}) { colorPrint(format, FgYellow, a...) }
// Blue is a convenient helper function to print with blue foreground. A
// newline is appended to format by default.
func Blue(format string, a ...interface{}) { colorPrint(format, FgBlue, a...) }
// Magenta is a convenient helper function to print with magenta foreground.
// A newline is appended to format by default.
func Magenta(format string, a ...interface{}) { colorPrint(format, FgMagenta, a...) }
// Cyan is a convenient helper function to print with cyan foreground. A
// newline is appended to format by default.
func Cyan(format string, a ...interface{}) { colorPrint(format, FgCyan, a...) }
// White is a convenient helper function to print with white foreground. A
// newline is appended to format by default.
func White(format string, a ...interface{}) { colorPrint(format, FgWhite, a...) }
// BlackString is a convenient helper function to return a string with black
// foreground.
func BlackString(format string, a ...interface{}) string { return colorString(format, FgBlack, a...) }
// RedString is a convenient helper function to return a string with red
// foreground.
func RedString(format string, a ...interface{}) string { return colorString(format, FgRed, a...) }
// GreenString is a convenient helper function to return a string with green
// foreground.
func GreenString(format string, a ...interface{}) string { return colorString(format, FgGreen, a...) }
// YellowString is a convenient helper function to return a string with yellow
// foreground.
func YellowString(format string, a ...interface{}) string { return colorString(format, FgYellow, a...) }
// BlueString is a convenient helper function to return a string with blue
// foreground.
func BlueString(format string, a ...interface{}) string { return colorString(format, FgBlue, a...) }
// MagentaString is a convenient helper function to return a string with magenta
// foreground.
func MagentaString(format string, a ...interface{}) string {
return colorString(format, FgMagenta, a...)
}
// CyanString is a convenient helper function to return a string with cyan
// foreground.
func CyanString(format string, a ...interface{}) string { return colorString(format, FgCyan, a...) }
// WhiteString is a convenient helper function to return a string with white
// foreground.
func WhiteString(format string, a ...interface{}) string { return colorString(format, FgWhite, a...) }
// HiBlack is a convenient helper function to print with hi-intensity black foreground. A
// newline is appended to format by default.
func HiBlack(format string, a ...interface{}) { colorPrint(format, FgHiBlack, a...) }
// HiRed is a convenient helper function to print with hi-intensity red foreground. A
// newline is appended to format by default.
func HiRed(format string, a ...interface{}) { colorPrint(format, FgHiRed, a...) }
// HiGreen is a convenient helper function to print with hi-intensity green foreground. A
// newline is appended to format by default.
func HiGreen(format string, a ...interface{}) { colorPrint(format, FgHiGreen, a...) }
// HiYellow is a convenient helper function to print with hi-intensity yellow foreground.
// A newline is appended to format by default.
func HiYellow(format string, a ...interface{}) { colorPrint(format, FgHiYellow, a...) }
// HiBlue is a convenient helper function to print with hi-intensity blue foreground. A
// newline is appended to format by default.
func HiBlue(format string, a ...interface{}) { colorPrint(format, FgHiBlue, a...) }
// HiMagenta is a convenient helper function to print with hi-intensity magenta foreground.
// A newline is appended to format by default.
func HiMagenta(format string, a ...interface{}) { colorPrint(format, FgHiMagenta, a...) }
// HiCyan is a convenient helper function to print with hi-intensity cyan foreground. A
// newline is appended to format by default.
func HiCyan(format string, a ...interface{}) { colorPrint(format, FgHiCyan, a...) }
// HiWhite is a convenient helper function to print with hi-intensity white foreground. A
// newline is appended to format by default.
func HiWhite(format string, a ...interface{}) { colorPrint(format, FgHiWhite, a...) }
// HiBlackString is a convenient helper function to return a string with hi-intensity black
// foreground.
func HiBlackString(format string, a ...interface{}) string {
return colorString(format, FgHiBlack, a...)
}
// HiRedString is a convenient helper function to return a string with hi-intensity red
// foreground.
func HiRedString(format string, a ...interface{}) string { return colorString(format, FgHiRed, a...) }
// HiGreenString is a convenient helper function to return a string with hi-intensity green
// foreground.
func HiGreenString(format string, a ...interface{}) string {
return colorString(format, FgHiGreen, a...)
}
// HiYellowString is a convenient helper function to return a string with hi-intensity yellow
// foreground.
func HiYellowString(format string, a ...interface{}) string {
return colorString(format, FgHiYellow, a...)
}
// HiBlueString is a convenient helper function to return a string with hi-intensity blue
// foreground.
func HiBlueString(format string, a ...interface{}) string { return colorString(format, FgHiBlue, a...) }
// HiMagentaString is a convenient helper function to return a string with hi-intensity magenta
// foreground.
func HiMagentaString(format string, a ...interface{}) string {
return colorString(format, FgHiMagenta, a...)
}
// HiCyanString is a convenient helper function to return a string with hi-intensity cyan
// foreground.
func HiCyanString(format string, a ...interface{}) string { return colorString(format, FgHiCyan, a...) }
// HiWhiteString is a convenient helper function to return a string with hi-intensity white
// foreground.
func HiWhiteString(format string, a ...interface{}) string {
return colorString(format, FgHiWhite, a...)
}
+133
View File
@@ -0,0 +1,133 @@
/*
Package color is an ANSI color package to output colorized or SGR defined
output to the standard output. The API can be used in several way, pick one
that suits you.
Use simple and default helper functions with predefined foreground colors:
color.Cyan("Prints text in cyan.")
// a newline will be appended automatically
color.Blue("Prints %s in blue.", "text")
// More default foreground colors..
color.Red("We have red")
color.Yellow("Yellow color too!")
color.Magenta("And many others ..")
// Hi-intensity colors
color.HiGreen("Bright green color.")
color.HiBlack("Bright black means gray..")
color.HiWhite("Shiny white color!")
However there are times where custom color mixes are required. Below are some
examples to create custom color objects and use the print functions of each
separate color object.
// Create a new color object
c := color.New(color.FgCyan).Add(color.Underline)
c.Println("Prints cyan text with an underline.")
// Or just add them to New()
d := color.New(color.FgCyan, color.Bold)
d.Printf("This prints bold cyan %s\n", "too!.")
// Mix up foreground and background colors, create new mixes!
red := color.New(color.FgRed)
boldRed := red.Add(color.Bold)
boldRed.Println("This will print text in bold red.")
whiteBackground := red.Add(color.BgWhite)
whiteBackground.Println("Red text with White background.")
// Use your own io.Writer output
color.New(color.FgBlue).Fprintln(myWriter, "blue color!")
blue := color.New(color.FgBlue)
blue.Fprint(myWriter, "This will print text in blue.")
You can create PrintXxx functions to simplify even more:
// Create a custom print function for convenient
red := color.New(color.FgRed).PrintfFunc()
red("warning")
red("error: %s", err)
// Mix up multiple attributes
notice := color.New(color.Bold, color.FgGreen).PrintlnFunc()
notice("don't forget this...")
You can also FprintXxx functions to pass your own io.Writer:
blue := color.New(FgBlue).FprintfFunc()
blue(myWriter, "important notice: %s", stars)
// Mix up with multiple attributes
success := color.New(color.Bold, color.FgGreen).FprintlnFunc()
success(myWriter, don't forget this...")
Or create SprintXxx functions to mix strings with other non-colorized strings:
yellow := New(FgYellow).SprintFunc()
red := New(FgRed).SprintFunc()
fmt.Printf("this is a %s and this is %s.\n", yellow("warning"), red("error"))
info := New(FgWhite, BgGreen).SprintFunc()
fmt.Printf("this %s rocks!\n", info("package"))
Windows support is enabled by default. All Print functions work as intended.
However only for color.SprintXXX functions, user should use fmt.FprintXXX and
set the output to color.Output:
fmt.Fprintf(color.Output, "Windows support: %s", color.GreenString("PASS"))
info := New(FgWhite, BgGreen).SprintFunc()
fmt.Fprintf(color.Output, "this %s rocks!\n", info("package"))
Using with existing code is possible. Just use the Set() method to set the
standard output to the given parameters. That way a rewrite of an existing
code is not required.
// Use handy standard colors.
color.Set(color.FgYellow)
fmt.Println("Existing text will be now in Yellow")
fmt.Printf("This one %s\n", "too")
color.Unset() // don't forget to unset
// You can mix up parameters
color.Set(color.FgMagenta, color.Bold)
defer color.Unset() // use it in your function
fmt.Println("All text will be now bold magenta.")
There might be a case where you want to disable color output (for example to
pipe the standard output of your app to somewhere else). `Color` has support to
disable colors both globally and for single color definition. For example
suppose you have a CLI app and a `--no-color` bool flag. You can easily disable
the color output with:
var flagNoColor = flag.Bool("no-color", false, "Disable color output")
if *flagNoColor {
color.NoColor = true // disables colorized output
}
It also has support for single color definitions (local). You can
disable/enable color output on the fly:
c := color.New(color.FgCyan)
c.Println("Prints cyan text")
c.DisableColor()
c.Println("This is printed without any color")
c.EnableColor()
c.Println("This prints again cyan...")
*/
package color
+8
View File
@@ -0,0 +1,8 @@
module github.com/fatih/color
go 1.13
require (
github.com/mattn/go-colorable v0.1.8
github.com/mattn/go-isatty v0.0.12
)
+7
View File
@@ -0,0 +1,7 @@
github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8=
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae h1:/WDfKMnPU+m5M4xB+6x4kaepxRw6jWvR5iDRdvjHgy8=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -0,0 +1,200 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// source: github.com/golang/protobuf/protoc-gen-go/descriptor/descriptor.proto
package descriptor
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
descriptorpb "google.golang.org/protobuf/types/descriptorpb"
reflect "reflect"
)
// Symbols defined in public import of google/protobuf/descriptor.proto.
type FieldDescriptorProto_Type = descriptorpb.FieldDescriptorProto_Type
const FieldDescriptorProto_TYPE_DOUBLE = descriptorpb.FieldDescriptorProto_TYPE_DOUBLE
const FieldDescriptorProto_TYPE_FLOAT = descriptorpb.FieldDescriptorProto_TYPE_FLOAT
const FieldDescriptorProto_TYPE_INT64 = descriptorpb.FieldDescriptorProto_TYPE_INT64
const FieldDescriptorProto_TYPE_UINT64 = descriptorpb.FieldDescriptorProto_TYPE_UINT64
const FieldDescriptorProto_TYPE_INT32 = descriptorpb.FieldDescriptorProto_TYPE_INT32
const FieldDescriptorProto_TYPE_FIXED64 = descriptorpb.FieldDescriptorProto_TYPE_FIXED64
const FieldDescriptorProto_TYPE_FIXED32 = descriptorpb.FieldDescriptorProto_TYPE_FIXED32
const FieldDescriptorProto_TYPE_BOOL = descriptorpb.FieldDescriptorProto_TYPE_BOOL
const FieldDescriptorProto_TYPE_STRING = descriptorpb.FieldDescriptorProto_TYPE_STRING
const FieldDescriptorProto_TYPE_GROUP = descriptorpb.FieldDescriptorProto_TYPE_GROUP
const FieldDescriptorProto_TYPE_MESSAGE = descriptorpb.FieldDescriptorProto_TYPE_MESSAGE
const FieldDescriptorProto_TYPE_BYTES = descriptorpb.FieldDescriptorProto_TYPE_BYTES
const FieldDescriptorProto_TYPE_UINT32 = descriptorpb.FieldDescriptorProto_TYPE_UINT32
const FieldDescriptorProto_TYPE_ENUM = descriptorpb.FieldDescriptorProto_TYPE_ENUM
const FieldDescriptorProto_TYPE_SFIXED32 = descriptorpb.FieldDescriptorProto_TYPE_SFIXED32
const FieldDescriptorProto_TYPE_SFIXED64 = descriptorpb.FieldDescriptorProto_TYPE_SFIXED64
const FieldDescriptorProto_TYPE_SINT32 = descriptorpb.FieldDescriptorProto_TYPE_SINT32
const FieldDescriptorProto_TYPE_SINT64 = descriptorpb.FieldDescriptorProto_TYPE_SINT64
var FieldDescriptorProto_Type_name = descriptorpb.FieldDescriptorProto_Type_name
var FieldDescriptorProto_Type_value = descriptorpb.FieldDescriptorProto_Type_value
type FieldDescriptorProto_Label = descriptorpb.FieldDescriptorProto_Label
const FieldDescriptorProto_LABEL_OPTIONAL = descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL
const FieldDescriptorProto_LABEL_REQUIRED = descriptorpb.FieldDescriptorProto_LABEL_REQUIRED
const FieldDescriptorProto_LABEL_REPEATED = descriptorpb.FieldDescriptorProto_LABEL_REPEATED
var FieldDescriptorProto_Label_name = descriptorpb.FieldDescriptorProto_Label_name
var FieldDescriptorProto_Label_value = descriptorpb.FieldDescriptorProto_Label_value
type FileOptions_OptimizeMode = descriptorpb.FileOptions_OptimizeMode
const FileOptions_SPEED = descriptorpb.FileOptions_SPEED
const FileOptions_CODE_SIZE = descriptorpb.FileOptions_CODE_SIZE
const FileOptions_LITE_RUNTIME = descriptorpb.FileOptions_LITE_RUNTIME
var FileOptions_OptimizeMode_name = descriptorpb.FileOptions_OptimizeMode_name
var FileOptions_OptimizeMode_value = descriptorpb.FileOptions_OptimizeMode_value
type FieldOptions_CType = descriptorpb.FieldOptions_CType
const FieldOptions_STRING = descriptorpb.FieldOptions_STRING
const FieldOptions_CORD = descriptorpb.FieldOptions_CORD
const FieldOptions_STRING_PIECE = descriptorpb.FieldOptions_STRING_PIECE
var FieldOptions_CType_name = descriptorpb.FieldOptions_CType_name
var FieldOptions_CType_value = descriptorpb.FieldOptions_CType_value
type FieldOptions_JSType = descriptorpb.FieldOptions_JSType
const FieldOptions_JS_NORMAL = descriptorpb.FieldOptions_JS_NORMAL
const FieldOptions_JS_STRING = descriptorpb.FieldOptions_JS_STRING
const FieldOptions_JS_NUMBER = descriptorpb.FieldOptions_JS_NUMBER
var FieldOptions_JSType_name = descriptorpb.FieldOptions_JSType_name
var FieldOptions_JSType_value = descriptorpb.FieldOptions_JSType_value
type MethodOptions_IdempotencyLevel = descriptorpb.MethodOptions_IdempotencyLevel
const MethodOptions_IDEMPOTENCY_UNKNOWN = descriptorpb.MethodOptions_IDEMPOTENCY_UNKNOWN
const MethodOptions_NO_SIDE_EFFECTS = descriptorpb.MethodOptions_NO_SIDE_EFFECTS
const MethodOptions_IDEMPOTENT = descriptorpb.MethodOptions_IDEMPOTENT
var MethodOptions_IdempotencyLevel_name = descriptorpb.MethodOptions_IdempotencyLevel_name
var MethodOptions_IdempotencyLevel_value = descriptorpb.MethodOptions_IdempotencyLevel_value
type FileDescriptorSet = descriptorpb.FileDescriptorSet
type FileDescriptorProto = descriptorpb.FileDescriptorProto
type DescriptorProto = descriptorpb.DescriptorProto
type ExtensionRangeOptions = descriptorpb.ExtensionRangeOptions
type FieldDescriptorProto = descriptorpb.FieldDescriptorProto
type OneofDescriptorProto = descriptorpb.OneofDescriptorProto
type EnumDescriptorProto = descriptorpb.EnumDescriptorProto
type EnumValueDescriptorProto = descriptorpb.EnumValueDescriptorProto
type ServiceDescriptorProto = descriptorpb.ServiceDescriptorProto
type MethodDescriptorProto = descriptorpb.MethodDescriptorProto
const Default_MethodDescriptorProto_ClientStreaming = descriptorpb.Default_MethodDescriptorProto_ClientStreaming
const Default_MethodDescriptorProto_ServerStreaming = descriptorpb.Default_MethodDescriptorProto_ServerStreaming
type FileOptions = descriptorpb.FileOptions
const Default_FileOptions_JavaMultipleFiles = descriptorpb.Default_FileOptions_JavaMultipleFiles
const Default_FileOptions_JavaStringCheckUtf8 = descriptorpb.Default_FileOptions_JavaStringCheckUtf8
const Default_FileOptions_OptimizeFor = descriptorpb.Default_FileOptions_OptimizeFor
const Default_FileOptions_CcGenericServices = descriptorpb.Default_FileOptions_CcGenericServices
const Default_FileOptions_JavaGenericServices = descriptorpb.Default_FileOptions_JavaGenericServices
const Default_FileOptions_PyGenericServices = descriptorpb.Default_FileOptions_PyGenericServices
const Default_FileOptions_PhpGenericServices = descriptorpb.Default_FileOptions_PhpGenericServices
const Default_FileOptions_Deprecated = descriptorpb.Default_FileOptions_Deprecated
const Default_FileOptions_CcEnableArenas = descriptorpb.Default_FileOptions_CcEnableArenas
type MessageOptions = descriptorpb.MessageOptions
const Default_MessageOptions_MessageSetWireFormat = descriptorpb.Default_MessageOptions_MessageSetWireFormat
const Default_MessageOptions_NoStandardDescriptorAccessor = descriptorpb.Default_MessageOptions_NoStandardDescriptorAccessor
const Default_MessageOptions_Deprecated = descriptorpb.Default_MessageOptions_Deprecated
type FieldOptions = descriptorpb.FieldOptions
const Default_FieldOptions_Ctype = descriptorpb.Default_FieldOptions_Ctype
const Default_FieldOptions_Jstype = descriptorpb.Default_FieldOptions_Jstype
const Default_FieldOptions_Lazy = descriptorpb.Default_FieldOptions_Lazy
const Default_FieldOptions_Deprecated = descriptorpb.Default_FieldOptions_Deprecated
const Default_FieldOptions_Weak = descriptorpb.Default_FieldOptions_Weak
type OneofOptions = descriptorpb.OneofOptions
type EnumOptions = descriptorpb.EnumOptions
const Default_EnumOptions_Deprecated = descriptorpb.Default_EnumOptions_Deprecated
type EnumValueOptions = descriptorpb.EnumValueOptions
const Default_EnumValueOptions_Deprecated = descriptorpb.Default_EnumValueOptions_Deprecated
type ServiceOptions = descriptorpb.ServiceOptions
const Default_ServiceOptions_Deprecated = descriptorpb.Default_ServiceOptions_Deprecated
type MethodOptions = descriptorpb.MethodOptions
const Default_MethodOptions_Deprecated = descriptorpb.Default_MethodOptions_Deprecated
const Default_MethodOptions_IdempotencyLevel = descriptorpb.Default_MethodOptions_IdempotencyLevel
type UninterpretedOption = descriptorpb.UninterpretedOption
type SourceCodeInfo = descriptorpb.SourceCodeInfo
type GeneratedCodeInfo = descriptorpb.GeneratedCodeInfo
type DescriptorProto_ExtensionRange = descriptorpb.DescriptorProto_ExtensionRange
type DescriptorProto_ReservedRange = descriptorpb.DescriptorProto_ReservedRange
type EnumDescriptorProto_EnumReservedRange = descriptorpb.EnumDescriptorProto_EnumReservedRange
type UninterpretedOption_NamePart = descriptorpb.UninterpretedOption_NamePart
type SourceCodeInfo_Location = descriptorpb.SourceCodeInfo_Location
type GeneratedCodeInfo_Annotation = descriptorpb.GeneratedCodeInfo_Annotation
var File_github_com_golang_protobuf_protoc_gen_go_descriptor_descriptor_proto protoreflect.FileDescriptor
var file_github_com_golang_protobuf_protoc_gen_go_descriptor_descriptor_proto_rawDesc = []byte{
0x0a, 0x44, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6f, 0x6c,
0x61, 0x6e, 0x67, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x70, 0x72, 0x6f,
0x74, 0x6f, 0x63, 0x2d, 0x67, 0x65, 0x6e, 0x2d, 0x67, 0x6f, 0x2f, 0x64, 0x65, 0x73, 0x63, 0x72,
0x69, 0x70, 0x74, 0x6f, 0x72, 0x2f, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x6f, 0x72,
0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x20, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70,
0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74,
0x6f, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x42, 0x40, 0x5a, 0x3e, 0x67, 0x69, 0x74, 0x68,
0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6f, 0x6c, 0x61, 0x6e, 0x67, 0x2f, 0x70, 0x72,
0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x2d, 0x67, 0x65,
0x6e, 0x2d, 0x67, 0x6f, 0x2f, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x6f, 0x72, 0x3b,
0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x6f, 0x72, 0x50, 0x00, 0x62, 0x06, 0x70, 0x72,
0x6f, 0x74, 0x6f, 0x32,
}
var file_github_com_golang_protobuf_protoc_gen_go_descriptor_descriptor_proto_goTypes = []interface{}{}
var file_github_com_golang_protobuf_protoc_gen_go_descriptor_descriptor_proto_depIdxs = []int32{
0, // [0:0] is the sub-list for method output_type
0, // [0:0] is the sub-list for method input_type
0, // [0:0] is the sub-list for extension type_name
0, // [0:0] is the sub-list for extension extendee
0, // [0:0] is the sub-list for field type_name
}
func init() { file_github_com_golang_protobuf_protoc_gen_go_descriptor_descriptor_proto_init() }
func file_github_com_golang_protobuf_protoc_gen_go_descriptor_descriptor_proto_init() {
if File_github_com_golang_protobuf_protoc_gen_go_descriptor_descriptor_proto != nil {
return
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_github_com_golang_protobuf_protoc_gen_go_descriptor_descriptor_proto_rawDesc,
NumEnums: 0,
NumMessages: 0,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_github_com_golang_protobuf_protoc_gen_go_descriptor_descriptor_proto_goTypes,
DependencyIndexes: file_github_com_golang_protobuf_protoc_gen_go_descriptor_descriptor_proto_depIdxs,
}.Build()
File_github_com_golang_protobuf_protoc_gen_go_descriptor_descriptor_proto = out.File
file_github_com_golang_protobuf_protoc_gen_go_descriptor_descriptor_proto_rawDesc = nil
file_github_com_golang_protobuf_protoc_gen_go_descriptor_descriptor_proto_goTypes = nil
file_github_com_golang_protobuf_protoc_gen_go_descriptor_descriptor_proto_depIdxs = nil
}
+62
View File
@@ -0,0 +1,62 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// source: github.com/golang/protobuf/ptypes/empty/empty.proto
package empty
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
emptypb "google.golang.org/protobuf/types/known/emptypb"
reflect "reflect"
)
// Symbols defined in public import of google/protobuf/empty.proto.
type Empty = emptypb.Empty
var File_github_com_golang_protobuf_ptypes_empty_empty_proto protoreflect.FileDescriptor
var file_github_com_golang_protobuf_ptypes_empty_empty_proto_rawDesc = []byte{
0x0a, 0x33, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6f, 0x6c,
0x61, 0x6e, 0x67, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x70, 0x74, 0x79,
0x70, 0x65, 0x73, 0x2f, 0x65, 0x6d, 0x70, 0x74, 0x79, 0x2f, 0x65, 0x6d, 0x70, 0x74, 0x79, 0x2e,
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1b, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72,
0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x65, 0x6d, 0x70, 0x74, 0x79, 0x2e, 0x70, 0x72, 0x6f,
0x74, 0x6f, 0x42, 0x2f, 0x5a, 0x2d, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d,
0x2f, 0x67, 0x6f, 0x6c, 0x61, 0x6e, 0x67, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66,
0x2f, 0x70, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2f, 0x65, 0x6d, 0x70, 0x74, 0x79, 0x3b, 0x65, 0x6d,
0x70, 0x74, 0x79, 0x50, 0x00, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var file_github_com_golang_protobuf_ptypes_empty_empty_proto_goTypes = []interface{}{}
var file_github_com_golang_protobuf_ptypes_empty_empty_proto_depIdxs = []int32{
0, // [0:0] is the sub-list for method output_type
0, // [0:0] is the sub-list for method input_type
0, // [0:0] is the sub-list for extension type_name
0, // [0:0] is the sub-list for extension extendee
0, // [0:0] is the sub-list for field type_name
}
func init() { file_github_com_golang_protobuf_ptypes_empty_empty_proto_init() }
func file_github_com_golang_protobuf_ptypes_empty_empty_proto_init() {
if File_github_com_golang_protobuf_ptypes_empty_empty_proto != nil {
return
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_github_com_golang_protobuf_ptypes_empty_empty_proto_rawDesc,
NumEnums: 0,
NumMessages: 0,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_github_com_golang_protobuf_ptypes_empty_empty_proto_goTypes,
DependencyIndexes: file_github_com_golang_protobuf_ptypes_empty_empty_proto_depIdxs,
}.Build()
File_github_com_golang_protobuf_ptypes_empty_empty_proto = out.File
file_github_com_golang_protobuf_ptypes_empty_empty_proto_rawDesc = nil
file_github_com_golang_protobuf_ptypes_empty_empty_proto_goTypes = nil
file_github_com_golang_protobuf_ptypes_empty_empty_proto_depIdxs = nil
}
+106
View File
@@ -0,0 +1,106 @@
package backend
import (
"encoding/json"
"time"
)
// User represents a Grafana user.
type User struct {
Login string
Name string
Email string
Role string
}
// AppInstanceSettings represents settings for an app instance.
//
// In Grafana an app instance is an app plugin of certain
// type that have been configured and enabled in a Grafana organization.
type AppInstanceSettings struct {
// JSONData repeats the properties at this level of the object (excluding DataSourceConfig), and also includes any
// custom properties associated with the plugin config instance.
JSONData json.RawMessage
// DecryptedSecureJSONData contains key,value pairs where the encrypted configuration plugin instance in Grafana
// server have been decrypted before passing them to the plugin.
DecryptedSecureJSONData map[string]string
// Updated is the last time this plugin instance's configuration was updated.
Updated time.Time
}
// DataSourceInstanceSettings represents settings for a data source instance.
//
// In Grafana a data source instance is a data source plugin of certain
// type that have been configured and created in a Grafana organization.
type DataSourceInstanceSettings struct {
// ID is the Grafana assigned numeric identifier of the the data source instance.
ID int64
// UID is the Grafana assigned string identifier of the the data source instance.
UID string
// Name is the configured name of the data source instance.
Name string
// URL is the configured URL of a data source instance (e.g. the URL of an API endpoint).
URL string
// User is a configured user for a data source instance. This is not a Grafana user, rather an arbitrary string.
User string
// Database is the configured database for a data source instance. (e.g. the default Database a SQL data source would connect to).
Database string
// BasicAuthEnabled indicates if this data source instance should use basic authentication.
BasicAuthEnabled bool
// BasicAuthUser is the configured user for basic authentication. (e.g. when a data source uses basic
// authentication to connect to whatever API it fetches data from).
BasicAuthUser string
// JSONData contains the raw DataSourceConfig as JSON as stored by Grafana server. It repeats the properties in
// this object and includes custom properties.
JSONData json.RawMessage
// DecryptedSecureJSONData contains key,value pairs where the encrypted configuration in Grafana server have been
// decrypted before passing them to the plugin.
DecryptedSecureJSONData map[string]string
// Updated is the last time the configuration for the data source instance was updated.
Updated time.Time
}
// PluginContext holds contextual information about a plugin request, such as
// Grafana organization, user and plugin instance settings.
type PluginContext struct {
// OrgID is The Grafana organization identifier the request originating from.
OrgID int64
// PluginID is the unique identifier of the plugin that the request is for.
PluginID string
// User is the Grafana user making the request.
//
// Will not be provided if Grafana backend initiated the request,
// for example when request is coming from Grafana Alerting.
User *User
// AppInstanceSettings is the configured app instance settings.
//
// In Grafana an app instance is an app plugin of certain
// type that have been configured and enabled in a Grafana organization.
//
// Will only be set if request targeting an app instance.
AppInstanceSettings *AppInstanceSettings
// DataSourceConfig is the configured data source instance
// settings.
//
// In Grafana a data source instance is a data source plugin of certain
// type that have been configured and created in a Grafana organization.
//
// Will only be set if request targeting a data source instance.
DataSourceInstanceSettings *DataSourceInstanceSettings
}
@@ -0,0 +1,204 @@
package backend
import (
"errors"
"time"
"github.com/grafana/grafana-plugin-sdk-go/data"
"github.com/grafana/grafana-plugin-sdk-go/genproto/pluginv2"
)
// ConvertFromProtobuf has a collection of methods for converting from the autogenerated
// protobuf go code to our SDK objects. This object exists to attach a collection
// of conversion methods to.
//
// This is used internally by the SDK and inside Grafana-server, plugin authors should not
// need this functionality.
type ConvertFromProtobuf struct{}
// FromProto returns a new ConvertFromProtobuf.
func FromProto() ConvertFromProtobuf {
return ConvertFromProtobuf{}
}
// User converts protobuf version of a User to the SDK version.
func (f ConvertFromProtobuf) User(user *pluginv2.User) *User {
if user == nil {
return nil
}
return &User{
Login: user.Login,
Name: user.Name,
Email: user.Email,
Role: user.Role,
}
}
// AppInstanceSettings converts protobuf version of an AppInstanceSettings to the SDK version.
func (f ConvertFromProtobuf) AppInstanceSettings(proto *pluginv2.AppInstanceSettings) *AppInstanceSettings {
if proto == nil {
return nil
}
return &AppInstanceSettings{
JSONData: proto.JsonData,
DecryptedSecureJSONData: proto.DecryptedSecureJsonData,
Updated: time.Unix(0, proto.LastUpdatedMS*int64(time.Millisecond)),
}
}
// DataSourceInstanceSettings converts protobuf version of a DataSourceInstanceSettings to the SDK version.
func (f ConvertFromProtobuf) DataSourceInstanceSettings(proto *pluginv2.DataSourceInstanceSettings) *DataSourceInstanceSettings {
if proto == nil {
return nil
}
return &DataSourceInstanceSettings{
ID: proto.Id,
UID: proto.Uid,
Name: proto.Name,
URL: proto.Url,
User: proto.User,
Database: proto.Database,
BasicAuthEnabled: proto.BasicAuthEnabled,
BasicAuthUser: proto.BasicAuthUser,
JSONData: proto.JsonData,
DecryptedSecureJSONData: proto.DecryptedSecureJsonData,
Updated: time.Unix(0, proto.LastUpdatedMS*int64(time.Millisecond)),
}
}
// PluginContext converts protobuf version of a PluginContext to the SDK version.
func (f ConvertFromProtobuf) PluginContext(proto *pluginv2.PluginContext) PluginContext {
return PluginContext{
OrgID: proto.OrgId,
PluginID: proto.PluginId,
User: f.User(proto.User),
AppInstanceSettings: f.AppInstanceSettings(proto.AppInstanceSettings),
DataSourceInstanceSettings: f.DataSourceInstanceSettings(proto.DataSourceInstanceSettings),
}
}
// TimeRange converts protobuf version of a TimeRange to the SDK version.
func (f ConvertFromProtobuf) TimeRange(proto *pluginv2.TimeRange) TimeRange {
return TimeRange{
From: time.Unix(0, proto.FromEpochMS*int64(time.Millisecond)),
To: time.Unix(0, proto.ToEpochMS*int64(time.Millisecond)),
}
}
// DataQuery converts protobuf version of a DataQuery to the SDK version.
func (f ConvertFromProtobuf) DataQuery(proto *pluginv2.DataQuery) *DataQuery {
return &DataQuery{
RefID: proto.RefId,
QueryType: proto.QueryType,
MaxDataPoints: proto.MaxDataPoints,
TimeRange: f.TimeRange(proto.TimeRange),
Interval: time.Duration(proto.IntervalMS) * time.Millisecond,
JSON: proto.Json,
}
}
// QueryDataRequest converts protobuf version of a QueryDataRequest to the SDK version.
func (f ConvertFromProtobuf) QueryDataRequest(protoReq *pluginv2.QueryDataRequest) *QueryDataRequest {
queries := make([]DataQuery, len(protoReq.Queries))
for i, q := range protoReq.Queries {
queries[i] = *f.DataQuery(q)
}
return &QueryDataRequest{
PluginContext: f.PluginContext(protoReq.PluginContext),
Headers: protoReq.Headers,
Queries: queries,
}
}
// QueryDataResponse converts protobuf version of a QueryDataResponse to the SDK version.
func (f ConvertFromProtobuf) QueryDataResponse(protoRes *pluginv2.QueryDataResponse) (*QueryDataResponse, error) {
qdr := &QueryDataResponse{
Responses: make(Responses, len(protoRes.Responses)),
}
for refID, res := range protoRes.Responses {
frames, err := data.UnmarshalArrowFrames(res.Frames)
if err != nil {
return nil, err
}
dr := DataResponse{
Frames: frames,
}
if res.Error != "" {
dr.Error = errors.New(res.Error)
}
qdr.Responses[refID] = dr
}
return qdr, nil
}
// CallResourceRequest converts protobuf version of a CallResourceRequest to the SDK version.
func (f ConvertFromProtobuf) CallResourceRequest(protoReq *pluginv2.CallResourceRequest) *CallResourceRequest {
headers := map[string][]string{}
for k, values := range protoReq.Headers {
headers[k] = values.Values
}
return &CallResourceRequest{
PluginContext: f.PluginContext(protoReq.PluginContext),
Path: protoReq.Path,
Method: protoReq.Method,
URL: protoReq.Url,
Headers: headers,
Body: protoReq.Body,
}
}
// CallResourceResponse converts protobuf version of a CallResourceResponse to the SDK version.
func (f ConvertFromProtobuf) CallResourceResponse(protoResp *pluginv2.CallResourceResponse) *CallResourceResponse {
headers := map[string][]string{}
for k, values := range protoResp.Headers {
headers[k] = values.Values
}
return &CallResourceResponse{
Status: int(protoResp.Code),
Body: protoResp.Body,
Headers: headers,
}
}
// CheckHealthRequest converts protobuf version of a CheckHealthRequest to the SDK version.
func (f ConvertFromProtobuf) CheckHealthRequest(protoReq *pluginv2.CheckHealthRequest) *CheckHealthRequest {
return &CheckHealthRequest{
PluginContext: f.PluginContext(protoReq.PluginContext),
}
}
// CheckHealthResponse converts protobuf version of a HealthCheckResponse to the SDK version.
func (f ConvertFromProtobuf) CheckHealthResponse(protoResp *pluginv2.CheckHealthResponse) *CheckHealthResult {
status := HealthStatusUnknown
switch protoResp.Status {
case pluginv2.CheckHealthResponse_ERROR:
status = HealthStatusError
case pluginv2.CheckHealthResponse_OK:
status = HealthStatusOk
}
return &CheckHealthResult{
Status: status,
Message: protoResp.Message,
JSONDetails: protoResp.JsonDetails,
}
}
// CollectMetricsResponse converts protobuf version of a CollectMetricsResponse to the SDK version.
func (f ConvertFromProtobuf) CollectMetricsResponse(protoResp *pluginv2.CollectMetricsResponse) *CollectMetricsResult {
var prometheusMetrics []byte
if protoResp.Metrics != nil {
prometheusMetrics = protoResp.Metrics.Prometheus
}
return &CollectMetricsResult{
PrometheusMetrics: prometheusMetrics,
}
}
@@ -0,0 +1,197 @@
package backend
import (
"time"
"github.com/grafana/grafana-plugin-sdk-go/genproto/pluginv2"
)
// ConvertToProtobuf has a collection of methods for converting the autogenerated
// protobuf go code to our SDK objects. This object exists to attach a collection
// of conversion methods to.
//
// This is used internally by the SDK and inside Grafana-server, plugin authors should not
// need this functionality.
type ConvertToProtobuf struct{}
// ToProto returns a new ConvertToProtobuf.
func ToProto() ConvertToProtobuf {
return ConvertToProtobuf{}
}
// User converts the SDK version of a User to the protobuf version.
func (t ConvertToProtobuf) User(user *User) *pluginv2.User {
if user == nil {
return nil
}
return &pluginv2.User{
Login: user.Login,
Name: user.Name,
Email: user.Email,
Role: user.Role,
}
}
// AppInstanceSettings converts the SDK version of an AppInstanceSettings to the protobuf version.
func (t ConvertToProtobuf) AppInstanceSettings(s *AppInstanceSettings) *pluginv2.AppInstanceSettings {
if s == nil {
return nil
}
return &pluginv2.AppInstanceSettings{
JsonData: s.JSONData,
DecryptedSecureJsonData: s.DecryptedSecureJSONData,
LastUpdatedMS: s.Updated.UnixNano() / int64(time.Millisecond),
}
}
// DataSourceInstanceSettings converts the SDK version of a DataSourceInstanceSettings to the protobuf version.
func (t ConvertToProtobuf) DataSourceInstanceSettings(s *DataSourceInstanceSettings) *pluginv2.DataSourceInstanceSettings {
if s == nil {
return nil
}
return &pluginv2.DataSourceInstanceSettings{
Id: s.ID,
Uid: s.UID,
Name: s.Name,
Url: s.URL,
User: s.User,
Database: s.Database,
BasicAuthEnabled: s.BasicAuthEnabled,
BasicAuthUser: s.BasicAuthUser,
JsonData: s.JSONData,
DecryptedSecureJsonData: s.DecryptedSecureJSONData,
LastUpdatedMS: s.Updated.UnixNano() / int64(time.Millisecond),
}
}
// PluginContext converts the SDK version of a PluginContext to the protobuf version.
func (t ConvertToProtobuf) PluginContext(pluginCtx PluginContext) *pluginv2.PluginContext {
return &pluginv2.PluginContext{
OrgId: pluginCtx.OrgID,
PluginId: pluginCtx.PluginID,
User: t.User(pluginCtx.User),
AppInstanceSettings: t.AppInstanceSettings(pluginCtx.AppInstanceSettings),
DataSourceInstanceSettings: t.DataSourceInstanceSettings(pluginCtx.DataSourceInstanceSettings),
}
}
// TimeRange converts the SDK version of a TimeRange to the protobuf version.
func (t ConvertToProtobuf) TimeRange(tr TimeRange) *pluginv2.TimeRange {
return &pluginv2.TimeRange{
FromEpochMS: tr.From.UnixNano() / int64(time.Millisecond),
ToEpochMS: tr.To.UnixNano() / int64(time.Millisecond),
}
}
// HealthStatus converts the SDK version of a HealthStatus to the protobuf version.
func (t ConvertToProtobuf) HealthStatus(status HealthStatus) pluginv2.CheckHealthResponse_HealthStatus {
switch status {
case HealthStatusUnknown:
return pluginv2.CheckHealthResponse_UNKNOWN
case HealthStatusOk:
return pluginv2.CheckHealthResponse_OK
case HealthStatusError:
return pluginv2.CheckHealthResponse_ERROR
}
panic("unsupported protobuf health status type in sdk")
}
// CheckHealthResponse converts the SDK version of a CheckHealthResponse to the protobuf version.
func (t ConvertToProtobuf) CheckHealthResponse(res *CheckHealthResult) *pluginv2.CheckHealthResponse {
return &pluginv2.CheckHealthResponse{
Status: t.HealthStatus(res.Status),
Message: res.Message,
JsonDetails: res.JSONDetails,
}
}
// DataQuery converts the SDK version of a DataQuery to the protobuf version.
func (t ConvertToProtobuf) DataQuery(q DataQuery) *pluginv2.DataQuery {
return &pluginv2.DataQuery{
RefId: q.RefID,
QueryType: q.QueryType,
MaxDataPoints: q.MaxDataPoints,
IntervalMS: q.Interval.Milliseconds(),
TimeRange: t.TimeRange(q.TimeRange),
Json: q.JSON,
}
}
// QueryDataRequest converts the SDK version of a QueryDataRequest to the protobuf version.
func (t ConvertToProtobuf) QueryDataRequest(req *QueryDataRequest) *pluginv2.QueryDataRequest {
queries := make([]*pluginv2.DataQuery, len(req.Queries))
for i, q := range req.Queries {
queries[i] = t.DataQuery(q)
}
return &pluginv2.QueryDataRequest{
PluginContext: t.PluginContext(req.PluginContext),
Headers: req.Headers,
Queries: queries,
}
}
// QueryDataResponse converts the SDK version of a QueryDataResponse to the protobuf version.
// It will set the RefID on the frames to the RefID key in Responses if a Frame's
// RefId property is an empty string.
func (t ConvertToProtobuf) QueryDataResponse(res *QueryDataResponse) (*pluginv2.QueryDataResponse, error) {
pQDR := &pluginv2.QueryDataResponse{
Responses: make(map[string]*pluginv2.DataResponse, len(res.Responses)),
}
for refID, dr := range res.Responses {
for _, f := range dr.Frames {
if f.RefID == "" {
f.RefID = refID
}
}
encodedFrames, err := dr.Frames.MarshalArrow()
if err != nil {
return nil, err
}
pDR := pluginv2.DataResponse{
Frames: encodedFrames,
}
if dr.Error != nil {
pDR.Error = dr.Error.Error()
}
pQDR.Responses[refID] = &pDR
}
return pQDR, nil
}
// CallResourceResponse converts the SDK version of a CallResourceResponse to the protobuf version.
func (t ConvertToProtobuf) CallResourceResponse(resp *CallResourceResponse) *pluginv2.CallResourceResponse {
headers := map[string]*pluginv2.StringList{}
for key, values := range resp.Headers {
headers[key] = &pluginv2.StringList{Values: values}
}
return &pluginv2.CallResourceResponse{
Headers: headers,
Code: int32(resp.Status),
Body: resp.Body,
}
}
// CallResourceRequest converts the SDK version of a CallResourceRequest to the protobuf version.
func (t ConvertToProtobuf) CallResourceRequest(req *CallResourceRequest) *pluginv2.CallResourceRequest {
protoReq := &pluginv2.CallResourceRequest{
PluginContext: t.PluginContext(req.PluginContext),
Path: req.Path,
Method: req.Method,
Url: req.URL,
Body: req.Body,
}
if req.Headers == nil {
return protoReq
}
protoReq.Headers = make(map[string]*pluginv2.StringList, len(protoReq.Headers))
for k, values := range req.Headers {
protoReq.Headers[k] = &pluginv2.StringList{Values: values}
}
return protoReq
}
+108
View File
@@ -0,0 +1,108 @@
package backend
import (
"context"
"encoding/json"
"time"
"github.com/grafana/grafana-plugin-sdk-go/data"
)
// QueryDataHandler handles data queries.
type QueryDataHandler interface {
// QueryData handles multiple queries and returns multiple responses.
// req contains the queries []DataQuery (where each query contains RefID as a unique identifier).
// The QueryDataResponse contains a map of RefID to the response for each query, and each response
// contains Frames ([]*Frame).
//
// The Frames' RefID property, when it is an empty string, will be automatically set to
// the RefID in QueryDataResponse.Responses map. This is done before the QueryDataResponse is
// sent to Grafana. Therefore one does not need to be set that property on frames when using this method.
QueryData(ctx context.Context, req *QueryDataRequest) (*QueryDataResponse, error)
}
// QueryDataHandlerFunc is an adapter to allow the use of
// ordinary functions as backend.QueryDataHandler. If f is a function
// with the appropriate signature, QueryDataHandlerFunc(f) is a
// Handler that calls f.
type QueryDataHandlerFunc func(ctx context.Context, req *QueryDataRequest) (*QueryDataResponse, error)
// QueryData calls fn(ctx, req).
func (fn QueryDataHandlerFunc) QueryData(ctx context.Context, req *QueryDataRequest) (*QueryDataResponse, error) {
return fn(ctx, req)
}
// QueryDataRequest contains a single request which contains multiple queries.
// It is the input type for a QueryData call.
type QueryDataRequest struct {
PluginContext PluginContext
Headers map[string]string
Queries []DataQuery
}
// DataQuery represents a single query as sent from the frontend.
// A slice of DataQuery makes up the Queries property of a QueryDataRequest.
type DataQuery struct {
// RefID is the unique identifier of the query, set by the frontend call.
RefID string
// QueryType is an optional identifier for the type of query.
// It can be used to distinguish different types of queries.
QueryType string
// MaxDataPoints is the maximum number of datapoints that should be returned from a time series query.
MaxDataPoints int64
// Interval is the suggested duration between time points in a time series query.
Interval time.Duration
// TimeRange is the Start and End of the query as sent by the frontend.
TimeRange TimeRange
// JSON is the raw JSON query and includes the above properties as well as custom properties.
JSON json.RawMessage
}
// QueryDataResponse contains the results from a QueryDataRequest.
// It is the return type of a QueryData call.
type QueryDataResponse struct {
// Responses is a map of RefIDs (Unique Query ID) to *DataResponse.
Responses Responses
}
// NewQueryDataResponse returns a QueryDataResponse with the Responses property initialized.
func NewQueryDataResponse() *QueryDataResponse {
return &QueryDataResponse{
Responses: make(Responses),
}
}
// Responses is a map of RefIDs (Unique Query ID) to DataResponses.
// The QueryData method the QueryDataHandler method will set the RefId
// property on the DataRespones' frames based on these RefIDs.
type Responses map[string]DataResponse
// DataResponse contains the results from a DataQuery.
// A map of RefIDs (unique query identifers) to this type makes up the Responses property of a QueryDataResponse.
// The Error property is used to allow for partial success responses from the containing QueryDataResponse.
type DataResponse struct {
// The data returned from the Query. Each Frame repeats the RefID.
Frames data.Frames
// Error is a property to be set if the the corresponding DataQuery has an error.
Error error
}
// TimeRange represents a time range for a query and is a property of DataQuery.
type TimeRange struct {
// From is the start time of the query.
From time.Time
// To is the end time of the query.
To time.Time
}
// Duration returns a time.Duration representing the amount of time between From and To.
func (tr TimeRange) Duration() time.Duration {
return tr.To.Sub(tr.From)
}
@@ -0,0 +1,27 @@
package backend
import (
"context"
"github.com/grafana/grafana-plugin-sdk-go/genproto/pluginv2"
)
// dataSDKAdapter adapter between low level plugin protocol and SDK interfaces.
type dataSDKAdapter struct {
queryDataHandler QueryDataHandler
}
func newDataSDKAdapter(handler QueryDataHandler) *dataSDKAdapter {
return &dataSDKAdapter{
queryDataHandler: handler,
}
}
func (a *dataSDKAdapter) QueryData(ctx context.Context, req *pluginv2.QueryDataRequest) (*pluginv2.QueryDataResponse, error) {
resp, err := a.queryDataHandler.QueryData(ctx, FromProto().QueryDataRequest(req))
if err != nil {
return nil, err
}
return ToProto().QueryDataResponse(resp)
}
+85
View File
@@ -0,0 +1,85 @@
package backend
import (
"context"
"strconv"
)
// CheckHealthHandler enables users to send health check
// requests to a backend plugin
type CheckHealthHandler interface {
CheckHealth(ctx context.Context, req *CheckHealthRequest) (*CheckHealthResult, error)
}
// CheckHealthHandlerFunc is an adapter to allow the use of
// ordinary functions as backend.CheckHealthHandler. If f is a function
// with the appropriate signature, CheckHealthHandlerFunc(f) is a
// Handler that calls f.
type CheckHealthHandlerFunc func(ctx context.Context, req *CheckHealthRequest) (*CheckHealthResult, error)
// CheckHealth calls fn(ctx, req).
func (fn CheckHealthHandlerFunc) CheckHealth(ctx context.Context, req *CheckHealthRequest) (*CheckHealthResult, error) {
return fn(ctx, req)
}
// HealthStatus is the status of the plugin.
type HealthStatus int
const (
// HealthStatusUnknown means the status of the plugin is unknown.
HealthStatusUnknown HealthStatus = iota
// HealthStatusOk means the status of the plugin is good.
HealthStatusOk
// HealthStatusError means the plugin is in an error state.
HealthStatusError
)
var healthStatusNames = map[int]string{
0: "UNKNOWN",
1: "OK",
2: "ERROR",
}
// String textual represntation of the status.
func (hs HealthStatus) String() string {
s, exists := healthStatusNames[int(hs)]
if exists {
return s
}
return strconv.Itoa(int(hs))
}
// CheckHealthRequest contains the healthcheck request
type CheckHealthRequest struct {
PluginContext PluginContext
}
// CheckHealthResult contains the healthcheck response
type CheckHealthResult struct {
Status HealthStatus
Message string
JSONDetails []byte
}
// CollectMetricsHandler handles metric collection.
type CollectMetricsHandler interface {
CollectMetrics(ctx context.Context) (*CollectMetricsResult, error)
}
// CollectMetricsHandlerFunc is an adapter to allow the use of
// ordinary functions as backend.CollectMetricsHandler. If f is a function
// with the appropriate signature, CollectMetricsHandlerFunc(f) is a
// Handler that calls f.
type CollectMetricsHandlerFunc func(ctx context.Context) (*CollectMetricsResult, error)
// CollectMetrics calls fn(ctx, req).
func (fn CollectMetricsHandlerFunc) CollectMetrics(ctx context.Context) (*CollectMetricsResult, error) {
return fn(ctx)
}
// CollectMetricsResult collect metrics result.
type CollectMetricsResult struct {
PrometheusMetrics []byte
}
@@ -0,0 +1,58 @@
package backend
import (
"bytes"
"context"
"github.com/grafana/grafana-plugin-sdk-go/genproto/pluginv2"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/common/expfmt"
)
// diagnosticsSDKAdapter adapter between low level plugin protocol and SDK interfaces.
type diagnosticsSDKAdapter struct {
metricGatherer prometheus.Gatherer
checkHealthHandler CheckHealthHandler
}
func newDiagnosticsSDKAdapter(metricGatherer prometheus.Gatherer, checkHealthHandler CheckHealthHandler) *diagnosticsSDKAdapter {
return &diagnosticsSDKAdapter{
metricGatherer: metricGatherer,
checkHealthHandler: checkHealthHandler,
}
}
func (a *diagnosticsSDKAdapter) CollectMetrics(ctx context.Context, protoReq *pluginv2.CollectMetricsRequest) (*pluginv2.CollectMetricsResponse, error) {
mfs, err := a.metricGatherer.Gather()
if err != nil {
return nil, err
}
var buf bytes.Buffer
for _, mf := range mfs {
_, err := expfmt.MetricFamilyToText(&buf, mf)
if err != nil {
return nil, err
}
}
return &pluginv2.CollectMetricsResponse{
Metrics: &pluginv2.CollectMetricsResponse_Payload{
Prometheus: buf.Bytes(),
},
}, nil
}
func (a *diagnosticsSDKAdapter) CheckHealth(ctx context.Context, protoReq *pluginv2.CheckHealthRequest) (*pluginv2.CheckHealthResponse, error) {
if a.checkHealthHandler != nil {
res, err := a.checkHealthHandler.CheckHealth(ctx, FromProto().CheckHealthRequest(protoReq))
if err != nil {
return nil, err
}
return ToProto().CheckHealthResponse(res), nil
}
return &pluginv2.CheckHealthResponse{
Status: pluginv2.CheckHealthResponse_OK,
}, nil
}
+2
View File
@@ -0,0 +1,2 @@
// Package backend provides SDK handler interfaces and contracts for implementing and serving backend plugins.
package backend
+24
View File
@@ -0,0 +1,24 @@
package backend
import "github.com/grafana/grafana-plugin-sdk-go/data"
// FrameResponse creates a DataResponse that contains the Framer's data.Frames.
func FrameResponse(f data.Framer) *DataResponse {
frames, err := f.Frames()
return &DataResponse{
Frames: frames,
Error: err,
}
}
// FrameResponseWithError creates a DataResponse with the error's contents (if not nil), and the Framer's data.Frames.
// This function is particularly useful if you have a function that returns `(StructX, error)`, where StructX implements Framer, which is a very common pattern.
func FrameResponseWithError(f data.Framer, err error) *DataResponse {
if err != nil {
return &DataResponse{
Error: err,
}
}
return FrameResponse(f)
}
@@ -0,0 +1,5 @@
// Package grpcplugin provides support for serving plugin over gRPC.
//
// This package is internal and meant to be used internally by the
// SDK and Grafana.
package grpcplugin
@@ -0,0 +1,60 @@
package grpcplugin
import (
"context"
"github.com/grafana/grafana-plugin-sdk-go/genproto/pluginv2"
plugin "github.com/hashicorp/go-plugin"
"google.golang.org/grpc"
)
// DataServer represents a data server.
type DataServer interface {
pluginv2.DataServer
}
// DataClient represents a data client.
type DataClient interface {
pluginv2.DataClient
}
// DataGRPCPlugin implements the GRPCPlugin interface from github.com/hashicorp/go-plugin.
type DataGRPCPlugin struct {
plugin.NetRPCUnsupportedPlugin
plugin.GRPCPlugin
DataServer DataServer
}
// GRPCServer registers p as a data gRPC server.
func (p *DataGRPCPlugin) GRPCServer(broker *plugin.GRPCBroker, s *grpc.Server) error {
pluginv2.RegisterDataServer(s, &dataGRPCServer{
server: p.DataServer,
})
return nil
}
// GRPCClient returns c as a data gRPC client.
func (p *DataGRPCPlugin) GRPCClient(ctx context.Context, broker *plugin.GRPCBroker, c *grpc.ClientConn) (interface{}, error) {
return &dataGRPCClient{client: pluginv2.NewDataClient(c)}, nil
}
type dataGRPCServer struct {
server DataServer
}
// QueryData queries s for data.
func (s *dataGRPCServer) QueryData(ctx context.Context, req *pluginv2.QueryDataRequest) (*pluginv2.QueryDataResponse, error) {
return s.server.QueryData(ctx, req)
}
type dataGRPCClient struct {
client pluginv2.DataClient
}
// QueryData queries m for data.
func (m *dataGRPCClient) QueryData(ctx context.Context, req *pluginv2.QueryDataRequest, opts ...grpc.CallOption) (*pluginv2.QueryDataResponse, error) {
return m.client.QueryData(ctx, req, opts...)
}
var _ DataServer = &dataGRPCServer{}
var _ DataClient = &dataGRPCClient{}
@@ -0,0 +1,70 @@
package grpcplugin
import (
"context"
"github.com/grafana/grafana-plugin-sdk-go/genproto/pluginv2"
plugin "github.com/hashicorp/go-plugin"
"google.golang.org/grpc"
)
// DiagnosticsServer is the server API for the Diagnostics service.
type DiagnosticsServer interface {
pluginv2.DiagnosticsServer
}
// DiagnosticsClient is the client API for the Diagnostics service.
type DiagnosticsClient interface {
pluginv2.DiagnosticsClient
}
// DiagnosticsGRPCPlugin implements the GRPCPlugin interface from github.com/hashicorp/go-plugin.
type DiagnosticsGRPCPlugin struct {
plugin.NetRPCUnsupportedPlugin
plugin.GRPCPlugin
DiagnosticsServer DiagnosticsServer
}
// GRPCServer registers p as a diagnostics gRPC server.
func (p *DiagnosticsGRPCPlugin) GRPCServer(broker *plugin.GRPCBroker, s *grpc.Server) error {
pluginv2.RegisterDiagnosticsServer(s, &diagnosticsGRPCServer{
server: p.DiagnosticsServer,
})
return nil
}
// GRPCClient returns c as a diagnostics gRPC client.
func (p *DiagnosticsGRPCPlugin) GRPCClient(ctx context.Context, broker *plugin.GRPCBroker, c *grpc.ClientConn) (interface{}, error) {
return &diagnosticsGRPCClient{client: pluginv2.NewDiagnosticsClient(c)}, nil
}
type diagnosticsGRPCServer struct {
server DiagnosticsServer
}
// CollectMetrics collects metrics.
func (s *diagnosticsGRPCServer) CollectMetrics(ctx context.Context, req *pluginv2.CollectMetricsRequest) (*pluginv2.CollectMetricsResponse, error) {
return s.server.CollectMetrics(ctx, req)
}
// CheckHealth checks health.
func (s *diagnosticsGRPCServer) CheckHealth(ctx context.Context, req *pluginv2.CheckHealthRequest) (*pluginv2.CheckHealthResponse, error) {
return s.server.CheckHealth(ctx, req)
}
type diagnosticsGRPCClient struct {
client pluginv2.DiagnosticsClient
}
// CollectMetrics collects metrics.
func (s *diagnosticsGRPCClient) CollectMetrics(ctx context.Context, req *pluginv2.CollectMetricsRequest, opts ...grpc.CallOption) (*pluginv2.CollectMetricsResponse, error) {
return s.client.CollectMetrics(ctx, req, opts...)
}
// CheckHealth checks health.
func (s *diagnosticsGRPCClient) CheckHealth(ctx context.Context, req *pluginv2.CheckHealthRequest, opts ...grpc.CallOption) (*pluginv2.CheckHealthResponse, error) {
return s.client.CheckHealth(ctx, req, opts...)
}
var _ DiagnosticsServer = &diagnosticsGRPCServer{}
var _ DiagnosticsClient = &diagnosticsGRPCClient{}
@@ -0,0 +1,60 @@
package grpcplugin
import (
"context"
"github.com/grafana/grafana-plugin-sdk-go/genproto/pluginv2"
plugin "github.com/hashicorp/go-plugin"
"google.golang.org/grpc"
)
// ResourceServer is the server API for the Resource service.
type ResourceServer interface {
pluginv2.ResourceServer
}
// ResourceClient is the client API for the Resource service.
type ResourceClient interface {
pluginv2.ResourceClient
}
// ResourceGRPCPlugin implements the GRPCPlugin interface from github.com/hashicorp/go-plugin.
type ResourceGRPCPlugin struct {
plugin.NetRPCUnsupportedPlugin
plugin.GRPCPlugin
ResourceServer ResourceServer
}
// GRPCServer registers p as a resource gRPC server.
func (p *ResourceGRPCPlugin) GRPCServer(broker *plugin.GRPCBroker, s *grpc.Server) error {
pluginv2.RegisterResourceServer(s, &resourceGRPCServer{
server: p.ResourceServer,
})
return nil
}
// GRPCClient returns c as a resource gRPC client.
func (p *ResourceGRPCPlugin) GRPCClient(ctx context.Context, broker *plugin.GRPCBroker, c *grpc.ClientConn) (interface{}, error) {
return &resourceGRPCClient{client: pluginv2.NewResourceClient(c)}, nil
}
type resourceGRPCServer struct {
server ResourceServer
}
// CallResource calls a resource.
func (s *resourceGRPCServer) CallResource(req *pluginv2.CallResourceRequest, srv pluginv2.Resource_CallResourceServer) error {
return s.server.CallResource(req, srv)
}
type resourceGRPCClient struct {
client pluginv2.ResourceClient
}
// CallResource calls a resource.
func (m *resourceGRPCClient) CallResource(ctx context.Context, req *pluginv2.CallResourceRequest, opts ...grpc.CallOption) (pluginv2.Resource_CallResourceClient, error) {
return m.client.CallResource(ctx, req, opts...)
}
var _ ResourceServer = &resourceGRPCServer{}
var _ ResourceClient = &resourceGRPCClient{}
@@ -0,0 +1,27 @@
package grpcplugin
import (
plugin "github.com/hashicorp/go-plugin"
)
const (
// ProtocolVersion is the current (latest) supported protocol version.
ProtocolVersion = 2
// MagicCookieKey is the the magic cookie key that will be used for negotiating
// between plugin host and client.
// Should NEVER be changed.
MagicCookieKey = "grafana_plugin_type"
// MagicCookieValue is the the magic cookie value that will be used for negotiating
// between plugin host and client.
// Should NEVER be changed.
MagicCookieValue = "datasource"
)
// handshake is the HandshakeConfig used to configure clients and servers.
var handshake = plugin.HandshakeConfig{
ProtocolVersion: ProtocolVersion,
MagicCookieKey: MagicCookieKey,
MagicCookieValue: MagicCookieValue,
}
@@ -0,0 +1,62 @@
package grpcplugin
import (
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
plugin "github.com/hashicorp/go-plugin"
"google.golang.org/grpc"
)
// ServeOpts contains options for serving plugins.
type ServeOpts struct {
DiagnosticsServer DiagnosticsServer
ResourceServer ResourceServer
DataServer DataServer
// GRPCServer factory method for creating GRPC server.
// If nil, the default one will be used.
GRPCServer func(options []grpc.ServerOption) *grpc.Server
}
// Serve starts serving the plugin over gRPC.
func Serve(opts ServeOpts) error {
versionedPlugins := make(map[int]plugin.PluginSet)
pSet := make(plugin.PluginSet)
if opts.DiagnosticsServer != nil {
pSet["diagnostics"] = &DiagnosticsGRPCPlugin{
DiagnosticsServer: opts.DiagnosticsServer,
}
}
if opts.ResourceServer != nil {
pSet["resource"] = &ResourceGRPCPlugin{
ResourceServer: opts.ResourceServer,
}
}
if opts.DataServer != nil {
pSet["data"] = &DataGRPCPlugin{
DataServer: opts.DataServer,
}
}
versionedPlugins[ProtocolVersion] = pSet
if opts.GRPCServer == nil {
opts.GRPCServer = plugin.DefaultGRPCServer
}
plugKeys := []string{}
for k := range pSet {
plugKeys = append(plugKeys, k)
}
log.DefaultLogger.Debug("Serving plugin", "plugins", plugKeys)
plugin.Serve(&plugin.ServeConfig{
HandshakeConfig: handshake,
VersionedPlugins: versionedPlugins,
GRPCServer: opts.GRPCServer,
})
log.DefaultLogger.Debug("Plugin server exited")
return nil
}
+9
View File
@@ -0,0 +1,9 @@
package backend
import (
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
)
// Logger is the default logger instance. This can be used directly to log from
// your plugin to grafana-server with calls like backend.Logger.Debug(...).
var Logger log.Logger = log.DefaultLogger
+50
View File
@@ -0,0 +1,50 @@
// Package log provides a logging interface to send logs from plugins to Grafana server.
package log
import (
hclog "github.com/hashicorp/go-hclog"
)
// Logger is the main Logger interface.
type Logger interface {
Debug(msg string, args ...interface{})
Info(msg string, args ...interface{})
Warn(msg string, args ...interface{})
Error(msg string, args ...interface{})
}
// New creates a new logger.
func New() Logger {
return &hclogWrapper{
logger: hclog.New(&hclog.LoggerOptions{
// Use debug as level since anything less severe is suppressed.
Level: hclog.Debug,
// Use JSON format to make the output in Grafana format and work
// when using multiple arguments such as Debug("message", "key", "value").
JSONFormat: true,
}),
}
}
type hclogWrapper struct {
logger hclog.Logger
}
func (l *hclogWrapper) Debug(msg string, args ...interface{}) {
l.logger.Debug(msg, args...)
}
func (l *hclogWrapper) Info(msg string, args ...interface{}) {
l.logger.Info(msg, args...)
}
func (l *hclogWrapper) Warn(msg string, args ...interface{}) {
l.logger.Warn(msg, args...)
}
func (l *hclogWrapper) Error(msg string, args ...interface{}) {
l.logger.Error(msg, args...)
}
// DefaultLogger is the default logger.
var DefaultLogger = New()
+43
View File
@@ -0,0 +1,43 @@
package backend
import (
"context"
)
// CallResourceRequest represents a request for a resource call.
type CallResourceRequest struct {
PluginContext PluginContext
Path string
Method string
URL string
Headers map[string][]string
Body []byte
}
// CallResourceResponse represents a response from a resource call.
type CallResourceResponse struct {
Status int
Headers map[string][]string
Body []byte
}
// CallResourceResponseSender is used for sending resource call responses.
type CallResourceResponseSender interface {
Send(*CallResourceResponse) error
}
// CallResourceHandler handles resource calls.
type CallResourceHandler interface {
CallResource(ctx context.Context, req *CallResourceRequest, sender CallResourceResponseSender) error
}
// CallResourceHandlerFunc is an adapter to allow the use of
// ordinary functions as backend.CallResourceHandler. If f is a function
// with the appropriate signature, CallResourceHandlerFunc(f) is a
// Handler that calls f.
type CallResourceHandlerFunc func(ctx context.Context, req *CallResourceRequest, sender CallResourceResponseSender) error
// CallResource calls fn(ctx, req, sender).
func (fn CallResourceHandlerFunc) CallResource(ctx context.Context, req *CallResourceRequest, sender CallResourceResponseSender) error {
return fn(ctx, req, sender)
}
@@ -0,0 +1,38 @@
package backend
import (
"net/http"
"github.com/grafana/grafana-plugin-sdk-go/genproto/pluginv2"
)
// resourceSDKAdapter adapter between low level plugin protocol and SDK interfaces.
type resourceSDKAdapter struct {
callResourceHandler CallResourceHandler
}
func newResourceSDKAdapter(handler CallResourceHandler) *resourceSDKAdapter {
return &resourceSDKAdapter{
callResourceHandler: handler,
}
}
type callResourceResponseSenderFunc func(resp *CallResourceResponse) error
func (fn callResourceResponseSenderFunc) Send(resp *CallResourceResponse) error {
return fn(resp)
}
func (a *resourceSDKAdapter) CallResource(protoReq *pluginv2.CallResourceRequest, protoSrv pluginv2.Resource_CallResourceServer) error {
if a.callResourceHandler == nil {
return protoSrv.Send(&pluginv2.CallResourceResponse{
Code: http.StatusNotImplemented,
})
}
fn := callResourceResponseSenderFunc(func(resp *CallResourceResponse) error {
return protoSrv.Send(ToProto().CallResourceResponse(resp))
})
return a.callResourceHandler.CallResource(protoSrv.Context(), FromProto().CallResourceRequest(protoReq), fn)
}
+81
View File
@@ -0,0 +1,81 @@
package backend
import (
"github.com/grafana/grafana-plugin-sdk-go/backend/grpcplugin"
grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware"
grpc_prometheus "github.com/grpc-ecosystem/go-grpc-prometheus"
"github.com/prometheus/client_golang/prometheus"
"google.golang.org/grpc"
)
const defaultServerMaxReceiveMessageSize = 1024 * 1024 * 16
// GRPCSettings settings for gRPC.
type GRPCSettings struct {
// MaxReceiveMsgSize the max gRPC message size in bytes the plugin can receive.
// If this is <= 0, gRPC uses the default 16MB.
MaxReceiveMsgSize int
// MaxSendMsgSize the max gRPC message size in bytes the plugin can send.
// If this is <= 0, gRPC uses the default `math.MaxInt32`.
MaxSendMsgSize int
}
// ServeOpts options for serving plugins.
type ServeOpts struct {
// CheckHealthHandler handler for health checks.
CheckHealthHandler CheckHealthHandler
// CallResourceHandler handler for resource calls.
// Optional to implement.
CallResourceHandler CallResourceHandler
// QueryDataHandler handler for data queries.
// Required to implement if data source.
QueryDataHandler QueryDataHandler
// GRPCSettings settings for gPRC.
GRPCSettings GRPCSettings
}
// Serve starts serving the plugin over gRPC.
func Serve(opts ServeOpts) error {
pluginOpts := grpcplugin.ServeOpts{
DiagnosticsServer: newDiagnosticsSDKAdapter(prometheus.DefaultGatherer, opts.CheckHealthHandler),
}
if opts.CallResourceHandler != nil {
pluginOpts.ResourceServer = newResourceSDKAdapter(opts.CallResourceHandler)
}
if opts.QueryDataHandler != nil {
pluginOpts.DataServer = newDataSDKAdapter(opts.QueryDataHandler)
}
grpc_prometheus.EnableHandlingTimeHistogram()
grpcMiddlewares := []grpc.ServerOption{
grpc.StreamInterceptor(grpc_middleware.ChainStreamServer(
grpc_prometheus.StreamServerInterceptor,
)),
grpc.UnaryInterceptor(grpc_middleware.ChainUnaryServer(
grpc_prometheus.UnaryServerInterceptor,
)),
}
if opts.GRPCSettings.MaxReceiveMsgSize <= 0 {
opts.GRPCSettings.MaxReceiveMsgSize = defaultServerMaxReceiveMessageSize
}
grpcMiddlewares = append([]grpc.ServerOption{grpc.MaxRecvMsgSize(opts.GRPCSettings.MaxReceiveMsgSize)}, grpcMiddlewares...)
if opts.GRPCSettings.MaxSendMsgSize > 0 {
grpcMiddlewares = append([]grpc.ServerOption{grpc.MaxSendMsgSize(opts.GRPCSettings.MaxSendMsgSize)}, grpcMiddlewares...)
}
pluginOpts.GRPCServer = func(opts []grpc.ServerOption) *grpc.Server {
opts = append(opts, grpcMiddlewares...)
return grpc.NewServer(opts...)
}
return grpcplugin.Serve(pluginOpts)
}
+56
View File
@@ -0,0 +1,56 @@
package backend
import (
"fmt"
"net/http"
"net/http/pprof"
"os"
)
const (
// PluginProfilerEnv is a constant for the GF_PLUGINS_PROFILER environment variable used to enable pprof.
PluginProfilerEnv = "GF_PLUGINS_PROFILER"
// PluginProfilerPortEnv is a constant for the GF_PLUGINS_PROFILER_PORT environment variable use to specify a pprof port (default 6060).
PluginProfilerPortEnv = "GF_PLUGINS_PROFILER_PORT"
)
// SetupPluginEnvironment will read the environment variables and apply the
// standard environment behavior.
//
// As the SDK evolves, this will likely change.
//
// Currently this function enables and configures profiling with pprof.
func SetupPluginEnvironment(pluginID string) {
// Enable profiler
profilerEnabled := false
if value, ok := os.LookupEnv(PluginProfilerEnv); ok {
// compare value to plugin name
if value == pluginID {
profilerEnabled = true
}
}
Logger.Info("Profiler", "enabled", profilerEnabled)
if profilerEnabled {
profilerPort := "6060"
if value, ok := os.LookupEnv(PluginProfilerPortEnv); ok {
profilerPort = value
}
Logger.Info("Profiler", "port", profilerPort)
portConfig := fmt.Sprintf(":%s", profilerPort)
r := http.NewServeMux()
r.HandleFunc("/debug/pprof/", pprof.Index)
r.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
r.HandleFunc("/debug/pprof/profile", pprof.Profile)
r.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
r.HandleFunc("/debug/pprof/trace", pprof.Trace)
go func() {
if err := http.ListenAndServe(portConfig, r); err != nil {
Logger.Error("Error Running profiler: %s", err.Error())
}
}()
}
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,330 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
package pluginv2
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
)
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
// Requires gRPC-Go v1.32.0 or later.
const _ = grpc.SupportPackageIsVersion7
// ResourceClient is the client API for Resource service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type ResourceClient interface {
CallResource(ctx context.Context, in *CallResourceRequest, opts ...grpc.CallOption) (Resource_CallResourceClient, error)
}
type resourceClient struct {
cc grpc.ClientConnInterface
}
func NewResourceClient(cc grpc.ClientConnInterface) ResourceClient {
return &resourceClient{cc}
}
func (c *resourceClient) CallResource(ctx context.Context, in *CallResourceRequest, opts ...grpc.CallOption) (Resource_CallResourceClient, error) {
stream, err := c.cc.NewStream(ctx, &Resource_ServiceDesc.Streams[0], "/pluginv2.Resource/CallResource", opts...)
if err != nil {
return nil, err
}
x := &resourceCallResourceClient{stream}
if err := x.ClientStream.SendMsg(in); err != nil {
return nil, err
}
if err := x.ClientStream.CloseSend(); err != nil {
return nil, err
}
return x, nil
}
type Resource_CallResourceClient interface {
Recv() (*CallResourceResponse, error)
grpc.ClientStream
}
type resourceCallResourceClient struct {
grpc.ClientStream
}
func (x *resourceCallResourceClient) Recv() (*CallResourceResponse, error) {
m := new(CallResourceResponse)
if err := x.ClientStream.RecvMsg(m); err != nil {
return nil, err
}
return m, nil
}
// ResourceServer is the server API for Resource service.
// All implementations should embed UnimplementedResourceServer
// for forward compatibility
type ResourceServer interface {
CallResource(*CallResourceRequest, Resource_CallResourceServer) error
}
// UnimplementedResourceServer should be embedded to have forward compatible implementations.
type UnimplementedResourceServer struct {
}
func (UnimplementedResourceServer) CallResource(*CallResourceRequest, Resource_CallResourceServer) error {
return status.Errorf(codes.Unimplemented, "method CallResource not implemented")
}
// UnsafeResourceServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to ResourceServer will
// result in compilation errors.
type UnsafeResourceServer interface {
mustEmbedUnimplementedResourceServer()
}
func RegisterResourceServer(s grpc.ServiceRegistrar, srv ResourceServer) {
s.RegisterService(&Resource_ServiceDesc, srv)
}
func _Resource_CallResource_Handler(srv interface{}, stream grpc.ServerStream) error {
m := new(CallResourceRequest)
if err := stream.RecvMsg(m); err != nil {
return err
}
return srv.(ResourceServer).CallResource(m, &resourceCallResourceServer{stream})
}
type Resource_CallResourceServer interface {
Send(*CallResourceResponse) error
grpc.ServerStream
}
type resourceCallResourceServer struct {
grpc.ServerStream
}
func (x *resourceCallResourceServer) Send(m *CallResourceResponse) error {
return x.ServerStream.SendMsg(m)
}
// Resource_ServiceDesc is the grpc.ServiceDesc for Resource service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var Resource_ServiceDesc = grpc.ServiceDesc{
ServiceName: "pluginv2.Resource",
HandlerType: (*ResourceServer)(nil),
Methods: []grpc.MethodDesc{},
Streams: []grpc.StreamDesc{
{
StreamName: "CallResource",
Handler: _Resource_CallResource_Handler,
ServerStreams: true,
},
},
Metadata: "backend.proto",
}
// DataClient is the client API for Data service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type DataClient interface {
QueryData(ctx context.Context, in *QueryDataRequest, opts ...grpc.CallOption) (*QueryDataResponse, error)
}
type dataClient struct {
cc grpc.ClientConnInterface
}
func NewDataClient(cc grpc.ClientConnInterface) DataClient {
return &dataClient{cc}
}
func (c *dataClient) QueryData(ctx context.Context, in *QueryDataRequest, opts ...grpc.CallOption) (*QueryDataResponse, error) {
out := new(QueryDataResponse)
err := c.cc.Invoke(ctx, "/pluginv2.Data/QueryData", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// DataServer is the server API for Data service.
// All implementations should embed UnimplementedDataServer
// for forward compatibility
type DataServer interface {
QueryData(context.Context, *QueryDataRequest) (*QueryDataResponse, error)
}
// UnimplementedDataServer should be embedded to have forward compatible implementations.
type UnimplementedDataServer struct {
}
func (UnimplementedDataServer) QueryData(context.Context, *QueryDataRequest) (*QueryDataResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method QueryData not implemented")
}
// UnsafeDataServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to DataServer will
// result in compilation errors.
type UnsafeDataServer interface {
mustEmbedUnimplementedDataServer()
}
func RegisterDataServer(s grpc.ServiceRegistrar, srv DataServer) {
s.RegisterService(&Data_ServiceDesc, srv)
}
func _Data_QueryData_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(QueryDataRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(DataServer).QueryData(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/pluginv2.Data/QueryData",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(DataServer).QueryData(ctx, req.(*QueryDataRequest))
}
return interceptor(ctx, in, info, handler)
}
// Data_ServiceDesc is the grpc.ServiceDesc for Data service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var Data_ServiceDesc = grpc.ServiceDesc{
ServiceName: "pluginv2.Data",
HandlerType: (*DataServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "QueryData",
Handler: _Data_QueryData_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "backend.proto",
}
// DiagnosticsClient is the client API for Diagnostics service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type DiagnosticsClient interface {
CheckHealth(ctx context.Context, in *CheckHealthRequest, opts ...grpc.CallOption) (*CheckHealthResponse, error)
CollectMetrics(ctx context.Context, in *CollectMetricsRequest, opts ...grpc.CallOption) (*CollectMetricsResponse, error)
}
type diagnosticsClient struct {
cc grpc.ClientConnInterface
}
func NewDiagnosticsClient(cc grpc.ClientConnInterface) DiagnosticsClient {
return &diagnosticsClient{cc}
}
func (c *diagnosticsClient) CheckHealth(ctx context.Context, in *CheckHealthRequest, opts ...grpc.CallOption) (*CheckHealthResponse, error) {
out := new(CheckHealthResponse)
err := c.cc.Invoke(ctx, "/pluginv2.Diagnostics/CheckHealth", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *diagnosticsClient) CollectMetrics(ctx context.Context, in *CollectMetricsRequest, opts ...grpc.CallOption) (*CollectMetricsResponse, error) {
out := new(CollectMetricsResponse)
err := c.cc.Invoke(ctx, "/pluginv2.Diagnostics/CollectMetrics", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// DiagnosticsServer is the server API for Diagnostics service.
// All implementations should embed UnimplementedDiagnosticsServer
// for forward compatibility
type DiagnosticsServer interface {
CheckHealth(context.Context, *CheckHealthRequest) (*CheckHealthResponse, error)
CollectMetrics(context.Context, *CollectMetricsRequest) (*CollectMetricsResponse, error)
}
// UnimplementedDiagnosticsServer should be embedded to have forward compatible implementations.
type UnimplementedDiagnosticsServer struct {
}
func (UnimplementedDiagnosticsServer) CheckHealth(context.Context, *CheckHealthRequest) (*CheckHealthResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method CheckHealth not implemented")
}
func (UnimplementedDiagnosticsServer) CollectMetrics(context.Context, *CollectMetricsRequest) (*CollectMetricsResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method CollectMetrics not implemented")
}
// UnsafeDiagnosticsServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to DiagnosticsServer will
// result in compilation errors.
type UnsafeDiagnosticsServer interface {
mustEmbedUnimplementedDiagnosticsServer()
}
func RegisterDiagnosticsServer(s grpc.ServiceRegistrar, srv DiagnosticsServer) {
s.RegisterService(&Diagnostics_ServiceDesc, srv)
}
func _Diagnostics_CheckHealth_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(CheckHealthRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(DiagnosticsServer).CheckHealth(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/pluginv2.Diagnostics/CheckHealth",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(DiagnosticsServer).CheckHealth(ctx, req.(*CheckHealthRequest))
}
return interceptor(ctx, in, info, handler)
}
func _Diagnostics_CollectMetrics_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(CollectMetricsRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(DiagnosticsServer).CollectMetrics(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/pluginv2.Diagnostics/CollectMetrics",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(DiagnosticsServer).CollectMetrics(ctx, req.(*CollectMetricsRequest))
}
return interceptor(ctx, in, info, handler)
}
// Diagnostics_ServiceDesc is the grpc.ServiceDesc for Diagnostics service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var Diagnostics_ServiceDesc = grpc.ServiceDesc{
ServiceName: "pluginv2.Diagnostics",
HandlerType: (*DiagnosticsServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "CheckHealth",
Handler: _Diagnostics_CheckHealth_Handler,
},
{
MethodName: "CollectMetrics",
Handler: _Diagnostics_CollectMetrics_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "backend.proto",
}
+222
View File
@@ -0,0 +1,222 @@
package bus
import (
"context"
"errors"
"fmt"
"reflect"
"github.com/opentracing/opentracing-go"
)
// HandlerFunc defines a handler function interface.
type HandlerFunc interface{}
// Msg defines a message interface.
type Msg interface{}
// ErrHandlerNotFound defines an error if a handler is not found
var ErrHandlerNotFound = errors.New("handler not found")
// TransactionManager defines a transaction interface
type TransactionManager interface {
InTransaction(ctx context.Context, fn func(ctx context.Context) error) error
}
// Bus type defines the bus interface structure
type Bus interface {
Dispatch(msg Msg) error
DispatchCtx(ctx context.Context, msg Msg) error
Publish(msg Msg) error
// InTransaction starts a transaction and store it in the context.
// The caller can then pass a function with multiple DispatchCtx calls that
// all will be executed in the same transaction. InTransaction will rollback if the
// callback returns an error.
InTransaction(ctx context.Context, fn func(ctx context.Context) error) error
AddHandler(handler HandlerFunc)
AddHandlerCtx(handler HandlerFunc)
AddEventListener(handler HandlerFunc)
// SetTransactionManager allows the user to replace the internal
// noop TransactionManager that is responsible for managing
// transactions in `InTransaction`
SetTransactionManager(tm TransactionManager)
}
// InTransaction defines an in transaction function
func (b *InProcBus) InTransaction(ctx context.Context, fn func(ctx context.Context) error) error {
return b.txMng.InTransaction(ctx, fn)
}
// InProcBus defines the bus structure
type InProcBus struct {
handlers map[string]HandlerFunc
handlersWithCtx map[string]HandlerFunc
listeners map[string][]HandlerFunc
txMng TransactionManager
}
// temp stuff, not sure how to handle bus instance, and init yet
var globalBus = New()
// New initialize the bus
func New() Bus {
bus := &InProcBus{}
bus.handlers = make(map[string]HandlerFunc)
bus.handlersWithCtx = make(map[string]HandlerFunc)
bus.listeners = make(map[string][]HandlerFunc)
bus.txMng = &noopTransactionManager{}
return bus
}
// Want to get rid of global bus
func GetBus() Bus {
return globalBus
}
// SetTransactionManager function assign a transaction manager to the bus.
func (b *InProcBus) SetTransactionManager(tm TransactionManager) {
b.txMng = tm
}
// DispatchCtx function dispatch a message to the bus context.
func (b *InProcBus) DispatchCtx(ctx context.Context, msg Msg) error {
var msgName = reflect.TypeOf(msg).Elem().Name()
span, ctx := opentracing.StartSpanFromContext(ctx, "bus - "+msgName)
defer span.Finish()
span.SetTag("msg", msgName)
var handler = b.handlersWithCtx[msgName]
if handler == nil {
return ErrHandlerNotFound
}
var params = []reflect.Value{}
params = append(params, reflect.ValueOf(ctx))
params = append(params, reflect.ValueOf(msg))
ret := reflect.ValueOf(handler).Call(params)
err := ret[0].Interface()
if err == nil {
return nil
}
return err.(error)
}
// Dispatch function dispatch a message to the bus.
func (b *InProcBus) Dispatch(msg Msg) error {
var msgName = reflect.TypeOf(msg).Elem().Name()
withCtx := true
handler := b.handlersWithCtx[msgName]
if handler == nil {
withCtx = false
handler = b.handlers[msgName]
if handler == nil {
return ErrHandlerNotFound
}
}
var params = []reflect.Value{}
if withCtx {
params = append(params, reflect.ValueOf(context.Background()))
}
params = append(params, reflect.ValueOf(msg))
ret := reflect.ValueOf(handler).Call(params)
err := ret[0].Interface()
if err == nil {
return nil
}
return err.(error)
}
// Publish function publish a message to the bus listener.
func (b *InProcBus) Publish(msg Msg) error {
var msgName = reflect.TypeOf(msg).Elem().Name()
var listeners = b.listeners[msgName]
var params = make([]reflect.Value, 1)
params[0] = reflect.ValueOf(msg)
for _, listenerHandler := range listeners {
ret := reflect.ValueOf(listenerHandler).Call(params)
e := ret[0].Interface()
if e != nil {
err, ok := e.(error)
if ok {
return err
}
return fmt.Errorf("expected listener to return an error, got '%T'", e)
}
}
return nil
}
func (b *InProcBus) AddHandler(handler HandlerFunc) {
handlerType := reflect.TypeOf(handler)
queryTypeName := handlerType.In(0).Elem().Name()
b.handlers[queryTypeName] = handler
}
func (b *InProcBus) AddHandlerCtx(handler HandlerFunc) {
handlerType := reflect.TypeOf(handler)
queryTypeName := handlerType.In(1).Elem().Name()
b.handlersWithCtx[queryTypeName] = handler
}
func (b *InProcBus) AddEventListener(handler HandlerFunc) {
handlerType := reflect.TypeOf(handler)
eventName := handlerType.In(0).Elem().Name()
_, exists := b.listeners[eventName]
if !exists {
b.listeners[eventName] = make([]HandlerFunc, 0)
}
b.listeners[eventName] = append(b.listeners[eventName], handler)
}
// AddHandler attaches a handler function to the global bus.
// Package level function.
func AddHandler(implName string, handler HandlerFunc) {
globalBus.AddHandler(handler)
}
// AddHandlerCtx attaches a handler function to the global bus context.
// Package level function.
func AddHandlerCtx(implName string, handler HandlerFunc) {
globalBus.AddHandlerCtx(handler)
}
// AddEventListener attaches a handler function to the event listener.
// Package level function.
func AddEventListener(handler HandlerFunc) {
globalBus.AddEventListener(handler)
}
func Dispatch(msg Msg) error {
return globalBus.Dispatch(msg)
}
func DispatchCtx(ctx context.Context, msg Msg) error {
return globalBus.DispatchCtx(ctx, msg)
}
func Publish(msg Msg) error {
return globalBus.Publish(msg)
}
func ClearBusHandlers() {
globalBus = New()
}
type noopTransactionManager struct{}
func (*noopTransactionManager) InTransaction(ctx context.Context, fn func(ctx context.Context) error) error {
return fn(ctx)
}
+141
View File
@@ -0,0 +1,141 @@
package null
import (
"database/sql"
"encoding/json"
"fmt"
"math"
"reflect"
"strconv"
)
const (
nullString = "null"
)
// Float is a nullable float64.
// It does not consider zero values to be null.
// It will decode to null, not zero, if null.
type Float struct {
sql.NullFloat64
}
// NewFloat creates a new Float
func NewFloat(f float64, valid bool) Float {
return Float{
NullFloat64: sql.NullFloat64{
Float64: f,
Valid: valid,
},
}
}
// FloatFrom creates a new Float that will always be valid.
func FloatFrom(f float64) Float {
return NewFloat(f, true)
}
// FloatFromPtr creates a new Float that be null if f is nil.
func FloatFromPtr(f *float64) Float {
if f == nil {
return NewFloat(0, false)
}
return NewFloat(*f, true)
}
// FloatFromString creates a new Float from string f.
// If the string is equal to the value of nullString then the Float will be null.
// An empty string f will return an error.
func FloatFromString(f string, nullString string) (Float, error) {
if f == nullString {
return FloatFromPtr(nil), nil
}
fV, err := strconv.ParseFloat(f, 64)
if err != nil {
return Float{}, err
}
return FloatFrom(fV), nil
}
// UnmarshalJSON implements json.Unmarshaler.
// It supports number and null input.
// 0 will not be considered a null Float.
// It also supports unmarshaling a sql.NullFloat64.
func (f *Float) UnmarshalJSON(data []byte) error {
var err error
var v interface{}
if err = json.Unmarshal(data, &v); err != nil {
return err
}
switch x := v.(type) {
case float64:
f.Float64 = x
case map[string]interface{}:
err = json.Unmarshal(data, &f.NullFloat64)
case nil:
f.Valid = false
return nil
default:
err = fmt.Errorf("json: cannot unmarshal %v into Go value of type null.Float", reflect.TypeOf(v).Name())
}
f.Valid = err == nil
return err
}
// UnmarshalText implements encoding.TextUnmarshaler.
// It will unmarshal to a null Float if the input is a blank or not an integer.
// It will return an error if the input is not an integer, blank, or "null".
func (f *Float) UnmarshalText(text []byte) error {
str := string(text)
if str == "" || str == nullString {
f.Valid = false
return nil
}
var err error
f.Float64, err = strconv.ParseFloat(string(text), 64)
f.Valid = err == nil
return err
}
// MarshalJSON implements json.Marshaler.
// It will encode null if this Float is null.
func (f Float) MarshalJSON() ([]byte, error) {
if !f.Valid || math.IsNaN(f.Float64) {
return []byte(nullString), nil
}
return []byte(strconv.FormatFloat(f.Float64, 'f', -1, 64)), nil
}
// MarshalText implements encoding.TextMarshaler.
// It will encode a blank string if this Float is null.
func (f Float) MarshalText() ([]byte, error) {
if !f.Valid {
return []byte{}, nil
}
return []byte(strconv.FormatFloat(f.Float64, 'f', -1, 64)), nil
}
// MarshalText implements encoding.TextMarshaler.
// It will encode a blank string if this Float is null.
func (f Float) String() string {
if !f.Valid {
return nullString
}
return fmt.Sprintf("%1.3f", f.Float64)
}
// FullString returns float as string in full precision
func (f Float) FullString() string {
if !f.Valid {
return nullString
}
return fmt.Sprintf("%f", f.Float64)
}
// IsZero returns true for invalid Floats, for future omitempty support (Go 1.4?)
// A non-null Float with a 0 value will not be considered zero.
func (f Float) IsZero() bool {
return !f.Valid
}
+269
View File
@@ -0,0 +1,269 @@
package expr
import (
"context"
"fmt"
"strings"
"time"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana/pkg/components/gtime"
"github.com/grafana/grafana/pkg/expr/mathexp"
)
// Command is an interface for all expression commands.
type Command interface {
NeedsVars() []string
Execute(c context.Context, vars mathexp.Vars) (mathexp.Results, error)
}
// MathCommand is a command for a math expression such as "1 + $GA / 2"
type MathCommand struct {
RawExpression string
Expression *mathexp.Expr
refID string
}
// NewMathCommand creates a new MathCommand. It will return an error
// if there is an error parsing expr.
func NewMathCommand(refID, expr string) (*MathCommand, error) {
parsedExpr, err := mathexp.New(expr)
if err != nil {
return nil, err
}
return &MathCommand{
RawExpression: expr,
Expression: parsedExpr,
refID: refID,
}, nil
}
// UnmarshalMathCommand creates a MathCommand from Grafana's frontend query.
func UnmarshalMathCommand(rn *rawNode) (*MathCommand, error) {
rawExpr, ok := rn.Query["expression"]
if !ok {
return nil, fmt.Errorf("math command for refId %v is missing an expression", rn.RefID)
}
exprString, ok := rawExpr.(string)
if !ok {
return nil, fmt.Errorf("expected math command for refId %v expression to be a string, got %T", rn.RefID, rawExpr)
}
gm, err := NewMathCommand(rn.RefID, exprString)
if err != nil {
return nil, fmt.Errorf("invalid math command type in '%v': %v", rn.RefID, err)
}
return gm, nil
}
// NeedsVars returns the variable names (refIds) that are dependencies
// to execute the command and allows the command to fulfill the Command interface.
func (gm *MathCommand) NeedsVars() []string {
return gm.Expression.VarNames
}
// Execute runs the command and returns the results or an error if the command
// failed to execute.
func (gm *MathCommand) Execute(ctx context.Context, vars mathexp.Vars) (mathexp.Results, error) {
return gm.Expression.Execute(gm.refID, vars)
}
// ReduceCommand is an expression command for reduction of a timeseries such as a min, mean, or max.
type ReduceCommand struct {
Reducer string
VarToReduce string
refID string
}
// NewReduceCommand creates a new ReduceCMD.
func NewReduceCommand(refID, reducer, varToReduce string) *ReduceCommand {
// TODO: validate reducer here, before execution
return &ReduceCommand{
Reducer: reducer,
VarToReduce: varToReduce,
refID: refID,
}
}
// UnmarshalReduceCommand creates a MathCMD from Grafana's frontend query.
func UnmarshalReduceCommand(rn *rawNode) (*ReduceCommand, error) {
rawVar, ok := rn.Query["expression"]
if !ok {
return nil, fmt.Errorf("no variable specified to reduce for refId %v", rn.RefID)
}
varToReduce, ok := rawVar.(string)
if !ok {
return nil, fmt.Errorf("expected reduce variable to be a string, got %T for refId %v", rawVar, rn.RefID)
}
varToReduce = strings.TrimPrefix(varToReduce, "$")
rawReducer, ok := rn.Query["reducer"]
if !ok {
return nil, fmt.Errorf("no reducer specified for refId %v", rn.RefID)
}
redFunc, ok := rawReducer.(string)
if !ok {
return nil, fmt.Errorf("expected reducer to be a string, got %T for refId %v", rawReducer, rn.RefID)
}
return NewReduceCommand(rn.RefID, redFunc, varToReduce), nil
}
// NeedsVars returns the variable names (refIds) that are dependencies
// to execute the command and allows the command to fulfill the Command interface.
func (gr *ReduceCommand) NeedsVars() []string {
return []string{gr.VarToReduce}
}
// Execute runs the command and returns the results or an error if the command
// failed to execute.
func (gr *ReduceCommand) Execute(ctx context.Context, vars mathexp.Vars) (mathexp.Results, error) {
newRes := mathexp.Results{}
for _, val := range vars[gr.VarToReduce].Values {
series, ok := val.(mathexp.Series)
if !ok {
return newRes, fmt.Errorf("can only reduce type series, got type %v", val.Type())
}
num, err := series.Reduce(gr.refID, gr.Reducer)
if err != nil {
return newRes, err
}
newRes.Values = append(newRes.Values, num)
}
return newRes, nil
}
// ResampleCommand is an expression command for resampling of a timeseries.
type ResampleCommand struct {
Window time.Duration
VarToResample string
Downsampler string
Upsampler string
TimeRange backend.TimeRange
refID string
}
// NewResampleCommand creates a new ResampleCMD.
func NewResampleCommand(refID, rawWindow, varToResample string, downsampler string, upsampler string, tr backend.TimeRange) (*ResampleCommand, error) {
// TODO: validate reducer here, before execution
window, err := gtime.ParseDuration(rawWindow)
if err != nil {
return nil, fmt.Errorf(`failed to parse resample "window" duration field %q: %w`, window, err)
}
return &ResampleCommand{
Window: window,
VarToResample: varToResample,
Downsampler: downsampler,
Upsampler: upsampler,
TimeRange: tr,
refID: refID,
}, nil
}
// UnmarshalResampleCommand creates a ResampleCMD from Grafana's frontend query.
func UnmarshalResampleCommand(rn *rawNode) (*ResampleCommand, error) {
rawVar, ok := rn.Query["expression"]
if !ok {
return nil, fmt.Errorf("no variable to resample for refId %v", rn.RefID)
}
varToReduce, ok := rawVar.(string)
if !ok {
return nil, fmt.Errorf("expected resample input variable to be type string, but got type %T for refId %v", rawVar, rn.RefID)
}
varToReduce = strings.TrimPrefix(varToReduce, "$")
varToResample := varToReduce
rawWindow, ok := rn.Query["window"]
if !ok {
return nil, fmt.Errorf("no time duration specified for the window in resample command for refId %v", rn.RefID)
}
window, ok := rawWindow.(string)
if !ok {
return nil, fmt.Errorf("expected resample window to be a string, got %T for refId %v", rawWindow, rn.RefID)
}
rawDownsampler, ok := rn.Query["downsampler"]
if !ok {
return nil, fmt.Errorf("no downsampler function specified in resample command for refId %v", rn.RefID)
}
downsampler, ok := rawDownsampler.(string)
if !ok {
return nil, fmt.Errorf("expected resample downsampler to be a string, got type %T for refId %v", downsampler, rn.RefID)
}
rawUpsampler, ok := rn.Query["upsampler"]
if !ok {
return nil, fmt.Errorf("no downsampler specified in resample command for refId %v", rn.RefID)
}
upsampler, ok := rawUpsampler.(string)
if !ok {
return nil, fmt.Errorf("expected resample downsampler to be a string, got type %T for refId %v", upsampler, rn.RefID)
}
return NewResampleCommand(rn.RefID, window, varToResample, downsampler, upsampler, rn.TimeRange)
}
// NeedsVars returns the variable names (refIds) that are dependencies
// to execute the command and allows the command to fulfill the Command interface.
func (gr *ResampleCommand) NeedsVars() []string {
return []string{gr.VarToResample}
}
// Execute runs the command and returns the results or an error if the command
// failed to execute.
func (gr *ResampleCommand) Execute(ctx context.Context, vars mathexp.Vars) (mathexp.Results, error) {
newRes := mathexp.Results{}
for _, val := range vars[gr.VarToResample].Values {
series, ok := val.(mathexp.Series)
if !ok {
return newRes, fmt.Errorf("can only resample type series, got type %v", val.Type())
}
num, err := series.Resample(gr.refID, gr.Window, gr.Downsampler, gr.Upsampler, gr.TimeRange)
if err != nil {
return newRes, err
}
newRes.Values = append(newRes.Values, num)
}
return newRes, nil
}
// CommandType is the type of the expression command.
type CommandType int
const (
// TypeUnknown is the CMDType for an unrecognized expression type.
TypeUnknown CommandType = iota
// TypeMath is the CMDType for a math expression.
TypeMath
// TypeReduce is the CMDType for a reduction expression.
TypeReduce
// TypeResample is the CMDType for a resampling expression.
TypeResample
)
func (gt CommandType) String() string {
switch gt {
case TypeMath:
return "math"
case TypeReduce:
return "reduce"
case TypeResample:
return "resample"
default:
return "unknown"
}
}
// ParseCommandType returns a CommandType from its string representation.
func ParseCommandType(s string) (CommandType, error) {
switch s {
case "math":
return TypeMath, nil
case "reduce":
return TypeReduce, nil
case "resample":
return TypeResample, nil
default:
return TypeUnknown, fmt.Errorf("'%v' is not a recognized expression type", s)
}
}
+183
View File
@@ -0,0 +1,183 @@
package expr
import (
"context"
"encoding/json"
"fmt"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana/pkg/expr/mathexp"
"gonum.org/v1/gonum/graph"
"gonum.org/v1/gonum/graph/simple"
"gonum.org/v1/gonum/graph/topo"
)
// NodeType is the type of a DPNode. Currently either a expression command or datasource query.
type NodeType int
const (
// TypeCMDNode is a NodeType for expression commands.
TypeCMDNode NodeType = iota
// TypeDatasourceNode is a NodeType for datasource queries.
TypeDatasourceNode
)
// Node is a node in a Data Pipeline. Node is either a expression command or a datasource query.
type Node interface {
ID() int64 // ID() allows the gonum graph node interface to be fulfilled
NodeType() NodeType
RefID() string
Execute(c context.Context, vars mathexp.Vars) (mathexp.Results, error)
String() string
}
// DataPipeline is an ordered set of nodes returned from DPGraph processing.
type DataPipeline []Node
// execute runs all the command/datasource requests in the pipeline return a
// map of the refId of the of each command
func (dp *DataPipeline) execute(c context.Context) (mathexp.Vars, error) {
vars := make(mathexp.Vars)
for _, node := range *dp {
res, err := node.Execute(c, vars)
if err != nil {
return nil, err
}
vars[node.RefID()] = res
}
return vars, nil
}
// BuildPipeline builds a graph of the nodes, and returns the nodes in an
// executable order.
func buildPipeline(req *backend.QueryDataRequest) (DataPipeline, error) {
graph, err := buildDependencyGraph(req)
if err != nil {
return nil, err
}
nodes, err := buildExecutionOrder(graph)
if err != nil {
return nil, err
}
return nodes, nil
}
// buildDependencyGraph returns a dependency graph for a set of queries.
func buildDependencyGraph(req *backend.QueryDataRequest) (*simple.DirectedGraph, error) {
graph, err := buildGraph(req)
if err != nil {
return nil, err
}
registry := buildNodeRegistry(graph)
if err := buildGraphEdges(graph, registry); err != nil {
return nil, err
}
return graph, nil
}
// buildExecutionOrder returns a sequence of nodes ordered by dependency.
func buildExecutionOrder(graph *simple.DirectedGraph) ([]Node, error) {
sortedNodes, err := topo.Sort(graph)
if err != nil {
return nil, err
}
nodes := make([]Node, len(sortedNodes))
for i, v := range sortedNodes {
nodes[i] = v.(Node)
}
return nodes, nil
}
// buildNodeRegistry returns a lookup table for reference IDs to respective node.
func buildNodeRegistry(g *simple.DirectedGraph) map[string]Node {
res := make(map[string]Node)
nodeIt := g.Nodes()
for nodeIt.Next() {
if dpNode, ok := nodeIt.Node().(Node); ok {
res[dpNode.RefID()] = dpNode
}
}
return res
}
// buildGraph creates a new graph populated with nodes for every query.
func buildGraph(req *backend.QueryDataRequest) (*simple.DirectedGraph, error) {
dp := simple.NewDirectedGraph()
for _, query := range req.Queries {
rawQueryProp := make(map[string]interface{})
err := json.Unmarshal(query.JSON, &rawQueryProp)
if err != nil {
return nil, err
}
rn := &rawNode{
Query: rawQueryProp,
RefID: query.RefID,
TimeRange: query.TimeRange,
QueryType: query.QueryType,
}
dsName, err := rn.GetDatasourceName()
if err != nil {
return nil, err
}
var node graph.Node
switch dsName {
case DatasourceName:
node, err = buildCMDNode(dp, rn)
default: // If it's not an expression query, it's a data source query.
node, err = buildDSNode(dp, rn, req.PluginContext.OrgID)
}
if err != nil {
return nil, err
}
dp.AddNode(node)
}
return dp, nil
}
// buildGraphEdges generates graph edges based on each node's dependencies.
func buildGraphEdges(dp *simple.DirectedGraph, registry map[string]Node) error {
nodeIt := dp.Nodes()
for nodeIt.Next() {
node := nodeIt.Node().(Node)
if node.NodeType() != TypeCMDNode {
// datasource node, nothing to do for now. Although if we want expression results to be
// used as datasource query params some day this will need change
continue
}
cmdNode := node.(*CMDNode)
for _, neededVar := range cmdNode.Command.NeedsVars() {
neededNode, ok := registry[neededVar]
if !ok {
return fmt.Errorf("unable to find dependent node '%v'", neededVar)
}
if neededNode.ID() == cmdNode.ID() {
return fmt.Errorf("can not add self referencing node for var '%v' ", neededVar)
}
edge := dp.NewEdge(neededNode, cmdNode)
dp.SetEdge(edge)
}
}
return nil
}
+539
View File
@@ -0,0 +1,539 @@
package mathexp
import (
"fmt"
"math"
"reflect"
"runtime"
"github.com/grafana/grafana-plugin-sdk-go/data"
"github.com/grafana/grafana/pkg/expr/mathexp/parse"
)
// Expr holds a parsed math command expression.
type Expr struct {
*parse.Tree
}
// State embeds a parsed Expr with variables and their results
// so the expression can act on them.
type State struct {
*Expr
Vars Vars
// Could hold more properties that change behavior around:
// - Unions (How many result A and many Result B in case A + B are joined)
// - NaN/Null behavior
RefID string
}
// Vars holds the results of datasource queries or other expression commands.
type Vars map[string]Results
// New creates a new expression tree
func New(expr string, funcs ...map[string]parse.Func) (*Expr, error) {
funcs = append(funcs, builtins)
t, err := parse.Parse(expr, funcs...)
if err != nil {
return nil, err
}
e := &Expr{
Tree: t,
}
return e, nil
}
// Execute applies a parse expression to the context and executes it
func (e *Expr) Execute(refID string, vars Vars) (r Results, err error) {
s := &State{
Expr: e,
Vars: vars,
RefID: refID,
}
return e.executeState(s)
}
func (e *Expr) executeState(s *State) (r Results, err error) {
defer errRecover(&err, s)
r, err = s.walk(e.Tree.Root)
return
}
// errRecover is the handler that turns panics into returns from the top
// level of Parse.
func errRecover(errp *error, s *State) {
e := recover()
if e != nil {
switch err := e.(type) {
case runtime.Error:
panic(e)
case error:
*errp = err
default:
panic(e)
}
}
}
// walk is the top level function to walk a parsed expression
// with its associate variables
func (e *State) walk(node parse.Node) (res Results, err error) {
switch node := node.(type) {
case *parse.ScalarNode:
res = NewScalarResults(e.RefID, &node.Float64)
case *parse.VarNode:
res = e.Vars[node.Name]
case *parse.BinaryNode:
res, err = e.walkBinary(node)
case *parse.UnaryNode:
res, err = e.walkUnary(node)
case *parse.FuncNode:
res, err = e.walkFunc(node)
default:
return res, fmt.Errorf("expr: can not walk node type: %s", node.Type())
}
return
}
func (e *State) walkUnary(node *parse.UnaryNode) (Results, error) {
a, err := e.walk(node.Arg)
if err != nil {
return Results{}, err
}
newResults := Results{}
for _, val := range a.Values {
var newVal Value
switch rt := val.(type) {
case Scalar:
newVal = NewScalar(e.RefID, nil)
f := rt.GetFloat64Value()
if f != nil {
newF, err := unaryOp(node.OpStr, *f)
if err != nil {
return newResults, err
}
newVal = NewScalar(e.RefID, &newF)
}
case Number:
newVal, err = e.unaryNumber(rt, node.OpStr)
case Series:
newVal, err = e.unarySeries(rt, node.OpStr)
default:
return newResults, fmt.Errorf("can not perform a unary operation on type %v", rt.Type())
}
if err != nil {
return newResults, err
}
newResults.Values = append(newResults.Values, newVal)
}
return newResults, nil
}
func (e *State) unarySeries(s Series, op string) (Series, error) {
newSeries := NewSeries(e.RefID, s.GetLabels(), s.TimeIdx, s.TimeIsNullable, s.ValueIdx, s.ValueIsNullable, s.Len())
for i := 0; i < s.Len(); i++ {
t, f := s.GetPoint(i)
if f == nil {
if err := newSeries.SetPoint(i, t, nil); err != nil {
return newSeries, err
}
continue
}
newF, err := unaryOp(op, *f)
if err != nil {
return newSeries, err
}
if err := newSeries.SetPoint(i, t, &newF); err != nil {
return newSeries, err
}
}
return newSeries, nil
}
func (e *State) unaryNumber(n Number, op string) (Number, error) {
newNumber := NewNumber(e.RefID, n.GetLabels())
f := n.GetFloat64Value()
if f != nil {
newF, err := unaryOp(op, *f)
if err != nil {
return newNumber, err
}
newNumber.SetValue(&newF)
}
return newNumber, nil
}
// unaryOp performs a unary operation on a float.
func unaryOp(op string, a float64) (r float64, err error) {
if math.IsNaN(a) {
return math.NaN(), nil
}
switch op {
case "!":
if a == 0 {
r = 1
} else {
r = 0
}
case "-":
r = -a
default:
return r, fmt.Errorf("expr: unknown unary operator %s", op)
}
return
}
// Union holds to Values from Two sets where their labels are compatible (TODO: define compatible).
// This is a intermediate container for Binary operations such (e.g. A + B).
type Union struct {
Labels data.Labels
A, B Value
}
// union creates Union objects based on the Labels attached to each Series or Number
// within a collection of Series or Numbers. The Unions are used with binary
// operations. The labels of the Union will the taken from result with a greater
// number of tags.
func union(aResults, bResults Results) []*Union {
unions := []*Union{}
if len(aResults.Values) == 0 || len(bResults.Values) == 0 {
return unions
}
for _, a := range aResults.Values {
for _, b := range bResults.Values {
var labels data.Labels
aLabels := a.GetLabels()
bLabels := b.GetLabels()
switch {
case aLabels.Equals(bLabels) || len(aLabels) == 0 || len(bLabels) == 0:
l := aLabels
if len(aLabels) == 0 {
l = bLabels
}
labels = l
case len(aLabels) == len(bLabels):
continue // invalid union, drop for now
case aLabels.Contains(bLabels):
labels = aLabels
case bLabels.Contains(aLabels):
labels = bLabels
default:
continue
}
u := &Union{
Labels: labels,
A: a,
B: b,
}
unions = append(unions, u)
}
}
if len(unions) == 0 && len(aResults.Values) == 1 && len(bResults.Values) == 1 {
// In the case of only 1 thing on each side of the operator, we combine them
// and strip the tags.
// This isn't ideal for understanding behavior, but will make more stuff work when
// combining different datasources without munging.
// This choice is highly questionable in the long term.
unions = append(unions, &Union{
A: aResults.Values[0],
B: bResults.Values[0],
})
}
return unions
}
func (e *State) walkBinary(node *parse.BinaryNode) (Results, error) {
res := Results{Values{}}
ar, err := e.walk(node.Args[0])
if err != nil {
return res, err
}
br, err := e.walk(node.Args[1])
if err != nil {
return res, err
}
unions := union(ar, br)
for _, uni := range unions {
var value Value
switch at := uni.A.(type) {
case Scalar:
aFloat := at.GetFloat64Value()
switch bt := uni.B.(type) {
// Scalar op Scalar
case Scalar:
bFloat := bt.GetFloat64Value()
if aFloat == nil || bFloat == nil {
value = NewScalar(e.RefID, nil)
break
}
f := math.NaN()
if aFloat != nil && bFloat != nil {
f, err = binaryOp(node.OpStr, *aFloat, *bFloat)
if err != nil {
return res, err
}
}
value = NewScalar(e.RefID, &f)
// Scalar op Scalar
case Number:
value, err = e.biScalarNumber(uni.Labels, node.OpStr, bt, aFloat, false)
// Scalar op Series
case Series:
value, err = e.biSeriesNumber(uni.Labels, node.OpStr, bt, aFloat, false)
default:
return res, fmt.Errorf("not implemented: binary %v on %T and %T", node.OpStr, uni.A, uni.B)
}
case Series:
switch bt := uni.B.(type) {
// Series Op Scalar
case Scalar:
bFloat := bt.GetFloat64Value()
value, err = e.biSeriesNumber(uni.Labels, node.OpStr, at, bFloat, true)
// case Series Op Number
case Number:
bFloat := bt.GetFloat64Value()
value, err = e.biSeriesNumber(uni.Labels, node.OpStr, at, bFloat, true)
// case Series op Series
case Series:
value, err = e.biSeriesSeries(uni.Labels, node.OpStr, at, bt)
default:
return res, fmt.Errorf("not implemented: binary %v on %T and %T", node.OpStr, uni.A, uni.B)
}
case Number:
aFloat := at.GetFloat64Value()
switch bt := uni.B.(type) {
case Scalar:
bFloat := bt.GetFloat64Value()
value, err = e.biScalarNumber(uni.Labels, node.OpStr, at, bFloat, true)
case Number:
bFloat := bt.GetFloat64Value()
value, err = e.biScalarNumber(uni.Labels, node.OpStr, at, bFloat, true)
case Series:
value, err = e.biSeriesNumber(uni.Labels, node.OpStr, bt, aFloat, false)
default:
return res, fmt.Errorf("not implemented: binary %v on %T and %T", node.OpStr, uni.A, uni.B)
}
default:
return res, fmt.Errorf("not implemented: binary %v on %T and %T", node.OpStr, uni.A, uni.B)
}
if err != nil {
return res, err
}
res.Values = append(res.Values, value)
}
return res, nil
}
// binaryOp performs a binary operations (e.g. A+B or A>B) on two
// float values
// nolint:gocyclo
func binaryOp(op string, a, b float64) (r float64, err error) {
// Test short circuit before NaN.
switch op {
case "||":
if a != 0 {
return 1, nil
}
case "&&":
if a == 0 {
return 0, nil
}
}
if math.IsNaN(a) || math.IsNaN(b) {
return math.NaN(), nil
}
switch op {
case "+":
r = a + b
case "*":
r = a * b
case "-":
r = a - b
case "/":
r = a / b
case "**":
r = math.Pow(a, b)
case "%":
r = math.Mod(a, b)
case "==":
if a == b {
r = 1
} else {
r = 0
}
case ">":
if a > b {
r = 1
} else {
r = 0
}
case "!=":
if a != b {
r = 1
} else {
r = 0
}
case "<":
if a < b {
r = 1
} else {
r = 0
}
case ">=":
if a >= b {
r = 1
} else {
r = 0
}
case "<=":
if a <= b {
r = 1
} else {
r = 0
}
case "||":
if a != 0 || b != 0 {
r = 1
} else {
r = 0
}
case "&&":
if a != 0 && b != 0 {
r = 1
} else {
r = 0
}
default:
return r, fmt.Errorf("expr: unknown operator %s", op)
}
return r, nil
}
func (e *State) biScalarNumber(labels data.Labels, op string, number Number, scalarVal *float64, numberFirst bool) (Number, error) {
newNumber := NewNumber(e.RefID, labels)
f := number.GetFloat64Value()
if f == nil || scalarVal == nil {
newNumber.SetValue(nil)
return newNumber, nil
}
nF := math.NaN()
var err error
if numberFirst {
nF, err = binaryOp(op, *f, *scalarVal)
} else {
nF, err = binaryOp(op, *scalarVal, *f)
}
if err != nil {
return newNumber, err
}
newNumber.SetValue(&nF)
return newNumber, nil
}
func (e *State) biSeriesNumber(labels data.Labels, op string, s Series, scalarVal *float64, seriesFirst bool) (Series, error) {
newSeries := NewSeries(e.RefID, labels, s.TimeIdx, s.TimeIsNullable, s.ValueIdx, s.ValueIsNullable, s.Len())
var err error
for i := 0; i < s.Len(); i++ {
nF := math.NaN()
t, f := s.GetPoint(i)
if f == nil || scalarVal == nil {
if err := newSeries.SetPoint(i, t, nil); err != nil {
return newSeries, err
}
continue
}
if seriesFirst {
nF, err = binaryOp(op, *f, *scalarVal)
} else {
nF, err = binaryOp(op, *scalarVal, *f)
}
if err != nil {
return newSeries, err
}
if err := newSeries.SetPoint(i, t, &nF); err != nil {
return newSeries, err
}
}
return newSeries, nil
}
// ... if would you like some series with your series and then get some series, or is that enough series?
// biSeriesSeries performs a the binary operation for each value in the two series where the times
// are equal. If there are datapoints in A or B that do not share a time, they will be dropped.
func (e *State) biSeriesSeries(labels data.Labels, op string, aSeries, bSeries Series) (Series, error) {
bPoints := make(map[string]*float64)
for i := 0; i < bSeries.Len(); i++ {
t, f := bSeries.GetPoint(i)
if t != nil {
bPoints[t.UTC().String()] = f
}
}
newSeries := NewSeries(
e.RefID, labels, aSeries.TimeIdx, aSeries.TimeIsNullable || bSeries.TimeIsNullable, aSeries.ValueIdx,
aSeries.ValueIsNullable || bSeries.ValueIsNullable, 0,
)
for aIdx := 0; aIdx < aSeries.Len(); aIdx++ {
aTime, aF := aSeries.GetPoint(aIdx)
bF, ok := bPoints[aTime.UTC().String()]
if !ok {
continue
}
if aF == nil || bF == nil {
if err := newSeries.AppendPoint(aIdx, aTime, nil); err != nil {
return newSeries, err
}
continue
}
nF, err := binaryOp(op, *aF, *bF)
if err != nil {
return newSeries, err
}
if err := newSeries.AppendPoint(aIdx, aTime, &nF); err != nil {
return newSeries, err
}
}
return newSeries, nil
}
func (e *State) walkFunc(node *parse.FuncNode) (Results, error) {
var res Results
var err error
var in []reflect.Value
for _, a := range node.Args {
var v interface{}
switch t := a.(type) {
case *parse.StringNode:
v = t.Text
case *parse.VarNode:
v = e.Vars[t.Name]
case *parse.ScalarNode:
v = NewScalarResults(e.RefID, &t.Float64)
case *parse.FuncNode:
v, err = e.walkFunc(t)
case *parse.UnaryNode:
v, err = e.walkUnary(t)
case *parse.BinaryNode:
v, err = e.walkBinary(t)
default:
return res, fmt.Errorf("expr: unknown func arg type: %T", t)
}
if err != nil {
return res, err
}
in = append(in, reflect.ValueOf(v))
}
f := reflect.ValueOf(node.F.F)
fr := f.Call(append([]reflect.Value{reflect.ValueOf(e)}, in...))
res = fr[0].Interface().(Results)
if len(fr) > 1 && !fr[1].IsNil() {
err := fr[1].Interface().(error)
if err != nil {
return res, err
}
}
return res, nil
}
+118
View File
@@ -0,0 +1,118 @@
package mathexp
import (
"math"
"github.com/grafana/grafana/pkg/expr/mathexp/parse"
)
var builtins = map[string]parse.Func{
"abs": {
Args: []parse.ReturnType{parse.TypeVariantSet},
VariantReturn: true,
F: abs,
},
"log": {
Args: []parse.ReturnType{parse.TypeVariantSet},
VariantReturn: true,
F: log,
},
"nan": {
Return: parse.TypeScalar,
F: nan,
},
"inf": {
Return: parse.TypeScalar,
F: inf,
},
"null": {
Return: parse.TypeScalar,
F: null,
},
}
// abs returns the absolute value for each result in NumberSet, SeriesSet, or Scalar
func abs(e *State, varSet Results) (Results, error) {
newRes := Results{}
for _, res := range varSet.Values {
newVal, err := perFloat(e, res, math.Abs)
if err != nil {
return newRes, err
}
newRes.Values = append(newRes.Values, newVal)
}
return newRes, nil
}
// log returns the natural logarithm value for each result in NumberSet, SeriesSet, or Scalar
func log(e *State, varSet Results) (Results, error) {
newRes := Results{}
for _, res := range varSet.Values {
newVal, err := perFloat(e, res, math.Log)
if err != nil {
return newRes, err
}
newRes.Values = append(newRes.Values, newVal)
}
return newRes, nil
}
// nan returns a scalar nan value
func nan(e *State) Results {
aNaN := math.NaN()
return NewScalarResults(e.RefID, &aNaN)
}
// inf returns a scalar positive infinity value
func inf(e *State) Results {
aInf := math.Inf(0)
return NewScalarResults(e.RefID, &aInf)
}
// null returns a null scalar value
func null(e *State) Results {
return NewScalarResults(e.RefID, nil)
}
func perFloat(e *State, val Value, floatF func(x float64) float64) (Value, error) {
var newVal Value
switch val.Type() {
case parse.TypeNumberSet:
n := NewNumber(e.RefID, val.GetLabels())
f := val.(Number).GetFloat64Value()
nF := math.NaN()
if f != nil {
nF = floatF(*f)
}
n.SetValue(&nF)
newVal = n
case parse.TypeScalar:
f := val.(Scalar).GetFloat64Value()
nF := math.NaN()
if f != nil {
nF = floatF(*f)
}
newVal = NewScalar(e.RefID, &nF)
case parse.TypeSeriesSet:
resSeries := val.(Series)
newSeries := NewSeries(
e.RefID, resSeries.GetLabels(), resSeries.TimeIdx, resSeries.TimeIsNullable, resSeries.ValueIdx,
resSeries.ValueIsNullable, resSeries.Len(),
)
for i := 0; i < resSeries.Len(); i++ {
t, f := resSeries.GetPoint(i)
nF := math.NaN()
if f != nil {
nF = floatF(*f)
}
if err := newSeries.SetPoint(i, t, &nF); err != nil {
return newSeries, err
}
}
newVal = newSeries
default:
// TODO: Should we deal with TypeString, TypeVariantSet?
}
return newVal, nil
}
+354
View File
@@ -0,0 +1,354 @@
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package parse
import (
"fmt"
"strings"
"unicode"
"unicode/utf8"
)
// item represents a token or text string returned from the scanner.
type item struct {
typ itemType // The type of this item.
pos Pos // The starting position, in bytes, of this item in the input string.
val string // The value of this item.
}
func (i item) String() string {
switch {
case i.typ == itemEOF:
return "EOF"
case i.typ == itemError:
return i.val
case len(i.val) > 10:
return fmt.Sprintf("%.10q...", i.val)
}
return fmt.Sprintf("%q", i.val)
}
// itemType identifies the type of lex items.
type itemType int
const (
itemError itemType = iota // error occurred; value is text of error
itemEOF
itemNot // '!'
itemAnd // '&&'
itemOr // '||'
itemGreater // '>'
itemLess // '<'
itemGreaterEq // '>='
itemLessEq // '<='
itemEq // '=='
itemNotEq // '!='
itemPlus // '+'
itemMinus // '-'
itemMult // '*'
itemDiv // '/'
itemMod // '%'
itemNumber // simple number
itemComma
itemLeftParen
itemRightParen
itemString
itemFunc
itemVar // e.g. $A
itemPow // '**'
)
const eof = -1
// stateFn represents the state of the scanner as a function that returns the next state.
type stateFn func(*lexer) stateFn
// lexer holds the state of the scanner.
type lexer struct {
input string // the string being scanned
state stateFn // the next lexing function to enter
pos Pos // current position in the input
start Pos // start position of this item
width Pos // width of last rune read from input
lastPos Pos // position of most recent item returned by nextItem
items chan item // channel of scanned items
}
// next returns the next rune in the input.
func (l *lexer) next() rune {
if int(l.pos) >= len(l.input) {
l.width = 0
return eof
}
r, w := utf8.DecodeRuneInString(l.input[l.pos:])
l.width = Pos(w)
l.pos += l.width
return r
}
// peek returns but does not consume the next rune in the input.
// nolint:unused
func (l *lexer) peek() rune {
r := l.next()
l.backup()
return r
}
// backup steps back one rune. Can only be called once per call of next.
func (l *lexer) backup() {
l.pos -= l.width
}
// emit passes an item back to the client.
func (l *lexer) emit(t itemType) {
l.items <- item{t, l.start, l.input[l.start:l.pos]}
l.start = l.pos
}
// accept consumes the next rune if it's from the valid set.
func (l *lexer) accept(valid string) bool {
if strings.ContainsRune(valid, l.next()) {
return true
}
l.backup()
return false
}
// acceptRun consumes a run of runes from the valid set.
func (l *lexer) acceptRun(valid string) {
for strings.ContainsRune(valid, l.next()) {
}
l.backup()
}
// ignore skips over the pending input before this point.
func (l *lexer) ignore() {
l.start = l.pos
}
// lineNumber reports which line we're on, based on the position of
// the previous item returned by nextItem. Doing it this way
// means we don't have to worry about peek double counting.
// nolint:unused
func (l *lexer) lineNumber() int {
return 1 + strings.Count(l.input[:l.lastPos], "\n")
}
// errorf returns an error token and terminates the scan by passing
// back a nil pointer that will be the next state, terminating l.nextItem.
func (l *lexer) errorf(format string, args ...interface{}) stateFn {
l.items <- item{itemError, l.start, fmt.Sprintf(format, args...)}
return nil
}
// nextItem returns the next item from the input.
func (l *lexer) nextItem() item {
item := <-l.items
l.lastPos = item.pos
return item
}
// lex creates a new scanner for the input string.
func lex(input string) *lexer {
l := &lexer{
input: input,
items: make(chan item),
}
go l.run()
return l
}
// run runs the state machine for the lexer.
func (l *lexer) run() {
for l.state = lexItem; l.state != nil; {
l.state = l.state(l)
}
}
// state functions
func lexItem(l *lexer) stateFn {
Loop:
for {
switch r := l.next(); {
case r == '$':
return lexVar
case isSymbol(r):
return lexSymbol
case isNumber(r):
l.backup()
return lexNumber
case unicode.IsLetter(r):
return lexFunc
case r == '(':
l.emit(itemLeftParen)
case r == ')':
l.emit(itemRightParen)
case r == '"':
return lexString
case r == ',':
l.emit(itemComma)
case isSpace(r):
l.ignore()
case r == eof:
l.emit(itemEOF)
break Loop
default:
return l.errorf("invalid character: %s", string(r))
}
}
return nil
}
// lexNumber scans a number: decimal, octal, hex, float, or imaginary. This
// isn't a perfect number scanner - for instance it accepts "." and "0x0.2"
// and "089" - but when it's wrong the input is invalid and the parser (via
// strconv) will notice.
func lexNumber(l *lexer) stateFn {
if !l.scanNumber() {
return l.errorf("bad number syntax: %q", l.input[l.start:l.pos])
}
l.emit(itemNumber)
return lexItem
}
func (l *lexer) scanNumber() bool {
// Is it hex?
digits := "0123456789"
if l.accept("0") && l.accept("xX") {
digits = "0123456789abcdefABCDEF"
}
l.acceptRun(digits)
if l.accept(".") {
l.acceptRun(digits)
}
if l.accept("eE") {
l.accept("+-")
l.acceptRun("0123456789")
}
return true
}
const symbols = "!<>=&|+-*/%"
func lexSymbol(l *lexer) stateFn {
l.acceptRun(symbols)
s := l.input[l.start:l.pos]
switch s {
case "!":
l.emit(itemNot)
case "&&":
l.emit(itemAnd)
case "||":
l.emit(itemOr)
case ">":
l.emit(itemGreater)
case "<":
l.emit(itemLess)
case ">=":
l.emit(itemGreaterEq)
case "<=":
l.emit(itemLessEq)
case "==":
l.emit(itemEq)
case "!=":
l.emit(itemNotEq)
case "+":
l.emit(itemPlus)
case "-":
l.emit(itemMinus)
case "*":
l.emit(itemMult)
case "**":
l.emit(itemPow)
case "/":
l.emit(itemDiv)
case "%":
l.emit(itemMod)
default:
l.emit(itemError)
}
return lexItem
}
func lexFunc(l *lexer) stateFn {
for {
switch r := l.next(); {
case unicode.IsLetter(r):
// absorb
default:
l.backup()
l.emit(itemFunc)
return lexItem
}
}
}
func lexVar(l *lexer) stateFn {
hasChar := false
if l.peek() == '{' {
_ = l.next()
for {
switch r := l.next(); {
case r == '}':
if !hasChar {
return l.errorf("incomplete variable")
}
l.emit(itemVar)
return lexItem
case r == eof:
return l.errorf("unterminated variable missing closing }")
case isVarchar(r) || isSpace(r):
hasChar = true
default:
return l.errorf("unsupported variable character")
}
}
}
for {
switch r := l.next(); {
case isVarchar(r):
hasChar = true
// absorb
default:
if !hasChar {
return l.errorf("incomplete variable")
}
l.backup()
l.emit(itemVar)
return lexItem
}
}
}
func lexString(l *lexer) stateFn {
for {
switch l.next() {
case '"':
l.emit(itemString)
return lexItem
case eof:
return l.errorf("unterminated string")
}
}
}
// isSpace reports whether r is a space character.
func isSpace(r rune) bool {
return unicode.IsSpace(r)
}
func isVarchar(r rune) bool {
return r == '_' || unicode.IsLetter(r) || unicode.IsDigit(r)
}
func isSymbol(r rune) bool {
return strings.ContainsRune(symbols, r)
}
func isNumber(r rune) bool {
return unicode.IsDigit(r) || r == '.'
}
+422
View File
@@ -0,0 +1,422 @@
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Parse nodes.
package parse
import (
"fmt"
"strconv"
)
// A Node is an element in the parse tree. The interface is trivial.
// The interface contains an unexported method so that only
// types local to this package can satisfy it.
type Node interface {
Type() NodeType
String() string
StringAST() string
Position() Pos // byte position of start of node in full original input string
Check(*Tree) error // performs type checking for itself and sub-nodes
Return() ReturnType
// Make sure only functions in this package can create Nodes.
unexported()
}
// NodeType identifies the type of a parse tree node.
type NodeType int
// Pos represents a byte position in the original input text from which
// this template was parsed.
type Pos int
// Position returns the integer Position of p
func (p Pos) Position() Pos {
return p
}
// unexported keeps Node implementations local to the package.
// All implementations embed Pos, so this takes care of it.
func (Pos) unexported() {
}
// Type returns itself and provides an easy default implementation
// for embedding in a Node. Embedded in all non-trivial Nodes.
func (t NodeType) Type() NodeType {
return t
}
const (
// NodeFunc is a function call.
NodeFunc NodeType = iota
// NodeBinary is a binary operator: math, logical, compare
NodeBinary
// NodeUnary is unary operator: !, -
NodeUnary
// NodeString is string constant.
NodeString
// NodeNumber is a numerical constant (Scalar).
NodeNumber
// NodeVar is variable: $A
NodeVar
)
// String returns the string representation of the NodeType
func (t NodeType) String() string {
switch t {
case NodeFunc:
return "NodeFunc"
case NodeBinary:
return "NodeBinary"
case NodeUnary:
return "NodeUnary"
case NodeString:
return "NodeString"
case NodeNumber:
return "NodeNumber"
default:
return "NodeUnknown"
}
}
// Nodes.
// VarNode holds a variable reference.
type VarNode struct {
NodeType
Pos
Name string // Without the $ or {}
Text string // Raw
}
func newVar(pos Pos, name, text string) *VarNode {
return &VarNode{NodeType: NodeVar, Pos: pos, Name: name, Text: text}
}
// Type returns the Type of the VarNode so it fulfills the Node interface.
func (n *VarNode) Type() NodeType { return NodeVar }
// String returns the string representation of the VarNode so it fulfills the Node interface.
func (n *VarNode) String() string { return n.Text }
// StringAST returns the string representation of abstract syntax tree of the VarNode so it fulfills the Node interface.
func (n *VarNode) StringAST() string { return n.String() }
// Check performs parse time checking on the VarNode so it fulfills the Node interface.
func (n *VarNode) Check(*Tree) error {
return nil
}
// Return returns the result type of the VarNode so it fulfills the Node interface.
func (n *VarNode) Return() ReturnType {
return TypeSeriesSet // Vars are only time series for now I guess....
}
// FuncNode holds a function invocation.
type FuncNode struct {
NodeType
Pos
Name string
F *Func
Args []Node
Prefix string
}
func newFunc(pos Pos, name string, f Func) *FuncNode {
return &FuncNode{NodeType: NodeFunc, Pos: pos, Name: name, F: &f}
}
func (f *FuncNode) append(arg Node) {
f.Args = append(f.Args, arg)
}
// String returns the string representation of the FuncNode so it fulfills the Node interface.
func (f *FuncNode) String() string {
s := f.Name + "("
for i, arg := range f.Args {
if i > 0 {
s += ", "
}
s += arg.String()
}
s += ")"
return s
}
// StringAST returns the string representation of abstract syntax tree of the FuncNode so it fulfills the Node interface.
func (f *FuncNode) StringAST() string {
s := f.Name + "("
for i, arg := range f.Args {
if i > 0 {
s += ", "
}
s += arg.StringAST()
}
s += ")"
return s
}
// Check performs parse time checking on the FuncNode so it fulfills the Node interface.
func (f *FuncNode) Check(t *Tree) error {
if len(f.Args) < len(f.F.Args) {
return fmt.Errorf("parse: not enough arguments for %s", f.Name)
} else if len(f.Args) > len(f.F.Args) {
return fmt.Errorf("parse: too many arguments for %s", f.Name)
}
for i, arg := range f.Args {
funcType := f.F.Args[i]
argType := arg.Return()
// if funcType == TypeNumberSet && argType == TypeScalar {
// argType = TypeNumberSet
// }
if funcType == TypeVariantSet {
if !(argType == TypeNumberSet || argType == TypeSeriesSet || argType == TypeScalar) {
return fmt.Errorf("parse: expected %v or %v for argument %v, got %v", TypeNumberSet, TypeSeriesSet, i, argType)
}
} else if funcType != argType {
return fmt.Errorf("parse: expected %v, got %v for argument %v (%v)", funcType, argType, i, arg.String())
}
if err := arg.Check(t); err != nil {
return err
}
}
if f.F.Check != nil {
return f.F.Check(t, f)
}
return nil
}
// Return returns the result type of the FuncNode so it fulfills the Node interface.
func (f *FuncNode) Return() ReturnType {
return f.F.Return
}
// ScalarNode holds a number: signed or unsigned integer or float.
// The value is parsed and stored under all the types that can represent the value.
// This simulates in a small amount of code the behavior of Go's ideal constants.
type ScalarNode struct {
NodeType
Pos
IsUint bool // Number has an unsigned integral value.
IsFloat bool // Number has a floating-point value.
Uint64 uint64 // The unsigned integer value.
Float64 float64 // The floating-point value.
Text string // The original textual representation from the input.
}
func newNumber(pos Pos, text string) (*ScalarNode, error) {
n := &ScalarNode{NodeType: NodeNumber, Pos: pos, Text: text}
// Do integer test first so we get 0x123 etc.
u, err := strconv.ParseUint(text, 0, 64) // will fail for -0.
if err == nil {
n.IsUint = true
n.Uint64 = u
}
// If an integer extraction succeeded, promote the float.
if n.IsUint {
n.IsFloat = true
n.Float64 = float64(n.Uint64)
} else {
f, err := strconv.ParseFloat(text, 64)
if err == nil {
n.IsFloat = true
n.Float64 = f
// If a floating-point extraction succeeded, extract the int if needed.
if !n.IsUint && float64(uint64(f)) == f {
n.IsUint = true
n.Uint64 = uint64(f)
}
}
}
if !n.IsUint && !n.IsFloat {
return nil, fmt.Errorf("illegal number syntax: %q", text)
}
return n, nil
}
// String returns the string representation of the ScalarNode so it fulfills the Node interface.
func (n *ScalarNode) String() string {
return n.Text
}
// StringAST returns the string representation of abstract syntax tree of the ScalarNode so it fulfills the Node interface.
func (n *ScalarNode) StringAST() string {
return n.String()
}
// Check performs parse time checking on the ScalarNode so it fulfills the Node interface.
func (n *ScalarNode) Check(*Tree) error {
return nil
}
// Return returns the result type of the ScalarNode so it fulfills the Node interface.
func (n *ScalarNode) Return() ReturnType {
return TypeScalar
}
// StringNode holds a string constant. The value has been "unquoted".
type StringNode struct {
NodeType
Pos
Quoted string // The original text of the string, with quotes.
Text string // The string, after quote processing.
}
func newString(pos Pos, orig, text string) *StringNode {
return &StringNode{NodeType: NodeString, Pos: pos, Quoted: orig, Text: text}
}
// String returns the string representation of the StringNode so it fulfills the Node interface.
func (s *StringNode) String() string {
return s.Quoted
}
// StringAST returns the string representation of abstract syntax tree of the StringNode so it fulfills the Node interface.
func (s *StringNode) StringAST() string {
return s.String()
}
// Check performs parse time checking on the StringNode so it fulfills the Node interface.
func (s *StringNode) Check(*Tree) error {
return nil
}
// Return returns the result type of the TypeString so it fulfills the Node interface.
func (s *StringNode) Return() ReturnType {
return TypeString
}
// BinaryNode holds two arguments and an operator.
type BinaryNode struct {
NodeType
Pos
Args [2]Node
Operator item
OpStr string
}
func newBinary(operator item, arg1, arg2 Node) *BinaryNode {
return &BinaryNode{NodeType: NodeBinary, Pos: operator.pos, Args: [2]Node{arg1, arg2}, Operator: operator, OpStr: operator.val}
}
// String returns the string representation of the BinaryNode so it fulfills the Node interface.
func (b *BinaryNode) String() string {
return fmt.Sprintf("%s %s %s", b.Args[0], b.Operator.val, b.Args[1])
}
// StringAST returns the string representation of abstract syntax tree of the BinaryNode so it fulfills the Node interface.
func (b *BinaryNode) StringAST() string {
return fmt.Sprintf("%s(%s, %s)", b.Operator.val, b.Args[0], b.Args[1])
}
// Check performs parse time checking on the BinaryNode so it fulfills the Node interface.
func (b *BinaryNode) Check(t *Tree) error {
return nil
}
// Return returns the result type of the BinaryNode so it fulfills the Node interface.
func (b *BinaryNode) Return() ReturnType {
t0 := b.Args[0].Return()
t1 := b.Args[1].Return()
if t1 > t0 {
return t1
}
return t0
}
// UnaryNode holds one argument and an operator.
type UnaryNode struct {
NodeType
Pos
Arg Node
Operator item
OpStr string
}
func newUnary(operator item, arg Node) *UnaryNode {
return &UnaryNode{NodeType: NodeUnary, Pos: operator.pos, Arg: arg, Operator: operator, OpStr: operator.val}
}
// String returns the string representation of the UnaryNode so it fulfills the Node interface.
func (u *UnaryNode) String() string {
return fmt.Sprintf("%s%s", u.Operator.val, u.Arg)
}
// StringAST returns the string representation of abstract syntax tree of the UnaryNode so it fulfills the Node interface.
func (u *UnaryNode) StringAST() string {
return fmt.Sprintf("%s(%s)", u.Operator.val, u.Arg)
}
// Check performs parse time checking on the UnaryNode so it fulfills the Node interface.
func (u *UnaryNode) Check(t *Tree) error {
switch rt := u.Arg.Return(); rt {
case TypeNumberSet, TypeSeriesSet, TypeScalar:
return u.Arg.Check(t)
default:
return fmt.Errorf(`parse: type error in %s, expected "number", got %s`, u, rt)
}
}
// Return returns the result type of the UnaryNode so it fulfills the Node interface.
func (u *UnaryNode) Return() ReturnType {
return u.Arg.Return()
}
// Walk invokes f on n and sub-nodes of n.
func Walk(n Node, f func(Node)) {
f(n)
switch n := n.(type) {
case *BinaryNode:
Walk(n.Args[0], f)
Walk(n.Args[1], f)
case *FuncNode:
for _, a := range n.Args {
Walk(a, f)
}
case *ScalarNode, *StringNode:
// Ignore since these node types have no sub nodes.
case *UnaryNode:
Walk(n.Arg, f)
default:
panic(fmt.Errorf("other type: %T", n))
}
}
// ReturnType represents the type that is returned from a node.
type ReturnType int
const (
// TypeString is a single string.
TypeString ReturnType = iota
// TypeScalar is a unlabled number constant.
TypeScalar
// TypeNumberSet is a collection of labelled numbers.
TypeNumberSet
// TypeSeriesSet is a collection of labelled time series.
TypeSeriesSet
// TypeVariantSet is a collection of the same type Number, Series, or Scalar.
TypeVariantSet
)
// String returns a string representation of the ReturnType.
func (f ReturnType) String() string {
switch f {
case TypeNumberSet:
return "numberSet"
case TypeString:
return "string"
case TypeSeriesSet:
return "seriesSet"
case TypeScalar:
return "scalar"
case TypeVariantSet:
return "variant"
default:
return "unknown"
}
}
+354
View File
@@ -0,0 +1,354 @@
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package parse builds parse trees for expressions as defined by expr. Clients
// should use that package to construct expressions rather than this one, which
// provides shared internal data structures not intended for general use.
package parse
import (
"fmt"
"runtime"
"strconv"
"strings"
)
// Tree is the representation of a single parsed expression.
type Tree struct {
Text string // text parsed to create the expression.
Root Node // top-level root of the tree, returns a number.
VarNames []string
funcs []map[string]Func
// Parsing only; cleared after parse.
lex *lexer
token [1]item // one-token lookahead for parser.
peekCount int
}
// Func holds the structure of a parsed function call.
type Func struct {
Args []ReturnType
Return ReturnType
F interface{}
VariantReturn bool
Check func(*Tree, *FuncNode) error
}
// Parse returns a Tree, created by parsing the expression described in the
// argument string. If an error is encountered, parsing stops and an empty Tree
// is returned with the error.
func Parse(text string, funcs ...map[string]Func) (t *Tree, err error) {
t = New()
t.Text = text
err = t.Parse(text, funcs...)
return
}
// next returns the next token.
func (t *Tree) next() item {
if t.peekCount > 0 {
t.peekCount--
} else {
t.token[0] = t.lex.nextItem()
}
return t.token[t.peekCount]
}
// backup backs the input stream up one token.
func (t *Tree) backup() {
t.peekCount++
}
// peek returns but does not consume the next token.
func (t *Tree) peek() item {
if t.peekCount > 0 {
return t.token[t.peekCount-1]
}
t.peekCount = 1
t.token[0] = t.lex.nextItem()
return t.token[0]
}
// Parsing.
// New allocates a new parse tree with the given name.
func New(funcs ...map[string]Func) *Tree {
return &Tree{
funcs: funcs,
}
}
// errorf formats the error and terminates processing.
func (t *Tree) errorf(format string, args ...interface{}) {
t.Root = nil
format = fmt.Sprintf("expr: %s", format)
panic(fmt.Errorf(format, args...))
}
// error terminates processing.
func (t *Tree) error(err error) {
t.errorf("%s", err)
}
// expect consumes the next token and guarantees it has the required type.
func (t *Tree) expect(expected itemType, context string) item {
token := t.next()
if token.typ != expected {
t.unexpected(token, context)
}
return token
}
// expectOneOf consumes the next token and guarantees it has one of the required types.
// nolint:unused
func (t *Tree) expectOneOf(expected1, expected2 itemType, context string) item {
token := t.next()
if token.typ != expected1 && token.typ != expected2 {
t.unexpected(token, context)
}
return token
}
// unexpected complains about the token and terminates processing.
func (t *Tree) unexpected(token item, context string) {
t.errorf("unexpected %s in %s", token, context)
}
// recover is the handler that turns panics into returns from the top level of Parse.
func (t *Tree) recover(errp *error) {
e := recover()
if e != nil {
if _, ok := e.(runtime.Error); ok {
panic(e)
}
if t != nil {
t.stopParse()
}
*errp = e.(error)
}
}
// startParse initializes the parser, using the lexer.
func (t *Tree) startParse(funcs []map[string]Func, lex *lexer) {
t.Root = nil
t.lex = lex
t.funcs = funcs
}
// stopParse terminates parsing.
func (t *Tree) stopParse() {
t.lex = nil
}
// Parse parses the expression definition string to construct a representation
// of the expression for execution.
func (t *Tree) Parse(text string, funcs ...map[string]Func) (err error) {
defer t.recover(&err)
t.startParse(funcs, lex(text))
t.Text = text
t.parse()
t.stopParse()
return nil
}
// parse is the top-level parser for an expression.
// It runs to EOF.
func (t *Tree) parse() {
t.Root = t.O()
t.expect(itemEOF, "root input")
if err := t.Root.Check(t); err != nil {
t.error(err)
}
}
/* Grammar:
O -> A {"||" A}
A -> C {"&&" C}
C -> P {( "==" | "!=" | ">" | ">=" | "<" | "<=") P}
P -> M {( "+" | "-" ) M}
M -> E {( "*" | "/" ) F}
E -> F {( "**" ) F}
F -> v | "(" O ")" | "!" O | "-" O
v -> number | func(..) | queryVar
Func -> name "(" param {"," param} ")"
param -> number | "string" | queryVar
*/
// expr:
// O is A {"||" A} in the grammar.
func (t *Tree) O() Node {
n := t.A()
for {
switch t.peek().typ {
case itemOr:
n = newBinary(t.next(), n, t.A())
default:
return n
}
}
}
// A is C {"&&" C} in the grammar.
func (t *Tree) A() Node {
n := t.C()
for {
switch t.peek().typ {
case itemAnd:
n = newBinary(t.next(), n, t.C())
default:
return n
}
}
}
// C is C -> P {( "==" | "!=" | ">" | ">=" | "<" | "<=") P} in the grammar.
func (t *Tree) C() Node {
n := t.P()
for {
switch t.peek().typ {
case itemEq, itemNotEq, itemGreater, itemGreaterEq, itemLess, itemLessEq:
n = newBinary(t.next(), n, t.P())
default:
return n
}
}
}
// P is M {( "+" | "-" ) M} in the grammar.
func (t *Tree) P() Node {
n := t.M()
for {
switch t.peek().typ {
case itemPlus, itemMinus:
n = newBinary(t.next(), n, t.M())
default:
return n
}
}
}
// M is E {( "*" | "/" ) F} in the grammar.
func (t *Tree) M() Node {
n := t.E()
for {
switch t.peek().typ {
case itemMult, itemDiv, itemMod:
n = newBinary(t.next(), n, t.E())
default:
return n
}
}
}
// E is F {( "**" ) F} in the grammar.
func (t *Tree) E() Node {
n := t.F()
for {
switch t.peek().typ {
case itemPow:
n = newBinary(t.next(), n, t.F())
default:
return n
}
}
}
// F is v | "(" O ")" | "!" O | "-" O in the grammar.
func (t *Tree) F() Node {
switch token := t.peek(); token.typ {
case itemNumber, itemFunc, itemVar:
return t.v()
case itemNot, itemMinus:
return newUnary(t.next(), t.F())
case itemLeftParen:
t.next()
n := t.O()
t.expect(itemRightParen, "input: F()")
return n
default:
t.unexpected(token, "input: F()")
}
return nil
}
// V is number | func(..) | queryVar in the grammar.
func (t *Tree) v() Node {
switch token := t.next(); token.typ {
case itemNumber:
n, err := newNumber(token.pos, token.val)
if err != nil {
t.error(err)
}
return n
case itemFunc:
t.backup()
return t.Func()
case itemVar:
t.backup()
return t.Var()
default:
t.unexpected(token, "input: v()")
}
return nil
}
// Var is queryVar in the grammar.
func (t *Tree) Var() (v *VarNode) {
token := t.next()
varNoPrefix := strings.TrimPrefix(token.val, "$")
varNoBraces := strings.TrimSuffix(strings.TrimPrefix(varNoPrefix, "{"), "}")
t.VarNames = append(t.VarNames, varNoBraces)
return newVar(token.pos, varNoBraces, token.val)
}
// Func parses a FuncNode.
func (t *Tree) Func() (f *FuncNode) {
token := t.next()
funcv, ok := t.GetFunction(token.val)
if !ok {
t.errorf("non existent function %s", token.val)
}
f = newFunc(token.pos, token.val, funcv)
t.expect(itemLeftParen, "func")
for {
switch token = t.next(); token.typ {
default:
t.backup()
node := t.O()
f.append(node)
if len(f.Args) == 1 && f.F.VariantReturn {
f.F.Return = node.Return()
}
case itemString:
s, err := strconv.Unquote(token.val)
if err != nil {
t.errorf("Unquoting error: %s", err)
}
f.append(newString(token.pos, token.val, s))
case itemRightParen:
return
}
}
}
// GetFunction gets a parsed Func from the functions available on the tree's func property.
func (t *Tree) GetFunction(name string) (v Func, ok bool) {
for _, funcMap := range t.funcs {
if funcMap == nil {
continue
}
if v, ok = funcMap[name]; ok {
return
}
}
return
}
// String returns a string representation of the parse tree.
func (t *Tree) String() string {
return t.Root.String()
}
+101
View File
@@ -0,0 +1,101 @@
package mathexp
import (
"fmt"
"math"
"github.com/grafana/grafana-plugin-sdk-go/data"
)
func Sum(v *data.Field) *float64 {
var sum float64
for i := 0; i < v.Len(); i++ {
if f, ok := v.At(i).(*float64); ok {
if f == nil || math.IsNaN(*f) {
nan := math.NaN()
return &nan
}
sum += *f
}
}
return &sum
}
func Avg(v *data.Field) *float64 {
sum := Sum(v)
f := *sum / float64(v.Len())
return &f
}
func Min(fv *data.Field) *float64 {
var f float64
if fv.Len() == 0 {
nan := math.NaN()
return &nan
}
for i := 0; i < fv.Len(); i++ {
if v, ok := fv.At(i).(*float64); ok {
if v == nil || math.IsNaN(*v) {
nan := math.NaN()
return &nan
}
if i == 0 || *v < f {
f = *v
}
}
}
return &f
}
func Max(fv *data.Field) *float64 {
var f float64
if fv.Len() == 0 {
nan := math.NaN()
return &nan
}
for i := 0; i < fv.Len(); i++ {
if v, ok := fv.At(i).(*float64); ok {
if v == nil || math.IsNaN(*v) {
nan := math.NaN()
return &nan
}
if i == 0 || *v > f {
f = *v
}
}
}
return &f
}
func Count(fv *data.Field) *float64 {
f := float64(fv.Len())
return &f
}
// Reduce turns the Series into a Number based on the given reduction function
func (s Series) Reduce(refID, rFunc string) (Number, error) {
var l data.Labels
if s.GetLabels() != nil {
l = s.GetLabels().Copy()
}
number := NewNumber(refID, l)
var f *float64
fVec := s.Frame.Fields[1]
switch rFunc {
case "sum":
f = Sum(fVec)
case "mean":
f = Avg(fVec)
case "min":
f = Min(fVec)
case "max":
f = Max(fVec)
case "count":
f = Count(fVec)
default:
return number, fmt.Errorf("reduction %v not implemented", rFunc)
}
number.SetValue(f)
return number, nil
}
+83
View File
@@ -0,0 +1,83 @@
package mathexp
import (
"fmt"
"time"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/data"
)
// Resample turns the Series into a Number based on the given reduction function
func (s Series) Resample(refID string, interval time.Duration, downsampler string, upsampler string, tr backend.TimeRange) (Series, error) {
newSeriesLength := int(float64(tr.To.Sub(tr.From).Nanoseconds()) / float64(interval.Nanoseconds()))
if newSeriesLength <= 0 {
return s, fmt.Errorf("the series cannot be sampled further; the time range is shorter than the interval")
}
resampled := NewSeries(refID, s.GetLabels(), s.TimeIdx, s.TimeIsNullable, s.ValueIdx, s.ValueIsNullable, newSeriesLength+1)
bookmark := 0
var lastSeen *float64
idx := 0
t := tr.From
for !t.After(tr.To) && idx <= newSeriesLength {
vals := make([]*float64, 0)
sIdx := bookmark
for {
if sIdx == s.Len() {
break
}
st, v := s.GetPoint(sIdx)
if st.After(t) {
break
}
bookmark++
sIdx++
lastSeen = v
vals = append(vals, v)
}
var value *float64
if len(vals) == 0 { // upsampling
switch upsampler {
case "pad":
if lastSeen != nil {
value = lastSeen
} else {
value = nil
}
case "backfilling":
if sIdx == s.Len() { // no vals left
value = nil
} else {
_, value = s.GetPoint(sIdx)
}
case "fillna":
value = nil
default:
return s, fmt.Errorf("upsampling %v not implemented", upsampler)
}
} else { // downsampling
fVec := data.NewField("", s.GetLabels(), vals)
var tmp *float64
switch downsampler {
case "sum":
tmp = Sum(fVec)
case "mean":
tmp = Avg(fVec)
case "min":
tmp = Min(fVec)
case "max":
tmp = Max(fVec)
default:
return s, fmt.Errorf("downsampling %v not implemented", downsampler)
}
value = tmp
}
tv := t // his is required otherwise all points keep the latest timestamp; anything better?
if err := resampled.SetPoint(idx, &tv, value); err != nil {
return resampled, err
}
t = t.Add(interval)
idx++
}
return resampled, nil
}
+201
View File
@@ -0,0 +1,201 @@
package mathexp
import (
"fmt"
"sort"
"time"
"github.com/grafana/grafana-plugin-sdk-go/data"
"github.com/grafana/grafana/pkg/expr/mathexp/parse"
)
// Series has time.Time and ...? *float64 fields.
type Series struct {
Frame *data.Frame
TimeIsNullable bool
TimeIdx int
ValueIsNullable bool
ValueIdx int
// TODO:
// - Multiple Value Fields
// - Value can be different number types
}
// SeriesFromFrame validates that the dataframe can be considered a Series type
// and populate meta information on Series about the frame.
func SeriesFromFrame(frame *data.Frame) (s Series, err error) {
if len(frame.Fields) != 2 {
return s, fmt.Errorf("frame must have exactly two fields to be a series, has %v", len(frame.Fields))
}
foundTime := false
foundValue := false
for i, field := range frame.Fields {
switch field.Type() {
case data.FieldTypeTime:
s.TimeIdx = i
foundTime = true
case data.FieldTypeNullableTime:
s.TimeIsNullable = true
foundTime = true
s.TimeIdx = i
case data.FieldTypeFloat64:
foundValue = true
s.ValueIdx = i
case data.FieldTypeNullableFloat64:
s.ValueIsNullable = true
foundValue = true
s.ValueIdx = i
default:
// Handle default case
}
}
if !foundTime {
return s, fmt.Errorf("no time column found in frame %v", frame.Name)
}
if !foundValue {
return s, fmt.Errorf("no float64 value column found in frame %v", frame.Name)
}
s.Frame = frame
return s, nil
}
// NewSeries returns a dataframe of type Series.
func NewSeries(refID string, labels data.Labels, timeIdx int, nullableTime bool, valueIdx int, nullableValue bool, size int) Series {
fields := make([]*data.Field, 2)
if nullableValue {
fields[valueIdx] = data.NewField(refID, labels, make([]*float64, size))
} else {
fields[valueIdx] = data.NewField(refID, labels, make([]float64, size))
}
if nullableTime {
fields[timeIdx] = data.NewField("Time", nil, make([]*time.Time, size))
} else {
fields[timeIdx] = data.NewField("Time", nil, make([]time.Time, size))
}
return Series{
Frame: data.NewFrame("", fields...),
TimeIsNullable: nullableTime,
TimeIdx: timeIdx,
ValueIsNullable: nullableValue,
ValueIdx: valueIdx,
}
}
// Type returns the Value type and allows it to fulfill the Value interface.
func (s Series) Type() parse.ReturnType { return parse.TypeSeriesSet }
// Value returns the actual value allows it to fulfill the Value interface.
func (s Series) Value() interface{} { return &s }
func (s Series) GetLabels() data.Labels { return s.Frame.Fields[s.ValueIdx].Labels }
func (s Series) SetLabels(ls data.Labels) { s.Frame.Fields[s.ValueIdx].Labels = ls }
func (s Series) GetName() string { return s.Frame.Name }
// AsDataFrame returns the underlying *data.Frame.
func (s Series) AsDataFrame() *data.Frame { return s.Frame }
// GetPoint returns the time and value at the specified index.
func (s Series) GetPoint(pointIdx int) (*time.Time, *float64) {
return s.GetTime(pointIdx), s.GetValue(pointIdx)
}
// SetPoint sets the time and value on the corresponding vectors at the specified index.
func (s Series) SetPoint(pointIdx int, t *time.Time, f *float64) (err error) {
if s.TimeIsNullable {
s.Frame.Fields[s.TimeIdx].Set(pointIdx, t)
} else {
if t == nil {
return fmt.Errorf("can not set null time value on non-nullable time field for series name %v", s.Frame.Name)
}
s.Frame.Fields[s.TimeIdx].Set(pointIdx, *t)
}
if s.ValueIsNullable {
s.Frame.Fields[s.ValueIdx].Set(pointIdx, f)
} else {
if f == nil {
return fmt.Errorf("can not set null float value on non-nullable float field for series name %v", s.Frame.Name)
}
s.Frame.Fields[s.ValueIdx].Set(pointIdx, *f)
}
return
}
// AppendPoint appends a point (time/value).
func (s Series) AppendPoint(pointIdx int, t *time.Time, f *float64) (err error) {
if s.TimeIsNullable {
s.Frame.Fields[s.TimeIdx].Append(t)
} else {
if t == nil {
return fmt.Errorf("can not append null time value on non-nullable time field for series name %v", s.Frame.Name)
}
s.Frame.Fields[s.TimeIdx].Append(*t)
}
if s.ValueIsNullable {
s.Frame.Fields[s.ValueIdx].Append(f)
} else {
if f == nil {
return fmt.Errorf("can not append null float value on non-nullable float field for series name %v", s.Frame.Name)
}
s.Frame.Fields[s.ValueIdx].Append(*f)
}
return
}
// Len returns the length of the series.
func (s Series) Len() int {
return s.Frame.Fields[0].Len()
}
// GetTime returns the time at the specified index.
func (s Series) GetTime(pointIdx int) *time.Time {
if s.TimeIsNullable {
return s.Frame.Fields[s.TimeIdx].At(pointIdx).(*time.Time)
}
t := s.Frame.Fields[s.TimeIdx].At(pointIdx).(time.Time)
return &t
}
// GetValue returns the float value at the specified index.
func (s Series) GetValue(pointIdx int) *float64 {
if s.ValueIsNullable {
return s.Frame.Fields[s.ValueIdx].At(pointIdx).(*float64)
}
f := s.Frame.Fields[s.ValueIdx].At(pointIdx).(float64)
return &f
}
// SortByTime sorts the series by the time from oldest to newest.
// If desc is true, it will sort from newest to oldest.
// If any time values are nil, it will panic.
func (s Series) SortByTime(desc bool) {
if desc {
sort.Sort(sort.Reverse(SortSeriesByTime(s)))
return
}
sort.Sort(SortSeriesByTime(s))
}
// SortSeriesByTime allows a Series to be sorted by time
// the sort interface will panic if any timestamps are null
type SortSeriesByTime Series
func (ss SortSeriesByTime) Len() int { return Series(ss).Len() }
func (ss SortSeriesByTime) Swap(i, j int) {
iTimeVal, iFVal := Series(ss).GetPoint(i)
jTimeVal, jFVal := Series(ss).GetPoint(j)
_ = Series(ss).SetPoint(j, iTimeVal, iFVal)
_ = Series(ss).SetPoint(i, jTimeVal, jFVal)
}
func (ss SortSeriesByTime) Less(i, j int) bool {
iTimeVal := Series(ss).GetTime(i)
jTimeVal := Series(ss).GetTime(j)
return iTimeVal.Before(*jTimeVal)
}
+107
View File
@@ -0,0 +1,107 @@
package mathexp
import (
"github.com/grafana/grafana-plugin-sdk-go/data"
"github.com/grafana/grafana/pkg/expr/mathexp/parse"
)
// Results is a container for Value interfaces.
type Results struct {
Values Values
}
// Values is a slice of Value interfaces
type Values []Value
// AsDataFrames returns each value as a slice of frames.
func (vals Values) AsDataFrames(refID string) []*data.Frame {
frames := make([]*data.Frame, len(vals))
for i, v := range vals {
frames[i] = v.AsDataFrame()
frames[i].RefID = refID
}
return frames
}
// Value is the interface that holds different types such as a Scalar, Series, or Number.
// all Value implementations should be a *data.Frame
type Value interface {
Type() parse.ReturnType
Value() interface{}
GetLabels() data.Labels
SetLabels(data.Labels)
AsDataFrame() *data.Frame
}
// Scalar is the type that holds a single number constant.
// Before returning from an expression it will be wrapped in a
// data frame.
type Scalar struct{ Frame *data.Frame }
// Type returns the Value type and allows it to fulfill the Value interface.
func (s Scalar) Type() parse.ReturnType { return parse.TypeScalar }
// Value returns the actual value allows it to fulfill the Value interface.
func (s Scalar) Value() interface{} { return s }
func (s Scalar) GetLabels() data.Labels { return nil }
func (s Scalar) SetLabels(ls data.Labels) {}
// AsDataFrame returns the underlying *data.Frame.
func (s Scalar) AsDataFrame() *data.Frame { return s.Frame }
// NewScalar creates a Scalar holding value f.
func NewScalar(name string, f *float64) Scalar {
frame := data.NewFrame("",
data.NewField(name, nil, []*float64{f}),
)
return Scalar{frame}
}
// NewScalarResults creates a Results holding a single Scalar
func NewScalarResults(name string, f *float64) Results {
return Results{
Values: []Value{NewScalar(name, f)},
}
}
// GetFloat64Value retrieves the single scalar value from the data
func (s Scalar) GetFloat64Value() *float64 {
return s.Frame.At(0, 0).(*float64)
}
// Number hold a labelled single number values.
type Number struct{ Frame *data.Frame }
// Type returns the Value type and allows it to fulfill the Value interface.
func (n Number) Type() parse.ReturnType { return parse.TypeNumberSet }
// Value returns the actual value allows it to fulfill the Value interface.
func (n Number) Value() interface{} { return &n }
func (n Number) GetLabels() data.Labels { return n.Frame.Fields[0].Labels }
func (n Number) SetLabels(ls data.Labels) { n.Frame.Fields[0].Labels = ls }
// AsDataFrame returns the underlying *data.Frame.
func (n Number) AsDataFrame() *data.Frame { return n.Frame }
// SetValue sets the value of the Number to float64 pointer f
func (n Number) SetValue(f *float64) {
n.Frame.Set(0, 0, f)
}
// GetFloat64Value retrieves the single scalar value from the data
func (n Number) GetFloat64Value() *float64 {
return n.Frame.At(0, 0).(*float64)
}
// NewNumber returns a data that holds a float64Vector
func NewNumber(name string, labels data.Labels) Number {
return Number{
data.NewFrame("",
data.NewField(name, labels, make([]*float64, 1)),
),
}
}
+362
View File
@@ -0,0 +1,362 @@
package expr
import (
"context"
"encoding/json"
"fmt"
"time"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/data"
"github.com/grafana/grafana/pkg/expr/mathexp"
"gonum.org/v1/gonum/graph/simple"
)
// baseNode includes commmon properties used across DPNodes.
type baseNode struct {
id int64
refID string
}
type rawNode struct {
RefID string `json:"refId"`
Query map[string]interface{}
QueryType string
TimeRange backend.TimeRange
}
func (rn *rawNode) GetDatasourceName() (string, error) {
rawDs, ok := rn.Query["datasource"]
if !ok {
return "", fmt.Errorf("no datasource in query for refId %v", rn.RefID)
}
dsName, ok := rawDs.(string)
if !ok {
return "", fmt.Errorf("expted datasource identifier to be a string, got %T", rawDs)
}
return dsName, nil
}
func (rn *rawNode) GetCommandType() (c CommandType, err error) {
rawType, ok := rn.Query["type"]
if !ok {
return c, fmt.Errorf("no expression command type in query for refId %v", rn.RefID)
}
typeString, ok := rawType.(string)
if !ok {
return c, fmt.Errorf("expected expression command type to be a string, got type %T", rawType)
}
return ParseCommandType(typeString)
}
// String returns a string representation of the node. In particular for
// %v formating in error messages.
func (b *baseNode) String() string {
return b.refID
}
// CMDNode is a DPNode that holds an expression command.
type CMDNode struct {
baseNode
CMDType CommandType
Command Command
}
// ID returns the id of the node so it can fulfill the gonum's graph Node interface.
func (b *baseNode) ID() int64 {
return b.id
}
// RefID returns the refId of the node.
func (b *baseNode) RefID() string {
return b.refID
}
// NodeType returns the data pipeline node type.
func (gn *CMDNode) NodeType() NodeType {
return TypeCMDNode
}
// Execute runs the node and adds the results to vars. If the node requires
// other nodes they must have already been executed and their results must
// already by in vars.
func (gn *CMDNode) Execute(ctx context.Context, vars mathexp.Vars) (mathexp.Results, error) {
return gn.Command.Execute(ctx, vars)
}
func buildCMDNode(dp *simple.DirectedGraph, rn *rawNode) (*CMDNode, error) {
commandType, err := rn.GetCommandType()
if err != nil {
return nil, fmt.Errorf("invalid expression command type in '%v'", rn.RefID)
}
node := &CMDNode{
baseNode: baseNode{
id: dp.NewNode().ID(),
refID: rn.RefID,
},
}
switch commandType {
case TypeMath:
node.Command, err = UnmarshalMathCommand(rn)
case TypeReduce:
node.Command, err = UnmarshalReduceCommand(rn)
case TypeResample:
node.Command, err = UnmarshalResampleCommand(rn)
default:
return nil, fmt.Errorf("expression command type '%v' in '%v' not implemented", commandType, rn.RefID)
}
if err != nil {
return nil, err
}
return node, nil
}
const (
defaultIntervalMS = int64(64)
defaultMaxDP = int64(5000)
)
// DSNode is a DPNode that holds a datasource request.
type DSNode struct {
baseNode
query json.RawMessage
datasourceID int64
datasourceUID string
orgID int64
queryType string
timeRange backend.TimeRange
intervalMS int64
maxDP int64
}
// NodeType returns the data pipeline node type.
func (dn *DSNode) NodeType() NodeType {
return TypeDatasourceNode
}
func buildDSNode(dp *simple.DirectedGraph, rn *rawNode, orgID int64) (*DSNode, error) {
encodedQuery, err := json.Marshal(rn.Query)
if err != nil {
return nil, err
}
dsNode := &DSNode{
baseNode: baseNode{
id: dp.NewNode().ID(),
refID: rn.RefID,
},
orgID: orgID,
query: json.RawMessage(encodedQuery),
queryType: rn.QueryType,
intervalMS: defaultIntervalMS,
maxDP: defaultMaxDP,
timeRange: rn.TimeRange,
}
rawDsID, ok := rn.Query["datasourceId"]
switch ok {
case true:
floatDsID, ok := rawDsID.(float64)
if !ok {
return nil, fmt.Errorf("expected datasourceId to be a float64, got type %T for refId %v", rawDsID, rn.RefID)
}
dsNode.datasourceID = int64(floatDsID)
default:
rawDsUID, ok := rn.Query["datasourceUid"]
if !ok {
return nil, fmt.Errorf("neither datasourceId or datasourceUid in expression data source request for refId %v", rn.RefID)
}
strDsUID, ok := rawDsUID.(string)
if !ok {
return nil, fmt.Errorf("expected datasourceUid to be a string, got type %T for refId %v", rawDsUID, rn.RefID)
}
dsNode.datasourceUID = strDsUID
}
var floatIntervalMS float64
if rawIntervalMS := rn.Query["intervalMs"]; ok {
if floatIntervalMS, ok = rawIntervalMS.(float64); !ok {
return nil, fmt.Errorf("expected intervalMs to be an float64, got type %T for refId %v", rawIntervalMS, rn.RefID)
}
dsNode.intervalMS = int64(floatIntervalMS)
}
var floatMaxDP float64
if rawMaxDP := rn.Query["maxDataPoints"]; ok {
if floatMaxDP, ok = rawMaxDP.(float64); !ok {
return nil, fmt.Errorf("expected maxDataPoints to be an float64, got type %T for refId %v", rawMaxDP, rn.RefID)
}
dsNode.maxDP = int64(floatMaxDP)
}
return dsNode, nil
}
// Execute runs the node and adds the results to vars. If the node requires
// other nodes they must have already been executed and their results must
// already by in vars.
func (dn *DSNode) Execute(ctx context.Context, vars mathexp.Vars) (mathexp.Results, error) {
pc := backend.PluginContext{
OrgID: dn.orgID,
DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{
ID: dn.datasourceID,
UID: dn.datasourceUID,
},
}
q := []backend.DataQuery{
{
RefID: dn.refID,
MaxDataPoints: dn.maxDP,
Interval: time.Duration(int64(time.Millisecond) * dn.intervalMS),
JSON: dn.query,
TimeRange: dn.timeRange,
QueryType: dn.queryType,
},
}
resp, err := QueryData(ctx, &backend.QueryDataRequest{
PluginContext: pc,
Queries: q,
})
if err != nil {
return mathexp.Results{}, err
}
vals := make([]mathexp.Value, 0)
for refID, qr := range resp.Responses {
if len(qr.Frames) == 1 {
frame := qr.Frames[0]
if frame.TimeSeriesSchema().Type == data.TimeSeriesTypeNot && isNumberTable(frame) {
backend.Logger.Debug("expression datasource query (numberSet)", "query", refID)
numberSet, err := extractNumberSet(frame)
if err != nil {
return mathexp.Results{}, err
}
for _, n := range numberSet {
vals = append(vals, n)
}
return mathexp.Results{
Values: vals,
}, nil
}
}
for _, frame := range qr.Frames {
backend.Logger.Debug("expression datasource query (seriesSet)", "query", refID)
series, err := WideToMany(frame)
if err != nil {
return mathexp.Results{}, err
}
for _, s := range series {
vals = append(vals, s)
}
}
}
return mathexp.Results{
Values: vals,
}, nil
}
func isNumberTable(frame *data.Frame) bool {
if frame == nil || frame.Fields == nil {
return false
}
numericCount := 0
stringCount := 0
otherCount := 0
for _, field := range frame.Fields {
fType := field.Type()
switch {
case fType.Numeric():
numericCount++
case fType == data.FieldTypeString || fType == data.FieldTypeNullableString:
stringCount++
default:
otherCount++
}
}
return numericCount == 1 && otherCount == 0
}
func extractNumberSet(frame *data.Frame) ([]mathexp.Number, error) {
numericField := 0
stringFieldIdxs := []int{}
stringFieldNames := []string{}
for i, field := range frame.Fields {
fType := field.Type()
switch {
case fType.Numeric():
numericField = i
case fType == data.FieldTypeString || fType == data.FieldTypeNullableString:
stringFieldIdxs = append(stringFieldIdxs, i)
stringFieldNames = append(stringFieldNames, field.Name)
}
}
numbers := make([]mathexp.Number, frame.Rows())
for rowIdx := 0; rowIdx < frame.Rows(); rowIdx++ {
val, _ := frame.FloatAt(numericField, rowIdx)
var labels data.Labels
for i := 0; i < len(stringFieldIdxs); i++ {
if i == 0 {
labels = make(data.Labels)
}
key := stringFieldNames[i] // TODO check for duplicate string column names
val, _ := frame.ConcreteAt(stringFieldIdxs[i], rowIdx)
labels[key] = val.(string) // TODO check assertion / return error
}
n := mathexp.NewNumber("", labels)
n.SetValue(&val)
numbers[rowIdx] = n
}
return numbers, nil
}
// WideToMany converts a data package wide type Frame to one or multiple Series. A series
// is created for each value type column of wide frame.
//
// This might not be a good idea long term, but works now as an adapter/shim.
func WideToMany(frame *data.Frame) ([]mathexp.Series, error) {
tsSchema := frame.TimeSeriesSchema()
if tsSchema.Type != data.TimeSeriesTypeWide {
return nil, fmt.Errorf("input data must be a wide series but got type %s (input refid)", tsSchema.Type)
}
if len(tsSchema.ValueIndices) == 1 {
s, err := mathexp.SeriesFromFrame(frame)
if err != nil {
return nil, err
}
return []mathexp.Series{s}, nil
}
series := []mathexp.Series{}
for _, valIdx := range tsSchema.ValueIndices {
l := frame.Rows()
f := data.NewFrameOfFieldTypes(frame.Name, l, frame.Fields[tsSchema.TimeIndex].Type(), frame.Fields[valIdx].Type())
f.Fields[0].Name = frame.Fields[tsSchema.TimeIndex].Name
f.Fields[1].Name = frame.Fields[valIdx].Name
if frame.Fields[valIdx].Labels != nil {
f.Fields[1].Labels = frame.Fields[valIdx].Labels.Copy()
}
for i := 0; i < l; i++ {
f.SetRow(i, frame.Fields[tsSchema.TimeIndex].CopyAt(i), frame.Fields[valIdx].CopyAt(i))
}
s, err := mathexp.SeriesFromFrame(f)
if err != nil {
return nil, err
}
series = append(series, s)
}
return series, nil
}
+52
View File
@@ -0,0 +1,52 @@
package expr
import (
"context"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana/pkg/setting"
)
// DatasourceName is the string constant used as the datasource name in requests
// to identify it as an expression command.
const DatasourceName = "__expr__"
// DatasourceID is the fake datasource id used in requests to identify it as an
// expression command.
const DatasourceID = -100
// DatasourceUID is the fake datasource uid used in requests to identify it as an
// expression command.
const DatasourceUID = "-100"
// Service is service representation for expression handling.
type Service struct {
Cfg *setting.Cfg
}
func (s *Service) isDisabled() bool {
if s.Cfg == nil {
return true
}
return !s.Cfg.ExpressionsEnabled
}
// BuildPipeline builds a pipeline from a request.
func (s *Service) BuildPipeline(req *backend.QueryDataRequest) (DataPipeline, error) {
return buildPipeline(req)
}
// ExecutePipeline executes an expression pipeline and returns all the results.
func (s *Service) ExecutePipeline(ctx context.Context, pipeline DataPipeline) (*backend.QueryDataResponse, error) {
res := backend.NewQueryDataResponse()
vars, err := pipeline.execute(ctx)
if err != nil {
return nil, err
}
for refID, val := range vars {
res.Responses[refID] = backend.DataResponse{
Frames: val.Values.AsDataFrames(refID),
}
}
return res, nil
}
+249
View File
@@ -0,0 +1,249 @@
package expr
import (
"encoding/json"
"fmt"
"strconv"
"time"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/tsdb"
"github.com/prometheus/client_golang/prometheus"
"golang.org/x/net/context"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
var (
expressionsQuerySummary *prometheus.SummaryVec
)
func init() {
expressionsQuerySummary = prometheus.NewSummaryVec(
prometheus.SummaryOpts{
Name: "expressions_queries_duration_milliseconds",
Help: "Expressions query summary",
Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001},
},
[]string{"status"},
)
prometheus.MustRegister(expressionsQuerySummary)
}
// WrapTransformData creates and executes transform requests
func (s *Service) WrapTransformData(ctx context.Context, query *tsdb.TsdbQuery) (*tsdb.Response, error) {
sdkReq := &backend.QueryDataRequest{
PluginContext: backend.PluginContext{
OrgID: query.User.OrgId,
},
Queries: []backend.DataQuery{},
}
for _, q := range query.Queries {
modelJSON, err := q.Model.MarshalJSON()
if err != nil {
return nil, err
}
sdkReq.Queries = append(sdkReq.Queries, backend.DataQuery{
JSON: modelJSON,
Interval: time.Duration(q.IntervalMs) * time.Millisecond,
RefID: q.RefId,
MaxDataPoints: q.MaxDataPoints,
QueryType: q.QueryType,
TimeRange: backend.TimeRange{
From: query.TimeRange.GetFromAsTimeUTC(),
To: query.TimeRange.GetToAsTimeUTC(),
},
})
}
pbRes, err := s.TransformData(ctx, sdkReq)
if err != nil {
return nil, err
}
tR := &tsdb.Response{
Results: make(map[string]*tsdb.QueryResult, len(pbRes.Responses)),
}
for refID, res := range pbRes.Responses {
tRes := &tsdb.QueryResult{
RefId: refID,
Dataframes: tsdb.NewDecodedDataFrames(res.Frames),
}
// if len(res.JsonMeta) != 0 {
// tRes.Meta = simplejson.NewFromAny(res.JsonMeta)
// }
if res.Error != nil {
tRes.Error = res.Error
tRes.ErrorString = res.Error.Error()
}
tR.Results[refID] = tRes
}
return tR, nil
}
// TransformData takes Queries which are either expressions nodes
// or are datasource requests.
func (s *Service) TransformData(ctx context.Context, req *backend.QueryDataRequest) (r *backend.QueryDataResponse, err error) {
if s.isDisabled() {
return nil, status.Error(codes.PermissionDenied, "Expressions are disabled")
}
start := time.Now()
defer func() {
var respStatus string
switch {
case err == nil:
respStatus = "success"
default:
respStatus = "failure"
}
duration := float64(time.Since(start).Nanoseconds()) / float64(time.Millisecond)
expressionsQuerySummary.WithLabelValues(respStatus).Observe(duration)
}()
// Build the pipeline from the request, checking for ordering issues (e.g. loops)
// and parsing graph nodes from the queries.
pipeline, err := s.BuildPipeline(req)
if err != nil {
return nil, status.Error(codes.InvalidArgument, err.Error())
}
// Execute the pipeline
responses, err := s.ExecutePipeline(ctx, pipeline)
if err != nil {
return nil, status.Error(codes.Unknown, err.Error())
}
// Get which queries have the Hide property so they those queries' results
// can be excluded from the response.
hidden, err := hiddenRefIDs(req.Queries)
if err != nil {
return nil, status.Error((codes.Internal), err.Error())
}
if len(hidden) != 0 {
filteredRes := backend.NewQueryDataResponse()
for refID, res := range responses.Responses {
if _, ok := hidden[refID]; !ok {
filteredRes.Responses[refID] = res
}
}
responses = filteredRes
}
return responses, nil
}
func hiddenRefIDs(queries []backend.DataQuery) (map[string]struct{}, error) {
hidden := make(map[string]struct{})
for _, query := range queries {
hide := struct {
Hide bool `json:"hide"`
}{}
if err := json.Unmarshal(query.JSON, &hide); err != nil {
return nil, err
}
if hide.Hide {
hidden[query.RefID] = struct{}{}
}
}
return hidden, nil
}
// QueryData is called used to query datasources that are not expression commands, but are used
// alongside expressions and/or are the input of an expression command.
func QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
if len(req.Queries) == 0 {
return nil, fmt.Errorf("zero queries found in datasource request")
}
datasourceID := int64(0)
var datasourceUID string
if req.PluginContext.DataSourceInstanceSettings != nil {
datasourceID = req.PluginContext.DataSourceInstanceSettings.ID
datasourceUID = req.PluginContext.DataSourceInstanceSettings.UID
}
getDsInfo := &models.GetDataSourceQuery{
OrgId: req.PluginContext.OrgID,
Id: datasourceID,
Uid: datasourceUID,
}
if err := bus.Dispatch(getDsInfo); err != nil {
return nil, fmt.Errorf("could not find datasource: %w", err)
}
// Convert plugin-model (datasource) queries to tsdb queries
queries := make([]*tsdb.Query, len(req.Queries))
for i, query := range req.Queries {
sj, err := simplejson.NewJson(query.JSON)
if err != nil {
return nil, err
}
queries[i] = &tsdb.Query{
RefId: query.RefID,
IntervalMs: query.Interval.Milliseconds(),
MaxDataPoints: query.MaxDataPoints,
QueryType: query.QueryType,
DataSource: getDsInfo.Result,
Model: sj,
}
}
// For now take Time Range from first query.
timeRange := tsdb.NewTimeRange(strconv.FormatInt(req.Queries[0].TimeRange.From.Unix()*1000, 10), strconv.FormatInt(req.Queries[0].TimeRange.To.Unix()*1000, 10))
tQ := &tsdb.TsdbQuery{
TimeRange: timeRange,
Queries: queries,
}
// Execute the converted queries
tsdbRes, err := tsdb.HandleRequest(ctx, getDsInfo.Result, tQ)
if err != nil {
return nil, err
}
// Convert tsdb results (map) to plugin-model/datasource (slice) results.
// Only error, tsdb.Series, and encoded Dataframes responses are mapped.
responses := make(map[string]backend.DataResponse, len(tsdbRes.Results))
for refID, res := range tsdbRes.Results {
pRes := backend.DataResponse{}
if res.Error != nil {
pRes.Error = res.Error
}
if res.Dataframes != nil {
decoded, err := res.Dataframes.Decoded()
if err != nil {
return nil, err
}
pRes.Frames = decoded
responses[refID] = pRes
continue
}
for _, series := range res.Series {
frame, err := tsdb.SeriesToFrame(series)
frame.RefID = refID
if err != nil {
return nil, err
}
pRes.Frames = append(pRes.Frames, frame)
}
responses[refID] = pRes
}
return &backend.QueryDataResponse{
Responses: responses,
}, nil
}
@@ -0,0 +1,288 @@
package eval
import (
"encoding/json"
"fmt"
"time"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana/pkg/expr"
)
const defaultMaxDataPoints float64 = 100
const defaultIntervalMS float64 = 1000
// Duration is a type used for marshalling durations.
type Duration time.Duration
func (d Duration) String() string {
return time.Duration(d).String()
}
func (d Duration) MarshalJSON() ([]byte, error) {
return json.Marshal(time.Duration(d).Seconds())
}
func (d *Duration) UnmarshalJSON(b []byte) error {
var v interface{}
if err := json.Unmarshal(b, &v); err != nil {
return err
}
switch value := v.(type) {
case float64:
*d = Duration(time.Duration(value) * time.Second)
return nil
default:
return fmt.Errorf("invalid duration %v", v)
}
}
// RelativeTimeRange is the per query start and end time
// for requests.
type RelativeTimeRange struct {
From Duration `json:"from"`
To Duration `json:"to"`
}
// isValid checks that From duration is greater than To duration.
func (rtr *RelativeTimeRange) isValid() bool {
return rtr.From > rtr.To
}
func (rtr *RelativeTimeRange) toTimeRange(now time.Time) backend.TimeRange {
return backend.TimeRange{
From: now.Add(-time.Duration(rtr.From)),
To: now.Add(-time.Duration(rtr.To)),
}
}
// AlertQuery represents a single query associated with an alert definition.
type AlertQuery struct {
// RefID is the unique identifier of the query, set by the frontend call.
RefID string `json:"refId"`
// QueryType is an optional identifier for the type of query.
// It can be used to distinguish different types of queries.
QueryType string `json:"queryType"`
// RelativeTimeRange is the relative Start and End of the query as sent by the frontend.
RelativeTimeRange RelativeTimeRange `json:"relativeTimeRange"`
DatasourceUID string `json:"-"`
// JSON is the raw JSON query and includes the above properties as well as custom properties.
Model json.RawMessage `json:"model"`
modelProps map[string]interface{}
}
func (aq *AlertQuery) setModelProps() error {
aq.modelProps = make(map[string]interface{})
err := json.Unmarshal(aq.Model, &aq.modelProps)
if err != nil {
return fmt.Errorf("failed to unmarshal query model: %w", err)
}
return nil
}
// setDatasource sets DatasourceID.
// If it's an expression sets DefaultExprDatasourceID.
func (aq *AlertQuery) setDatasource() error {
if aq.modelProps == nil {
err := aq.setModelProps()
if err != nil {
return err
}
}
dsName, ok := aq.modelProps["datasource"]
if !ok {
return fmt.Errorf("failed to get datasource from query model")
}
if dsName == expr.DatasourceName {
aq.DatasourceUID = expr.DatasourceUID
aq.modelProps["datasourceUid"] = expr.DatasourceUID
return nil
}
i, ok := aq.modelProps["datasourceUid"]
if !ok {
return fmt.Errorf("failed to get datasourceUid from query model")
}
dsUID, ok := i.(string)
if !ok {
return fmt.Errorf("failed to cast datasourceUid to string: %v", i)
}
aq.DatasourceUID = dsUID
return nil
}
// IsExpression returns true if the alert query is an expression.
func (aq *AlertQuery) IsExpression() (bool, error) {
err := aq.setDatasource()
if err != nil {
return false, err
}
return aq.DatasourceUID == expr.DatasourceUID, nil
}
// setMaxDatapoints sets the model maxDataPoints if it's missing or invalid
func (aq *AlertQuery) setMaxDatapoints() error {
if aq.modelProps == nil {
err := aq.setModelProps()
if err != nil {
return err
}
}
i, ok := aq.modelProps["maxDataPoints"] // GEL requires maxDataPoints inside the query JSON
if !ok {
aq.modelProps["maxDataPoints"] = defaultMaxDataPoints
}
maxDataPoints, ok := i.(float64)
if !ok || maxDataPoints == 0 {
aq.modelProps["maxDataPoints"] = defaultMaxDataPoints
}
return nil
}
func (aq *AlertQuery) getMaxDatapoints() (int64, error) {
err := aq.setMaxDatapoints()
if err != nil {
return 0, err
}
maxDataPoints, ok := aq.modelProps["maxDataPoints"].(float64)
if !ok {
return 0, fmt.Errorf("failed to cast maxDataPoints to float64: %v", aq.modelProps["maxDataPoints"])
}
return int64(maxDataPoints), nil
}
// setIntervalMS sets the model IntervalMs if it's missing or invalid
func (aq *AlertQuery) setIntervalMS() error {
if aq.modelProps == nil {
err := aq.setModelProps()
if err != nil {
return err
}
}
i, ok := aq.modelProps["intervalMs"] // GEL requires intervalMs inside the query JSON
if !ok {
aq.modelProps["intervalMs"] = defaultIntervalMS
}
intervalMs, ok := i.(float64)
if !ok || intervalMs == 0 {
aq.modelProps["intervalMs"] = defaultIntervalMS
}
return nil
}
func (aq *AlertQuery) getIntervalMS() (int64, error) {
err := aq.setIntervalMS()
if err != nil {
return 0, err
}
intervalMs, ok := aq.modelProps["intervalMs"].(float64)
if !ok {
return 0, fmt.Errorf("failed to cast intervalMs to float64: %v", aq.modelProps["intervalMs"])
}
return int64(intervalMs), nil
}
func (aq *AlertQuery) getIntervalDuration() (time.Duration, error) {
err := aq.setIntervalMS()
if err != nil {
return 0, err
}
intervalMs, ok := aq.modelProps["intervalMs"].(float64)
if !ok {
return 0, fmt.Errorf("failed to cast intervalMs to float64: %v", aq.modelProps["intervalMs"])
}
return time.Duration(intervalMs) * time.Millisecond, nil
}
// GetDatasource returns the query datasource identifier.
func (aq *AlertQuery) GetDatasource() (string, error) {
err := aq.setDatasource()
if err != nil {
return "", err
}
return aq.DatasourceUID, nil
}
func (aq *AlertQuery) getModel() ([]byte, error) {
err := aq.setDatasource()
if err != nil {
return nil, err
}
err = aq.setMaxDatapoints()
if err != nil {
return nil, err
}
err = aq.setIntervalMS()
if err != nil {
return nil, err
}
model, err := json.Marshal(aq.modelProps)
if err != nil {
return nil, fmt.Errorf("unable to marshal query model: %w", err)
}
return model, nil
}
func (aq *AlertQuery) setQueryType() error {
if aq.modelProps == nil {
err := aq.setModelProps()
if err != nil {
return err
}
}
i, ok := aq.modelProps["queryType"]
if !ok {
return nil
}
queryType, ok := i.(string)
if !ok {
return fmt.Errorf("failed to get queryType from query model: %v", i)
}
aq.QueryType = queryType
return nil
}
// PreSave sets query's properties.
// It should be called before being saved.
func (aq *AlertQuery) PreSave() error {
if err := aq.setDatasource(); err != nil {
return fmt.Errorf("failed to set datasource to query model: %w", err)
}
if err := aq.setQueryType(); err != nil {
return fmt.Errorf("failed to set query type to query model: %w", err)
}
// override model
model, err := aq.getModel()
if err != nil {
return err
}
aq.Model = model
isExpression, err := aq.IsExpression()
if err != nil {
return err
}
if ok := isExpression || aq.RelativeTimeRange.isValid(); !ok {
return fmt.Errorf("invalid relative time range: %+v", aq.RelativeTimeRange)
}
return nil
}
+237
View File
@@ -0,0 +1,237 @@
// Package eval executes the condition for an alert definition, evaluates the condition results, and
// returns the alert instance states.
package eval
import (
"context"
"fmt"
"time"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/data"
"github.com/grafana/grafana/pkg/expr"
)
const alertingEvaluationTimeout = 30 * time.Second
type Evaluator struct {
Cfg *setting.Cfg
}
// invalidEvalResultFormatError is an error for invalid format of the alert definition evaluation results.
type invalidEvalResultFormatError struct {
refID string
reason string
err error
}
func (e *invalidEvalResultFormatError) Error() string {
s := fmt.Sprintf("invalid format of evaluation results for the alert definition %s: %s", e.refID, e.reason)
if e.err != nil {
s = fmt.Sprintf("%s: %s", s, e.err.Error())
}
return s
}
func (e *invalidEvalResultFormatError) Unwrap() error {
return e.err
}
// Condition contains backend expressions and queries and the RefID
// of the query or expression that will be evaluated.
type Condition struct {
RefID string `json:"refId"`
OrgID int64 `json:"-"`
QueriesAndExpressions []AlertQuery `json:"queriesAndExpressions"`
}
// ExecutionResults contains the unevaluated results from executing
// a condition.
type ExecutionResults struct {
AlertDefinitionID int64
Error error
Results data.Frames
}
// Results is a slice of evaluated alert instances states.
type Results []result
// result contains the evaluated state of an alert instance
// identified by its labels.
type result struct {
Instance data.Labels
State state // Enum
}
// state is an enum of the evaluation state for an alert instance.
type state int
const (
// Normal is the eval state for an alert instance condition
// that evaluated to false.
Normal state = iota
// Alerting is the eval state for an alert instance condition
// that evaluated to false.
Alerting
)
func (s state) String() string {
return [...]string{"Normal", "Alerting"}[s]
}
// IsValid checks the condition's validity.
func (c Condition) IsValid() bool {
// TODO search for refIDs in QueriesAndExpressions
return len(c.QueriesAndExpressions) != 0
}
// AlertExecCtx is the context provided for executing an alert condition.
type AlertExecCtx struct {
OrgID int64
ExpressionsEnabled bool
Ctx context.Context
}
// execute runs the Condition's expressions or queries.
func (c *Condition) execute(ctx AlertExecCtx, now time.Time) (*ExecutionResults, error) {
result := ExecutionResults{}
if !c.IsValid() {
return nil, fmt.Errorf("invalid conditions")
// TODO: Things probably
}
queryDataReq := &backend.QueryDataRequest{
PluginContext: backend.PluginContext{
OrgID: ctx.OrgID,
},
Queries: []backend.DataQuery{},
}
for i := range c.QueriesAndExpressions {
q := c.QueriesAndExpressions[i]
model, err := q.getModel()
if err != nil {
return nil, fmt.Errorf("failed to get query model: %w", err)
}
interval, err := q.getIntervalDuration()
if err != nil {
return nil, fmt.Errorf("failed to retrieve intervalMs from the model: %w", err)
}
maxDatapoints, err := q.getMaxDatapoints()
if err != nil {
return nil, fmt.Errorf("failed to retrieve maxDatapoints from the model: %w", err)
}
queryDataReq.Queries = append(queryDataReq.Queries, backend.DataQuery{
JSON: model,
Interval: interval,
RefID: q.RefID,
MaxDataPoints: maxDatapoints,
QueryType: q.QueryType,
TimeRange: q.RelativeTimeRange.toTimeRange(now),
})
}
exprService := expr.Service{Cfg: &setting.Cfg{ExpressionsEnabled: ctx.ExpressionsEnabled}}
pbRes, err := exprService.TransformData(ctx.Ctx, queryDataReq)
if err != nil {
return &result, err
}
for refID, res := range pbRes.Responses {
if refID != c.RefID {
continue
}
result.Results = res.Frames
}
if len(result.Results) == 0 {
err = fmt.Errorf("no GEL results")
result.Error = err
return &result, err
}
return &result, nil
}
// evaluateExecutionResult takes the ExecutionResult, and returns a frame where
// each column is a string type that holds a string representing its state.
func evaluateExecutionResult(results *ExecutionResults) (Results, error) {
evalResults := make([]result, 0)
labels := make(map[string]bool)
for _, f := range results.Results {
rowLen, err := f.RowLen()
if err != nil {
return nil, &invalidEvalResultFormatError{refID: f.RefID, reason: "unable to get frame row length", err: err}
}
if rowLen > 1 {
return nil, &invalidEvalResultFormatError{refID: f.RefID, reason: fmt.Sprintf("unexpected row length: %d instead of 1", rowLen)}
}
if len(f.Fields) > 1 {
return nil, &invalidEvalResultFormatError{refID: f.RefID, reason: fmt.Sprintf("unexpected field length: %d instead of 1", len(f.Fields))}
}
if f.Fields[0].Type() != data.FieldTypeNullableFloat64 {
return nil, &invalidEvalResultFormatError{refID: f.RefID, reason: fmt.Sprintf("invalid field type: %d", f.Fields[0].Type())}
}
labelsStr := f.Fields[0].Labels.String()
_, ok := labels[labelsStr]
if ok {
return nil, &invalidEvalResultFormatError{refID: f.RefID, reason: fmt.Sprintf("frame cannot uniquely be identified by its labels: %s", labelsStr)}
}
labels[labelsStr] = true
state := Normal
val, err := f.Fields[0].FloatAt(0)
if err != nil || val != 0 {
state = Alerting
}
evalResults = append(evalResults, result{
Instance: f.Fields[0].Labels,
State: state,
})
}
return evalResults, nil
}
// AsDataFrame forms the EvalResults in Frame suitable for displaying in the table panel of the front end.
// This may be temporary, as there might be a fair amount we want to display in the frontend, and it might not make sense to store that in data.Frame.
// For the first pass, I would expect a Frame with a single row, and a column for each instance with a boolean value.
func (evalResults Results) AsDataFrame() data.Frame {
fields := make([]*data.Field, 0)
for _, evalResult := range evalResults {
fields = append(fields, data.NewField("", evalResult.Instance, []bool{evalResult.State != Normal}))
}
f := data.NewFrame("", fields...)
return *f
}
// ConditionEval executes conditions and evaluates the result.
func (e *Evaluator) ConditionEval(condition *Condition, now time.Time) (Results, error) {
alertCtx, cancelFn := context.WithTimeout(context.Background(), alertingEvaluationTimeout)
defer cancelFn()
alertExecCtx := AlertExecCtx{OrgID: condition.OrgID, Ctx: alertCtx, ExpressionsEnabled: e.Cfg.ExpressionsEnabled}
execResult, err := condition.execute(alertExecCtx, now)
if err != nil {
return nil, fmt.Errorf("failed to execute conditions: %w", err)
}
evalResults, err := evaluateExecutionResult(execResult)
if err != nil {
return nil, fmt.Errorf("failed to evaluate results: %w", err)
}
return evalResults, nil
}
+37
View File
@@ -0,0 +1,37 @@
package tsdb
import (
"time"
"github.com/grafana/grafana-plugin-sdk-go/data"
)
// SeriesToFrame converts a TimeSeries to a sdk Frame
func SeriesToFrame(series *TimeSeries) (*data.Frame, error) {
timeVec := make([]*time.Time, len(series.Points))
floatVec := make([]*float64, len(series.Points))
for idx, point := range series.Points {
timeVec[idx], floatVec[idx] = convertTSDBTimePoint(point)
}
frame := data.NewFrame(series.Name,
data.NewField("time", nil, timeVec),
data.NewField("value", data.Labels(series.Tags), floatVec),
)
return frame, nil
}
// convertTSDBTimePoint coverts a tsdb.TimePoint into two values appropriate
// for Series values.
func convertTSDBTimePoint(point TimePoint) (t *time.Time, f *float64) {
timeIdx, valueIdx := 1, 0
if point[timeIdx].Valid { // Assuming valid is null?
tI := int64(point[timeIdx].Float64)
uT := time.Unix(tI/int64(1e+3), (tI%int64(1e+3))*int64(1e+6)) // time.Time from millisecond unix ts
t = &uT
}
if point[valueIdx].Valid {
f = &point[valueIdx].Float64
}
return
}
+222
View File
@@ -0,0 +1,222 @@
package tsdb
import (
"fmt"
"regexp"
"strings"
"time"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/models"
)
var (
defaultRes int64 = 1500
defaultMinInterval = time.Millisecond * 1
year = time.Hour * 24 * 365
day = time.Hour * 24
)
type Interval struct {
Text string
Value time.Duration
}
type intervalCalculator struct {
minInterval time.Duration
}
type IntervalCalculator interface {
Calculate(timeRange *TimeRange, minInterval time.Duration) Interval
}
type IntervalOptions struct {
MinInterval time.Duration
}
func NewIntervalCalculator(opt *IntervalOptions) *intervalCalculator {
if opt == nil {
opt = &IntervalOptions{}
}
calc := &intervalCalculator{}
if opt.MinInterval == 0 {
calc.minInterval = defaultMinInterval
} else {
calc.minInterval = opt.MinInterval
}
return calc
}
func (i *Interval) Milliseconds() int64 {
return i.Value.Nanoseconds() / int64(time.Millisecond)
}
func (ic *intervalCalculator) Calculate(timerange *TimeRange, minInterval time.Duration) Interval {
to := timerange.MustGetTo().UnixNano()
from := timerange.MustGetFrom().UnixNano()
interval := time.Duration((to - from) / defaultRes)
if interval < minInterval {
return Interval{Text: FormatDuration(minInterval), Value: minInterval}
}
rounded := roundInterval(interval)
return Interval{Text: FormatDuration(rounded), Value: rounded}
}
func GetIntervalFrom(dsInfo *models.DataSource, queryModel *simplejson.Json, defaultInterval time.Duration) (time.Duration, error) {
interval := queryModel.Get("interval").MustString("")
if interval == "" && dsInfo.JsonData != nil {
dsInterval := dsInfo.JsonData.Get("timeInterval").MustString("")
if dsInterval != "" {
interval = dsInterval
}
}
if interval == "" {
return defaultInterval, nil
}
interval = strings.Replace(strings.Replace(interval, "<", "", 1), ">", "", 1)
isPureNum, err := regexp.MatchString(`^\d+$`, interval)
if err != nil {
return time.Duration(0), err
}
if isPureNum {
interval += "s"
}
parsedInterval, err := time.ParseDuration(interval)
if err != nil {
return time.Duration(0), err
}
return parsedInterval, nil
}
// FormatDuration converts a duration into the kbn format e.g. 1m 2h or 3d
func FormatDuration(inter time.Duration) string {
if inter >= year {
return fmt.Sprintf("%dy", inter/year)
}
if inter >= day {
return fmt.Sprintf("%dd", inter/day)
}
if inter >= time.Hour {
return fmt.Sprintf("%dh", inter/time.Hour)
}
if inter >= time.Minute {
return fmt.Sprintf("%dm", inter/time.Minute)
}
if inter >= time.Second {
return fmt.Sprintf("%ds", inter/time.Second)
}
if inter >= time.Millisecond {
return fmt.Sprintf("%dms", inter/time.Millisecond)
}
return "1ms"
}
//nolint: gocyclo
func roundInterval(interval time.Duration) time.Duration {
switch {
// 0.015s
case interval <= 15*time.Millisecond:
return time.Millisecond * 10 // 0.01s
// 0.035s
case interval <= 35*time.Millisecond:
return time.Millisecond * 20 // 0.02s
// 0.075s
case interval <= 75*time.Millisecond:
return time.Millisecond * 50 // 0.05s
// 0.15s
case interval <= 150*time.Millisecond:
return time.Millisecond * 100 // 0.1s
// 0.35s
case interval <= 350*time.Millisecond:
return time.Millisecond * 200 // 0.2s
// 0.75s
case interval <= 750*time.Millisecond:
return time.Millisecond * 500 // 0.5s
// 1.5s
case interval <= 1500*time.Millisecond:
return time.Millisecond * 1000 // 1s
// 3.5s
case interval <= 3500*time.Millisecond:
return time.Millisecond * 2000 // 2s
// 7.5s
case interval <= 7500*time.Millisecond:
return time.Millisecond * 5000 // 5s
// 12.5s
case interval <= 12500*time.Millisecond:
return time.Millisecond * 10000 // 10s
// 17.5s
case interval <= 17500*time.Millisecond:
return time.Millisecond * 15000 // 15s
// 25s
case interval <= 25000*time.Millisecond:
return time.Millisecond * 20000 // 20s
// 45s
case interval <= 45000*time.Millisecond:
return time.Millisecond * 30000 // 30s
// 1.5m
case interval <= 90000*time.Millisecond:
return time.Millisecond * 60000 // 1m
// 3.5m
case interval <= 210000*time.Millisecond:
return time.Millisecond * 120000 // 2m
// 7.5m
case interval <= 450000*time.Millisecond:
return time.Millisecond * 300000 // 5m
// 12.5m
case interval <= 750000*time.Millisecond:
return time.Millisecond * 600000 // 10m
// 12.5m
case interval <= 1050000*time.Millisecond:
return time.Millisecond * 900000 // 15m
// 25m
case interval <= 1500000*time.Millisecond:
return time.Millisecond * 1200000 // 20m
// 45m
case interval <= 2700000*time.Millisecond:
return time.Millisecond * 1800000 // 30m
// 1.5h
case interval <= 5400000*time.Millisecond:
return time.Millisecond * 3600000 // 1h
// 2.5h
case interval <= 9000000*time.Millisecond:
return time.Millisecond * 7200000 // 2h
// 4.5h
case interval <= 16200000*time.Millisecond:
return time.Millisecond * 10800000 // 3h
// 9h
case interval <= 32400000*time.Millisecond:
return time.Millisecond * 21600000 // 6h
// 24h
case interval <= 86400000*time.Millisecond:
return time.Millisecond * 43200000 // 12h
// 48h
case interval <= 172800000*time.Millisecond:
return time.Millisecond * 86400000 // 24h
// 1w
case interval <= 604800000*time.Millisecond:
return time.Millisecond * 86400000 // 24h
// 3w
case interval <= 1814400000*time.Millisecond:
return time.Millisecond * 604800000 // 1w
// 2y
case interval < 3628800000*time.Millisecond:
return time.Millisecond * 2592000000 // 30d
default:
return time.Millisecond * 31536000000 // 1y
}
}
+272
View File
@@ -0,0 +1,272 @@
package tsdb
import (
"encoding/base64"
"encoding/json"
"fmt"
"github.com/grafana/grafana-plugin-sdk-go/data"
"github.com/grafana/grafana/pkg/components/null"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/models"
jsoniter "github.com/json-iterator/go"
)
// TsdbQuery contains all information about a query request.
type TsdbQuery struct {
TimeRange *TimeRange
Queries []*Query
Headers map[string]string
Debug bool
User *models.SignedInUser
}
type Query struct {
RefId string `json:"refId"`
Model *simplejson.Json `json:"model,omitempty"`
DataSource *models.DataSource `json:"datasource"`
MaxDataPoints int64 `json:"maxDataPoints"`
IntervalMs int64 `json:"intervalMs"`
QueryType string `json:"queryType"`
}
type Response struct {
Results map[string]*QueryResult `json:"results"`
Message string `json:"message,omitempty"`
}
type QueryResult struct {
Error error `json:"-"`
ErrorString string `json:"error,omitempty"`
RefId string `json:"refId"`
Meta *simplejson.Json `json:"meta,omitempty"`
Series TimeSeriesSlice `json:"series"`
Tables []*Table `json:"tables"`
Dataframes DataFrames `json:"dataframes"`
}
// UnmarshalJSON deserializes a QueryResult from JSON.
//
// Deserialization support is required by tests.
func (r *QueryResult) UnmarshalJSON(b []byte) error {
m := map[string]interface{}{}
// TODO: Use JSON decoder
if err := json.Unmarshal(b, &m); err != nil {
return err
}
refID, ok := m["refId"].(string)
if !ok {
return fmt.Errorf("can't decode field refId - not a string")
}
var meta *simplejson.Json
if m["meta"] != nil {
mm, ok := m["meta"].(map[string]interface{})
if !ok {
return fmt.Errorf("can't decode field meta - not a JSON object")
}
meta = simplejson.NewFromAny(mm)
}
var series TimeSeriesSlice
/* TODO
if m["series"] != nil {
}
*/
var tables []*Table
if m["tables"] != nil {
ts, ok := m["tables"].([]interface{})
if !ok {
return fmt.Errorf("can't decode field tables - not an array of Tables")
}
for _, ti := range ts {
tm, ok := ti.(map[string]interface{})
if !ok {
return fmt.Errorf("can't decode field tables - not an array of Tables")
}
var columns []TableColumn
cs, ok := tm["columns"].([]interface{})
if !ok {
return fmt.Errorf("can't decode field tables - not an array of Tables")
}
for _, ci := range cs {
cm, ok := ci.(map[string]interface{})
if !ok {
return fmt.Errorf("can't decode field tables - not an array of Tables")
}
val, ok := cm["text"].(string)
if !ok {
return fmt.Errorf("can't decode field tables - not an array of Tables")
}
columns = append(columns, TableColumn{Text: val})
}
rs, ok := tm["rows"].([]interface{})
if !ok {
return fmt.Errorf("can't decode field tables - not an array of Tables")
}
var rows []RowValues
for _, ri := range rs {
vals, ok := ri.([]interface{})
if !ok {
return fmt.Errorf("can't decode field tables - not an array of Tables")
}
rows = append(rows, vals)
}
tables = append(tables, &Table{
Columns: columns,
Rows: rows,
})
}
}
var dfs *dataFrames
if m["dataframes"] != nil {
raw, ok := m["dataframes"].([]interface{})
if !ok {
return fmt.Errorf("can't decode field dataframes - not an array of byte arrays")
}
var encoded [][]byte
for _, ra := range raw {
encS, ok := ra.(string)
if !ok {
return fmt.Errorf("can't decode field dataframes - not an array of byte arrays")
}
enc, err := base64.StdEncoding.DecodeString(encS)
if err != nil {
return fmt.Errorf("can't decode field dataframes - not an array of arrow frames")
}
encoded = append(encoded, enc)
}
decoded, err := data.UnmarshalArrowFrames(encoded)
if err != nil {
return err
}
dfs = &dataFrames{
decoded: decoded,
encoded: encoded,
}
}
r.RefId = refID
r.Meta = meta
r.Series = series
r.Tables = tables
if dfs != nil {
r.Dataframes = dfs
}
return nil
}
type TimeSeries struct {
Name string `json:"name"`
Points TimeSeriesPoints `json:"points"`
Tags map[string]string `json:"tags,omitempty"`
}
type Table struct {
Columns []TableColumn `json:"columns"`
Rows []RowValues `json:"rows"`
}
type TableColumn struct {
Text string `json:"text"`
}
type RowValues []interface{}
type TimePoint [2]null.Float
type TimeSeriesPoints []TimePoint
type TimeSeriesSlice []*TimeSeries
func NewQueryResult() *QueryResult {
return &QueryResult{
Series: make(TimeSeriesSlice, 0),
}
}
func NewTimePoint(value null.Float, timestamp float64) TimePoint {
return TimePoint{value, null.FloatFrom(timestamp)}
}
// DataFrames is an interface for retrieving encoded and decoded data frames.
//
// See NewDecodedDataFrames and NewEncodedDataFrames for more information.
type DataFrames interface {
// Encoded encodes Frames into a slice of []byte.
// If an error occurs [][]byte will be nil.
// The encoded result, if any, will be cached and returned next time Encoded is called.
Encoded() ([][]byte, error)
// Decoded decodes a slice of Arrow encoded frames to data.Frames ([]*data.Frame).
// If an error occurs Frames will be nil.
// The decoded result, if any, will be cached and returned next time Decoded is called.
Decoded() (data.Frames, error)
}
type dataFrames struct {
decoded data.Frames
encoded [][]byte
}
// NewDecodedDataFrames instantiates DataFrames from decoded frames.
//
// This should be the primary function for creating DataFrames if you're implementing a plugin.
// In a Grafana alerting scenario it needs to operate on decoded frames, which is why this function is
// preferrable. When encoded data frames are needed, e.g. returned from Grafana HTTP API, it will
// happen automatically when MarshalJSON() is called.
func NewDecodedDataFrames(decodedFrames data.Frames) DataFrames {
return &dataFrames{
decoded: decodedFrames,
}
}
// NewEncodedDataFrames instantiates DataFrames from encoded frames.
//
// This one is primarily used for creating DataFrames when receiving encoded data frames from an external
// plugin or similar. This may allow the encoded data frames to be returned to Grafana UI without any additional
// decoding/encoding required. In Grafana alerting scenario it needs to operate on decoded data frames why encoded
// frames needs to be decoded before usage.
func NewEncodedDataFrames(encodedFrames [][]byte) DataFrames {
return &dataFrames{
encoded: encodedFrames,
}
}
func (df *dataFrames) Encoded() ([][]byte, error) {
if df.encoded == nil {
encoded, err := df.decoded.MarshalArrow()
if err != nil {
return nil, err
}
df.encoded = encoded
}
return df.encoded, nil
}
func (df *dataFrames) Decoded() (data.Frames, error) {
if df.decoded == nil {
decoded, err := data.UnmarshalArrowFrames(df.encoded)
if err != nil {
return nil, err
}
df.decoded = decoded
}
return df.decoded, nil
}
func (df *dataFrames) MarshalJSON() ([]byte, error) {
encoded, err := df.Encoded()
if err != nil {
return nil, err
}
// Use a configuration that's compatible with the standard library
// to minimize the risk of introducing bugs. This will make sure
// that map keys is ordered.
jsonCfg := jsoniter.ConfigCompatibleWithStandardLibrary
return jsonCfg.Marshal(encoded)
}
+23
View File
@@ -0,0 +1,23 @@
package tsdb
import (
"context"
"github.com/grafana/grafana/pkg/models"
)
type TsdbQueryEndpoint interface {
Query(ctx context.Context, ds *models.DataSource, query *TsdbQuery) (*Response, error)
}
var registry map[string]GetTsdbQueryEndpointFn
type GetTsdbQueryEndpointFn func(dsInfo *models.DataSource) (TsdbQueryEndpoint, error)
func init() {
registry = make(map[string]GetTsdbQueryEndpointFn)
}
func RegisterTsdbQueryEndpoint(pluginId string, fn GetTsdbQueryEndpointFn) {
registry[pluginId] = fn
}
+26
View File
@@ -0,0 +1,26 @@
package tsdb
import (
"context"
"fmt"
"github.com/grafana/grafana/pkg/models"
)
type HandleRequestFunc func(ctx context.Context, dsInfo *models.DataSource, req *TsdbQuery) (*Response, error)
func HandleRequest(ctx context.Context, dsInfo *models.DataSource, req *TsdbQuery) (*Response, error) {
var endpoint TsdbQueryEndpoint
fn, exists := registry[dsInfo.Type]
if !exists {
return nil, fmt.Errorf("could not find executor for data source type: %s", dsInfo.Type)
}
var err error
endpoint, err = fn(dsInfo)
if err != nil {
return nil, err
}
return endpoint.Query(ctx, dsInfo, req)
}
+132
View File
@@ -0,0 +1,132 @@
package tsdb
import (
"strconv"
"strings"
"time"
"github.com/timberio/go-datemath"
)
func NewTimeRange(from, to string) *TimeRange {
return &TimeRange{
From: from,
To: to,
now: time.Now(),
}
}
func NewFakeTimeRange(from, to string, now time.Time) *TimeRange {
return &TimeRange{
From: from,
To: to,
now: now,
}
}
type TimeRange struct {
From string
To string
now time.Time
}
func (tr *TimeRange) GetFromAsMsEpoch() int64 {
return tr.MustGetFrom().UnixNano() / int64(time.Millisecond)
}
func (tr *TimeRange) GetFromAsSecondsEpoch() int64 {
return tr.GetFromAsMsEpoch() / 1000
}
func (tr *TimeRange) GetFromAsTimeUTC() time.Time {
return tr.MustGetFrom().UTC()
}
func (tr *TimeRange) GetToAsMsEpoch() int64 {
return tr.MustGetTo().UnixNano() / int64(time.Millisecond)
}
func (tr *TimeRange) GetToAsSecondsEpoch() int64 {
return tr.GetToAsMsEpoch() / 1000
}
func (tr *TimeRange) GetToAsTimeUTC() time.Time {
return tr.MustGetTo().UTC()
}
func (tr *TimeRange) MustGetFrom() time.Time {
res, err := tr.ParseFrom()
if err != nil {
return time.Unix(0, 0)
}
return res
}
func (tr *TimeRange) MustGetTo() time.Time {
res, err := tr.ParseTo()
if err != nil {
return time.Unix(0, 0)
}
return res
}
func tryParseUnixMsEpoch(val string) (time.Time, bool) {
if val, err := strconv.ParseInt(val, 10, 64); err == nil {
seconds := val / 1000
nano := (val - seconds*1000) * 1000000
return time.Unix(seconds, nano), true
}
return time.Time{}, false
}
func (tr *TimeRange) ParseFrom() (time.Time, error) {
return parse(tr.From, tr.now, false, nil)
}
func (tr *TimeRange) ParseTo() (time.Time, error) {
return parse(tr.To, tr.now, true, nil)
}
func (tr *TimeRange) ParseFromWithLocation(location *time.Location) (time.Time, error) {
return parse(tr.From, tr.now, false, location)
}
func (tr *TimeRange) ParseToWithLocation(location *time.Location) (time.Time, error) {
return parse(tr.To, tr.now, true, location)
}
func parse(s string, now time.Time, withRoundUp bool, location *time.Location) (time.Time, error) {
if res, ok := tryParseUnixMsEpoch(s); ok {
return res, nil
}
diff, err := time.ParseDuration("-" + s)
if err != nil {
options := []func(*datemath.Options){
datemath.WithNow(now),
datemath.WithRoundUp(withRoundUp),
}
if location != nil {
options = append(options, datemath.WithLocation(location))
}
return datemath.ParseAndEvaluate(s, options...)
}
return now.Add(diff), nil
}
// EpochPrecisionToMs converts epoch precision to millisecond, if needed.
// Only seconds to milliseconds supported right now
func EpochPrecisionToMs(value float64) float64 {
s := strconv.FormatFloat(value, 'e', -1, 64)
if strings.HasSuffix(s, "e+09") {
return value * float64(1e3)
}
if strings.HasSuffix(s, "e+18") {
return value / float64(time.Millisecond)
}
return value
}
@@ -0,0 +1,204 @@
# Created by .ignore support plugin (hsz.mobi)
### Go template
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a
*.so
# Folders
_obj
_test
# Architecture specific extensions/prefixes
*.[568vq]
[568vq].out
*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*
_testmain.go
*.exe
*.test
*.prof
### Windows template
# Windows image file caches
Thumbs.db
ehthumbs.db
# Folder config file
Desktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msm
*.msp
# Windows shortcuts
*.lnk
### Kate template
# Swap Files #
.*.kate-swp
.swp.*
### SublimeText template
# cache files for sublime text
*.tmlanguage.cache
*.tmPreferences.cache
*.stTheme.cache
# workspace files are user-specific
*.sublime-workspace
# project files should be checked into the repository, unless a significant
# proportion of contributors will probably not be using SublimeText
# *.sublime-project
# sftp configuration file
sftp-config.json
### Linux template
*~
# temporary files which can be created if a process still has a handle open of a deleted file
.fuse_hidden*
# KDE directory preferences
.directory
# Linux trash folder which might appear on any partition or disk
.Trash-*
### JetBrains template
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff:
.idea
.idea/tasks.xml
.idea/dictionaries
.idea/vcs.xml
.idea/jsLibraryMappings.xml
# Sensitive or high-churn files:
.idea/dataSources.ids
.idea/dataSources.xml
.idea/dataSources.local.xml
.idea/sqlDataSources.xml
.idea/dynamic.xml
.idea/uiDesigner.xml
# Gradle:
.idea/gradle.xml
.idea/libraries
# Mongo Explorer plugin:
.idea/mongoSettings.xml
## File-based project format:
*.iws
## Plugin-specific files:
# IntelliJ
/out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
### Xcode template
# Xcode
#
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
## Build generated
build/
DerivedData/
## Various settings
*.pbxuser
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3
xcuserdata/
## Other
*.moved-aside
*.xccheckout
*.xcscmblueprint
### Eclipse template
.metadata
bin/
tmp/
*.tmp
*.bak
*.swp
*~.nib
local.properties
.settings/
.loadpath
.recommenders
# Eclipse Core
.project
# External tool builders
.externalToolBuilders/
# Locally stored "Eclipse launch configurations"
*.launch
# PyDev specific (Python IDE for Eclipse)
*.pydevproject
# CDT-specific (C/C++ Development Tooling)
.cproject
# JDT-specific (Eclipse Java Development Tools)
.classpath
# Java annotation processor (APT)
.factorypath
# PDT-specific (PHP Development Tools)
.buildpath
# sbteclipse plugin
.target
# Tern plugin
.tern-project
# TeXlipse plugin
.texlipse
# STS (Spring Tool Suite)
.springBeans
# Code Recommenders
.recommenders/
coverage.txt
#vendor
vendor/
.envrc
+18
View File
@@ -0,0 +1,18 @@
sudo: false
language: go
go:
- 1.11.x
- 1.12.x
- 1.13.x
- 1.14.x
- 1.15.x
env:
global:
- GO111MODULE=on
script:
- make test
after_success:
- bash <(curl -s https://codecov.io/bash)
+51
View File
@@ -0,0 +1,51 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
Types of changes:
- `Added` for new features.
- `Changed` for changes in existing functionality.
- `Deprecated` for soon-to-be removed features.
- `Removed` for now removed features.
- `Fixed` for any bug fixes.
- `Security` in case of vulnerabilities.
## [Unreleased]
### Added
- [#223](https://github.com/grpc-ecosystem/go-grpc-middleware/pull/223) Add go-kit logging middleware - [adrien-f](https://github.com/adrien-f)
## [v1.1.0] - 2019-09-12
### Added
- [#226](https://github.com/grpc-ecosystem/go-grpc-middleware/pull/226) Support for go modules.
- [#221](https://github.com/grpc-ecosystem/go-grpc-middleware/pull/221) logging/zap add support for gRPC LoggerV2 - [kush-patel-hs](https://github.com/kush-patel-hs)
- [#181](https://github.com/grpc-ecosystem/go-grpc-middleware/pull/181) Rate Limit support - [ceshihao](https://github.com/ceshihao)
- [#161](https://github.com/grpc-ecosystem/go-grpc-middleware/pull/161) Retry on server stream call - [lonnblad](https://github.com/lonnblad)
- [#152](https://github.com/grpc-ecosystem/go-grpc-middleware/pull/152) Exponential backoff functions - [polyfloyd](https://github.com/polyfloyd)
- [#147](https://github.com/grpc-ecosystem/go-grpc-middleware/pull/147) Jaeger support for ctxtags extraction - [vporoshok](https://github.com/vporoshok)
- [#184](https://github.com/grpc-ecosystem/go-grpc-middleware/pull/184) ctxTags identifies if the call was sampled
### Deprecated
- [#201](https://github.com/grpc-ecosystem/go-grpc-middleware/pull/201) `golang.org/x/net/context` - [houz42](https://github.com/houz42)
- [#183](https://github.com/grpc-ecosystem/go-grpc-middleware/pull/183) Documentation Generation in favour of <godoc.org>.
### Fixed
- [172](https://github.com/grpc-ecosystem/go-grpc-middleware/pull/172) Passing ctx into retry and recover - [johanbrandhorst](https://github.com/johanbrandhorst)
- Numerious documentation fixes.
## v1.0.0 - 2018-05-08
### Added
- grpc_auth
- grpc_ctxtags
- grpc_zap
- grpc_logrus
- grpc_opentracing
- grpc_retry
- grpc_validator
- grpc_recovery
[Unreleased]: https://github.com/grpc-ecosystem/go-grpc-middleware/compare/v1.1.0...HEAD
[v1.1.0]: https://github.com/grpc-ecosystem/go-grpc-middleware/compare/v1.0.0...v1.1.0
+20
View File
@@ -0,0 +1,20 @@
# Contributing
We would love to have people submit pull requests and help make `grpc-ecosystem/go-grpc-middleware` even better 👍.
Fork, then clone the repo:
```bash
git clone git@github.com:your-username/go-grpc-middleware.git
```
Before checking in please run the following:
```bash
make all
```
This will `vet`, `fmt`, regenerate documentation and run all tests.
Push to your fork and open a pull request.
+201
View File
@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed 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.
+85
View File
@@ -0,0 +1,85 @@
# Go gRPC Middleware
[![Travis Build](https://travis-ci.org/grpc-ecosystem/go-grpc-middleware.svg?branch=master)](https://travis-ci.org/grpc-ecosystem/go-grpc-middleware)
[![Go Report Card](https://goreportcard.com/badge/github.com/grpc-ecosystem/go-grpc-middleware)](https://goreportcard.com/report/github.com/grpc-ecosystem/go-grpc-middleware)
[![GoDoc](http://img.shields.io/badge/GoDoc-Reference-blue.svg)](https://godoc.org/github.com/grpc-ecosystem/go-grpc-middleware)
[![SourceGraph](https://sourcegraph.com/github.com/grpc-ecosystem/go-grpc-middleware/-/badge.svg)](https://sourcegraph.com/github.com/grpc-ecosystem/go-grpc-middleware/?badge)
[![codecov](https://codecov.io/gh/grpc-ecosystem/go-grpc-middleware/branch/master/graph/badge.svg)](https://codecov.io/gh/grpc-ecosystem/go-grpc-middleware)
[![Apache 2.0 License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE)
[![quality: production](https://img.shields.io/badge/quality-production-orange.svg)](#status)
[![Slack](https://img.shields.io/badge/slack-%23grpc--middleware-brightgreen)](https://slack.com/share/IRUQCFC23/9Tm7hxRFVKKNoajQfMOcUiIk/enQtODc4ODI4NTIyMDcxLWM5NDA0ZTE4Njg5YjRjYWZkMTI5MzQwNDY3YzBjMzE1YzdjOGM5ZjI1NDNiM2JmNzI2YjM5ODE5OTRiNTEyOWE)
[gRPC Go](https://github.com/grpc/grpc-go) Middleware: interceptors, helpers, utilities.
## Middleware
[gRPC Go](https://github.com/grpc/grpc-go) recently acquired support for
Interceptors, i.e. [middleware](https://medium.com/@matryer/writing-middleware-in-golang-and-how-go-makes-it-so-much-fun-4375c1246e81#.gv7tdlghs)
that is executed either on the gRPC Server before the request is passed onto the user's application logic, or on the gRPC client around the user call. It is a perfect way to implement
common patterns: auth, logging, message, validation, retries or monitoring.
These are generic building blocks that make it easy to build multiple microservices easily.
The purpose of this repository is to act as a go-to point for such reusable functionality. It contains
some of them itself, but also will link to useful external repos.
`grpc_middleware` itself provides support for chaining interceptors, here's an example:
```go
import "github.com/grpc-ecosystem/go-grpc-middleware"
myServer := grpc.NewServer(
grpc.StreamInterceptor(grpc_middleware.ChainStreamServer(
grpc_ctxtags.StreamServerInterceptor(),
grpc_opentracing.StreamServerInterceptor(),
grpc_prometheus.StreamServerInterceptor,
grpc_zap.StreamServerInterceptor(zapLogger),
grpc_auth.StreamServerInterceptor(myAuthFunction),
grpc_recovery.StreamServerInterceptor(),
)),
grpc.UnaryInterceptor(grpc_middleware.ChainUnaryServer(
grpc_ctxtags.UnaryServerInterceptor(),
grpc_opentracing.UnaryServerInterceptor(),
grpc_prometheus.UnaryServerInterceptor,
grpc_zap.UnaryServerInterceptor(zapLogger),
grpc_auth.UnaryServerInterceptor(myAuthFunction),
grpc_recovery.UnaryServerInterceptor(),
)),
)
```
## Interceptors
*Please send a PR to add new interceptors or middleware to this list*
#### Auth
* [`grpc_auth`](auth) - a customizable (via `AuthFunc`) piece of auth middleware
#### Logging
* [`grpc_ctxtags`](tags/) - a library that adds a `Tag` map to context, with data populated from request body
* [`grpc_zap`](logging/zap/) - integration of [zap](https://github.com/uber-go/zap) logging library into gRPC handlers.
* [`grpc_logrus`](logging/logrus/) - integration of [logrus](https://github.com/sirupsen/logrus) logging library into gRPC handlers.
* [`grpc_kit`](logging/kit/) - integration of [go-kit](https://github.com/go-kit/kit/tree/master/log) logging library into gRPC handlers.
#### Monitoring
* [`grpc_prometheus`⚡](https://github.com/grpc-ecosystem/go-grpc-prometheus) - Prometheus client-side and server-side monitoring middleware
* [`otgrpc`⚡](https://github.com/grpc-ecosystem/grpc-opentracing/tree/master/go/otgrpc) - [OpenTracing](http://opentracing.io/) client-side and server-side interceptors
* [`grpc_opentracing`](tracing/opentracing) - [OpenTracing](http://opentracing.io/) client-side and server-side interceptors with support for streaming and handler-returned tags
#### Client
* [`grpc_retry`](retry/) - a generic gRPC response code retry mechanism, client-side middleware
#### Server
* [`grpc_validator`](validator/) - codegen inbound message validation from `.proto` options
* [`grpc_recovery`](recovery/) - turn panics into gRPC errors
* [`ratelimit`](ratelimit/) - grpc rate limiting by your own limiter
## Status
This code has been running in *production* since May 2016 as the basis of the gRPC micro services stack at [Improbable](https://improbable.io).
Additional tooling will be added, and contributions are welcome.
## License
`go-grpc-middleware` is released under the Apache 2.0 license. See the [LICENSE](LICENSE) file for details.
+120
View File
@@ -0,0 +1,120 @@
// Copyright 2016 Michal Witkowski. All Rights Reserved.
// See LICENSE for licensing terms.
// gRPC Server Interceptor chaining middleware.
package grpc_middleware
import (
"context"
"google.golang.org/grpc"
)
// ChainUnaryServer creates a single interceptor out of a chain of many interceptors.
//
// Execution is done in left-to-right order, including passing of context.
// For example ChainUnaryServer(one, two, three) will execute one before two before three, and three
// will see context changes of one and two.
func ChainUnaryServer(interceptors ...grpc.UnaryServerInterceptor) grpc.UnaryServerInterceptor {
n := len(interceptors)
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
chainer := func(currentInter grpc.UnaryServerInterceptor, currentHandler grpc.UnaryHandler) grpc.UnaryHandler {
return func(currentCtx context.Context, currentReq interface{}) (interface{}, error) {
return currentInter(currentCtx, currentReq, info, currentHandler)
}
}
chainedHandler := handler
for i := n - 1; i >= 0; i-- {
chainedHandler = chainer(interceptors[i], chainedHandler)
}
return chainedHandler(ctx, req)
}
}
// ChainStreamServer creates a single interceptor out of a chain of many interceptors.
//
// Execution is done in left-to-right order, including passing of context.
// For example ChainUnaryServer(one, two, three) will execute one before two before three.
// If you want to pass context between interceptors, use WrapServerStream.
func ChainStreamServer(interceptors ...grpc.StreamServerInterceptor) grpc.StreamServerInterceptor {
n := len(interceptors)
return func(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
chainer := func(currentInter grpc.StreamServerInterceptor, currentHandler grpc.StreamHandler) grpc.StreamHandler {
return func(currentSrv interface{}, currentStream grpc.ServerStream) error {
return currentInter(currentSrv, currentStream, info, currentHandler)
}
}
chainedHandler := handler
for i := n - 1; i >= 0; i-- {
chainedHandler = chainer(interceptors[i], chainedHandler)
}
return chainedHandler(srv, ss)
}
}
// ChainUnaryClient creates a single interceptor out of a chain of many interceptors.
//
// Execution is done in left-to-right order, including passing of context.
// For example ChainUnaryClient(one, two, three) will execute one before two before three.
func ChainUnaryClient(interceptors ...grpc.UnaryClientInterceptor) grpc.UnaryClientInterceptor {
n := len(interceptors)
return func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
chainer := func(currentInter grpc.UnaryClientInterceptor, currentInvoker grpc.UnaryInvoker) grpc.UnaryInvoker {
return func(currentCtx context.Context, currentMethod string, currentReq, currentRepl interface{}, currentConn *grpc.ClientConn, currentOpts ...grpc.CallOption) error {
return currentInter(currentCtx, currentMethod, currentReq, currentRepl, currentConn, currentInvoker, currentOpts...)
}
}
chainedInvoker := invoker
for i := n - 1; i >= 0; i-- {
chainedInvoker = chainer(interceptors[i], chainedInvoker)
}
return chainedInvoker(ctx, method, req, reply, cc, opts...)
}
}
// ChainStreamClient creates a single interceptor out of a chain of many interceptors.
//
// Execution is done in left-to-right order, including passing of context.
// For example ChainStreamClient(one, two, three) will execute one before two before three.
func ChainStreamClient(interceptors ...grpc.StreamClientInterceptor) grpc.StreamClientInterceptor {
n := len(interceptors)
return func(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, streamer grpc.Streamer, opts ...grpc.CallOption) (grpc.ClientStream, error) {
chainer := func(currentInter grpc.StreamClientInterceptor, currentStreamer grpc.Streamer) grpc.Streamer {
return func(currentCtx context.Context, currentDesc *grpc.StreamDesc, currentConn *grpc.ClientConn, currentMethod string, currentOpts ...grpc.CallOption) (grpc.ClientStream, error) {
return currentInter(currentCtx, currentDesc, currentConn, currentMethod, currentStreamer, currentOpts...)
}
}
chainedStreamer := streamer
for i := n - 1; i >= 0; i-- {
chainedStreamer = chainer(interceptors[i], chainedStreamer)
}
return chainedStreamer(ctx, desc, cc, method, opts...)
}
}
// Chain creates a single interceptor out of a chain of many interceptors.
//
// WithUnaryServerChain is a grpc.Server config option that accepts multiple unary interceptors.
// Basically syntactic sugar.
func WithUnaryServerChain(interceptors ...grpc.UnaryServerInterceptor) grpc.ServerOption {
return grpc.UnaryInterceptor(ChainUnaryServer(interceptors...))
}
// WithStreamServerChain is a grpc.Server config option that accepts multiple stream interceptors.
// Basically syntactic sugar.
func WithStreamServerChain(interceptors ...grpc.StreamServerInterceptor) grpc.ServerOption {
return grpc.StreamInterceptor(ChainStreamServer(interceptors...))
}
+69
View File
@@ -0,0 +1,69 @@
// Copyright 2016 Michal Witkowski. All Rights Reserved.
// See LICENSE for licensing terms.
/*
`grpc_middleware` is a collection of gRPC middleware packages: interceptors, helpers and tools.
Middleware
gRPC is a fantastic RPC middleware, which sees a lot of adoption in the Golang world. However, the
upstream gRPC codebase is relatively bare bones.
This package, and most of its child packages provides commonly needed middleware for gRPC:
client-side interceptors for retires, server-side interceptors for input validation and auth,
functions for chaining said interceptors, metadata convenience methods and more.
Chaining
By default, gRPC doesn't allow one to have more than one interceptor either on the client nor on
the server side. `grpc_middleware` provides convenient chaining methods
Simple way of turning a multiple interceptors into a single interceptor. Here's an example for
server chaining:
myServer := grpc.NewServer(
grpc.StreamInterceptor(grpc_middleware.ChainStreamServer(loggingStream, monitoringStream, authStream)),
grpc.UnaryInterceptor(grpc_middleware.ChainUnaryServer(loggingUnary, monitoringUnary, authUnary)),
)
These interceptors will be executed from left to right: logging, monitoring and auth.
Here's an example for client side chaining:
clientConn, err = grpc.Dial(
address,
grpc.WithUnaryInterceptor(grpc_middleware.ChainUnaryClient(monitoringClientUnary, retryUnary)),
grpc.WithStreamInterceptor(grpc_middleware.ChainStreamClient(monitoringClientStream, retryStream)),
)
client = pb_testproto.NewTestServiceClient(clientConn)
resp, err := client.PingEmpty(s.ctx, &myservice.Request{Msg: "hello"})
These interceptors will be executed from left to right: monitoring and then retry logic.
The retry interceptor will call every interceptor that follows it whenever when a retry happens.
Writing Your Own
Implementing your own interceptor is pretty trivial: there are interfaces for that. But the interesting
bit exposing common data to handlers (and other middleware), similarly to HTTP Middleware design.
For example, you may want to pass the identity of the caller from the auth interceptor all the way
to the handling function.
For example, a client side interceptor example for auth looks like:
func FakeAuthUnaryInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
newCtx := context.WithValue(ctx, "user_id", "john@example.com")
return handler(newCtx, req)
}
Unfortunately, it's not as easy for streaming RPCs. These have the `context.Context` embedded within
the `grpc.ServerStream` object. To pass values through context, a wrapper (`WrappedServerStream`) is
needed. For example:
func FakeAuthStreamingInterceptor(srv interface{}, stream grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
newStream := grpc_middleware.WrapServerStream(stream)
newStream.WrappedContext = context.WithValue(ctx, "user_id", "john@example.com")
return handler(srv, newStream)
}
*/
package grpc_middleware
+25
View File
@@ -0,0 +1,25 @@
module github.com/grpc-ecosystem/go-grpc-middleware
require (
github.com/go-kit/kit v0.9.0
github.com/go-logfmt/logfmt v0.4.0 // indirect
github.com/go-stack/stack v1.8.0 // indirect
github.com/gogo/protobuf v1.2.1
github.com/golang/protobuf v1.3.3
github.com/opentracing/opentracing-go v1.1.0
github.com/pkg/errors v0.8.1 // indirect
github.com/sirupsen/logrus v1.4.2
github.com/stretchr/testify v1.4.0
go.uber.org/atomic v1.4.0 // indirect
go.uber.org/multierr v1.1.0 // indirect
go.uber.org/zap v1.10.0
golang.org/x/exp v0.0.0-20200331195152-e8c3332aa8e5 // indirect
golang.org/x/net v0.0.0-20200421231249-e086a090c8fd
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be
golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f // indirect
golang.org/x/text v0.3.2 // indirect
google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215 // indirect
google.golang.org/grpc v1.29.1
)
go 1.14
+140
View File
@@ -0,0 +1,140 @@
cloud.google.com/go v0.26.0 h1:e0WKqKTd5BnrG8aKH3J3h+QvEIQtSUcf2n5UZ5ZgLtQ=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-kit/kit v0.9.0 h1:wDJmvq38kDhkVxi50ni9ykkdUr1PKgqKOoi01fa0Mdk=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.4.0 h1:MP4Eh7ZCb31lleYCFuwm0oe4/YGak+5l1vA2NOE80nA=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 h1:T+h1c/A9Gawja4Y9mFVWj2vyii2bbUNDw3kt9VxK2EY=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/opentracing/opentracing-go v1.1.0 h1:pWlfV3Bxv7k65HYwkikxat0+s3pV4bsqf19k25Ur8rU=
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
go.uber.org/atomic v1.4.0 h1:cxzIVoETapQEqDhQu3QfnvXAV4AlzcvUCxkVUFw3+EU=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/zap v1.10.0 h1:ORx85nbTijNz8ljznvCMR1ZBIPKFn3jQrag10X2AsuM=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4 h1:c2HOrn5iMezYjSlGPncknSEr/8x5LELb/ilJbXi9DEA=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20200331195152-e8c3332aa8e5 h1:FR+oGxGfbQu1d+jglI3rCkjAjUnhRSZcUxr+DqlDLNo=
golang.org/x/exp v0.0.0-20200331195152-e8c3332aa8e5/go.mod h1:4M0jN8W1tt0AVLNr8HDosyJCDCDuyL9N9+3m7wDWgKw=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b h1:GgiSbuUyC0BlbUmHQBgFqu32eiRR/CEYdjOjOd4zE6Y=
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200421231249-e086a090c8fd h1:QPwSajcTUrFriMF1nJ3XzgoqakqQEsnZf9LdXdi2nkI=
golang.org/x/net v0.0.0-20200421231249-e086a090c8fd/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be h1:vEDujvNQGv4jgYKudGeI/+DAX4Jffq6hpD55MmoEvKs=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f h1:gWF768j/LaZugp8dyS4UwsslYCYz9XgFxvlgsn0n9H8=
golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa h1:5E4dL8+NgFOgjwbTKz+OOEGGhP+ectTmF842l6KjupQ=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898 h1:/atklqdjdhuosWIl6AIbOeHJjicWYPqR9bpxqxYG2pA=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.1.0 h1:igQkv0AAhEIvTEpD5LIpAfav2eeVO9HBTjvKHVJPRSs=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8 h1:Nw54tB0rB7hY/N0NQvRW8DG4Yk3Q6T9cu9RcFQDu1tc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55 h1:gSJIx1SDwno+2ElGhA4+qG2zF97qiUzTM+rQ0klBOcE=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215 h1:0Uz5jLJQioKgVozXa1gzGbzYxbb/rhQEVvSWxzw5oUs=
google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/grpc v1.19.0 h1:cfg4PD8YEdSFnm7qLV4++93WcmhH2nIUhMjhdCvl3j8=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.29.1 h1:EC2SB8S04d2r73uptxphDSUG+kTKVgjRPF+N3xpxRB4=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+17
View File
@@ -0,0 +1,17 @@
SHELL=/bin/bash
GOFILES_NOVENDOR = $(shell go list ./... | grep -v /vendor/)
all: vet fmt test
fmt:
go fmt $(GOFILES_NOVENDOR)
vet:
# do not check lostcancel, they are intentional.
go vet -lostcancel=false $(GOFILES_NOVENDOR)
test: vet
./scripts/test_all.sh
.PHONY: all test
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

+30
View File
@@ -0,0 +1,30 @@
// Copyright 2016 Michal Witkowski. All Rights Reserved.
// See LICENSE for licensing terms.
package grpc_middleware
import (
"context"
"google.golang.org/grpc"
)
// WrappedServerStream is a thin wrapper around grpc.ServerStream that allows modifying context.
type WrappedServerStream struct {
grpc.ServerStream
// WrappedContext is the wrapper's own Context. You can assign it.
WrappedContext context.Context
}
// Context returns the wrapper's WrappedContext, overwriting the nested grpc.ServerStream.Context()
func (w *WrappedServerStream) Context() context.Context {
return w.WrappedContext
}
// WrapServerStream returns a ServerStream that has the ability to overwrite context.
func WrapServerStream(stream grpc.ServerStream) *WrappedServerStream {
if existing, ok := stream.(*WrappedServerStream); ok {
return existing
}
return &WrappedServerStream{ServerStream: stream, WrappedContext: stream.Context()}
}
@@ -0,0 +1,201 @@
#vendor
vendor/
# Created by .ignore support plugin (hsz.mobi)
coverage.txt
### Go template
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a
*.so
# Folders
_obj
_test
# Architecture specific extensions/prefixes
*.[568vq]
[568vq].out
*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*
_testmain.go
*.exe
*.test
*.prof
### Windows template
# Windows image file caches
Thumbs.db
ehthumbs.db
# Folder config file
Desktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msm
*.msp
# Windows shortcuts
*.lnk
### Kate template
# Swap Files #
.*.kate-swp
.swp.*
### SublimeText template
# cache files for sublime text
*.tmlanguage.cache
*.tmPreferences.cache
*.stTheme.cache
# workspace files are user-specific
*.sublime-workspace
# project files should be checked into the repository, unless a significant
# proportion of contributors will probably not be using SublimeText
# *.sublime-project
# sftp configuration file
sftp-config.json
### Linux template
*~
# temporary files which can be created if a process still has a handle open of a deleted file
.fuse_hidden*
# KDE directory preferences
.directory
# Linux trash folder which might appear on any partition or disk
.Trash-*
### JetBrains template
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff:
.idea
.idea/tasks.xml
.idea/dictionaries
.idea/vcs.xml
.idea/jsLibraryMappings.xml
# Sensitive or high-churn files:
.idea/dataSources.ids
.idea/dataSources.xml
.idea/dataSources.local.xml
.idea/sqlDataSources.xml
.idea/dynamic.xml
.idea/uiDesigner.xml
# Gradle:
.idea/gradle.xml
.idea/libraries
# Mongo Explorer plugin:
.idea/mongoSettings.xml
## File-based project format:
*.iws
## Plugin-specific files:
# IntelliJ
/out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
### Xcode template
# Xcode
#
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
## Build generated
build/
DerivedData/
## Various settings
*.pbxuser
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3
xcuserdata/
## Other
*.moved-aside
*.xccheckout
*.xcscmblueprint
### Eclipse template
.metadata
bin/
tmp/
*.tmp
*.bak
*.swp
*~.nib
local.properties
.settings/
.loadpath
.recommenders
# Eclipse Core
.project
# External tool builders
.externalToolBuilders/
# Locally stored "Eclipse launch configurations"
*.launch
# PyDev specific (Python IDE for Eclipse)
*.pydevproject
# CDT-specific (C/C++ Development Tooling)
.cproject
# JDT-specific (Eclipse Java Development Tools)
.classpath
# Java annotation processor (APT)
.factorypath
# PDT-specific (PHP Development Tools)
.buildpath
# sbteclipse plugin
.target
# Tern plugin
.tern-project
# TeXlipse plugin
.texlipse
# STS (Spring Tool Suite)
.springBeans
# Code Recommenders
.recommenders/
+23
View File
@@ -0,0 +1,23 @@
sudo: false
language: go
go:
- 1.9.x
- 1.10.x
- 1.11.x
- 1.12.x
- master
env:
- GO111MODULE=on
install:
# With Go modules (eg Go >= 1.11), it isn't necessary anymore to 'go get' the dependencies before running the tests.
- if [[ $TRAVIS_GO_VERSION =~ ^1\.(7|8|9|10)\. ]]; then go get github.com/prometheus/client_golang/prometheus; fi
- if [[ $TRAVIS_GO_VERSION =~ ^1\.(7|8|9|10)\. ]]; then go get google.golang.org/grpc; fi
- if [[ $TRAVIS_GO_VERSION =~ ^1\.(7|8|9|10)\. ]]; then go get github.com/stretchr/testify; fi
script:
- make test
after_success:
- bash <(curl -s https://codecov.io/bash)
+28
View File
@@ -0,0 +1,28 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Added
* Support for error unwrapping. (Supported for `github.com/pkg/errors` and native wrapping added in go1.13)
## [1.2.0](https://github.com/grpc-ecosystem/go-grpc-prometheus/releases/tag/v1.2.0) - 2018-06-04
### Added
* Require go 1.9 or later and test against these versions in CI.
* Provide metrics object as `prometheus.Collector`, for conventional metric registration.
* Support non-default/global Prometheus registry.
* Allow configuring counters with `prometheus.CounterOpts`.
### Changed
* Remove usage of deprecated `grpc.Code()`.
* Remove usage of deprecated `grpc.Errorf` and replace with `status.Errorf`.
---
This changelog was started with version `v1.2.0`, for earlier versions refer to the respective [GitHub releases](https://github.com/grpc-ecosystem/go-grpc-prometheus/releases).
+201
View File
@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed 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.
+250
View File
@@ -0,0 +1,250 @@
# Go gRPC Interceptors for Prometheus monitoring
[![Travis Build](https://travis-ci.org/grpc-ecosystem/go-grpc-prometheus.svg)](https://travis-ci.org/grpc-ecosystem/go-grpc-prometheus)
[![Go Report Card](https://goreportcard.com/badge/github.com/grpc-ecosystem/go-grpc-prometheus)](http://goreportcard.com/report/grpc-ecosystem/go-grpc-prometheus)
[![GoDoc](http://img.shields.io/badge/GoDoc-Reference-blue.svg)](https://godoc.org/github.com/grpc-ecosystem/go-grpc-prometheus)
[![SourceGraph](https://sourcegraph.com/github.com/grpc-ecosystem/go-grpc-prometheus/-/badge.svg)](https://sourcegraph.com/github.com/grpc-ecosystem/go-grpc-prometheus/?badge)
[![codecov](https://codecov.io/gh/grpc-ecosystem/go-grpc-prometheus/branch/master/graph/badge.svg)](https://codecov.io/gh/grpc-ecosystem/go-grpc-prometheus)
[![Slack](https://img.shields.io/badge/join%20slack-%23go--grpc--prometheus-brightgreen.svg)](https://join.slack.com/t/improbable-eng/shared_invite/enQtMzQ1ODcyMzQ5MjM4LWY5ZWZmNGM2ODc5MmViNmQ3ZTA3ZTY3NzQwOTBlMTkzZmIxZTIxODk0OWU3YjZhNWVlNDU3MDlkZGViZjhkMjc)
[![Apache 2.0 License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE)
[Prometheus](https://prometheus.io/) monitoring for your [gRPC Go](https://github.com/grpc/grpc-go) servers and clients.
A sister implementation for [gRPC Java](https://github.com/grpc/grpc-java) (same metrics, same semantics) is in [grpc-ecosystem/java-grpc-prometheus](https://github.com/grpc-ecosystem/java-grpc-prometheus).
## Interceptors
[gRPC Go](https://github.com/grpc/grpc-go) recently acquired support for Interceptors, i.e. middleware that is executed
by a gRPC Server before the request is passed onto the user's application logic. It is a perfect way to implement
common patterns: auth, logging and... monitoring.
To use Interceptors in chains, please see [`go-grpc-middleware`](https://github.com/mwitkow/go-grpc-middleware).
This library requires Go 1.9 or later.
## Usage
There are two types of interceptors: client-side and server-side. This package provides monitoring Interceptors for both.
### Server-side
```go
import "github.com/grpc-ecosystem/go-grpc-prometheus"
...
// Initialize your gRPC server's interceptor.
myServer := grpc.NewServer(
grpc.StreamInterceptor(grpc_prometheus.StreamServerInterceptor),
grpc.UnaryInterceptor(grpc_prometheus.UnaryServerInterceptor),
)
// Register your gRPC service implementations.
myservice.RegisterMyServiceServer(s.server, &myServiceImpl{})
// After all your registrations, make sure all of the Prometheus metrics are initialized.
grpc_prometheus.Register(myServer)
// Register Prometheus metrics handler.
http.Handle("/metrics", promhttp.Handler())
...
```
### Client-side
```go
import "github.com/grpc-ecosystem/go-grpc-prometheus"
...
clientConn, err = grpc.Dial(
address,
grpc.WithUnaryInterceptor(grpc_prometheus.UnaryClientInterceptor),
grpc.WithStreamInterceptor(grpc_prometheus.StreamClientInterceptor)
)
client = pb_testproto.NewTestServiceClient(clientConn)
resp, err := client.PingEmpty(s.ctx, &myservice.Request{Msg: "hello"})
...
```
# Metrics
## Labels
All server-side metrics start with `grpc_server` as Prometheus subsystem name. All client-side metrics start with `grpc_client`. Both of them have mirror-concepts. Similarly all methods
contain the same rich labels:
* `grpc_service` - the [gRPC service](http://www.grpc.io/docs/#defining-a-service) name, which is the combination of protobuf `package` and
the `grpc_service` section name. E.g. for `package = mwitkow.testproto` and
`service TestService` the label will be `grpc_service="mwitkow.testproto.TestService"`
* `grpc_method` - the name of the method called on the gRPC service. E.g.
`grpc_method="Ping"`
* `grpc_type` - the gRPC [type of request](http://www.grpc.io/docs/guides/concepts.html#rpc-life-cycle).
Differentiating between the two is important especially for latency measurements.
- `unary` is single request, single response RPC
- `client_stream` is a multi-request, single response RPC
- `server_stream` is a single request, multi-response RPC
- `bidi_stream` is a multi-request, multi-response RPC
Additionally for completed RPCs, the following labels are used:
* `grpc_code` - the human-readable [gRPC status code](https://github.com/grpc/grpc-go/blob/master/codes/codes.go).
The list of all statuses is to long, but here are some common ones:
- `OK` - means the RPC was successful
- `IllegalArgument` - RPC contained bad values
- `Internal` - server-side error not disclosed to the clients
## Counters
The counters and their up to date documentation is in [server_reporter.go](server_reporter.go) and [client_reporter.go](client_reporter.go)
the respective Prometheus handler (usually `/metrics`).
For the purpose of this documentation we will only discuss `grpc_server` metrics. The `grpc_client` ones contain mirror concepts.
For simplicity, let's assume we're tracking a single server-side RPC call of [`mwitkow.testproto.TestService`](examples/testproto/test.proto),
calling the method `PingList`. The call succeeds and returns 20 messages in the stream.
First, immediately after the server receives the call it will increment the
`grpc_server_started_total` and start the handling time clock (if histograms are enabled).
```jsoniq
grpc_server_started_total{grpc_method="PingList",grpc_service="mwitkow.testproto.TestService",grpc_type="server_stream"} 1
```
Then the user logic gets invoked. It receives one message from the client containing the request
(it's a `server_stream`):
```jsoniq
grpc_server_msg_received_total{grpc_method="PingList",grpc_service="mwitkow.testproto.TestService",grpc_type="server_stream"} 1
```
The user logic may return an error, or send multiple messages back to the client. In this case, on
each of the 20 messages sent back, a counter will be incremented:
```jsoniq
grpc_server_msg_sent_total{grpc_method="PingList",grpc_service="mwitkow.testproto.TestService",grpc_type="server_stream"} 20
```
After the call completes, its status (`OK` or other [gRPC status code](https://github.com/grpc/grpc-go/blob/master/codes/codes.go))
and the relevant call labels increment the `grpc_server_handled_total` counter.
```jsoniq
grpc_server_handled_total{grpc_code="OK",grpc_method="PingList",grpc_service="mwitkow.testproto.TestService",grpc_type="server_stream"} 1
```
## Histograms
[Prometheus histograms](https://prometheus.io/docs/concepts/metric_types/#histogram) are a great way
to measure latency distributions of your RPCs. However, since it is bad practice to have metrics
of [high cardinality](https://prometheus.io/docs/practices/instrumentation/#do-not-overuse-labels)
the latency monitoring metrics are disabled by default. To enable them please call the following
in your server initialization code:
```jsoniq
grpc_prometheus.EnableHandlingTimeHistogram()
```
After the call completes, its handling time will be recorded in a [Prometheus histogram](https://prometheus.io/docs/concepts/metric_types/#histogram)
variable `grpc_server_handling_seconds`. The histogram variable contains three sub-metrics:
* `grpc_server_handling_seconds_count` - the count of all completed RPCs by status and method
* `grpc_server_handling_seconds_sum` - cumulative time of RPCs by status and method, useful for
calculating average handling times
* `grpc_server_handling_seconds_bucket` - contains the counts of RPCs by status and method in respective
handling-time buckets. These buckets can be used by Prometheus to estimate SLAs (see [here](https://prometheus.io/docs/practices/histograms/))
The counter values will look as follows:
```jsoniq
grpc_server_handling_seconds_bucket{grpc_code="OK",grpc_method="PingList",grpc_service="mwitkow.testproto.TestService",grpc_type="server_stream",le="0.005"} 1
grpc_server_handling_seconds_bucket{grpc_code="OK",grpc_method="PingList",grpc_service="mwitkow.testproto.TestService",grpc_type="server_stream",le="0.01"} 1
grpc_server_handling_seconds_bucket{grpc_code="OK",grpc_method="PingList",grpc_service="mwitkow.testproto.TestService",grpc_type="server_stream",le="0.025"} 1
grpc_server_handling_seconds_bucket{grpc_code="OK",grpc_method="PingList",grpc_service="mwitkow.testproto.TestService",grpc_type="server_stream",le="0.05"} 1
grpc_server_handling_seconds_bucket{grpc_code="OK",grpc_method="PingList",grpc_service="mwitkow.testproto.TestService",grpc_type="server_stream",le="0.1"} 1
grpc_server_handling_seconds_bucket{grpc_code="OK",grpc_method="PingList",grpc_service="mwitkow.testproto.TestService",grpc_type="server_stream",le="0.25"} 1
grpc_server_handling_seconds_bucket{grpc_code="OK",grpc_method="PingList",grpc_service="mwitkow.testproto.TestService",grpc_type="server_stream",le="0.5"} 1
grpc_server_handling_seconds_bucket{grpc_code="OK",grpc_method="PingList",grpc_service="mwitkow.testproto.TestService",grpc_type="server_stream",le="1"} 1
grpc_server_handling_seconds_bucket{grpc_code="OK",grpc_method="PingList",grpc_service="mwitkow.testproto.TestService",grpc_type="server_stream",le="2.5"} 1
grpc_server_handling_seconds_bucket{grpc_code="OK",grpc_method="PingList",grpc_service="mwitkow.testproto.TestService",grpc_type="server_stream",le="5"} 1
grpc_server_handling_seconds_bucket{grpc_code="OK",grpc_method="PingList",grpc_service="mwitkow.testproto.TestService",grpc_type="server_stream",le="10"} 1
grpc_server_handling_seconds_bucket{grpc_code="OK",grpc_method="PingList",grpc_service="mwitkow.testproto.TestService",grpc_type="server_stream",le="+Inf"} 1
grpc_server_handling_seconds_sum{grpc_code="OK",grpc_method="PingList",grpc_service="mwitkow.testproto.TestService",grpc_type="server_stream"} 0.0003866430000000001
grpc_server_handling_seconds_count{grpc_code="OK",grpc_method="PingList",grpc_service="mwitkow.testproto.TestService",grpc_type="server_stream"} 1
```
## Useful query examples
Prometheus philosophy is to provide raw metrics to the monitoring system, and
let the aggregations be handled there. The verbosity of above metrics make it possible to have that
flexibility. Here's a couple of useful monitoring queries:
### request inbound rate
```jsoniq
sum(rate(grpc_server_started_total{job="foo"}[1m])) by (grpc_service)
```
For `job="foo"` (common label to differentiate between Prometheus monitoring targets), calculate the
rate of requests per second (1 minute window) for each gRPC `grpc_service` that the job has. Please note
how the `grpc_method` is being omitted here: all methods of a given gRPC service will be summed together.
### unary request error rate
```jsoniq
sum(rate(grpc_server_handled_total{job="foo",grpc_type="unary",grpc_code!="OK"}[1m])) by (grpc_service)
```
For `job="foo"`, calculate the per-`grpc_service` rate of `unary` (1:1) RPCs that failed, i.e. the
ones that didn't finish with `OK` code.
### unary request error percentage
```jsoniq
sum(rate(grpc_server_handled_total{job="foo",grpc_type="unary",grpc_code!="OK"}[1m])) by (grpc_service)
/
sum(rate(grpc_server_started_total{job="foo",grpc_type="unary"}[1m])) by (grpc_service)
* 100.0
```
For `job="foo"`, calculate the percentage of failed requests by service. It's easy to notice that
this is a combination of the two above examples. This is an example of a query you would like to
[alert on](https://prometheus.io/docs/alerting/rules/) in your system for SLA violations, e.g.
"no more than 1% requests should fail".
### average response stream size
```jsoniq
sum(rate(grpc_server_msg_sent_total{job="foo",grpc_type="server_stream"}[10m])) by (grpc_service)
/
sum(rate(grpc_server_started_total{job="foo",grpc_type="server_stream"}[10m])) by (grpc_service)
```
For `job="foo"` what is the `grpc_service`-wide `10m` average of messages returned for all `
server_stream` RPCs. This allows you to track the stream sizes returned by your system, e.g. allows
you to track when clients started to send "wide" queries that ret
Note the divisor is the number of started RPCs, in order to account for in-flight requests.
### 99%-tile latency of unary requests
```jsoniq
histogram_quantile(0.99,
sum(rate(grpc_server_handling_seconds_bucket{job="foo",grpc_type="unary"}[5m])) by (grpc_service,le)
)
```
For `job="foo"`, returns an 99%-tile [quantile estimation](https://prometheus.io/docs/practices/histograms/#quantiles)
of the handling time of RPCs per service. Please note the `5m` rate, this means that the quantile
estimation will take samples in a rolling `5m` window. When combined with other quantiles
(e.g. 50%, 90%), this query gives you tremendous insight into the responsiveness of your system
(e.g. impact of caching).
### percentage of slow unary queries (>250ms)
```jsoniq
100.0 - (
sum(rate(grpc_server_handling_seconds_bucket{job="foo",grpc_type="unary",le="0.25"}[5m])) by (grpc_service)
/
sum(rate(grpc_server_handling_seconds_count{job="foo",grpc_type="unary"}[5m])) by (grpc_service)
) * 100.0
```
For `job="foo"` calculate the by-`grpc_service` fraction of slow requests that took longer than `0.25`
seconds. This query is relatively complex, since the Prometheus aggregations use `le` (less or equal)
buckets, meaning that counting "fast" requests fractions is easier. However, simple maths helps.
This is an example of a query you would like to alert on in your system for SLA violations,
e.g. "less than 1% of requests are slower than 250ms".
## Status
This code has been used since August 2015 as the basis for monitoring of *production* gRPC micro services at [Improbable](https://improbable.io).
## License
`go-grpc-prometheus` is released under the Apache 2.0 license. See the [LICENSE](LICENSE) file for details.
+57
View File
@@ -0,0 +1,57 @@
// Copyright 2016 Michal Witkowski. All Rights Reserved.
// See LICENSE for licensing terms.
// gRPC Prometheus monitoring interceptors for client-side gRPC.
package grpc_prometheus
import (
prom "github.com/prometheus/client_golang/prometheus"
)
var (
// DefaultClientMetrics is the default instance of ClientMetrics. It is
// intended to be used in conjunction the default Prometheus metrics
// registry.
DefaultClientMetrics = NewClientMetrics()
// UnaryClientInterceptor is a gRPC client-side interceptor that provides Prometheus monitoring for Unary RPCs.
UnaryClientInterceptor = DefaultClientMetrics.UnaryClientInterceptor()
// StreamClientInterceptor is a gRPC client-side interceptor that provides Prometheus monitoring for Streaming RPCs.
StreamClientInterceptor = DefaultClientMetrics.StreamClientInterceptor()
)
func init() {
prom.MustRegister(DefaultClientMetrics.clientStartedCounter)
prom.MustRegister(DefaultClientMetrics.clientHandledCounter)
prom.MustRegister(DefaultClientMetrics.clientStreamMsgReceived)
prom.MustRegister(DefaultClientMetrics.clientStreamMsgSent)
}
// EnableClientHandlingTimeHistogram turns on recording of handling time of
// RPCs. Histogram metrics can be very expensive for Prometheus to retain and
// query. This function acts on the DefaultClientMetrics variable and the
// default Prometheus metrics registry.
func EnableClientHandlingTimeHistogram(opts ...HistogramOption) {
DefaultClientMetrics.EnableClientHandlingTimeHistogram(opts...)
prom.Register(DefaultClientMetrics.clientHandledHistogram)
}
// EnableClientStreamReceiveTimeHistogram turns on recording of
// single message receive time of streaming RPCs.
// This function acts on the DefaultClientMetrics variable and the
// default Prometheus metrics registry.
func EnableClientStreamReceiveTimeHistogram(opts ...HistogramOption) {
DefaultClientMetrics.EnableClientStreamReceiveTimeHistogram(opts...)
prom.Register(DefaultClientMetrics.clientStreamRecvHistogram)
}
// EnableClientStreamReceiveTimeHistogram turns on recording of
// single message send time of streaming RPCs.
// This function acts on the DefaultClientMetrics variable and the
// default Prometheus metrics registry.
func EnableClientStreamSendTimeHistogram(opts ...HistogramOption) {
DefaultClientMetrics.EnableClientStreamSendTimeHistogram(opts...)
prom.Register(DefaultClientMetrics.clientStreamSendHistogram)
}
+244
View File
@@ -0,0 +1,244 @@
package grpc_prometheus
import (
"context"
"io"
prom "github.com/prometheus/client_golang/prometheus"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
// ClientMetrics represents a collection of metrics to be registered on a
// Prometheus metrics registry for a gRPC client.
type ClientMetrics struct {
clientStartedCounter *prom.CounterVec
clientHandledCounter *prom.CounterVec
clientStreamMsgReceived *prom.CounterVec
clientStreamMsgSent *prom.CounterVec
clientHandledHistogramEnabled bool
clientHandledHistogramOpts prom.HistogramOpts
clientHandledHistogram *prom.HistogramVec
clientStreamRecvHistogramEnabled bool
clientStreamRecvHistogramOpts prom.HistogramOpts
clientStreamRecvHistogram *prom.HistogramVec
clientStreamSendHistogramEnabled bool
clientStreamSendHistogramOpts prom.HistogramOpts
clientStreamSendHistogram *prom.HistogramVec
}
// NewClientMetrics returns a ClientMetrics object. Use a new instance of
// ClientMetrics when not using the default Prometheus metrics registry, for
// example when wanting to control which metrics are added to a registry as
// opposed to automatically adding metrics via init functions.
func NewClientMetrics(counterOpts ...CounterOption) *ClientMetrics {
opts := counterOptions(counterOpts)
return &ClientMetrics{
clientStartedCounter: prom.NewCounterVec(
opts.apply(prom.CounterOpts{
Name: "grpc_client_started_total",
Help: "Total number of RPCs started on the client.",
}), []string{"grpc_type", "grpc_service", "grpc_method"}),
clientHandledCounter: prom.NewCounterVec(
opts.apply(prom.CounterOpts{
Name: "grpc_client_handled_total",
Help: "Total number of RPCs completed by the client, regardless of success or failure.",
}), []string{"grpc_type", "grpc_service", "grpc_method", "grpc_code"}),
clientStreamMsgReceived: prom.NewCounterVec(
opts.apply(prom.CounterOpts{
Name: "grpc_client_msg_received_total",
Help: "Total number of RPC stream messages received by the client.",
}), []string{"grpc_type", "grpc_service", "grpc_method"}),
clientStreamMsgSent: prom.NewCounterVec(
opts.apply(prom.CounterOpts{
Name: "grpc_client_msg_sent_total",
Help: "Total number of gRPC stream messages sent by the client.",
}), []string{"grpc_type", "grpc_service", "grpc_method"}),
clientHandledHistogramEnabled: false,
clientHandledHistogramOpts: prom.HistogramOpts{
Name: "grpc_client_handling_seconds",
Help: "Histogram of response latency (seconds) of the gRPC until it is finished by the application.",
Buckets: prom.DefBuckets,
},
clientHandledHistogram: nil,
clientStreamRecvHistogramEnabled: false,
clientStreamRecvHistogramOpts: prom.HistogramOpts{
Name: "grpc_client_msg_recv_handling_seconds",
Help: "Histogram of response latency (seconds) of the gRPC single message receive.",
Buckets: prom.DefBuckets,
},
clientStreamRecvHistogram: nil,
clientStreamSendHistogramEnabled: false,
clientStreamSendHistogramOpts: prom.HistogramOpts{
Name: "grpc_client_msg_send_handling_seconds",
Help: "Histogram of response latency (seconds) of the gRPC single message send.",
Buckets: prom.DefBuckets,
},
clientStreamSendHistogram: nil,
}
}
// Describe sends the super-set of all possible descriptors of metrics
// collected by this Collector to the provided channel and returns once
// the last descriptor has been sent.
func (m *ClientMetrics) Describe(ch chan<- *prom.Desc) {
m.clientStartedCounter.Describe(ch)
m.clientHandledCounter.Describe(ch)
m.clientStreamMsgReceived.Describe(ch)
m.clientStreamMsgSent.Describe(ch)
if m.clientHandledHistogramEnabled {
m.clientHandledHistogram.Describe(ch)
}
if m.clientStreamRecvHistogramEnabled {
m.clientStreamRecvHistogram.Describe(ch)
}
if m.clientStreamSendHistogramEnabled {
m.clientStreamSendHistogram.Describe(ch)
}
}
// Collect is called by the Prometheus registry when collecting
// metrics. The implementation sends each collected metric via the
// provided channel and returns once the last metric has been sent.
func (m *ClientMetrics) Collect(ch chan<- prom.Metric) {
m.clientStartedCounter.Collect(ch)
m.clientHandledCounter.Collect(ch)
m.clientStreamMsgReceived.Collect(ch)
m.clientStreamMsgSent.Collect(ch)
if m.clientHandledHistogramEnabled {
m.clientHandledHistogram.Collect(ch)
}
if m.clientStreamRecvHistogramEnabled {
m.clientStreamRecvHistogram.Collect(ch)
}
if m.clientStreamSendHistogramEnabled {
m.clientStreamSendHistogram.Collect(ch)
}
}
// EnableClientHandlingTimeHistogram turns on recording of handling time of RPCs.
// Histogram metrics can be very expensive for Prometheus to retain and query.
func (m *ClientMetrics) EnableClientHandlingTimeHistogram(opts ...HistogramOption) {
for _, o := range opts {
o(&m.clientHandledHistogramOpts)
}
if !m.clientHandledHistogramEnabled {
m.clientHandledHistogram = prom.NewHistogramVec(
m.clientHandledHistogramOpts,
[]string{"grpc_type", "grpc_service", "grpc_method"},
)
}
m.clientHandledHistogramEnabled = true
}
// EnableClientStreamReceiveTimeHistogram turns on recording of single message receive time of streaming RPCs.
// Histogram metrics can be very expensive for Prometheus to retain and query.
func (m *ClientMetrics) EnableClientStreamReceiveTimeHistogram(opts ...HistogramOption) {
for _, o := range opts {
o(&m.clientStreamRecvHistogramOpts)
}
if !m.clientStreamRecvHistogramEnabled {
m.clientStreamRecvHistogram = prom.NewHistogramVec(
m.clientStreamRecvHistogramOpts,
[]string{"grpc_type", "grpc_service", "grpc_method"},
)
}
m.clientStreamRecvHistogramEnabled = true
}
// EnableClientStreamSendTimeHistogram turns on recording of single message send time of streaming RPCs.
// Histogram metrics can be very expensive for Prometheus to retain and query.
func (m *ClientMetrics) EnableClientStreamSendTimeHistogram(opts ...HistogramOption) {
for _, o := range opts {
o(&m.clientStreamSendHistogramOpts)
}
if !m.clientStreamSendHistogramEnabled {
m.clientStreamSendHistogram = prom.NewHistogramVec(
m.clientStreamSendHistogramOpts,
[]string{"grpc_type", "grpc_service", "grpc_method"},
)
}
m.clientStreamSendHistogramEnabled = true
}
// UnaryClientInterceptor is a gRPC client-side interceptor that provides Prometheus monitoring for Unary RPCs.
func (m *ClientMetrics) UnaryClientInterceptor() func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
return func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
monitor := newClientReporter(m, Unary, method)
monitor.SentMessage()
err := invoker(ctx, method, req, reply, cc, opts...)
if err != nil {
monitor.ReceivedMessage()
}
st, _ := status.FromError(err)
monitor.Handled(st.Code())
return err
}
}
// StreamClientInterceptor is a gRPC client-side interceptor that provides Prometheus monitoring for Streaming RPCs.
func (m *ClientMetrics) StreamClientInterceptor() func(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, streamer grpc.Streamer, opts ...grpc.CallOption) (grpc.ClientStream, error) {
return func(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, streamer grpc.Streamer, opts ...grpc.CallOption) (grpc.ClientStream, error) {
monitor := newClientReporter(m, clientStreamType(desc), method)
clientStream, err := streamer(ctx, desc, cc, method, opts...)
if err != nil {
st, _ := status.FromError(err)
monitor.Handled(st.Code())
return nil, err
}
return &monitoredClientStream{clientStream, monitor}, nil
}
}
func clientStreamType(desc *grpc.StreamDesc) grpcType {
if desc.ClientStreams && !desc.ServerStreams {
return ClientStream
} else if !desc.ClientStreams && desc.ServerStreams {
return ServerStream
}
return BidiStream
}
// monitoredClientStream wraps grpc.ClientStream allowing each Sent/Recv of message to increment counters.
type monitoredClientStream struct {
grpc.ClientStream
monitor *clientReporter
}
func (s *monitoredClientStream) SendMsg(m interface{}) error {
timer := s.monitor.SendMessageTimer()
err := s.ClientStream.SendMsg(m)
timer.ObserveDuration()
if err == nil {
s.monitor.SentMessage()
}
return err
}
func (s *monitoredClientStream) RecvMsg(m interface{}) error {
timer := s.monitor.ReceiveMessageTimer()
err := s.ClientStream.RecvMsg(m)
timer.ObserveDuration()
if err == nil {
s.monitor.ReceivedMessage()
} else if err == io.EOF {
s.monitor.Handled(codes.OK)
} else {
st, _ := status.FromError(err)
s.monitor.Handled(st.Code())
}
return err
}
+79
View File
@@ -0,0 +1,79 @@
// Copyright 2016 Michal Witkowski. All Rights Reserved.
// See LICENSE for licensing terms.
package grpc_prometheus
import (
"time"
"github.com/prometheus/client_golang/prometheus"
"google.golang.org/grpc/codes"
)
type clientReporter struct {
metrics *ClientMetrics
rpcType grpcType
serviceName string
methodName string
startTime time.Time
}
func newClientReporter(m *ClientMetrics, rpcType grpcType, fullMethod string) *clientReporter {
r := &clientReporter{
metrics: m,
rpcType: rpcType,
}
if r.metrics.clientHandledHistogramEnabled {
r.startTime = time.Now()
}
r.serviceName, r.methodName = splitMethodName(fullMethod)
r.metrics.clientStartedCounter.WithLabelValues(string(r.rpcType), r.serviceName, r.methodName).Inc()
return r
}
// timer is a helper interface to time functions.
type timer interface {
ObserveDuration() time.Duration
}
type noOpTimer struct {
}
func (noOpTimer) ObserveDuration() time.Duration {
return 0
}
var emptyTimer = noOpTimer{}
func (r *clientReporter) ReceiveMessageTimer() timer {
if r.metrics.clientStreamRecvHistogramEnabled {
hist := r.metrics.clientStreamRecvHistogram.WithLabelValues(string(r.rpcType), r.serviceName, r.methodName)
return prometheus.NewTimer(hist)
}
return emptyTimer
}
func (r *clientReporter) ReceivedMessage() {
r.metrics.clientStreamMsgReceived.WithLabelValues(string(r.rpcType), r.serviceName, r.methodName).Inc()
}
func (r *clientReporter) SendMessageTimer() timer {
if r.metrics.clientStreamSendHistogramEnabled {
hist := r.metrics.clientStreamSendHistogram.WithLabelValues(string(r.rpcType), r.serviceName, r.methodName)
return prometheus.NewTimer(hist)
}
return emptyTimer
}
func (r *clientReporter) SentMessage() {
r.metrics.clientStreamMsgSent.WithLabelValues(string(r.rpcType), r.serviceName, r.methodName).Inc()
}
func (r *clientReporter) Handled(code codes.Code) {
r.metrics.clientHandledCounter.WithLabelValues(string(r.rpcType), r.serviceName, r.methodName, code.String()).Inc()
if r.metrics.clientHandledHistogramEnabled {
r.metrics.clientHandledHistogram.WithLabelValues(string(r.rpcType), r.serviceName, r.methodName).Observe(time.Since(r.startTime).Seconds())
}
}
+10
View File
@@ -0,0 +1,10 @@
module github.com/grpc-ecosystem/go-grpc-prometheus
require (
github.com/golang/protobuf v1.2.0
github.com/prometheus/client_golang v0.9.2
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90
github.com/stretchr/testify v1.3.0
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd
google.golang.org/grpc v1.18.0
)
+48
View File
@@ -0,0 +1,48 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 h1:xJ4a3vCFaGF/jqvzLMYoU8P317H5OQ+Via4RmuPwCS0=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v0.9.2 h1:awm861/B8OKDd2I/6o1dy3ra4BamzKhYOiGItCeZ740=
github.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 h1:S/YWwWx/RA8rT8tKFRuGUZhuA90OyIBpPCXkcbwU8DE=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.0.0-20181126121408-4724e9255275 h1:PnBWHBf+6L0jOqq0gIVUe6Yk0/QMZ640k6NvkxcBf+8=
github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a h1:9a8MnZMP0X2nLJdBg+pBmGgkJlSaKC2KaQmTCk1XDtE=
github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd h1:HuTn7WObtcDo9uEEU7rEqL0jYthdXAmZ6PP+meazmaU=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f h1:Bl/8QSvNqXvPGPGXa2z5xUTmV7VDcZyvRZ+QQXkXTZQ=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522 h1:Ve1ORMCxvRmSXBwJK+t3Oy+V2vRW2OetUQBq4rJIkZE=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8 h1:Nw54tB0rB7hY/N0NQvRW8DG4Yk3Q6T9cu9RcFQDu1tc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/grpc v1.18.0 h1:IZl7mfBGfbhYx2p2rKRtYgDFw6SBz+kclmxYrCksPPA=
google.golang.org/grpc v1.18.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+14
View File
@@ -0,0 +1,14 @@
GOFILES_NOVENDOR = $(shell go list ./... | grep -v /vendor/)
all: vet fmt test
fmt:
go fmt $(GOFILES_NOVENDOR)
vet:
go vet $(GOFILES_NOVENDOR)
test: vet
./scripts/test_all.sh
.PHONY: all vet test
+41
View File
@@ -0,0 +1,41 @@
package grpc_prometheus
import (
prom "github.com/prometheus/client_golang/prometheus"
)
// A CounterOption lets you add options to Counter metrics using With* funcs.
type CounterOption func(*prom.CounterOpts)
type counterOptions []CounterOption
func (co counterOptions) apply(o prom.CounterOpts) prom.CounterOpts {
for _, f := range co {
f(&o)
}
return o
}
// WithConstLabels allows you to add ConstLabels to Counter metrics.
func WithConstLabels(labels prom.Labels) CounterOption {
return func(o *prom.CounterOpts) {
o.ConstLabels = labels
}
}
// A HistogramOption lets you add options to Histogram metrics using With*
// funcs.
type HistogramOption func(*prom.HistogramOpts)
// WithHistogramBuckets allows you to specify custom bucket ranges for histograms if EnableHandlingTimeHistogram is on.
func WithHistogramBuckets(buckets []float64) HistogramOption {
return func(o *prom.HistogramOpts) { o.Buckets = buckets }
}
// WithHistogramConstLabels allows you to add custom ConstLabels to
// histograms metrics.
func WithHistogramConstLabels(labels prom.Labels) HistogramOption {
return func(o *prom.HistogramOpts) {
o.ConstLabels = labels
}
}
@@ -0,0 +1,57 @@
package grpcstatus
import (
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
type gRPCStatus interface {
GRPCStatus() *status.Status
}
func unwrapPkgErrorsGRPCStatus(err error) (*status.Status, bool) {
type causer interface {
Cause() error
}
// Unwrapping the github.com/pkg/errors causer interface, using `Cause` directly could miss some error implementing
// the `GRPCStatus` function so we have to check it on our selves.
unwrappedCauser := err
for unwrappedCauser != nil {
if s, ok := unwrappedCauser.(gRPCStatus); ok {
return s.GRPCStatus(), true
}
cause, ok := unwrappedCauser.(causer)
if !ok {
break
}
unwrappedCauser = cause.Cause()
}
return nil, false
}
// Since error can be wrapped and the `FromError` function only checks for `GRPCStatus` function
// and as a fallback uses the `Unknown` gRPC status we need to unwrap the error if possible to get the original status.
// pkg/errors and Go native errors packages have two different approaches so we try to unwrap both types.
// Eventually should be implemented in the go-grpc status function `FromError`. See https://github.com/grpc/grpc-go/issues/2934
func FromError(err error) (s *status.Status, ok bool) {
s, ok = status.FromError(err)
if ok {
return s, true
}
// Try to unwrap `github.com/pkg/errors` wrapped error
s, ok = unwrapPkgErrorsGRPCStatus(err)
if ok {
return s, true
}
// Try to unwrap native wrapped errors using `fmt.Errorf` and `%w`
s, ok = unwrapNativeWrappedGRPCStatus(err)
if ok {
return s, true
}
// We failed to unwrap any GRPSStatus so return default `Unknown`
return status.New(codes.Unknown, err.Error()), false
}
@@ -0,0 +1,11 @@
// +build !go1.13
package grpcstatus
import (
"google.golang.org/grpc/status"
)
func unwrapNativeWrappedGRPCStatus(err error) (*status.Status, bool) {
return nil, false
}
@@ -0,0 +1,17 @@
// +build go1.13
package grpcstatus
import (
"errors"
"google.golang.org/grpc/status"
)
func unwrapNativeWrappedGRPCStatus(err error) (*status.Status, bool) {
// Unwrapping the native Go unwrap interface
var unwrappedStatus gRPCStatus
if ok := errors.As(err, &unwrappedStatus); ok {
return unwrappedStatus.GRPCStatus(), true
}
return nil, false
}
+48
View File
@@ -0,0 +1,48 @@
// Copyright 2016 Michal Witkowski. All Rights Reserved.
// See LICENSE for licensing terms.
// gRPC Prometheus monitoring interceptors for server-side gRPC.
package grpc_prometheus
import (
prom "github.com/prometheus/client_golang/prometheus"
"google.golang.org/grpc"
)
var (
// DefaultServerMetrics is the default instance of ServerMetrics. It is
// intended to be used in conjunction the default Prometheus metrics
// registry.
DefaultServerMetrics = NewServerMetrics()
// UnaryServerInterceptor is a gRPC server-side interceptor that provides Prometheus monitoring for Unary RPCs.
UnaryServerInterceptor = DefaultServerMetrics.UnaryServerInterceptor()
// StreamServerInterceptor is a gRPC server-side interceptor that provides Prometheus monitoring for Streaming RPCs.
StreamServerInterceptor = DefaultServerMetrics.StreamServerInterceptor()
)
func init() {
prom.MustRegister(DefaultServerMetrics.serverStartedCounter)
prom.MustRegister(DefaultServerMetrics.serverHandledCounter)
prom.MustRegister(DefaultServerMetrics.serverStreamMsgReceived)
prom.MustRegister(DefaultServerMetrics.serverStreamMsgSent)
}
// Register takes a gRPC server and pre-initializes all counters to 0. This
// allows for easier monitoring in Prometheus (no missing metrics), and should
// be called *after* all services have been registered with the server. This
// function acts on the DefaultServerMetrics variable.
func Register(server *grpc.Server) {
DefaultServerMetrics.InitializeMetrics(server)
}
// EnableHandlingTimeHistogram turns on recording of handling time
// of RPCs. Histogram metrics can be very expensive for Prometheus
// to retain and query. This function acts on the DefaultServerMetrics
// variable and the default Prometheus metrics registry.
func EnableHandlingTimeHistogram(opts ...HistogramOption) {
DefaultServerMetrics.EnableHandlingTimeHistogram(opts...)
prom.Register(DefaultServerMetrics.serverHandledHistogram)
}
+186
View File
@@ -0,0 +1,186 @@
package grpc_prometheus
import (
"context"
"github.com/grpc-ecosystem/go-grpc-prometheus/packages/grpcstatus"
prom "github.com/prometheus/client_golang/prometheus"
"google.golang.org/grpc"
)
// ServerMetrics represents a collection of metrics to be registered on a
// Prometheus metrics registry for a gRPC server.
type ServerMetrics struct {
serverStartedCounter *prom.CounterVec
serverHandledCounter *prom.CounterVec
serverStreamMsgReceived *prom.CounterVec
serverStreamMsgSent *prom.CounterVec
serverHandledHistogramEnabled bool
serverHandledHistogramOpts prom.HistogramOpts
serverHandledHistogram *prom.HistogramVec
}
// NewServerMetrics returns a ServerMetrics object. Use a new instance of
// ServerMetrics when not using the default Prometheus metrics registry, for
// example when wanting to control which metrics are added to a registry as
// opposed to automatically adding metrics via init functions.
func NewServerMetrics(counterOpts ...CounterOption) *ServerMetrics {
opts := counterOptions(counterOpts)
return &ServerMetrics{
serverStartedCounter: prom.NewCounterVec(
opts.apply(prom.CounterOpts{
Name: "grpc_server_started_total",
Help: "Total number of RPCs started on the server.",
}), []string{"grpc_type", "grpc_service", "grpc_method"}),
serverHandledCounter: prom.NewCounterVec(
opts.apply(prom.CounterOpts{
Name: "grpc_server_handled_total",
Help: "Total number of RPCs completed on the server, regardless of success or failure.",
}), []string{"grpc_type", "grpc_service", "grpc_method", "grpc_code"}),
serverStreamMsgReceived: prom.NewCounterVec(
opts.apply(prom.CounterOpts{
Name: "grpc_server_msg_received_total",
Help: "Total number of RPC stream messages received on the server.",
}), []string{"grpc_type", "grpc_service", "grpc_method"}),
serverStreamMsgSent: prom.NewCounterVec(
opts.apply(prom.CounterOpts{
Name: "grpc_server_msg_sent_total",
Help: "Total number of gRPC stream messages sent by the server.",
}), []string{"grpc_type", "grpc_service", "grpc_method"}),
serverHandledHistogramEnabled: false,
serverHandledHistogramOpts: prom.HistogramOpts{
Name: "grpc_server_handling_seconds",
Help: "Histogram of response latency (seconds) of gRPC that had been application-level handled by the server.",
Buckets: prom.DefBuckets,
},
serverHandledHistogram: nil,
}
}
// EnableHandlingTimeHistogram enables histograms being registered when
// registering the ServerMetrics on a Prometheus registry. Histograms can be
// expensive on Prometheus servers. It takes options to configure histogram
// options such as the defined buckets.
func (m *ServerMetrics) EnableHandlingTimeHistogram(opts ...HistogramOption) {
for _, o := range opts {
o(&m.serverHandledHistogramOpts)
}
if !m.serverHandledHistogramEnabled {
m.serverHandledHistogram = prom.NewHistogramVec(
m.serverHandledHistogramOpts,
[]string{"grpc_type", "grpc_service", "grpc_method"},
)
}
m.serverHandledHistogramEnabled = true
}
// Describe sends the super-set of all possible descriptors of metrics
// collected by this Collector to the provided channel and returns once
// the last descriptor has been sent.
func (m *ServerMetrics) Describe(ch chan<- *prom.Desc) {
m.serverStartedCounter.Describe(ch)
m.serverHandledCounter.Describe(ch)
m.serverStreamMsgReceived.Describe(ch)
m.serverStreamMsgSent.Describe(ch)
if m.serverHandledHistogramEnabled {
m.serverHandledHistogram.Describe(ch)
}
}
// Collect is called by the Prometheus registry when collecting
// metrics. The implementation sends each collected metric via the
// provided channel and returns once the last metric has been sent.
func (m *ServerMetrics) Collect(ch chan<- prom.Metric) {
m.serverStartedCounter.Collect(ch)
m.serverHandledCounter.Collect(ch)
m.serverStreamMsgReceived.Collect(ch)
m.serverStreamMsgSent.Collect(ch)
if m.serverHandledHistogramEnabled {
m.serverHandledHistogram.Collect(ch)
}
}
// UnaryServerInterceptor is a gRPC server-side interceptor that provides Prometheus monitoring for Unary RPCs.
func (m *ServerMetrics) UnaryServerInterceptor() func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
monitor := newServerReporter(m, Unary, info.FullMethod)
monitor.ReceivedMessage()
resp, err := handler(ctx, req)
st, _ := grpcstatus.FromError(err)
monitor.Handled(st.Code())
if err == nil {
monitor.SentMessage()
}
return resp, err
}
}
// StreamServerInterceptor is a gRPC server-side interceptor that provides Prometheus monitoring for Streaming RPCs.
func (m *ServerMetrics) StreamServerInterceptor() func(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
return func(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
monitor := newServerReporter(m, streamRPCType(info), info.FullMethod)
err := handler(srv, &monitoredServerStream{ss, monitor})
st, _ := grpcstatus.FromError(err)
monitor.Handled(st.Code())
return err
}
}
// InitializeMetrics initializes all metrics, with their appropriate null
// value, for all gRPC methods registered on a gRPC server. This is useful, to
// ensure that all metrics exist when collecting and querying.
func (m *ServerMetrics) InitializeMetrics(server *grpc.Server) {
serviceInfo := server.GetServiceInfo()
for serviceName, info := range serviceInfo {
for _, mInfo := range info.Methods {
preRegisterMethod(m, serviceName, &mInfo)
}
}
}
func streamRPCType(info *grpc.StreamServerInfo) grpcType {
if info.IsClientStream && !info.IsServerStream {
return ClientStream
} else if !info.IsClientStream && info.IsServerStream {
return ServerStream
}
return BidiStream
}
// monitoredStream wraps grpc.ServerStream allowing each Sent/Recv of message to increment counters.
type monitoredServerStream struct {
grpc.ServerStream
monitor *serverReporter
}
func (s *monitoredServerStream) SendMsg(m interface{}) error {
err := s.ServerStream.SendMsg(m)
if err == nil {
s.monitor.SentMessage()
}
return err
}
func (s *monitoredServerStream) RecvMsg(m interface{}) error {
err := s.ServerStream.RecvMsg(m)
if err == nil {
s.monitor.ReceivedMessage()
}
return err
}
// preRegisterMethod is invoked on Register of a Server, allowing all gRPC services labels to be pre-populated.
func preRegisterMethod(metrics *ServerMetrics, serviceName string, mInfo *grpc.MethodInfo) {
methodName := mInfo.Name
methodType := string(typeFromMethodInfo(mInfo))
// These are just references (no increments), as just referencing will create the labels but not set values.
metrics.serverStartedCounter.GetMetricWithLabelValues(methodType, serviceName, methodName)
metrics.serverStreamMsgReceived.GetMetricWithLabelValues(methodType, serviceName, methodName)
metrics.serverStreamMsgSent.GetMetricWithLabelValues(methodType, serviceName, methodName)
if metrics.serverHandledHistogramEnabled {
metrics.serverHandledHistogram.GetMetricWithLabelValues(methodType, serviceName, methodName)
}
for _, code := range allCodes {
metrics.serverHandledCounter.GetMetricWithLabelValues(methodType, serviceName, methodName, code.String())
}
}
+46
View File
@@ -0,0 +1,46 @@
// Copyright 2016 Michal Witkowski. All Rights Reserved.
// See LICENSE for licensing terms.
package grpc_prometheus
import (
"time"
"google.golang.org/grpc/codes"
)
type serverReporter struct {
metrics *ServerMetrics
rpcType grpcType
serviceName string
methodName string
startTime time.Time
}
func newServerReporter(m *ServerMetrics, rpcType grpcType, fullMethod string) *serverReporter {
r := &serverReporter{
metrics: m,
rpcType: rpcType,
}
if r.metrics.serverHandledHistogramEnabled {
r.startTime = time.Now()
}
r.serviceName, r.methodName = splitMethodName(fullMethod)
r.metrics.serverStartedCounter.WithLabelValues(string(r.rpcType), r.serviceName, r.methodName).Inc()
return r
}
func (r *serverReporter) ReceivedMessage() {
r.metrics.serverStreamMsgReceived.WithLabelValues(string(r.rpcType), r.serviceName, r.methodName).Inc()
}
func (r *serverReporter) SentMessage() {
r.metrics.serverStreamMsgSent.WithLabelValues(string(r.rpcType), r.serviceName, r.methodName).Inc()
}
func (r *serverReporter) Handled(code codes.Code) {
r.metrics.serverHandledCounter.WithLabelValues(string(r.rpcType), r.serviceName, r.methodName, code.String()).Inc()
if r.metrics.serverHandledHistogramEnabled {
r.metrics.serverHandledHistogram.WithLabelValues(string(r.rpcType), r.serviceName, r.methodName).Observe(time.Since(r.startTime).Seconds())
}
}
+50
View File
@@ -0,0 +1,50 @@
// Copyright 2016 Michal Witkowski. All Rights Reserved.
// See LICENSE for licensing terms.
package grpc_prometheus
import (
"strings"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
)
type grpcType string
const (
Unary grpcType = "unary"
ClientStream grpcType = "client_stream"
ServerStream grpcType = "server_stream"
BidiStream grpcType = "bidi_stream"
)
var (
allCodes = []codes.Code{
codes.OK, codes.Canceled, codes.Unknown, codes.InvalidArgument, codes.DeadlineExceeded, codes.NotFound,
codes.AlreadyExists, codes.PermissionDenied, codes.Unauthenticated, codes.ResourceExhausted,
codes.FailedPrecondition, codes.Aborted, codes.OutOfRange, codes.Unimplemented, codes.Internal,
codes.Unavailable, codes.DataLoss,
}
)
func splitMethodName(fullMethodName string) (string, string) {
fullMethodName = strings.TrimPrefix(fullMethodName, "/") // remove leading slash
if i := strings.Index(fullMethodName, "/"); i >= 0 {
return fullMethodName[:i], fullMethodName[i+1:]
}
return "unknown", "unknown"
}
func typeFromMethodInfo(mInfo *grpc.MethodInfo) grpcType {
if !mInfo.IsClientStream && !mInfo.IsServerStream {
return Unary
}
if mInfo.IsClientStream && !mInfo.IsServerStream {
return ClientStream
}
if !mInfo.IsClientStream && mInfo.IsServerStream {
return ServerStream
}
return BidiStream
}
+1
View File
@@ -0,0 +1 @@
.idea*
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2017 HashiCorp
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+148
View File
@@ -0,0 +1,148 @@
# go-hclog
[![Go Documentation](http://img.shields.io/badge/go-documentation-blue.svg?style=flat-square)][godocs]
[godocs]: https://godoc.org/github.com/hashicorp/go-hclog
`go-hclog` is a package for Go that provides a simple key/value logging
interface for use in development and production environments.
It provides logging levels that provide decreased output based upon the
desired amount of output, unlike the standard library `log` package.
It provides `Printf` style logging of values via `hclog.Fmt()`.
It provides a human readable output mode for use in development as well as
JSON output mode for production.
## Stability Note
While this library is fully open source and HashiCorp will be maintaining it
(since we are and will be making extensive use of it), the API and output
format is subject to minor changes as we fully bake and vet it in our projects.
This notice will be removed once it's fully integrated into our major projects
and no further changes are anticipated.
## Installation and Docs
Install using `go get github.com/hashicorp/go-hclog`.
Full documentation is available at
http://godoc.org/github.com/hashicorp/go-hclog
## Usage
### Use the global logger
```go
hclog.Default().Info("hello world")
```
```text
2017-07-05T16:15:55.167-0700 [INFO ] hello world
```
(Note timestamps are removed in future examples for brevity.)
### Create a new logger
```go
appLogger := hclog.New(&hclog.LoggerOptions{
Name: "my-app",
Level: hclog.LevelFromString("DEBUG"),
})
```
### Emit an Info level message with 2 key/value pairs
```go
input := "5.5"
_, err := strconv.ParseInt(input, 10, 32)
if err != nil {
appLogger.Info("Invalid input for ParseInt", "input", input, "error", err)
}
```
```text
... [INFO ] my-app: Invalid input for ParseInt: input=5.5 error="strconv.ParseInt: parsing "5.5": invalid syntax"
```
### Create a new Logger for a major subsystem
```go
subsystemLogger := appLogger.Named("transport")
subsystemLogger.Info("we are transporting something")
```
```text
... [INFO ] my-app.transport: we are transporting something
```
Notice that logs emitted by `subsystemLogger` contain `my-app.transport`,
reflecting both the application and subsystem names.
### Create a new Logger with fixed key/value pairs
Using `With()` will include a specific key-value pair in all messages emitted
by that logger.
```go
requestID := "5fb446b6-6eba-821d-df1b-cd7501b6a363"
requestLogger := subsystemLogger.With("request", requestID)
requestLogger.Info("we are transporting a request")
```
```text
... [INFO ] my-app.transport: we are transporting a request: request=5fb446b6-6eba-821d-df1b-cd7501b6a363
```
This allows sub Loggers to be context specific without having to thread that
into all the callers.
### Using `hclog.Fmt()`
```go
var int totalBandwidth = 200
appLogger.Info("total bandwidth exceeded", "bandwidth", hclog.Fmt("%d GB/s", totalBandwidth))
```
```text
... [INFO ] my-app: total bandwidth exceeded: bandwidth="200 GB/s"
```
### Use this with code that uses the standard library logger
If you want to use the standard library's `log.Logger` interface you can wrap
`hclog.Logger` by calling the `StandardLogger()` method. This allows you to use
it with the familiar `Println()`, `Printf()`, etc. For example:
```go
stdLogger := appLogger.StandardLogger(&hclog.StandardLoggerOptions{
InferLevels: true,
})
// Printf() is provided by stdlib log.Logger interface, not hclog.Logger
stdLogger.Printf("[DEBUG] %+v", stdLogger)
```
```text
... [DEBUG] my-app: &{mu:{state:0 sema:0} prefix: flag:0 out:0xc42000a0a0 buf:[]}
```
Alternatively, you may configure the system-wide logger:
```go
// log the standard logger from 'import "log"'
log.SetOutput(appLogger.StandardWriter(&hclog.StandardLoggerOptions{InferLevels: true}))
log.SetPrefix("")
log.SetFlags(0)
log.Printf("[DEBUG] %d", 42)
```
```text
... [DEBUG] my-app: 42
```
Notice that if `appLogger` is initialized with the `INFO` log level _and_ you
specify `InferLevels: true`, you will not see any output here. You must change
`appLogger` to `DEBUG` to see output. See the docs for more information.
+27
View File
@@ -0,0 +1,27 @@
// +build !windows
package hclog
import (
"github.com/mattn/go-isatty"
)
// setColorization will mutate the values of this logger
// to approperately configure colorization options. It provides
// a wrapper to the output stream on Windows systems.
func (l *intLogger) setColorization(opts *LoggerOptions) {
switch opts.Color {
case ColorOff:
fallthrough
case ForceColor:
return
case AutoColor:
fi := l.checkWriterIsFile()
isUnixTerm := isatty.IsTerminal(fi.Fd())
isCygwinTerm := isatty.IsCygwinTerminal(fi.Fd())
isTerm := isUnixTerm || isCygwinTerm
if !isTerm {
l.writer.color = ColorOff
}
}
}
+33
View File
@@ -0,0 +1,33 @@
// +build windows
package hclog
import (
"os"
colorable "github.com/mattn/go-colorable"
"github.com/mattn/go-isatty"
)
// setColorization will mutate the values of this logger
// to approperately configure colorization options. It provides
// a wrapper to the output stream on Windows systems.
func (l *intLogger) setColorization(opts *LoggerOptions) {
switch opts.Color {
case ColorOff:
return
case ForceColor:
fi := l.checkWriterIsFile()
l.writer.w = colorable.NewColorable(fi)
case AutoColor:
fi := l.checkWriterIsFile()
isUnixTerm := isatty.IsTerminal(os.Stdout.Fd())
isCygwinTerm := isatty.IsCygwinTerminal(os.Stdout.Fd())
isTerm := isUnixTerm || isCygwinTerm
if !isTerm {
l.writer.color = ColorOff
return
}
l.writer.w = colorable.NewColorable(fi)
}
}
+38
View File
@@ -0,0 +1,38 @@
package hclog
import (
"context"
)
// WithContext inserts a logger into the context and is retrievable
// with FromContext. The optional args can be set with the same syntax as
// Logger.With to set fields on the inserted logger. This will not modify
// the logger argument in-place.
func WithContext(ctx context.Context, logger Logger, args ...interface{}) context.Context {
// While we could call logger.With even with zero args, we have this
// check to avoid unnecessary allocations around creating a copy of a
// logger.
if len(args) > 0 {
logger = logger.With(args...)
}
return context.WithValue(ctx, contextKey, logger)
}
// FromContext returns a logger from the context. This will return L()
// (the default logger) if no logger is found in the context. Therefore,
// this will never return a nil value.
func FromContext(ctx context.Context) Logger {
logger, _ := ctx.Value(contextKey).(Logger)
if logger == nil {
return L()
}
return logger
}
// Unexported new type so that our context key never collides with another.
type contextKeyType struct{}
// contextKey is the key used for the context to store the logger.
var contextKey = contextKeyType{}
+71
View File
@@ -0,0 +1,71 @@
package hclog
import (
"regexp"
"strings"
)
// ExcludeByMessage provides a simple way to build a list of log messages that
// can be queried and matched. This is meant to be used with the Exclude
// option on Options to suppress log messages. This does not hold any mutexs
// within itself, so normal usage would be to Add entries at setup and none after
// Exclude is going to be called. Exclude is called with a mutex held within
// the Logger, so that doesn't need to use a mutex. Example usage:
//
// f := new(ExcludeByMessage)
// f.Add("Noisy log message text")
// appLogger.Exclude = f.Exclude
type ExcludeByMessage struct {
messages map[string]struct{}
}
// Add a message to be filtered. Do not call this after Exclude is to be called
// due to concurrency issues.
func (f *ExcludeByMessage) Add(msg string) {
if f.messages == nil {
f.messages = make(map[string]struct{})
}
f.messages[msg] = struct{}{}
}
// Return true if the given message should be included
func (f *ExcludeByMessage) Exclude(level Level, msg string, args ...interface{}) bool {
_, ok := f.messages[msg]
return ok
}
// ExcludeByPrefix is a simple type to match a message string that has a common prefix.
type ExcludeByPrefix string
// Matches an message that starts with the prefix.
func (p ExcludeByPrefix) Exclude(level Level, msg string, args ...interface{}) bool {
return strings.HasPrefix(msg, string(p))
}
// ExcludeByRegexp takes a regexp and uses it to match a log message string. If it matches
// the log entry is excluded.
type ExcludeByRegexp struct {
Regexp *regexp.Regexp
}
// Exclude the log message if the message string matches the regexp
func (e ExcludeByRegexp) Exclude(level Level, msg string, args ...interface{}) bool {
return e.Regexp.MatchString(msg)
}
// ExcludeFuncs is a slice of functions that will called to see if a log entry
// should be filtered or not. It stops calling functions once at least one returns
// true.
type ExcludeFuncs []func(level Level, msg string, args ...interface{}) bool
// Calls each function until one of them returns true
func (ff ExcludeFuncs) Exclude(level Level, msg string, args ...interface{}) bool {
for _, f := range ff {
if f(level, msg, args...) {
return true
}
}
return false
}
+62
View File
@@ -0,0 +1,62 @@
package hclog
import (
"sync"
)
var (
protect sync.Once
def Logger
// DefaultOptions is used to create the Default logger. These are read
// only when the Default logger is created, so set them as soon as the
// process starts.
DefaultOptions = &LoggerOptions{
Level: DefaultLevel,
Output: DefaultOutput,
}
)
// Default returns a globally held logger. This can be a good starting
// place, and then you can use .With() and .Name() to create sub-loggers
// to be used in more specific contexts.
// The value of the Default logger can be set via SetDefault() or by
// changing the options in DefaultOptions.
//
// This method is goroutine safe, returning a global from memory, but
// cause should be used if SetDefault() is called it random times
// in the program as that may result in race conditions and an unexpected
// Logger being returned.
func Default() Logger {
protect.Do(func() {
// If SetDefault was used before Default() was called, we need to
// detect that here.
if def == nil {
def = New(DefaultOptions)
}
})
return def
}
// L is a short alias for Default().
func L() Logger {
return Default()
}
// SetDefault changes the logger to be returned by Default()and L()
// to the one given. This allows packages to use the default logger
// and have higher level packages change it to match the execution
// environment. It returns any old default if there is one.
//
// NOTE: This is expected to be called early in the program to setup
// a default logger. As such, it does not attempt to make itself
// not racy with regard to the value of the default logger. Ergo
// if it is called in goroutines, you may experience race conditions
// with other goroutines retrieving the default logger. Basically,
// don't do that.
func SetDefault(log Logger) Logger {
old := def
def = log
return old
}
+12
View File
@@ -0,0 +1,12 @@
module github.com/hashicorp/go-hclog
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fatih/color v1.7.0
github.com/mattn/go-colorable v0.1.4
github.com/mattn/go-isatty v0.0.10
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/stretchr/testify v1.2.2
)
go 1.13
Loaded 100 of 647 files, more files were not shown because too many files have changed in this diff. Show more