Commit 50b41130 by Torkel Ödegaard

feat(alerting): fixed s3 upload issue, progress on alerting on null/missing…

feat(alerting): fixed s3 upload issue, progress on alerting on null/missing data, updated ini package to get the support for line continuations
parent fc8f0721
{ {
"ImportPath": "github.com/grafana/grafana", "ImportPath": "github.com/grafana/grafana",
"GoVersion": "go1.5.1", "GoVersion": "go1.6.2",
"GodepVersion": "v60", "GodepVersion": "v60",
"Packages": [ "Packages": [
"./pkg/..." "./pkg/..."
...@@ -368,8 +368,8 @@ ...@@ -368,8 +368,8 @@
}, },
{ {
"ImportPath": "gopkg.in/ini.v1", "ImportPath": "gopkg.in/ini.v1",
"Comment": "v0-16-g1772191", "Comment": "v1.21.1",
"Rev": "177219109c97e7920c933e21c9b25f874357b237" "Rev": "6e4869b434bd001f6983749881c7ead3545887d8"
}, },
{ {
"ImportPath": "gopkg.in/macaron.v1", "ImportPath": "gopkg.in/macaron.v1",
......
testdata/conf_out.ini testdata/conf_out.ini
ini.sublime-project ini.sublime-project
ini.sublime-workspace ini.sublime-workspace
testdata/conf_reflect.ini
.idea
sudo: false
language: go
go:
- 1.4
- 1.5
- 1.6
- tip
script:
- go get -v github.com/smartystreets/goconvey
- go test -v -cover -race
notifications:
email:
- u@gogs.io
.PHONY: build test bench vet
build: vet bench
test:
go test -v -cover -race
bench:
go test -v -cover -race -test.bench=. -test.benchmem
vet:
go vet
// Copyright 2016 Unknwon
//
// 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.
package ini
import (
"fmt"
)
type ErrDelimiterNotFound struct {
Line string
}
func IsErrDelimiterNotFound(err error) bool {
_, ok := err.(ErrDelimiterNotFound)
return ok
}
func (err ErrDelimiterNotFound) Error() string {
return fmt.Sprintf("key-value delimiter not found: %s", err.Line)
}
// Copyright 2015 Unknwon
//
// 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.
package ini
import (
"bufio"
"bytes"
"fmt"
"io"
"strconv"
"strings"
"unicode"
)
type tokenType int
const (
_TOKEN_INVALID tokenType = iota
_TOKEN_COMMENT
_TOKEN_SECTION
_TOKEN_KEY
)
type parser struct {
buf *bufio.Reader
isEOF bool
count int
comment *bytes.Buffer
}
func newParser(r io.Reader) *parser {
return &parser{
buf: bufio.NewReader(r),
count: 1,
comment: &bytes.Buffer{},
}
}
// BOM handles header of BOM-UTF8 format.
// http://en.wikipedia.org/wiki/Byte_order_mark#Representations_of_byte_order_marks_by_encoding
func (p *parser) BOM() error {
mask, err := p.buf.Peek(3)
if err != nil && err != io.EOF {
return err
} else if len(mask) < 3 {
return nil
} else if mask[0] == 239 && mask[1] == 187 && mask[2] == 191 {
p.buf.Read(mask)
}
return nil
}
func (p *parser) readUntil(delim byte) ([]byte, error) {
data, err := p.buf.ReadBytes(delim)
if err != nil {
if err == io.EOF {
p.isEOF = true
} else {
return nil, err
}
}
return data, nil
}
func cleanComment(in []byte) ([]byte, bool) {
i := bytes.IndexAny(in, "#;")
if i == -1 {
return nil, false
}
return in[i:], true
}
func readKeyName(in []byte) (string, int, error) {
line := string(in)
// Check if key name surrounded by quotes.
var keyQuote string
if line[0] == '"' {
if len(line) > 6 && string(line[0:3]) == `"""` {
keyQuote = `"""`
} else {
keyQuote = `"`
}
} else if line[0] == '`' {
keyQuote = "`"
}
// Get out key name
endIdx := -1
if len(keyQuote) > 0 {
startIdx := len(keyQuote)
// FIXME: fail case -> """"""name"""=value
pos := strings.Index(line[startIdx:], keyQuote)
if pos == -1 {
return "", -1, fmt.Errorf("missing closing key quote: %s", line)
}
pos += startIdx
// Find key-value delimiter
i := strings.IndexAny(line[pos+startIdx:], "=:")
if i < 0 {
return "", -1, ErrDelimiterNotFound{line}
}
endIdx = pos + i
return strings.TrimSpace(line[startIdx:pos]), endIdx + startIdx + 1, nil
}
endIdx = strings.IndexAny(line, "=:")
if endIdx < 0 {
return "", -1, ErrDelimiterNotFound{line}
}
return strings.TrimSpace(line[0:endIdx]), endIdx + 1, nil
}
func (p *parser) readMultilines(line, val, valQuote string) (string, error) {
for {
data, err := p.readUntil('\n')
if err != nil {
return "", err
}
next := string(data)
pos := strings.LastIndex(next, valQuote)
if pos > -1 {
val += next[:pos]
comment, has := cleanComment([]byte(next[pos:]))
if has {
p.comment.Write(bytes.TrimSpace(comment))
}
break
}
val += next
if p.isEOF {
return "", fmt.Errorf("missing closing key quote from '%s' to '%s'", line, next)
}
}
return val, nil
}
func (p *parser) readContinuationLines(val string) (string, error) {
for {
data, err := p.readUntil('\n')
if err != nil {
return "", err
}
next := strings.TrimSpace(string(data))
if len(next) == 0 {
break
}
val += next
if val[len(val)-1] != '\\' {
break
}
val = val[:len(val)-1]
}
return val, nil
}
// hasSurroundedQuote check if and only if the first and last characters
// are quotes \" or \'.
// It returns false if any other parts also contain same kind of quotes.
func hasSurroundedQuote(in string, quote byte) bool {
return len(in) > 2 && in[0] == quote && in[len(in)-1] == quote &&
strings.IndexByte(in[1:], quote) == len(in)-2
}
func (p *parser) readValue(in []byte, ignoreContinuation bool) (string, error) {
line := strings.TrimLeftFunc(string(in), unicode.IsSpace)
if len(line) == 0 {
return "", nil
}
var valQuote string
if len(line) > 3 && string(line[0:3]) == `"""` {
valQuote = `"""`
} else if line[0] == '`' {
valQuote = "`"
}
if len(valQuote) > 0 {
startIdx := len(valQuote)
pos := strings.LastIndex(line[startIdx:], valQuote)
// Check for multi-line value
if pos == -1 {
return p.readMultilines(line, line[startIdx:], valQuote)
}
return line[startIdx : pos+startIdx], nil
}
// Won't be able to reach here if value only contains whitespace.
line = strings.TrimSpace(line)
// Check continuation lines when desired.
if !ignoreContinuation && line[len(line)-1] == '\\' {
return p.readContinuationLines(line[:len(line)-1])
}
i := strings.IndexAny(line, "#;")
if i > -1 {
p.comment.WriteString(line[i:])
line = strings.TrimSpace(line[:i])
}
// Trim single quotes
if hasSurroundedQuote(line, '\'') ||
hasSurroundedQuote(line, '"') {
line = line[1 : len(line)-1]
}
return line, nil
}
// parse parses data through an io.Reader.
func (f *File) parse(reader io.Reader) (err error) {
p := newParser(reader)
if err = p.BOM(); err != nil {
return fmt.Errorf("BOM: %v", err)
}
// Ignore error because default section name is never empty string.
section, _ := f.NewSection(DEFAULT_SECTION)
var line []byte
for !p.isEOF {
line, err = p.readUntil('\n')
if err != nil {
return err
}
line = bytes.TrimLeftFunc(line, unicode.IsSpace)
if len(line) == 0 {
continue
}
// Comments
if line[0] == '#' || line[0] == ';' {
// Note: we do not care ending line break,
// it is needed for adding second line,
// so just clean it once at the end when set to value.
p.comment.Write(line)
continue
}
// Section
if line[0] == '[' {
// Read to the next ']' (TODO: support quoted strings)
// TODO(unknwon): use LastIndexByte when stop supporting Go1.4
closeIdx := bytes.LastIndex(line, []byte("]"))
if closeIdx == -1 {
return fmt.Errorf("unclosed section: %s", line)
}
name := string(line[1:closeIdx])
section, err = f.NewSection(name)
if err != nil {
return err
}
comment, has := cleanComment(line[closeIdx+1:])
if has {
p.comment.Write(comment)
}
section.Comment = strings.TrimSpace(p.comment.String())
// Reset aotu-counter and comments
p.comment.Reset()
p.count = 1
continue
}
kname, offset, err := readKeyName(line)
if err != nil {
// Treat as boolean key when desired, and whole line is key name.
if IsErrDelimiterNotFound(err) && f.options.AllowBooleanKeys {
key, err := section.NewKey(string(line), "true")
if err != nil {
return err
}
key.isBooleanType = true
key.Comment = strings.TrimSpace(p.comment.String())
p.comment.Reset()
continue
}
return err
}
// Auto increment.
isAutoIncr := false
if kname == "-" {
isAutoIncr = true
kname = "#" + strconv.Itoa(p.count)
p.count++
}
key, err := section.NewKey(kname, "")
if err != nil {
return err
}
key.isAutoIncrement = isAutoIncr
value, err := p.readValue(line[offset:], f.options.IgnoreContinuation)
if err != nil {
return err
}
key.SetValue(value)
key.Comment = strings.TrimSpace(p.comment.String())
p.comment.Reset()
}
return nil
}
// Copyright 2014 Unknwon
//
// 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.
package ini
import (
"errors"
"fmt"
"strings"
)
// Section represents a config section.
type Section struct {
f *File
Comment string
name string
keys map[string]*Key
keyList []string
keysHash map[string]string
}
func newSection(f *File, name string) *Section {
return &Section{f, "", name, make(map[string]*Key), make([]string, 0, 10), make(map[string]string)}
}
// Name returns name of Section.
func (s *Section) Name() string {
return s.name
}
// NewKey creates a new key to given section.
func (s *Section) NewKey(name, val string) (*Key, error) {
if len(name) == 0 {
return nil, errors.New("error creating new key: empty key name")
} else if s.f.options.Insensitive {
name = strings.ToLower(name)
}
if s.f.BlockMode {
s.f.lock.Lock()
defer s.f.lock.Unlock()
}
if inSlice(name, s.keyList) {
s.keys[name].value = val
return s.keys[name], nil
}
s.keyList = append(s.keyList, name)
s.keys[name] = &Key{
s: s,
name: name,
value: val,
}
s.keysHash[name] = val
return s.keys[name], nil
}
// GetKey returns key in section by given name.
func (s *Section) GetKey(name string) (*Key, error) {
// FIXME: change to section level lock?
if s.f.BlockMode {
s.f.lock.RLock()
}
if s.f.options.Insensitive {
name = strings.ToLower(name)
}
key := s.keys[name]
if s.f.BlockMode {
s.f.lock.RUnlock()
}
if key == nil {
// Check if it is a child-section.
sname := s.name
for {
if i := strings.LastIndex(sname, "."); i > -1 {
sname = sname[:i]
sec, err := s.f.GetSection(sname)
if err != nil {
continue
}
return sec.GetKey(name)
} else {
break
}
}
return nil, fmt.Errorf("error when getting key of section '%s': key '%s' not exists", s.name, name)
}
return key, nil
}
// HasKey returns true if section contains a key with given name.
func (s *Section) HasKey(name string) bool {
key, _ := s.GetKey(name)
return key != nil
}
// Haskey is a backwards-compatible name for HasKey.
func (s *Section) Haskey(name string) bool {
return s.HasKey(name)
}
// HasValue returns true if section contains given raw value.
func (s *Section) HasValue(value string) bool {
if s.f.BlockMode {
s.f.lock.RLock()
defer s.f.lock.RUnlock()
}
for _, k := range s.keys {
if value == k.value {
return true
}
}
return false
}
// Key assumes named Key exists in section and returns a zero-value when not.
func (s *Section) Key(name string) *Key {
key, err := s.GetKey(name)
if err != nil {
// It's OK here because the only possible error is empty key name,
// but if it's empty, this piece of code won't be executed.
key, _ = s.NewKey(name, "")
return key
}
return key
}
// Keys returns list of keys of section.
func (s *Section) Keys() []*Key {
keys := make([]*Key, len(s.keyList))
for i := range s.keyList {
keys[i] = s.Key(s.keyList[i])
}
return keys
}
// ParentKeys returns list of keys of parent section.
func (s *Section) ParentKeys() []*Key {
var parentKeys []*Key
sname := s.name
for {
if i := strings.LastIndex(sname, "."); i > -1 {
sname = sname[:i]
sec, err := s.f.GetSection(sname)
if err != nil {
continue
}
parentKeys = append(parentKeys, sec.Keys()...)
} else {
break
}
}
return parentKeys
}
// KeyStrings returns list of key names of section.
func (s *Section) KeyStrings() []string {
list := make([]string, len(s.keyList))
copy(list, s.keyList)
return list
}
// KeysHash returns keys hash consisting of names and values.
func (s *Section) KeysHash() map[string]string {
if s.f.BlockMode {
s.f.lock.RLock()
defer s.f.lock.RUnlock()
}
hash := map[string]string{}
for key, value := range s.keysHash {
hash[key] = value
}
return hash
}
// DeleteKey deletes a key from section.
func (s *Section) DeleteKey(name string) {
if s.f.BlockMode {
s.f.lock.Lock()
defer s.f.lock.Unlock()
}
for i, k := range s.keyList {
if k == name {
s.keyList = append(s.keyList[:i], s.keyList[i+1:]...)
delete(s.keys, name)
return
}
}
}
// Copyright 2014 Unknwon
//
// 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.
package ini
import (
"strings"
"testing"
"time"
. "github.com/smartystreets/goconvey/convey"
)
type testNested struct {
Cities []string `delim:"|"`
Visits []time.Time
Note string
Unused int `ini:"-"`
}
type testEmbeded struct {
GPA float64
}
type testStruct struct {
Name string `ini:"NAME"`
Age int
Male bool
Money float64
Born time.Time
Others testNested
*testEmbeded `ini:"grade"`
Unused int `ini:"-"`
}
const _CONF_DATA_STRUCT = `
NAME = Unknwon
Age = 21
Male = true
Money = 1.25
Born = 1993-10-07T20:17:05Z
[Others]
Cities = HangZhou|Boston
Visits = 1993-10-07T20:17:05Z, 1993-10-07T20:17:05Z
Note = Hello world!
[grade]
GPA = 2.8
`
type unsupport struct {
Byte byte
}
type unsupport2 struct {
Others struct {
Cities byte
}
}
type unsupport3 struct {
Cities byte
}
type unsupport4 struct {
*unsupport3 `ini:"Others"`
}
type defaultValue struct {
Name string
Age int
Male bool
Money float64
Born time.Time
Cities []string
}
const _INVALID_DATA_CONF_STRUCT = `
Name =
Age = age
Male = 123
Money = money
Born = nil
Cities =
`
func Test_Struct(t *testing.T) {
Convey("Map file to struct", t, func() {
ts := new(testStruct)
So(MapTo(ts, []byte(_CONF_DATA_STRUCT)), ShouldBeNil)
So(ts.Name, ShouldEqual, "Unknwon")
So(ts.Age, ShouldEqual, 21)
So(ts.Male, ShouldBeTrue)
So(ts.Money, ShouldEqual, 1.25)
t, err := time.Parse(time.RFC3339, "1993-10-07T20:17:05Z")
So(err, ShouldBeNil)
So(ts.Born.String(), ShouldEqual, t.String())
So(strings.Join(ts.Others.Cities, ","), ShouldEqual, "HangZhou,Boston")
So(ts.Others.Visits[0].String(), ShouldEqual, t.String())
So(ts.Others.Note, ShouldEqual, "Hello world!")
So(ts.testEmbeded.GPA, ShouldEqual, 2.8)
})
Convey("Map to non-pointer struct", t, func() {
cfg, err := Load([]byte(_CONF_DATA_STRUCT))
So(err, ShouldBeNil)
So(cfg, ShouldNotBeNil)
So(cfg.MapTo(testStruct{}), ShouldNotBeNil)
})
Convey("Map to unsupported type", t, func() {
cfg, err := Load([]byte(_CONF_DATA_STRUCT))
So(err, ShouldBeNil)
So(cfg, ShouldNotBeNil)
cfg.NameMapper = func(raw string) string {
if raw == "Byte" {
return "NAME"
}
return raw
}
So(cfg.MapTo(&unsupport{}), ShouldNotBeNil)
So(cfg.MapTo(&unsupport2{}), ShouldNotBeNil)
So(cfg.MapTo(&unsupport4{}), ShouldNotBeNil)
})
Convey("Map from invalid data source", t, func() {
So(MapTo(&testStruct{}, "hi"), ShouldNotBeNil)
})
Convey("Map to wrong types and gain default values", t, func() {
cfg, err := Load([]byte(_INVALID_DATA_CONF_STRUCT))
So(err, ShouldBeNil)
t, err := time.Parse(time.RFC3339, "1993-10-07T20:17:05Z")
So(err, ShouldBeNil)
dv := &defaultValue{"Joe", 10, true, 1.25, t, []string{"HangZhou", "Boston"}}
So(cfg.MapTo(dv), ShouldBeNil)
So(dv.Name, ShouldEqual, "Joe")
So(dv.Age, ShouldEqual, 10)
So(dv.Male, ShouldBeTrue)
So(dv.Money, ShouldEqual, 1.25)
So(dv.Born.String(), ShouldEqual, t.String())
So(strings.Join(dv.Cities, ","), ShouldEqual, "HangZhou,Boston")
})
}
type testMapper struct {
PackageName string
}
func Test_NameGetter(t *testing.T) {
Convey("Test name mappers", t, func() {
So(MapToWithMapper(&testMapper{}, TitleUnderscore, []byte("packag_name=ini")), ShouldBeNil)
cfg, err := Load([]byte("PACKAGE_NAME=ini"))
So(err, ShouldBeNil)
So(cfg, ShouldNotBeNil)
cfg.NameMapper = AllCapsUnderscore
tg := new(testMapper)
So(cfg.MapTo(tg), ShouldBeNil)
So(tg.PackageName, ShouldEqual, "ini")
})
}
[author]
E-MAIL = u@gogs.io
\ No newline at end of file
...@@ -19,9 +19,9 @@ func NewImageUploader() (ImageUploader, error) { ...@@ -19,9 +19,9 @@ func NewImageUploader() (ImageUploader, error) {
return nil, err return nil, err
} }
bucket := s3sec.Key("secret_key").String() bucket := s3sec.Key("bucket_url").MustString("")
accessKey := s3sec.Key("access_key").String() accessKey := s3sec.Key("access_key").MustString("")
secretKey := s3sec.Key("secret_key").String() secretKey := s3sec.Key("secret_key").MustString("")
if bucket == "" { if bucket == "" {
return nil, fmt.Errorf("Could not find bucket setting for image.uploader.s3") return nil, fmt.Errorf("Could not find bucket setting for image.uploader.s3")
......
...@@ -3,7 +3,10 @@ package imguploader ...@@ -3,7 +3,10 @@ package imguploader
import ( import (
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"net/url"
"path"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/util"
"github.com/kr/s3/s3util" "github.com/kr/s3/s3util"
) )
...@@ -12,6 +15,7 @@ type S3Uploader struct { ...@@ -12,6 +15,7 @@ type S3Uploader struct {
bucket string bucket string
secretKey string secretKey string
accessKey string accessKey string
log log.Logger
} }
func NewS3Uploader(bucket, accessKey, secretKey string) *S3Uploader { func NewS3Uploader(bucket, accessKey, secretKey string) *S3Uploader {
...@@ -19,10 +23,11 @@ func NewS3Uploader(bucket, accessKey, secretKey string) *S3Uploader { ...@@ -19,10 +23,11 @@ func NewS3Uploader(bucket, accessKey, secretKey string) *S3Uploader {
bucket: bucket, bucket: bucket,
accessKey: accessKey, accessKey: accessKey,
secretKey: secretKey, secretKey: secretKey,
log: log.New("s3uploader"),
} }
} }
func (u *S3Uploader) Upload(path string) (string, error) { func (u *S3Uploader) Upload(imageDiskPath string) (string, error) {
s3util.DefaultConfig.AccessKey = u.accessKey s3util.DefaultConfig.AccessKey = u.accessKey
s3util.DefaultConfig.SecretKey = u.secretKey s3util.DefaultConfig.SecretKey = u.secretKey
...@@ -31,15 +36,26 @@ func (u *S3Uploader) Upload(path string) (string, error) { ...@@ -31,15 +36,26 @@ func (u *S3Uploader) Upload(path string) (string, error) {
header.Add("x-amz-acl", "public-read") header.Add("x-amz-acl", "public-read")
header.Add("Content-Type", "image/png") header.Add("Content-Type", "image/png")
fullUrl := u.bucket + util.GetRandomString(20) + ".png" var imageUrl *url.URL
writer, err := s3util.Create(fullUrl, header, nil) var err error
if imageUrl, err = url.Parse(u.bucket); err != nil {
return "", err
}
// add image to url
imageUrl.Path = path.Join(imageUrl.Path, util.GetRandomString(20)+".png")
imageUrlString := imageUrl.String()
log.Debug("Uploading image to s3", "url", imageUrlString)
writer, err := s3util.Create(imageUrlString, header, nil)
if err != nil { if err != nil {
return "", err return "", err
} }
defer writer.Close() defer writer.Close()
imgData, err := ioutil.ReadFile(path) imgData, err := ioutil.ReadFile(imageDiskPath)
if err != nil { if err != nil {
return "", err return "", err
} }
...@@ -49,5 +65,5 @@ func (u *S3Uploader) Upload(path string) (string, error) { ...@@ -49,5 +65,5 @@ func (u *S3Uploader) Upload(path string) (string, error) {
return "", err return "", err
} }
return fullUrl, nil return imageUrlString, nil
} }
...@@ -44,6 +44,10 @@ func newThresholdEvaludator(typ string, model *simplejson.Json) (*ThresholdEvalu ...@@ -44,6 +44,10 @@ func newThresholdEvaludator(typ string, model *simplejson.Json) (*ThresholdEvalu
} }
func (e *ThresholdEvaluator) Eval(reducedValue *float64) bool { func (e *ThresholdEvaluator) Eval(reducedValue *float64) bool {
if reducedValue == nil {
return false
}
switch e.Type { switch e.Type {
case "gt": case "gt":
return *reducedValue > e.Threshold return *reducedValue > e.Threshold
...@@ -83,6 +87,10 @@ func newRangedEvaluator(typ string, model *simplejson.Json) (*RangedEvaluator, e ...@@ -83,6 +87,10 @@ func newRangedEvaluator(typ string, model *simplejson.Json) (*RangedEvaluator, e
} }
func (e *RangedEvaluator) Eval(reducedValue *float64) bool { func (e *RangedEvaluator) Eval(reducedValue *float64) bool {
if reducedValue == nil {
return false
}
switch e.Type { switch e.Type {
case "within_range": case "within_range":
return (e.Lower < *reducedValue && e.Upper > *reducedValue) || (e.Upper < *reducedValue && e.Lower > *reducedValue) return (e.Lower < *reducedValue && e.Upper > *reducedValue) || (e.Upper < *reducedValue && e.Lower > *reducedValue)
......
package conditions package conditions
import "github.com/grafana/grafana/pkg/tsdb" import (
"math"
"github.com/grafana/grafana/pkg/tsdb"
)
type QueryReducer interface { type QueryReducer interface {
Reduce(timeSeries *tsdb.TimeSeries) *float64 Reduce(timeSeries *tsdb.TimeSeries) *float64
...@@ -15,41 +19,53 @@ func (s *SimpleReducer) Reduce(series *tsdb.TimeSeries) *float64 { ...@@ -15,41 +19,53 @@ func (s *SimpleReducer) Reduce(series *tsdb.TimeSeries) *float64 {
return nil return nil
} }
var value float64 = 0 value := float64(0)
allNull := true
switch s.Type { switch s.Type {
case "avg": case "avg":
for _, point := range series.Points { for _, point := range series.Points {
value += point[0] if point[0] != nil {
value += *point[0]
allNull = false
}
} }
value = value / float64(len(series.Points)) value = value / float64(len(series.Points))
case "sum": case "sum":
for _, point := range series.Points { for _, point := range series.Points {
value += point[0] if point[0] != nil {
value += *point[0]
allNull = false
}
} }
case "min": case "min":
for i, point := range series.Points { value = math.MaxFloat64
if i == 0 { for _, point := range series.Points {
value = point[0] if point[0] != nil {
allNull = false
if value > *point[0] {
value = *point[0]
} }
if value > point[0] {
value = point[0]
} }
} }
case "max": case "max":
value = -math.MaxFloat64
for _, point := range series.Points { for _, point := range series.Points {
if value < point[0] { if point[0] != nil {
value = point[0] allNull = false
if value < *point[0] {
value = *point[0]
}
} }
} }
case "mean":
meanPosition := int64(len(series.Points) / 2)
value = series.Points[meanPosition][0]
case "count": case "count":
value = float64(len(series.Points)) value = float64(len(series.Points))
} }
if allNull {
return nil
}
return &value return &value
} }
......
...@@ -37,31 +37,36 @@ type StateDescription struct { ...@@ -37,31 +37,36 @@ type StateDescription struct {
} }
func (c *EvalContext) GetStateModel() *StateDescription { func (c *EvalContext) GetStateModel() *StateDescription {
if c.Error != nil { switch c.Rule.State {
return &StateDescription{ case m.AlertStateOK:
Color: "#D63232",
Text: "EXECUTION ERROR",
}
}
if !c.Firing {
return &StateDescription{ return &StateDescription{
Color: "#36a64f", Color: "#36a64f",
Text: "OK", Text: "OK",
} }
case m.AlertStateUnknown:
return &StateDescription{
Color: "#888888",
Text: "UNKNOWN",
} }
case m.AlertStateExeuctionError:
if c.Rule.Severity == m.AlertSeverityWarning { return &StateDescription{
Color: "#000",
Text: "EXECUTION_ERROR",
}
case m.AlertStateWarning:
return &StateDescription{ return &StateDescription{
Color: "#fd821b", Color: "#fd821b",
Text: "WARNING", Text: "WARNING",
} }
} else { case m.AlertStateCritical:
return &StateDescription{ return &StateDescription{
Color: "#D63232", Color: "#D63232",
Text: "CRITICAL", Text: "CRITICAL",
} }
default:
panic("Unknown rule state " + c.Rule.State)
} }
} }
func (a *EvalContext) GetDurationMs() float64 { func (a *EvalContext) GetDurationMs() float64 {
......
...@@ -54,6 +54,15 @@ func (e *DefaultEvalHandler) retry(context *EvalContext) { ...@@ -54,6 +54,15 @@ func (e *DefaultEvalHandler) retry(context *EvalContext) {
} }
func (e *DefaultEvalHandler) eval(context *EvalContext) { func (e *DefaultEvalHandler) eval(context *EvalContext) {
defer func() {
if err := recover(); err != nil {
e.log.Error("Alerting rule eval panic", "error", err, "stack", log.Stack(1))
if panicErr, ok := err.(error); ok {
context.Error = panicErr
}
}
}()
for _, condition := range context.Rule.Conditions { for _, condition := range context.Rule.Conditions {
condition.Eval(context) condition.Eval(context)
......
...@@ -2,5 +2,5 @@ package graphite ...@@ -2,5 +2,5 @@ package graphite
type TargetResponseDTO struct { type TargetResponseDTO struct {
Target string `json:"target"` Target string `json:"target"`
DataPoints [][2]float64 `json:"datapoints"` DataPoints [][2]*float64 `json:"datapoints"`
} }
...@@ -47,12 +47,12 @@ type QueryResult struct { ...@@ -47,12 +47,12 @@ type QueryResult struct {
type TimeSeries struct { type TimeSeries struct {
Name string `json:"name"` Name string `json:"name"`
Points [][2]float64 `json:"points"` Points [][2]*float64 `json:"points"`
} }
type TimeSeriesSlice []*TimeSeries type TimeSeriesSlice []*TimeSeries
func NewTimeSeries(name string, points [][2]float64) *TimeSeries { func NewTimeSeries(name string, points [][2]*float64) *TimeSeries {
return &TimeSeries{ return &TimeSeries{
Name: name, Name: name,
Points: points, Points: points,
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment