Expose GrafanaManagedAlert properties
This commit is contained in:
1 parent
fd5fd392b2
commit
eb26bb8842
647 files changed
+131406
-5
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
@@ -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"`
|
||||
}
|
||||
@@ -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
@@ -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
@@ -0,0 +1,173 @@
|
||||
# color [](https://github.com/fatih/color/actions) [](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.
|
||||
|
||||

|
||||
|
||||
|
||||
## 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
@@ -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
@@ -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
@@ -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
@@ -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=
|
||||
+200
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
+204
@@ -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,
|
||||
}
|
||||
}
|
||||
+197
@@ -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
@@ -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)
|
||||
}
|
||||
+27
@@ -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
@@ -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
|
||||
}
|
||||
+58
@@ -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
@@ -0,0 +1,2 @@
|
||||
// Package backend provides SDK handler interfaces and contracts for implementing and serving backend plugins.
|
||||
package backend
|
||||
+24
@@ -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)
|
||||
}
|
||||
+5
@@ -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
|
||||
+60
@@ -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{}
|
||||
Generated
Vendored
+70
@@ -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{}
|
||||
Generated
Vendored
+60
@@ -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{}
|
||||
+27
@@ -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,
|
||||
}
|
||||
+62
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
}
|
||||
+38
@@ -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
@@ -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
@@ -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())
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
+1729
File diff suppressed because it is too large.
Load diff
Generated
Vendored
+330
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
+288
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,85 @@
|
||||
# Go gRPC Middleware
|
||||
|
||||
[](https://travis-ci.org/grpc-ecosystem/go-grpc-middleware)
|
||||
[](https://goreportcard.com/report/github.com/grpc-ecosystem/go-grpc-middleware)
|
||||
[](https://godoc.org/github.com/grpc-ecosystem/go-grpc-middleware)
|
||||
[](https://sourcegraph.com/github.com/grpc-ecosystem/go-grpc-middleware/?badge)
|
||||
[](https://codecov.io/gh/grpc-ecosystem/go-grpc-middleware)
|
||||
[](LICENSE)
|
||||
[](#status)
|
||||
[](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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 5.0 KiB |
+30
@@ -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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,250 @@
|
||||
# Go gRPC Interceptors for Prometheus monitoring
|
||||
|
||||
[](https://travis-ci.org/grpc-ecosystem/go-grpc-prometheus)
|
||||
[](http://goreportcard.com/report/grpc-ecosystem/go-grpc-prometheus)
|
||||
[](https://godoc.org/github.com/grpc-ecosystem/go-grpc-prometheus)
|
||||
[](https://sourcegraph.com/github.com/grpc-ecosystem/go-grpc-prometheus/?badge)
|
||||
[](https://codecov.io/gh/grpc-ecosystem/go-grpc-prometheus)
|
||||
[](https://join.slack.com/t/improbable-eng/shared_invite/enQtMzQ1ODcyMzQ5MjM4LWY5ZWZmNGM2ODc5MmViNmQ3ZTA3ZTY3NzQwOTBlMTkzZmIxZTIxODk0OWU3YjZhNWVlNDU3MDlkZGViZjhkMjc)
|
||||
[](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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+57
@@ -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
|
||||
}
|
||||
Generated
Vendored
+11
@@ -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
|
||||
}
|
||||
Generated
Vendored
+17
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1 @@
|
||||
.idea*
|
||||
+21
@@ -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
@@ -0,0 +1,148 @@
|
||||
# go-hclog
|
||||
|
||||
[][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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
Reference in new issue
Block a user