mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge branch 'master' into emails
This commit is contained in:
commit
f26824049f
@ -2,7 +2,7 @@
|
|||||||
root = true
|
root = true
|
||||||
|
|
||||||
[*.go]
|
[*.go]
|
||||||
indent_style = tabs
|
indent_style = tab
|
||||||
indent_size = 2
|
indent_size = 2
|
||||||
charset = utf-8
|
charset = utf-8
|
||||||
trim_trailing_whitespace = true
|
trim_trailing_whitespace = true
|
||||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -25,6 +25,7 @@ public/css/*.min.css
|
|||||||
*.swp
|
*.swp
|
||||||
.idea/
|
.idea/
|
||||||
*.iml
|
*.iml
|
||||||
|
.vscode/
|
||||||
|
|
||||||
/data/*
|
/data/*
|
||||||
/bin/*
|
/bin/*
|
||||||
@ -37,4 +38,4 @@ profile.cov
|
|||||||
.notouch
|
.notouch
|
||||||
/pkg/cmd/grafana-cli/grafana-cli
|
/pkg/cmd/grafana-cli/grafana-cli
|
||||||
/pkg/cmd/grafana-server/grafana-server
|
/pkg/cmd/grafana-server/grafana-server
|
||||||
/examples/*/dist
|
/examples/*/dist
|
||||||
|
@ -9,12 +9,17 @@
|
|||||||
* **Navigation**: Add search to org swithcer, closes [#2609](https://github.com/grafana/grafana/issues/2609)
|
* **Navigation**: Add search to org swithcer, closes [#2609](https://github.com/grafana/grafana/issues/2609)
|
||||||
* **Database**: Allow database config using one propertie, closes [#5456](https://github.com/grafana/grafana/pull/5456)
|
* **Database**: Allow database config using one propertie, closes [#5456](https://github.com/grafana/grafana/pull/5456)
|
||||||
* **Graphite**: Add support for groupByNode, closes [#5613](https://github.com/grafana/grafana/pull/5613)
|
* **Graphite**: Add support for groupByNode, closes [#5613](https://github.com/grafana/grafana/pull/5613)
|
||||||
|
* **Influxdb**: Add support for elapsed(), closes [#5827](https://github.com/grafana/grafana/pull/5827)
|
||||||
|
|
||||||
|
### Breaking changes
|
||||||
|
* **SystemD**: Change systemd description, closes [#5971](https://github.com/grafana/grafana/pull/5971)
|
||||||
|
|
||||||
# 3.1.2 (unreleased)
|
# 3.1.2 (unreleased)
|
||||||
* **Templating**: Fixed issue when combining row & panel repeats, fixes [#5790](https://github.com/grafana/grafana/issues/5790)
|
* **Templating**: Fixed issue when combining row & panel repeats, fixes [#5790](https://github.com/grafana/grafana/issues/5790)
|
||||||
* **Drag&Drop**: Fixed issue with drag and drop in latest Chrome(51+), fixes [#5767](https://github.com/grafana/grafana/issues/5767)
|
* **Drag&Drop**: Fixed issue with drag and drop in latest Chrome(51+), fixes [#5767](https://github.com/grafana/grafana/issues/5767)
|
||||||
* **Internal Metrics**: Fixed issue with dots in instance_name when sending internal metrics to Graphite, fixes [#5739](https://github.com/grafana/grafana/issues/5739)
|
* **Internal Metrics**: Fixed issue with dots in instance_name when sending internal metrics to Graphite, fixes [#5739](https://github.com/grafana/grafana/issues/5739)
|
||||||
* **Grafana-CLI**: Add default plugin path for MAC OS, fixes [#5806](https://github.com/grafana/grafana/issues/5806)
|
* **Grafana-CLI**: Add default plugin path for MAC OS, fixes [#5806](https://github.com/grafana/grafana/issues/5806)
|
||||||
|
* **Grafana-CLI**: Improve error message for upgrade-all command, fixes [#5885](https://github.com/grafana/grafana/issues/5885)
|
||||||
|
|
||||||
# 3.1.1 (2016-08-01)
|
# 3.1.1 (2016-08-01)
|
||||||
* **IFrame embedding**: Fixed issue of using full iframe height, fixes [#5605](https://github.com/grafana/grafana/issues/5606)
|
* **IFrame embedding**: Fixed issue of using full iframe height, fixes [#5605](https://github.com/grafana/grafana/issues/5606)
|
||||||
|
6
Godeps/Godeps.json
generated
6
Godeps/Godeps.json
generated
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"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 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"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",
|
||||||
|
2
Godeps/_workspace/src/gopkg.in/ini.v1/.gitignore
generated
vendored
2
Godeps/_workspace/src/gopkg.in/ini.v1/.gitignore
generated
vendored
@ -1,3 +1,5 @@
|
|||||||
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
|
||||||
|
16
Godeps/_workspace/src/gopkg.in/ini.v1/.travis.yml
generated
vendored
Normal file
16
Godeps/_workspace/src/gopkg.in/ini.v1/.travis.yml
generated
vendored
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
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
|
12
Godeps/_workspace/src/gopkg.in/ini.v1/Makefile
generated
vendored
Normal file
12
Godeps/_workspace/src/gopkg.in/ini.v1/Makefile
generated
vendored
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
.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
|
353
Godeps/_workspace/src/gopkg.in/ini.v1/README.md
generated
vendored
353
Godeps/_workspace/src/gopkg.in/ini.v1/README.md
generated
vendored
@ -1,6 +1,8 @@
|
|||||||
ini [](https://drone.io/github.com/go-ini/ini/latest) [](http://gocover.io/github.com/go-ini/ini)
|
INI [](https://travis-ci.org/go-ini/ini)
|
||||||
===
|
===
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
Package ini provides INI file read and write functionality in Go.
|
Package ini provides INI file read and write functionality in Go.
|
||||||
|
|
||||||
[简体中文](README_ZH.md)
|
[简体中文](README_ZH.md)
|
||||||
@ -20,13 +22,29 @@ Package ini provides INI file read and write functionality in Go.
|
|||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
|
To use a tagged revision:
|
||||||
|
|
||||||
go get gopkg.in/ini.v1
|
go get gopkg.in/ini.v1
|
||||||
|
|
||||||
|
To use with latest changes:
|
||||||
|
|
||||||
|
go get github.com/go-ini/ini
|
||||||
|
|
||||||
|
Please add `-u` flag to update in the future.
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
If you want to test on your machine, please apply `-t` flag:
|
||||||
|
|
||||||
|
go get -t gopkg.in/ini.v1
|
||||||
|
|
||||||
|
Please add `-u` flag to update in the future.
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
### Loading from data sources
|
### Loading from data sources
|
||||||
|
|
||||||
A **Data Source** is either raw data in type `[]byte` or a file name with type `string` and you can load **as many as** data sources you want. Passing other types will simply return an error.
|
A **Data Source** is either raw data in type `[]byte` or a file name with type `string` and you can load **as many data sources as you want**. Passing other types will simply return an error.
|
||||||
|
|
||||||
```go
|
```go
|
||||||
cfg, err := ini.Load([]byte("raw data"), "filename")
|
cfg, err := ini.Load([]byte("raw data"), "filename")
|
||||||
@ -38,12 +56,56 @@ Or start with an empty object:
|
|||||||
cfg := ini.Empty()
|
cfg := ini.Empty()
|
||||||
```
|
```
|
||||||
|
|
||||||
When you cannot decide how many data sources to load at the beginning, you still able to **Append()** them later.
|
When you cannot decide how many data sources to load at the beginning, you will still be able to **Append()** them later.
|
||||||
|
|
||||||
```go
|
```go
|
||||||
err := cfg.Append("other file", []byte("other raw data"))
|
err := cfg.Append("other file", []byte("other raw data"))
|
||||||
```
|
```
|
||||||
|
|
||||||
|
If you have a list of files with possibilities that some of them may not available at the time, and you don't know exactly which ones, you can use `LooseLoad` to ignore nonexistent files without returning error.
|
||||||
|
|
||||||
|
```go
|
||||||
|
cfg, err := ini.LooseLoad("filename", "filename_404")
|
||||||
|
```
|
||||||
|
|
||||||
|
The cool thing is, whenever the file is available to load while you're calling `Reload` method, it will be counted as usual.
|
||||||
|
|
||||||
|
#### Ignore cases of key name
|
||||||
|
|
||||||
|
When you do not care about cases of section and key names, you can use `InsensitiveLoad` to force all names to be lowercased while parsing.
|
||||||
|
|
||||||
|
```go
|
||||||
|
cfg, err := ini.InsensitiveLoad("filename")
|
||||||
|
//...
|
||||||
|
|
||||||
|
// sec1 and sec2 are the exactly same section object
|
||||||
|
sec1, err := cfg.GetSection("Section")
|
||||||
|
sec2, err := cfg.GetSection("SecTIOn")
|
||||||
|
|
||||||
|
// key1 and key2 are the exactly same key object
|
||||||
|
key1, err := cfg.GetKey("Key")
|
||||||
|
key2, err := cfg.GetKey("KeY")
|
||||||
|
```
|
||||||
|
|
||||||
|
#### MySQL-like boolean key
|
||||||
|
|
||||||
|
MySQL's configuration allows a key without value as follows:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[mysqld]
|
||||||
|
...
|
||||||
|
skip-host-cache
|
||||||
|
skip-name-resolve
|
||||||
|
```
|
||||||
|
|
||||||
|
By default, this is considered as missing value. But if you know you're going to deal with those cases, you can assign advanced load options:
|
||||||
|
|
||||||
|
```go
|
||||||
|
cfg, err := LoadSources(LoadOptions{AllowBooleanKeys: true}, "my.cnf"))
|
||||||
|
```
|
||||||
|
|
||||||
|
The value of those keys are always `true`, and when you save to a file, it will keep in the same foramt as you read.
|
||||||
|
|
||||||
### Working with sections
|
### Working with sections
|
||||||
|
|
||||||
To get a section, you would need to:
|
To get a section, you would need to:
|
||||||
@ -93,6 +155,12 @@ Same rule applies to key operations:
|
|||||||
key := cfg.Section("").Key("key name")
|
key := cfg.Section("").Key("key name")
|
||||||
```
|
```
|
||||||
|
|
||||||
|
To check if a key exists:
|
||||||
|
|
||||||
|
```go
|
||||||
|
yes := cfg.Section("").HasKey("key name")
|
||||||
|
```
|
||||||
|
|
||||||
To create a new key:
|
To create a new key:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
@ -102,14 +170,14 @@ err := cfg.Section("").NewKey("name", "value")
|
|||||||
To get a list of keys or key names:
|
To get a list of keys or key names:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
keys := cfg.Section().Keys()
|
keys := cfg.Section("").Keys()
|
||||||
names := cfg.Section().KeyStrings()
|
names := cfg.Section("").KeyStrings()
|
||||||
```
|
```
|
||||||
|
|
||||||
To get a clone hash of keys and corresponding values:
|
To get a clone hash of keys and corresponding values:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
hash := cfg.GetSection("").KeysHash()
|
hash := cfg.Section("").KeysHash()
|
||||||
```
|
```
|
||||||
|
|
||||||
### Working with values
|
### Working with values
|
||||||
@ -120,16 +188,41 @@ To get a string value:
|
|||||||
val := cfg.Section("").Key("key name").String()
|
val := cfg.Section("").Key("key name").String()
|
||||||
```
|
```
|
||||||
|
|
||||||
|
To validate key value on the fly:
|
||||||
|
|
||||||
|
```go
|
||||||
|
val := cfg.Section("").Key("key name").Validate(func(in string) string {
|
||||||
|
if len(in) == 0 {
|
||||||
|
return "default"
|
||||||
|
}
|
||||||
|
return in
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
If you do not want any auto-transformation (such as recursive read) for the values, you can get raw value directly (this way you get much better performance):
|
||||||
|
|
||||||
|
```go
|
||||||
|
val := cfg.Section("").Key("key name").Value()
|
||||||
|
```
|
||||||
|
|
||||||
|
To check if raw value exists:
|
||||||
|
|
||||||
|
```go
|
||||||
|
yes := cfg.Section("").HasValue("test value")
|
||||||
|
```
|
||||||
|
|
||||||
To get value with types:
|
To get value with types:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// For boolean values:
|
// For boolean values:
|
||||||
// true when value is: 1, t, T, TRUE, true, True, YES, yes, Yes, ON, on, On
|
// true when value is: 1, t, T, TRUE, true, True, YES, yes, Yes, y, ON, on, On
|
||||||
// false when value is: 0, f, F, FALSE, false, False, NO, no, No, OFF, off, Off
|
// false when value is: 0, f, F, FALSE, false, False, NO, no, No, n, OFF, off, Off
|
||||||
v, err = cfg.Section("").Key("BOOL").Bool()
|
v, err = cfg.Section("").Key("BOOL").Bool()
|
||||||
v, err = cfg.Section("").Key("FLOAT64").Float64()
|
v, err = cfg.Section("").Key("FLOAT64").Float64()
|
||||||
v, err = cfg.Section("").Key("INT").Int()
|
v, err = cfg.Section("").Key("INT").Int()
|
||||||
v, err = cfg.Section("").Key("INT64").Int64()
|
v, err = cfg.Section("").Key("INT64").Int64()
|
||||||
|
v, err = cfg.Section("").Key("UINT").Uint()
|
||||||
|
v, err = cfg.Section("").Key("UINT64").Uint64()
|
||||||
v, err = cfg.Section("").Key("TIME").TimeFormat(time.RFC3339)
|
v, err = cfg.Section("").Key("TIME").TimeFormat(time.RFC3339)
|
||||||
v, err = cfg.Section("").Key("TIME").Time() // RFC3339
|
v, err = cfg.Section("").Key("TIME").Time() // RFC3339
|
||||||
|
|
||||||
@ -137,6 +230,8 @@ v = cfg.Section("").Key("BOOL").MustBool()
|
|||||||
v = cfg.Section("").Key("FLOAT64").MustFloat64()
|
v = cfg.Section("").Key("FLOAT64").MustFloat64()
|
||||||
v = cfg.Section("").Key("INT").MustInt()
|
v = cfg.Section("").Key("INT").MustInt()
|
||||||
v = cfg.Section("").Key("INT64").MustInt64()
|
v = cfg.Section("").Key("INT64").MustInt64()
|
||||||
|
v = cfg.Section("").Key("UINT").MustUint()
|
||||||
|
v = cfg.Section("").Key("UINT64").MustUint64()
|
||||||
v = cfg.Section("").Key("TIME").MustTimeFormat(time.RFC3339)
|
v = cfg.Section("").Key("TIME").MustTimeFormat(time.RFC3339)
|
||||||
v = cfg.Section("").Key("TIME").MustTime() // RFC3339
|
v = cfg.Section("").Key("TIME").MustTime() // RFC3339
|
||||||
|
|
||||||
@ -144,11 +239,13 @@ v = cfg.Section("").Key("TIME").MustTime() // RFC3339
|
|||||||
// when key not found or fail to parse value to given type.
|
// when key not found or fail to parse value to given type.
|
||||||
// Except method MustString, which you have to pass a default value.
|
// Except method MustString, which you have to pass a default value.
|
||||||
|
|
||||||
v = cfg.Seciont("").Key("String").MustString("default")
|
v = cfg.Section("").Key("String").MustString("default")
|
||||||
v = cfg.Section("").Key("BOOL").MustBool(true)
|
v = cfg.Section("").Key("BOOL").MustBool(true)
|
||||||
v = cfg.Section("").Key("FLOAT64").MustFloat64(1.25)
|
v = cfg.Section("").Key("FLOAT64").MustFloat64(1.25)
|
||||||
v = cfg.Section("").Key("INT").MustInt(10)
|
v = cfg.Section("").Key("INT").MustInt(10)
|
||||||
v = cfg.Section("").Key("INT64").MustInt64(99)
|
v = cfg.Section("").Key("INT64").MustInt64(99)
|
||||||
|
v = cfg.Section("").Key("UINT").MustUint(3)
|
||||||
|
v = cfg.Section("").Key("UINT64").MustUint64(6)
|
||||||
v = cfg.Section("").Key("TIME").MustTimeFormat(time.RFC3339, time.Now())
|
v = cfg.Section("").Key("TIME").MustTimeFormat(time.RFC3339, time.Now())
|
||||||
v = cfg.Section("").Key("TIME").MustTime(time.Now()) // RFC3339
|
v = cfg.Section("").Key("TIME").MustTime(time.Now()) // RFC3339
|
||||||
```
|
```
|
||||||
@ -174,6 +271,42 @@ Earth
|
|||||||
------ end --- */
|
------ end --- */
|
||||||
```
|
```
|
||||||
|
|
||||||
|
That's cool, how about continuation lines?
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[advance]
|
||||||
|
two_lines = how about \
|
||||||
|
continuation lines?
|
||||||
|
lots_of_lines = 1 \
|
||||||
|
2 \
|
||||||
|
3 \
|
||||||
|
4
|
||||||
|
```
|
||||||
|
|
||||||
|
Piece of cake!
|
||||||
|
|
||||||
|
```go
|
||||||
|
cfg.Section("advance").Key("two_lines").String() // how about continuation lines?
|
||||||
|
cfg.Section("advance").Key("lots_of_lines").String() // 1 2 3 4
|
||||||
|
```
|
||||||
|
|
||||||
|
Well, I hate continuation lines, how do I disable that?
|
||||||
|
|
||||||
|
```go
|
||||||
|
cfg, err := ini.LoadSources(ini.LoadOptions{
|
||||||
|
IgnoreContinuation: true,
|
||||||
|
}, "filename")
|
||||||
|
```
|
||||||
|
|
||||||
|
Holy crap!
|
||||||
|
|
||||||
|
Note that single quotes around values will be stripped:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
foo = "some value" // foo: some value
|
||||||
|
bar = 'some value' // bar: some value
|
||||||
|
```
|
||||||
|
|
||||||
That's all? Hmm, no.
|
That's all? Hmm, no.
|
||||||
|
|
||||||
#### Helper methods of working with values
|
#### Helper methods of working with values
|
||||||
@ -185,6 +318,8 @@ v = cfg.Section("").Key("STRING").In("default", []string{"str", "arr", "types"})
|
|||||||
v = cfg.Section("").Key("FLOAT64").InFloat64(1.1, []float64{1.25, 2.5, 3.75})
|
v = cfg.Section("").Key("FLOAT64").InFloat64(1.1, []float64{1.25, 2.5, 3.75})
|
||||||
v = cfg.Section("").Key("INT").InInt(5, []int{10, 20, 30})
|
v = cfg.Section("").Key("INT").InInt(5, []int{10, 20, 30})
|
||||||
v = cfg.Section("").Key("INT64").InInt64(10, []int64{10, 20, 30})
|
v = cfg.Section("").Key("INT64").InInt64(10, []int64{10, 20, 30})
|
||||||
|
v = cfg.Section("").Key("UINT").InUint(4, []int{3, 6, 9})
|
||||||
|
v = cfg.Section("").Key("UINT64").InUint64(8, []int64{3, 6, 9})
|
||||||
v = cfg.Section("").Key("TIME").InTimeFormat(time.RFC3339, time.Now(), []time.Time{time1, time2, time3})
|
v = cfg.Section("").Key("TIME").InTimeFormat(time.RFC3339, time.Now(), []time.Time{time1, time2, time3})
|
||||||
v = cfg.Section("").Key("TIME").InTime(time.Now(), []time.Time{time1, time2, time3}) // RFC3339
|
v = cfg.Section("").Key("TIME").InTime(time.Now(), []time.Time{time1, time2, time3}) // RFC3339
|
||||||
```
|
```
|
||||||
@ -197,20 +332,74 @@ To validate value in a given range:
|
|||||||
vals = cfg.Section("").Key("FLOAT64").RangeFloat64(0.0, 1.1, 2.2)
|
vals = cfg.Section("").Key("FLOAT64").RangeFloat64(0.0, 1.1, 2.2)
|
||||||
vals = cfg.Section("").Key("INT").RangeInt(0, 10, 20)
|
vals = cfg.Section("").Key("INT").RangeInt(0, 10, 20)
|
||||||
vals = cfg.Section("").Key("INT64").RangeInt64(0, 10, 20)
|
vals = cfg.Section("").Key("INT64").RangeInt64(0, 10, 20)
|
||||||
|
vals = cfg.Section("").Key("UINT").RangeUint(0, 3, 9)
|
||||||
|
vals = cfg.Section("").Key("UINT64").RangeUint64(0, 3, 9)
|
||||||
vals = cfg.Section("").Key("TIME").RangeTimeFormat(time.RFC3339, time.Now(), minTime, maxTime)
|
vals = cfg.Section("").Key("TIME").RangeTimeFormat(time.RFC3339, time.Now(), minTime, maxTime)
|
||||||
vals = cfg.Section("").Key("TIME").RangeTime(time.Now(), minTime, maxTime) // RFC3339
|
vals = cfg.Section("").Key("TIME").RangeTime(time.Now(), minTime, maxTime) // RFC3339
|
||||||
```
|
```
|
||||||
|
|
||||||
To auto-split value into slice:
|
##### Auto-split values into a slice
|
||||||
|
|
||||||
|
To use zero value of type for invalid inputs:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
|
// Input: 1.1, 2.2, 3.3, 4.4 -> [1.1 2.2 3.3 4.4]
|
||||||
|
// Input: how, 2.2, are, you -> [0.0 2.2 0.0 0.0]
|
||||||
vals = cfg.Section("").Key("STRINGS").Strings(",")
|
vals = cfg.Section("").Key("STRINGS").Strings(",")
|
||||||
vals = cfg.Section("").Key("FLOAT64S").Float64s(",")
|
vals = cfg.Section("").Key("FLOAT64S").Float64s(",")
|
||||||
vals = cfg.Section("").Key("INTS").Ints(",")
|
vals = cfg.Section("").Key("INTS").Ints(",")
|
||||||
vals = cfg.Section("").Key("INT64S").Int64s(",")
|
vals = cfg.Section("").Key("INT64S").Int64s(",")
|
||||||
|
vals = cfg.Section("").Key("UINTS").Uints(",")
|
||||||
|
vals = cfg.Section("").Key("UINT64S").Uint64s(",")
|
||||||
vals = cfg.Section("").Key("TIMES").Times(",")
|
vals = cfg.Section("").Key("TIMES").Times(",")
|
||||||
```
|
```
|
||||||
|
|
||||||
|
To exclude invalid values out of result slice:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Input: 1.1, 2.2, 3.3, 4.4 -> [1.1 2.2 3.3 4.4]
|
||||||
|
// Input: how, 2.2, are, you -> [2.2]
|
||||||
|
vals = cfg.Section("").Key("FLOAT64S").ValidFloat64s(",")
|
||||||
|
vals = cfg.Section("").Key("INTS").ValidInts(",")
|
||||||
|
vals = cfg.Section("").Key("INT64S").ValidInt64s(",")
|
||||||
|
vals = cfg.Section("").Key("UINTS").ValidUints(",")
|
||||||
|
vals = cfg.Section("").Key("UINT64S").ValidUint64s(",")
|
||||||
|
vals = cfg.Section("").Key("TIMES").ValidTimes(",")
|
||||||
|
```
|
||||||
|
|
||||||
|
Or to return nothing but error when have invalid inputs:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Input: 1.1, 2.2, 3.3, 4.4 -> [1.1 2.2 3.3 4.4]
|
||||||
|
// Input: how, 2.2, are, you -> error
|
||||||
|
vals = cfg.Section("").Key("FLOAT64S").StrictFloat64s(",")
|
||||||
|
vals = cfg.Section("").Key("INTS").StrictInts(",")
|
||||||
|
vals = cfg.Section("").Key("INT64S").StrictInt64s(",")
|
||||||
|
vals = cfg.Section("").Key("UINTS").StrictUints(",")
|
||||||
|
vals = cfg.Section("").Key("UINT64S").StrictUint64s(",")
|
||||||
|
vals = cfg.Section("").Key("TIMES").StrictTimes(",")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Save your configuration
|
||||||
|
|
||||||
|
Finally, it's time to save your configuration to somewhere.
|
||||||
|
|
||||||
|
A typical way to save configuration is writing it to a file:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// ...
|
||||||
|
err = cfg.SaveTo("my.ini")
|
||||||
|
err = cfg.SaveToIndent("my.ini", "\t")
|
||||||
|
```
|
||||||
|
|
||||||
|
Another way to save is writing to a `io.Writer` interface:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// ...
|
||||||
|
cfg.WriteTo(writer)
|
||||||
|
cfg.WriteToIndent(writer, "\t")
|
||||||
|
```
|
||||||
|
|
||||||
## Advanced Usage
|
## Advanced Usage
|
||||||
|
|
||||||
### Recursive Values
|
### Recursive Values
|
||||||
@ -252,6 +441,12 @@ CLONE_URL = https://%(IMPORT_PATH)s
|
|||||||
cfg.Section("package.sub").Key("CLONE_URL").String() // https://gopkg.in/ini.v1
|
cfg.Section("package.sub").Key("CLONE_URL").String() // https://gopkg.in/ini.v1
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Retrieve parent keys available to a child section
|
||||||
|
|
||||||
|
```go
|
||||||
|
cfg.Section("package.sub").ParentKeys() // ["CLONE_URL"]
|
||||||
|
```
|
||||||
|
|
||||||
### Auto-increment Key Names
|
### Auto-increment Key Names
|
||||||
|
|
||||||
If key name is `-` in data source, then it would be seen as special syntax for auto-increment key name start from 1, and every section is independent on counter.
|
If key name is `-` in data source, then it would be seen as special syntax for auto-increment key name start from 1, and every section is independent on counter.
|
||||||
@ -327,9 +522,57 @@ p := &Person{
|
|||||||
// ...
|
// ...
|
||||||
```
|
```
|
||||||
|
|
||||||
|
It's really cool, but what's the point if you can't give me my file back from struct?
|
||||||
|
|
||||||
|
### Reflect From Struct
|
||||||
|
|
||||||
|
Why not?
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Embeded struct {
|
||||||
|
Dates []time.Time `delim:"|"`
|
||||||
|
Places []string `ini:"places,omitempty"`
|
||||||
|
None []int `ini:",omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Author struct {
|
||||||
|
Name string `ini:"NAME"`
|
||||||
|
Male bool
|
||||||
|
Age int
|
||||||
|
GPA float64
|
||||||
|
NeverMind string `ini:"-"`
|
||||||
|
*Embeded
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
a := &Author{"Unknwon", true, 21, 2.8, "",
|
||||||
|
&Embeded{
|
||||||
|
[]time.Time{time.Now(), time.Now()},
|
||||||
|
[]string{"HangZhou", "Boston"},
|
||||||
|
[]int{},
|
||||||
|
}}
|
||||||
|
cfg := ini.Empty()
|
||||||
|
err = ini.ReflectFrom(cfg, a)
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
So, what do I get?
|
||||||
|
|
||||||
|
```ini
|
||||||
|
NAME = Unknwon
|
||||||
|
Male = true
|
||||||
|
Age = 21
|
||||||
|
GPA = 2.8
|
||||||
|
|
||||||
|
[Embeded]
|
||||||
|
Dates = 2015-08-07T22:14:22+08:00|2015-08-07T22:14:22+08:00
|
||||||
|
places = HangZhou,Boston
|
||||||
|
```
|
||||||
|
|
||||||
#### Name Mapper
|
#### Name Mapper
|
||||||
|
|
||||||
To save your time and make your code cleaner, this library supports [`NameMapper`](https://gowalker.org/gopkg.in/ini.v1#NameMapper) between struct field and actual secion and key name.
|
To save your time and make your code cleaner, this library supports [`NameMapper`](https://gowalker.org/gopkg.in/ini.v1#NameMapper) between struct field and actual section and key name.
|
||||||
|
|
||||||
There are 2 built-in name mappers:
|
There are 2 built-in name mappers:
|
||||||
|
|
||||||
@ -339,15 +582,15 @@ There are 2 built-in name mappers:
|
|||||||
To use them:
|
To use them:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
type Info struct{
|
type Info struct {
|
||||||
PackageName string
|
PackageName string
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
err = ini.MapToWithMapper(&Info{}, ini.TitleUnderscore, []byte("packag_name=ini"))
|
err = ini.MapToWithMapper(&Info{}, ini.TitleUnderscore, []byte("package_name=ini"))
|
||||||
// ...
|
// ...
|
||||||
|
|
||||||
cfg, err := ini.Load("PACKAGE_NAME=ini")
|
cfg, err := ini.Load([]byte("PACKAGE_NAME=ini"))
|
||||||
// ...
|
// ...
|
||||||
info := new(Info)
|
info := new(Info)
|
||||||
cfg.NameMapper = ini.AllCapsUnderscore
|
cfg.NameMapper = ini.AllCapsUnderscore
|
||||||
@ -356,6 +599,88 @@ func main() {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Same rules of name mapper apply to `ini.ReflectFromWithMapper` function.
|
||||||
|
|
||||||
|
#### Value Mapper
|
||||||
|
|
||||||
|
To expand values (e.g. from environment variables), you can use the `ValueMapper` to transform values:
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Env struct {
|
||||||
|
Foo string `ini:"foo"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
cfg, err := ini.Load([]byte("[env]\nfoo = ${MY_VAR}\n")
|
||||||
|
cfg.ValueMapper = os.ExpandEnv
|
||||||
|
// ...
|
||||||
|
env := &Env{}
|
||||||
|
err = cfg.Section("env").MapTo(env)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This would set the value of `env.Foo` to the value of the environment variable `MY_VAR`.
|
||||||
|
|
||||||
|
#### Other Notes On Map/Reflect
|
||||||
|
|
||||||
|
Any embedded struct is treated as a section by default, and there is no automatic parent-child relations in map/reflect feature:
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Child struct {
|
||||||
|
Age string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Parent struct {
|
||||||
|
Name string
|
||||||
|
Child
|
||||||
|
}
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
City string
|
||||||
|
Parent
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Example configuration:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
City = Boston
|
||||||
|
|
||||||
|
[Parent]
|
||||||
|
Name = Unknwon
|
||||||
|
|
||||||
|
[Child]
|
||||||
|
Age = 21
|
||||||
|
```
|
||||||
|
|
||||||
|
What if, yes, I'm paranoid, I want embedded struct to be in the same section. Well, all roads lead to Rome.
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Child struct {
|
||||||
|
Age string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Parent struct {
|
||||||
|
Name string
|
||||||
|
Child `ini:"Parent"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
City string
|
||||||
|
Parent
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Example configuration:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
City = Boston
|
||||||
|
|
||||||
|
[Parent]
|
||||||
|
Name = Unknwon
|
||||||
|
Age = 21
|
||||||
|
```
|
||||||
|
|
||||||
## Getting Help
|
## Getting Help
|
||||||
|
|
||||||
- [API Documentation](https://gowalker.org/gopkg.in/ini.v1)
|
- [API Documentation](https://gowalker.org/gopkg.in/ini.v1)
|
||||||
|
339
Godeps/_workspace/src/gopkg.in/ini.v1/README_ZH.md
generated
vendored
339
Godeps/_workspace/src/gopkg.in/ini.v1/README_ZH.md
generated
vendored
@ -15,8 +15,24 @@
|
|||||||
|
|
||||||
## 下载安装
|
## 下载安装
|
||||||
|
|
||||||
|
使用一个特定版本:
|
||||||
|
|
||||||
go get gopkg.in/ini.v1
|
go get gopkg.in/ini.v1
|
||||||
|
|
||||||
|
使用最新版:
|
||||||
|
|
||||||
|
go get github.com/go-ini/ini
|
||||||
|
|
||||||
|
如需更新请添加 `-u` 选项。
|
||||||
|
|
||||||
|
### 测试安装
|
||||||
|
|
||||||
|
如果您想要在自己的机器上运行测试,请使用 `-t` 标记:
|
||||||
|
|
||||||
|
go get -t gopkg.in/ini.v1
|
||||||
|
|
||||||
|
如需更新请添加 `-u` 选项。
|
||||||
|
|
||||||
## 开始使用
|
## 开始使用
|
||||||
|
|
||||||
### 从数据源加载
|
### 从数据源加载
|
||||||
@ -39,6 +55,50 @@ cfg := ini.Empty()
|
|||||||
err := cfg.Append("other file", []byte("other raw data"))
|
err := cfg.Append("other file", []byte("other raw data"))
|
||||||
```
|
```
|
||||||
|
|
||||||
|
当您想要加载一系列文件,但是不能够确定其中哪些文件是不存在的,可以通过调用函数 `LooseLoad` 来忽略它们(`Load` 会因为文件不存在而返回错误):
|
||||||
|
|
||||||
|
```go
|
||||||
|
cfg, err := ini.LooseLoad("filename", "filename_404")
|
||||||
|
```
|
||||||
|
|
||||||
|
更牛逼的是,当那些之前不存在的文件在重新调用 `Reload` 方法的时候突然出现了,那么它们会被正常加载。
|
||||||
|
|
||||||
|
#### 忽略键名的大小写
|
||||||
|
|
||||||
|
有时候分区和键的名称大小写混合非常烦人,这个时候就可以通过 `InsensitiveLoad` 将所有分区和键名在读取里强制转换为小写:
|
||||||
|
|
||||||
|
```go
|
||||||
|
cfg, err := ini.InsensitiveLoad("filename")
|
||||||
|
//...
|
||||||
|
|
||||||
|
// sec1 和 sec2 指向同一个分区对象
|
||||||
|
sec1, err := cfg.GetSection("Section")
|
||||||
|
sec2, err := cfg.GetSection("SecTIOn")
|
||||||
|
|
||||||
|
// key1 和 key2 指向同一个键对象
|
||||||
|
key1, err := cfg.GetKey("Key")
|
||||||
|
key2, err := cfg.GetKey("KeY")
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 类似 MySQL 配置中的布尔值键
|
||||||
|
|
||||||
|
MySQL 的配置文件中会出现没有具体值的布尔类型的键:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[mysqld]
|
||||||
|
...
|
||||||
|
skip-host-cache
|
||||||
|
skip-name-resolve
|
||||||
|
```
|
||||||
|
|
||||||
|
默认情况下这被认为是缺失值而无法完成解析,但可以通过高级的加载选项对它们进行处理:
|
||||||
|
|
||||||
|
```go
|
||||||
|
cfg, err := LoadSources(LoadOptions{AllowBooleanKeys: true}, "my.cnf"))
|
||||||
|
```
|
||||||
|
|
||||||
|
这些键的值永远为 `true`,且在保存到文件时也只会输出键名。
|
||||||
|
|
||||||
### 操作分区(Section)
|
### 操作分区(Section)
|
||||||
|
|
||||||
获取指定分区:
|
获取指定分区:
|
||||||
@ -88,6 +148,12 @@ key, err := cfg.Section("").GetKey("key name")
|
|||||||
key := cfg.Section("").Key("key name")
|
key := cfg.Section("").Key("key name")
|
||||||
```
|
```
|
||||||
|
|
||||||
|
判断某个键是否存在:
|
||||||
|
|
||||||
|
```go
|
||||||
|
yes := cfg.Section("").HasKey("key name")
|
||||||
|
```
|
||||||
|
|
||||||
创建一个新的键:
|
创建一个新的键:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
@ -97,14 +163,14 @@ err := cfg.Section("").NewKey("name", "value")
|
|||||||
获取分区下的所有键或键名:
|
获取分区下的所有键或键名:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
keys := cfg.Section().Keys()
|
keys := cfg.Section("").Keys()
|
||||||
names := cfg.Section().KeyStrings()
|
names := cfg.Section("").KeyStrings()
|
||||||
```
|
```
|
||||||
|
|
||||||
获取分区下的所有键值对的克隆:
|
获取分区下的所有键值对的克隆:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
hash := cfg.GetSection("").KeysHash()
|
hash := cfg.Section("").KeysHash()
|
||||||
```
|
```
|
||||||
|
|
||||||
### 操作键值(Value)
|
### 操作键值(Value)
|
||||||
@ -115,16 +181,41 @@ hash := cfg.GetSection("").KeysHash()
|
|||||||
val := cfg.Section("").Key("key name").String()
|
val := cfg.Section("").Key("key name").String()
|
||||||
```
|
```
|
||||||
|
|
||||||
|
获取值的同时通过自定义函数进行处理验证:
|
||||||
|
|
||||||
|
```go
|
||||||
|
val := cfg.Section("").Key("key name").Validate(func(in string) string {
|
||||||
|
if len(in) == 0 {
|
||||||
|
return "default"
|
||||||
|
}
|
||||||
|
return in
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
如果您不需要任何对值的自动转变功能(例如递归读取),可以直接获取原值(这种方式性能最佳):
|
||||||
|
|
||||||
|
```go
|
||||||
|
val := cfg.Section("").Key("key name").Value()
|
||||||
|
```
|
||||||
|
|
||||||
|
判断某个原值是否存在:
|
||||||
|
|
||||||
|
```go
|
||||||
|
yes := cfg.Section("").HasValue("test value")
|
||||||
|
```
|
||||||
|
|
||||||
获取其它类型的值:
|
获取其它类型的值:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// 布尔值的规则:
|
// 布尔值的规则:
|
||||||
// true 当值为:1, t, T, TRUE, true, True, YES, yes, Yes, ON, on, On
|
// true 当值为:1, t, T, TRUE, true, True, YES, yes, Yes, y, ON, on, On
|
||||||
// false 当值为:0, f, F, FALSE, false, False, NO, no, No, OFF, off, Off
|
// false 当值为:0, f, F, FALSE, false, False, NO, no, No, n, OFF, off, Off
|
||||||
v, err = cfg.Section("").Key("BOOL").Bool()
|
v, err = cfg.Section("").Key("BOOL").Bool()
|
||||||
v, err = cfg.Section("").Key("FLOAT64").Float64()
|
v, err = cfg.Section("").Key("FLOAT64").Float64()
|
||||||
v, err = cfg.Section("").Key("INT").Int()
|
v, err = cfg.Section("").Key("INT").Int()
|
||||||
v, err = cfg.Section("").Key("INT64").Int64()
|
v, err = cfg.Section("").Key("INT64").Int64()
|
||||||
|
v, err = cfg.Section("").Key("UINT").Uint()
|
||||||
|
v, err = cfg.Section("").Key("UINT64").Uint64()
|
||||||
v, err = cfg.Section("").Key("TIME").TimeFormat(time.RFC3339)
|
v, err = cfg.Section("").Key("TIME").TimeFormat(time.RFC3339)
|
||||||
v, err = cfg.Section("").Key("TIME").Time() // RFC3339
|
v, err = cfg.Section("").Key("TIME").Time() // RFC3339
|
||||||
|
|
||||||
@ -132,6 +223,8 @@ v = cfg.Section("").Key("BOOL").MustBool()
|
|||||||
v = cfg.Section("").Key("FLOAT64").MustFloat64()
|
v = cfg.Section("").Key("FLOAT64").MustFloat64()
|
||||||
v = cfg.Section("").Key("INT").MustInt()
|
v = cfg.Section("").Key("INT").MustInt()
|
||||||
v = cfg.Section("").Key("INT64").MustInt64()
|
v = cfg.Section("").Key("INT64").MustInt64()
|
||||||
|
v = cfg.Section("").Key("UINT").MustUint()
|
||||||
|
v = cfg.Section("").Key("UINT64").MustUint64()
|
||||||
v = cfg.Section("").Key("TIME").MustTimeFormat(time.RFC3339)
|
v = cfg.Section("").Key("TIME").MustTimeFormat(time.RFC3339)
|
||||||
v = cfg.Section("").Key("TIME").MustTime() // RFC3339
|
v = cfg.Section("").Key("TIME").MustTime() // RFC3339
|
||||||
|
|
||||||
@ -144,6 +237,8 @@ v = cfg.Section("").Key("BOOL").MustBool(true)
|
|||||||
v = cfg.Section("").Key("FLOAT64").MustFloat64(1.25)
|
v = cfg.Section("").Key("FLOAT64").MustFloat64(1.25)
|
||||||
v = cfg.Section("").Key("INT").MustInt(10)
|
v = cfg.Section("").Key("INT").MustInt(10)
|
||||||
v = cfg.Section("").Key("INT64").MustInt64(99)
|
v = cfg.Section("").Key("INT64").MustInt64(99)
|
||||||
|
v = cfg.Section("").Key("UINT").MustUint(3)
|
||||||
|
v = cfg.Section("").Key("UINT64").MustUint64(6)
|
||||||
v = cfg.Section("").Key("TIME").MustTimeFormat(time.RFC3339, time.Now())
|
v = cfg.Section("").Key("TIME").MustTimeFormat(time.RFC3339, time.Now())
|
||||||
v = cfg.Section("").Key("TIME").MustTime(time.Now()) // RFC3339
|
v = cfg.Section("").Key("TIME").MustTime(time.Now()) // RFC3339
|
||||||
```
|
```
|
||||||
@ -169,6 +264,42 @@ Earth
|
|||||||
------ end --- */
|
------ end --- */
|
||||||
```
|
```
|
||||||
|
|
||||||
|
赞爆了!那要是我属于一行的内容写不下想要写到第二行怎么办?
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[advance]
|
||||||
|
two_lines = how about \
|
||||||
|
continuation lines?
|
||||||
|
lots_of_lines = 1 \
|
||||||
|
2 \
|
||||||
|
3 \
|
||||||
|
4
|
||||||
|
```
|
||||||
|
|
||||||
|
简直是小菜一碟!
|
||||||
|
|
||||||
|
```go
|
||||||
|
cfg.Section("advance").Key("two_lines").String() // how about continuation lines?
|
||||||
|
cfg.Section("advance").Key("lots_of_lines").String() // 1 2 3 4
|
||||||
|
```
|
||||||
|
|
||||||
|
可是我有时候觉得两行连在一起特别没劲,怎么才能不自动连接两行呢?
|
||||||
|
|
||||||
|
```go
|
||||||
|
cfg, err := ini.LoadSources(ini.LoadOptions{
|
||||||
|
IgnoreContinuation: true,
|
||||||
|
}, "filename")
|
||||||
|
```
|
||||||
|
|
||||||
|
哇靠给力啊!
|
||||||
|
|
||||||
|
需要注意的是,值两侧的单引号会被自动剔除:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
foo = "some value" // foo: some value
|
||||||
|
bar = 'some value' // bar: some value
|
||||||
|
```
|
||||||
|
|
||||||
这就是全部了?哈哈,当然不是。
|
这就是全部了?哈哈,当然不是。
|
||||||
|
|
||||||
#### 操作键值的辅助方法
|
#### 操作键值的辅助方法
|
||||||
@ -180,6 +311,8 @@ v = cfg.Section("").Key("STRING").In("default", []string{"str", "arr", "types"})
|
|||||||
v = cfg.Section("").Key("FLOAT64").InFloat64(1.1, []float64{1.25, 2.5, 3.75})
|
v = cfg.Section("").Key("FLOAT64").InFloat64(1.1, []float64{1.25, 2.5, 3.75})
|
||||||
v = cfg.Section("").Key("INT").InInt(5, []int{10, 20, 30})
|
v = cfg.Section("").Key("INT").InInt(5, []int{10, 20, 30})
|
||||||
v = cfg.Section("").Key("INT64").InInt64(10, []int64{10, 20, 30})
|
v = cfg.Section("").Key("INT64").InInt64(10, []int64{10, 20, 30})
|
||||||
|
v = cfg.Section("").Key("UINT").InUint(4, []int{3, 6, 9})
|
||||||
|
v = cfg.Section("").Key("UINT64").InUint64(8, []int64{3, 6, 9})
|
||||||
v = cfg.Section("").Key("TIME").InTimeFormat(time.RFC3339, time.Now(), []time.Time{time1, time2, time3})
|
v = cfg.Section("").Key("TIME").InTimeFormat(time.RFC3339, time.Now(), []time.Time{time1, time2, time3})
|
||||||
v = cfg.Section("").Key("TIME").InTime(time.Now(), []time.Time{time1, time2, time3}) // RFC3339
|
v = cfg.Section("").Key("TIME").InTime(time.Now(), []time.Time{time1, time2, time3}) // RFC3339
|
||||||
```
|
```
|
||||||
@ -192,20 +325,74 @@ v = cfg.Section("").Key("TIME").InTime(time.Now(), []time.Time{time1, time2, tim
|
|||||||
vals = cfg.Section("").Key("FLOAT64").RangeFloat64(0.0, 1.1, 2.2)
|
vals = cfg.Section("").Key("FLOAT64").RangeFloat64(0.0, 1.1, 2.2)
|
||||||
vals = cfg.Section("").Key("INT").RangeInt(0, 10, 20)
|
vals = cfg.Section("").Key("INT").RangeInt(0, 10, 20)
|
||||||
vals = cfg.Section("").Key("INT64").RangeInt64(0, 10, 20)
|
vals = cfg.Section("").Key("INT64").RangeInt64(0, 10, 20)
|
||||||
|
vals = cfg.Section("").Key("UINT").RangeUint(0, 3, 9)
|
||||||
|
vals = cfg.Section("").Key("UINT64").RangeUint64(0, 3, 9)
|
||||||
vals = cfg.Section("").Key("TIME").RangeTimeFormat(time.RFC3339, time.Now(), minTime, maxTime)
|
vals = cfg.Section("").Key("TIME").RangeTimeFormat(time.RFC3339, time.Now(), minTime, maxTime)
|
||||||
vals = cfg.Section("").Key("TIME").RangeTime(time.Now(), minTime, maxTime) // RFC3339
|
vals = cfg.Section("").Key("TIME").RangeTime(time.Now(), minTime, maxTime) // RFC3339
|
||||||
```
|
```
|
||||||
|
|
||||||
自动分割键值为切片(slice):
|
##### 自动分割键值到切片(slice)
|
||||||
|
|
||||||
|
当存在无效输入时,使用零值代替:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
|
// Input: 1.1, 2.2, 3.3, 4.4 -> [1.1 2.2 3.3 4.4]
|
||||||
|
// Input: how, 2.2, are, you -> [0.0 2.2 0.0 0.0]
|
||||||
vals = cfg.Section("").Key("STRINGS").Strings(",")
|
vals = cfg.Section("").Key("STRINGS").Strings(",")
|
||||||
vals = cfg.Section("").Key("FLOAT64S").Float64s(",")
|
vals = cfg.Section("").Key("FLOAT64S").Float64s(",")
|
||||||
vals = cfg.Section("").Key("INTS").Ints(",")
|
vals = cfg.Section("").Key("INTS").Ints(",")
|
||||||
vals = cfg.Section("").Key("INT64S").Int64s(",")
|
vals = cfg.Section("").Key("INT64S").Int64s(",")
|
||||||
|
vals = cfg.Section("").Key("UINTS").Uints(",")
|
||||||
|
vals = cfg.Section("").Key("UINT64S").Uint64s(",")
|
||||||
vals = cfg.Section("").Key("TIMES").Times(",")
|
vals = cfg.Section("").Key("TIMES").Times(",")
|
||||||
```
|
```
|
||||||
|
|
||||||
|
从结果切片中剔除无效输入:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Input: 1.1, 2.2, 3.3, 4.4 -> [1.1 2.2 3.3 4.4]
|
||||||
|
// Input: how, 2.2, are, you -> [2.2]
|
||||||
|
vals = cfg.Section("").Key("FLOAT64S").ValidFloat64s(",")
|
||||||
|
vals = cfg.Section("").Key("INTS").ValidInts(",")
|
||||||
|
vals = cfg.Section("").Key("INT64S").ValidInt64s(",")
|
||||||
|
vals = cfg.Section("").Key("UINTS").ValidUints(",")
|
||||||
|
vals = cfg.Section("").Key("UINT64S").ValidUint64s(",")
|
||||||
|
vals = cfg.Section("").Key("TIMES").ValidTimes(",")
|
||||||
|
```
|
||||||
|
|
||||||
|
当存在无效输入时,直接返回错误:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Input: 1.1, 2.2, 3.3, 4.4 -> [1.1 2.2 3.3 4.4]
|
||||||
|
// Input: how, 2.2, are, you -> error
|
||||||
|
vals = cfg.Section("").Key("FLOAT64S").StrictFloat64s(",")
|
||||||
|
vals = cfg.Section("").Key("INTS").StrictInts(",")
|
||||||
|
vals = cfg.Section("").Key("INT64S").StrictInt64s(",")
|
||||||
|
vals = cfg.Section("").Key("UINTS").StrictUints(",")
|
||||||
|
vals = cfg.Section("").Key("UINT64S").StrictUint64s(",")
|
||||||
|
vals = cfg.Section("").Key("TIMES").StrictTimes(",")
|
||||||
|
```
|
||||||
|
|
||||||
|
### 保存配置
|
||||||
|
|
||||||
|
终于到了这个时刻,是时候保存一下配置了。
|
||||||
|
|
||||||
|
比较原始的做法是输出配置到某个文件:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// ...
|
||||||
|
err = cfg.SaveTo("my.ini")
|
||||||
|
err = cfg.SaveToIndent("my.ini", "\t")
|
||||||
|
```
|
||||||
|
|
||||||
|
另一个比较高级的做法是写入到任何实现 `io.Writer` 接口的对象中:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// ...
|
||||||
|
cfg.WriteTo(writer)
|
||||||
|
cfg.WriteToIndent(writer, "\t")
|
||||||
|
```
|
||||||
|
|
||||||
### 高级用法
|
### 高级用法
|
||||||
|
|
||||||
#### 递归读取键值
|
#### 递归读取键值
|
||||||
@ -247,6 +434,12 @@ CLONE_URL = https://%(IMPORT_PATH)s
|
|||||||
cfg.Section("package.sub").Key("CLONE_URL").String() // https://gopkg.in/ini.v1
|
cfg.Section("package.sub").Key("CLONE_URL").String() // https://gopkg.in/ini.v1
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### 获取上级父分区下的所有键名
|
||||||
|
|
||||||
|
```go
|
||||||
|
cfg.Section("package.sub").ParentKeys() // ["CLONE_URL"]
|
||||||
|
```
|
||||||
|
|
||||||
#### 读取自增键名
|
#### 读取自增键名
|
||||||
|
|
||||||
如果数据源中的键名为 `-`,则认为该键使用了自增键名的特殊语法。计数器从 1 开始,并且分区之间是相互独立的。
|
如果数据源中的键名为 `-`,则认为该键使用了自增键名的特殊语法。计数器从 1 开始,并且分区之间是相互独立的。
|
||||||
@ -320,6 +513,54 @@ p := &Person{
|
|||||||
// ...
|
// ...
|
||||||
```
|
```
|
||||||
|
|
||||||
|
这样玩 INI 真的好酷啊!然而,如果不能还给我原来的配置文件,有什么卵用?
|
||||||
|
|
||||||
|
### 从结构反射
|
||||||
|
|
||||||
|
可是,我有说不能吗?
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Embeded struct {
|
||||||
|
Dates []time.Time `delim:"|"`
|
||||||
|
Places []string `ini:"places,omitempty"`
|
||||||
|
None []int `ini:",omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Author struct {
|
||||||
|
Name string `ini:"NAME"`
|
||||||
|
Male bool
|
||||||
|
Age int
|
||||||
|
GPA float64
|
||||||
|
NeverMind string `ini:"-"`
|
||||||
|
*Embeded
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
a := &Author{"Unknwon", true, 21, 2.8, "",
|
||||||
|
&Embeded{
|
||||||
|
[]time.Time{time.Now(), time.Now()},
|
||||||
|
[]string{"HangZhou", "Boston"},
|
||||||
|
[]int{},
|
||||||
|
}}
|
||||||
|
cfg := ini.Empty()
|
||||||
|
err = ini.ReflectFrom(cfg, a)
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
瞧瞧,奇迹发生了。
|
||||||
|
|
||||||
|
```ini
|
||||||
|
NAME = Unknwon
|
||||||
|
Male = true
|
||||||
|
Age = 21
|
||||||
|
GPA = 2.8
|
||||||
|
|
||||||
|
[Embeded]
|
||||||
|
Dates = 2015-08-07T22:14:22+08:00|2015-08-07T22:14:22+08:00
|
||||||
|
places = HangZhou,Boston
|
||||||
|
```
|
||||||
|
|
||||||
#### 名称映射器(Name Mapper)
|
#### 名称映射器(Name Mapper)
|
||||||
|
|
||||||
为了节省您的时间并简化代码,本库支持类型为 [`NameMapper`](https://gowalker.org/gopkg.in/ini.v1#NameMapper) 的名称映射器,该映射器负责结构字段名与分区名和键名之间的映射。
|
为了节省您的时间并简化代码,本库支持类型为 [`NameMapper`](https://gowalker.org/gopkg.in/ini.v1#NameMapper) 的名称映射器,该映射器负责结构字段名与分区名和键名之间的映射。
|
||||||
@ -337,10 +578,10 @@ type Info struct{
|
|||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
err = ini.MapToWithMapper(&Info{}, ini.TitleUnderscore, []byte("packag_name=ini"))
|
err = ini.MapToWithMapper(&Info{}, ini.TitleUnderscore, []byte("package_name=ini"))
|
||||||
// ...
|
// ...
|
||||||
|
|
||||||
cfg, err := ini.Load("PACKAGE_NAME=ini")
|
cfg, err := ini.Load([]byte("PACKAGE_NAME=ini"))
|
||||||
// ...
|
// ...
|
||||||
info := new(Info)
|
info := new(Info)
|
||||||
cfg.NameMapper = ini.AllCapsUnderscore
|
cfg.NameMapper = ini.AllCapsUnderscore
|
||||||
@ -349,6 +590,88 @@ func main() {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
使用函数 `ini.ReflectFromWithMapper` 时也可应用相同的规则。
|
||||||
|
|
||||||
|
#### 值映射器(Value Mapper)
|
||||||
|
|
||||||
|
值映射器允许使用一个自定义函数自动展开值的具体内容,例如:运行时获取环境变量:
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Env struct {
|
||||||
|
Foo string `ini:"foo"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
cfg, err := ini.Load([]byte("[env]\nfoo = ${MY_VAR}\n")
|
||||||
|
cfg.ValueMapper = os.ExpandEnv
|
||||||
|
// ...
|
||||||
|
env := &Env{}
|
||||||
|
err = cfg.Section("env").MapTo(env)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
本例中,`env.Foo` 将会是运行时所获取到环境变量 `MY_VAR` 的值。
|
||||||
|
|
||||||
|
#### 映射/反射的其它说明
|
||||||
|
|
||||||
|
任何嵌入的结构都会被默认认作一个不同的分区,并且不会自动产生所谓的父子分区关联:
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Child struct {
|
||||||
|
Age string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Parent struct {
|
||||||
|
Name string
|
||||||
|
Child
|
||||||
|
}
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
City string
|
||||||
|
Parent
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
示例配置文件:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
City = Boston
|
||||||
|
|
||||||
|
[Parent]
|
||||||
|
Name = Unknwon
|
||||||
|
|
||||||
|
[Child]
|
||||||
|
Age = 21
|
||||||
|
```
|
||||||
|
|
||||||
|
很好,但是,我就是要嵌入结构也在同一个分区。好吧,你爹是李刚!
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Child struct {
|
||||||
|
Age string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Parent struct {
|
||||||
|
Name string
|
||||||
|
Child `ini:"Parent"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
City string
|
||||||
|
Parent
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
示例配置文件:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
City = Boston
|
||||||
|
|
||||||
|
[Parent]
|
||||||
|
Name = Unknwon
|
||||||
|
Age = 21
|
||||||
|
```
|
||||||
|
|
||||||
## 获取帮助
|
## 获取帮助
|
||||||
|
|
||||||
- [API 文档](https://gowalker.org/gopkg.in/ini.v1)
|
- [API 文档](https://gowalker.org/gopkg.in/ini.v1)
|
||||||
|
32
Godeps/_workspace/src/gopkg.in/ini.v1/error.go
generated
vendored
Normal file
32
Godeps/_workspace/src/gopkg.in/ini.v1/error.go
generated
vendored
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
// 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)
|
||||||
|
}
|
878
Godeps/_workspace/src/gopkg.in/ini.v1/ini.go
generated
vendored
878
Godeps/_workspace/src/gopkg.in/ini.v1/ini.go
generated
vendored
File diff suppressed because it is too large
Load Diff
456
Godeps/_workspace/src/gopkg.in/ini.v1/ini_test.go
generated
vendored
456
Godeps/_workspace/src/gopkg.in/ini.v1/ini_test.go
generated
vendored
@ -1,456 +0,0 @@
|
|||||||
// 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 (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
. "github.com/smartystreets/goconvey/convey"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Test_Version(t *testing.T) {
|
|
||||||
Convey("Get version", t, func() {
|
|
||||||
So(Version(), ShouldEqual, _VERSION)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const _CONF_DATA = `
|
|
||||||
; Package name
|
|
||||||
NAME = ini
|
|
||||||
; Package version
|
|
||||||
VERSION = v1
|
|
||||||
; Package import path
|
|
||||||
IMPORT_PATH = gopkg.in/%(NAME)s.%(VERSION)s
|
|
||||||
|
|
||||||
# Information about package author
|
|
||||||
# Bio can be written in multiple lines.
|
|
||||||
[author]
|
|
||||||
NAME = Unknwon # Succeeding comment
|
|
||||||
E-MAIL = fake@localhost
|
|
||||||
GITHUB = https://github.com/%(NAME)s
|
|
||||||
BIO = """Gopher.
|
|
||||||
Coding addict.
|
|
||||||
Good man.
|
|
||||||
""" # Succeeding comment
|
|
||||||
|
|
||||||
[package]
|
|
||||||
CLONE_URL = https://%(IMPORT_PATH)s
|
|
||||||
|
|
||||||
[package.sub]
|
|
||||||
UNUSED_KEY = should be deleted
|
|
||||||
|
|
||||||
[features]
|
|
||||||
-: Support read/write comments of keys and sections
|
|
||||||
-: Support auto-increment of key names
|
|
||||||
-: Support load multiple files to overwrite key values
|
|
||||||
|
|
||||||
[types]
|
|
||||||
STRING = str
|
|
||||||
BOOL = true
|
|
||||||
BOOL_FALSE = false
|
|
||||||
FLOAT64 = 1.25
|
|
||||||
INT = 10
|
|
||||||
TIME = 2015-01-01T20:17:05Z
|
|
||||||
|
|
||||||
[array]
|
|
||||||
STRINGS = en, zh, de
|
|
||||||
FLOAT64S = 1.1, 2.2, 3.3
|
|
||||||
INTS = 1, 2, 3
|
|
||||||
TIMES = 2015-01-01T20:17:05Z,2015-01-01T20:17:05Z,2015-01-01T20:17:05Z
|
|
||||||
|
|
||||||
[note]
|
|
||||||
|
|
||||||
[advance]
|
|
||||||
true = """"2+3=5""""
|
|
||||||
"1+1=2" = true
|
|
||||||
"""6+1=7""" = true
|
|
||||||
"""` + "`" + `5+5` + "`" + `""" = 10
|
|
||||||
""""6+6"""" = 12
|
|
||||||
` + "`" + `7-2=4` + "`" + ` = false
|
|
||||||
ADDRESS = ` + "`" + `404 road,
|
|
||||||
NotFound, State, 50000` + "`"
|
|
||||||
|
|
||||||
func Test_Load(t *testing.T) {
|
|
||||||
Convey("Load from data sources", t, func() {
|
|
||||||
|
|
||||||
Convey("Load with empty data", func() {
|
|
||||||
So(Empty(), ShouldNotBeNil)
|
|
||||||
})
|
|
||||||
|
|
||||||
Convey("Load with multiple data sources", func() {
|
|
||||||
cfg, err := Load([]byte(_CONF_DATA), "testdata/conf.ini")
|
|
||||||
So(err, ShouldBeNil)
|
|
||||||
So(cfg, ShouldNotBeNil)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
Convey("Bad load process", t, func() {
|
|
||||||
|
|
||||||
Convey("Load from invalid data sources", func() {
|
|
||||||
_, err := Load(_CONF_DATA)
|
|
||||||
So(err, ShouldNotBeNil)
|
|
||||||
|
|
||||||
_, err = Load("testdata/404.ini")
|
|
||||||
So(err, ShouldNotBeNil)
|
|
||||||
|
|
||||||
_, err = Load(1)
|
|
||||||
So(err, ShouldNotBeNil)
|
|
||||||
|
|
||||||
_, err = Load([]byte(""), 1)
|
|
||||||
So(err, ShouldNotBeNil)
|
|
||||||
})
|
|
||||||
|
|
||||||
Convey("Load with empty section name", func() {
|
|
||||||
_, err := Load([]byte("[]"))
|
|
||||||
So(err, ShouldNotBeNil)
|
|
||||||
})
|
|
||||||
|
|
||||||
Convey("Load with bad keys", func() {
|
|
||||||
_, err := Load([]byte(`"""name`))
|
|
||||||
So(err, ShouldNotBeNil)
|
|
||||||
|
|
||||||
_, err = Load([]byte(`"""name"""`))
|
|
||||||
So(err, ShouldNotBeNil)
|
|
||||||
|
|
||||||
_, err = Load([]byte(`""=1`))
|
|
||||||
So(err, ShouldNotBeNil)
|
|
||||||
|
|
||||||
_, err = Load([]byte(`=`))
|
|
||||||
So(err, ShouldNotBeNil)
|
|
||||||
|
|
||||||
_, err = Load([]byte(`name`))
|
|
||||||
So(err, ShouldNotBeNil)
|
|
||||||
})
|
|
||||||
|
|
||||||
Convey("Load with bad values", func() {
|
|
||||||
_, err := Load([]byte(`name="""Unknwon`))
|
|
||||||
So(err, ShouldNotBeNil)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_Values(t *testing.T) {
|
|
||||||
Convey("Test getting and setting values", t, func() {
|
|
||||||
cfg, err := Load([]byte(_CONF_DATA), "testdata/conf.ini")
|
|
||||||
So(err, ShouldBeNil)
|
|
||||||
So(cfg, ShouldNotBeNil)
|
|
||||||
|
|
||||||
Convey("Get values in default section", func() {
|
|
||||||
sec := cfg.Section("")
|
|
||||||
So(sec, ShouldNotBeNil)
|
|
||||||
So(sec.Key("NAME").Value(), ShouldEqual, "ini")
|
|
||||||
So(sec.Key("NAME").String(), ShouldEqual, "ini")
|
|
||||||
So(sec.Key("NAME").Comment, ShouldEqual, "; Package name")
|
|
||||||
So(sec.Key("IMPORT_PATH").String(), ShouldEqual, "gopkg.in/ini.v1")
|
|
||||||
})
|
|
||||||
|
|
||||||
Convey("Get values in non-default section", func() {
|
|
||||||
sec := cfg.Section("author")
|
|
||||||
So(sec, ShouldNotBeNil)
|
|
||||||
So(sec.Key("NAME").String(), ShouldEqual, "Unknwon")
|
|
||||||
So(sec.Key("GITHUB").String(), ShouldEqual, "https://github.com/Unknwon")
|
|
||||||
|
|
||||||
sec = cfg.Section("package")
|
|
||||||
So(sec, ShouldNotBeNil)
|
|
||||||
So(sec.Key("CLONE_URL").String(), ShouldEqual, "https://gopkg.in/ini.v1")
|
|
||||||
})
|
|
||||||
|
|
||||||
Convey("Get auto-increment key names", func() {
|
|
||||||
keys := cfg.Section("features").Keys()
|
|
||||||
for i, k := range keys {
|
|
||||||
So(k.Name(), ShouldEqual, fmt.Sprintf("#%d", i+1))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
Convey("Get overwrite value", func() {
|
|
||||||
So(cfg.Section("author").Key("E-MAIL").String(), ShouldEqual, "u@gogs.io")
|
|
||||||
})
|
|
||||||
|
|
||||||
Convey("Get sections", func() {
|
|
||||||
sections := cfg.Sections()
|
|
||||||
for i, name := range []string{DEFAULT_SECTION, "author", "package", "package.sub", "features", "types", "array", "note", "advance"} {
|
|
||||||
So(sections[i].Name(), ShouldEqual, name)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
Convey("Get parent section value", func() {
|
|
||||||
So(cfg.Section("package.sub").Key("CLONE_URL").String(), ShouldEqual, "https://gopkg.in/ini.v1")
|
|
||||||
})
|
|
||||||
|
|
||||||
Convey("Get multiple line value", func() {
|
|
||||||
So(cfg.Section("author").Key("BIO").String(), ShouldEqual, "Gopher.\nCoding addict.\nGood man.\n")
|
|
||||||
})
|
|
||||||
|
|
||||||
Convey("Get values with type", func() {
|
|
||||||
sec := cfg.Section("types")
|
|
||||||
v1, err := sec.Key("BOOL").Bool()
|
|
||||||
So(err, ShouldBeNil)
|
|
||||||
So(v1, ShouldBeTrue)
|
|
||||||
|
|
||||||
v1, err = sec.Key("BOOL_FALSE").Bool()
|
|
||||||
So(err, ShouldBeNil)
|
|
||||||
So(v1, ShouldBeFalse)
|
|
||||||
|
|
||||||
v2, err := sec.Key("FLOAT64").Float64()
|
|
||||||
So(err, ShouldBeNil)
|
|
||||||
So(v2, ShouldEqual, 1.25)
|
|
||||||
|
|
||||||
v3, err := sec.Key("INT").Int()
|
|
||||||
So(err, ShouldBeNil)
|
|
||||||
So(v3, ShouldEqual, 10)
|
|
||||||
|
|
||||||
v4, err := sec.Key("INT").Int64()
|
|
||||||
So(err, ShouldBeNil)
|
|
||||||
So(v4, ShouldEqual, 10)
|
|
||||||
|
|
||||||
t, err := time.Parse(time.RFC3339, "2015-01-01T20:17:05Z")
|
|
||||||
So(err, ShouldBeNil)
|
|
||||||
v5, err := sec.Key("TIME").Time()
|
|
||||||
So(err, ShouldBeNil)
|
|
||||||
So(v5.String(), ShouldEqual, t.String())
|
|
||||||
|
|
||||||
Convey("Must get values with type", func() {
|
|
||||||
So(sec.Key("STRING").MustString("404"), ShouldEqual, "str")
|
|
||||||
So(sec.Key("BOOL").MustBool(), ShouldBeTrue)
|
|
||||||
So(sec.Key("FLOAT64").MustFloat64(), ShouldEqual, 1.25)
|
|
||||||
So(sec.Key("INT").MustInt(), ShouldEqual, 10)
|
|
||||||
So(sec.Key("INT").MustInt64(), ShouldEqual, 10)
|
|
||||||
So(sec.Key("TIME").MustTime().String(), ShouldEqual, t.String())
|
|
||||||
|
|
||||||
Convey("Must get values with default value", func() {
|
|
||||||
So(sec.Key("STRING_404").MustString("404"), ShouldEqual, "404")
|
|
||||||
So(sec.Key("BOOL_404").MustBool(true), ShouldBeTrue)
|
|
||||||
So(sec.Key("FLOAT64_404").MustFloat64(2.5), ShouldEqual, 2.5)
|
|
||||||
So(sec.Key("INT_404").MustInt(15), ShouldEqual, 15)
|
|
||||||
So(sec.Key("INT_404").MustInt64(15), ShouldEqual, 15)
|
|
||||||
|
|
||||||
t, err := time.Parse(time.RFC3339, "2014-01-01T20:17:05Z")
|
|
||||||
So(err, ShouldBeNil)
|
|
||||||
So(sec.Key("TIME_404").MustTime(t).String(), ShouldEqual, t.String())
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
Convey("Get value with candidates", func() {
|
|
||||||
sec := cfg.Section("types")
|
|
||||||
So(sec.Key("STRING").In("", []string{"str", "arr", "types"}), ShouldEqual, "str")
|
|
||||||
So(sec.Key("FLOAT64").InFloat64(0, []float64{1.25, 2.5, 3.75}), ShouldEqual, 1.25)
|
|
||||||
So(sec.Key("INT").InInt(0, []int{10, 20, 30}), ShouldEqual, 10)
|
|
||||||
So(sec.Key("INT").InInt64(0, []int64{10, 20, 30}), ShouldEqual, 10)
|
|
||||||
|
|
||||||
zt, err := time.Parse(time.RFC3339, "0001-01-01T01:00:00Z")
|
|
||||||
So(err, ShouldBeNil)
|
|
||||||
t, err := time.Parse(time.RFC3339, "2015-01-01T20:17:05Z")
|
|
||||||
So(err, ShouldBeNil)
|
|
||||||
So(sec.Key("TIME").InTime(zt, []time.Time{t, time.Now(), time.Now().Add(1 * time.Second)}).String(), ShouldEqual, t.String())
|
|
||||||
|
|
||||||
Convey("Get value with candidates and default value", func() {
|
|
||||||
So(sec.Key("STRING_404").In("str", []string{"str", "arr", "types"}), ShouldEqual, "str")
|
|
||||||
So(sec.Key("FLOAT64_404").InFloat64(1.25, []float64{1.25, 2.5, 3.75}), ShouldEqual, 1.25)
|
|
||||||
So(sec.Key("INT_404").InInt(10, []int{10, 20, 30}), ShouldEqual, 10)
|
|
||||||
So(sec.Key("INT64_404").InInt64(10, []int64{10, 20, 30}), ShouldEqual, 10)
|
|
||||||
So(sec.Key("TIME_404").InTime(t, []time.Time{time.Now(), time.Now(), time.Now().Add(1 * time.Second)}).String(), ShouldEqual, t.String())
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
Convey("Get values in range", func() {
|
|
||||||
sec := cfg.Section("types")
|
|
||||||
So(sec.Key("FLOAT64").RangeFloat64(0, 1, 2), ShouldEqual, 1.25)
|
|
||||||
So(sec.Key("INT").RangeInt(0, 10, 20), ShouldEqual, 10)
|
|
||||||
So(sec.Key("INT").RangeInt64(0, 10, 20), ShouldEqual, 10)
|
|
||||||
|
|
||||||
minT, err := time.Parse(time.RFC3339, "0001-01-01T01:00:00Z")
|
|
||||||
So(err, ShouldBeNil)
|
|
||||||
midT, err := time.Parse(time.RFC3339, "2013-01-01T01:00:00Z")
|
|
||||||
So(err, ShouldBeNil)
|
|
||||||
maxT, err := time.Parse(time.RFC3339, "9999-01-01T01:00:00Z")
|
|
||||||
So(err, ShouldBeNil)
|
|
||||||
t, err := time.Parse(time.RFC3339, "2015-01-01T20:17:05Z")
|
|
||||||
So(err, ShouldBeNil)
|
|
||||||
So(sec.Key("TIME").RangeTime(t, minT, maxT).String(), ShouldEqual, t.String())
|
|
||||||
|
|
||||||
Convey("Get value in range with default value", func() {
|
|
||||||
So(sec.Key("FLOAT64").RangeFloat64(5, 0, 1), ShouldEqual, 5)
|
|
||||||
So(sec.Key("INT").RangeInt(7, 0, 5), ShouldEqual, 7)
|
|
||||||
So(sec.Key("INT").RangeInt64(7, 0, 5), ShouldEqual, 7)
|
|
||||||
So(sec.Key("TIME").RangeTime(t, minT, midT).String(), ShouldEqual, t.String())
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
Convey("Get values into slice", func() {
|
|
||||||
sec := cfg.Section("array")
|
|
||||||
So(strings.Join(sec.Key("STRINGS").Strings(","), ","), ShouldEqual, "en,zh,de")
|
|
||||||
So(len(sec.Key("STRINGS_404").Strings(",")), ShouldEqual, 0)
|
|
||||||
|
|
||||||
vals1 := sec.Key("FLOAT64S").Float64s(",")
|
|
||||||
for i, v := range []float64{1.1, 2.2, 3.3} {
|
|
||||||
So(vals1[i], ShouldEqual, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
vals2 := sec.Key("INTS").Ints(",")
|
|
||||||
for i, v := range []int{1, 2, 3} {
|
|
||||||
So(vals2[i], ShouldEqual, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
vals3 := sec.Key("INTS").Int64s(",")
|
|
||||||
for i, v := range []int64{1, 2, 3} {
|
|
||||||
So(vals3[i], ShouldEqual, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
t, err := time.Parse(time.RFC3339, "2015-01-01T20:17:05Z")
|
|
||||||
So(err, ShouldBeNil)
|
|
||||||
vals4 := sec.Key("TIMES").Times(",")
|
|
||||||
for i, v := range []time.Time{t, t, t} {
|
|
||||||
So(vals4[i].String(), ShouldEqual, v.String())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
Convey("Get key hash", func() {
|
|
||||||
cfg.Section("").KeysHash()
|
|
||||||
})
|
|
||||||
|
|
||||||
Convey("Set key value", func() {
|
|
||||||
k := cfg.Section("author").Key("NAME")
|
|
||||||
k.SetValue("无闻")
|
|
||||||
So(k.String(), ShouldEqual, "无闻")
|
|
||||||
})
|
|
||||||
|
|
||||||
Convey("Get key strings", func() {
|
|
||||||
So(strings.Join(cfg.Section("types").KeyStrings(), ","), ShouldEqual, "STRING,BOOL,BOOL_FALSE,FLOAT64,INT,TIME")
|
|
||||||
})
|
|
||||||
|
|
||||||
Convey("Delete a key", func() {
|
|
||||||
cfg.Section("package.sub").DeleteKey("UNUSED_KEY")
|
|
||||||
_, err := cfg.Section("package.sub").GetKey("UNUSED_KEY")
|
|
||||||
So(err, ShouldNotBeNil)
|
|
||||||
})
|
|
||||||
|
|
||||||
Convey("Get section strings", func() {
|
|
||||||
So(strings.Join(cfg.SectionStrings(), ","), ShouldEqual, "DEFAULT,author,package,package.sub,features,types,array,note,advance")
|
|
||||||
})
|
|
||||||
|
|
||||||
Convey("Delete a section", func() {
|
|
||||||
cfg.DeleteSection("")
|
|
||||||
So(cfg.SectionStrings()[0], ShouldNotEqual, DEFAULT_SECTION)
|
|
||||||
})
|
|
||||||
|
|
||||||
Convey("Create new sections", func() {
|
|
||||||
cfg.NewSections("test", "test2")
|
|
||||||
_, err := cfg.GetSection("test")
|
|
||||||
So(err, ShouldBeNil)
|
|
||||||
_, err = cfg.GetSection("test2")
|
|
||||||
So(err, ShouldBeNil)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
Convey("Test getting and setting bad values", t, func() {
|
|
||||||
cfg, err := Load([]byte(_CONF_DATA), "testdata/conf.ini")
|
|
||||||
So(err, ShouldBeNil)
|
|
||||||
So(cfg, ShouldNotBeNil)
|
|
||||||
|
|
||||||
Convey("Create new key with empty name", func() {
|
|
||||||
k, err := cfg.Section("").NewKey("", "")
|
|
||||||
So(err, ShouldNotBeNil)
|
|
||||||
So(k, ShouldBeNil)
|
|
||||||
})
|
|
||||||
|
|
||||||
Convey("Create new section with empty name", func() {
|
|
||||||
s, err := cfg.NewSection("")
|
|
||||||
So(err, ShouldNotBeNil)
|
|
||||||
So(s, ShouldBeNil)
|
|
||||||
})
|
|
||||||
|
|
||||||
Convey("Create new sections with empty name", func() {
|
|
||||||
So(cfg.NewSections(""), ShouldNotBeNil)
|
|
||||||
})
|
|
||||||
|
|
||||||
Convey("Get section that not exists", func() {
|
|
||||||
s, err := cfg.GetSection("404")
|
|
||||||
So(err, ShouldNotBeNil)
|
|
||||||
So(s, ShouldBeNil)
|
|
||||||
|
|
||||||
s = cfg.Section("404")
|
|
||||||
So(s, ShouldNotBeNil)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_File_Append(t *testing.T) {
|
|
||||||
Convey("Append data sources", t, func() {
|
|
||||||
cfg, err := Load([]byte(""))
|
|
||||||
So(err, ShouldBeNil)
|
|
||||||
So(cfg, ShouldNotBeNil)
|
|
||||||
|
|
||||||
So(cfg.Append([]byte(""), []byte("")), ShouldBeNil)
|
|
||||||
|
|
||||||
Convey("Append bad data sources", func() {
|
|
||||||
So(cfg.Append(1), ShouldNotBeNil)
|
|
||||||
So(cfg.Append([]byte(""), 1), ShouldNotBeNil)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_File_SaveTo(t *testing.T) {
|
|
||||||
Convey("Save file", t, func() {
|
|
||||||
cfg, err := Load([]byte(_CONF_DATA), "testdata/conf.ini")
|
|
||||||
So(err, ShouldBeNil)
|
|
||||||
So(cfg, ShouldNotBeNil)
|
|
||||||
|
|
||||||
cfg.Section("").Key("NAME").Comment = "Package name"
|
|
||||||
cfg.Section("author").Comment = `Information about package author
|
|
||||||
# Bio can be written in multiple lines.`
|
|
||||||
So(cfg.SaveTo("testdata/conf_out.ini"), ShouldBeNil)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func Benchmark_Key_Value(b *testing.B) {
|
|
||||||
c, _ := Load([]byte(_CONF_DATA))
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
c.Section("").Key("NAME").Value()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Benchmark_Key_String(b *testing.B) {
|
|
||||||
c, _ := Load([]byte(_CONF_DATA))
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
c.Section("").Key("NAME").String()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Benchmark_Key_Value_NonBlock(b *testing.B) {
|
|
||||||
c, _ := Load([]byte(_CONF_DATA))
|
|
||||||
c.BlockMode = false
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
c.Section("").Key("NAME").Value()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Benchmark_Key_String_NonBlock(b *testing.B) {
|
|
||||||
c, _ := Load([]byte(_CONF_DATA))
|
|
||||||
c.BlockMode = false
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
c.Section("").Key("NAME").String()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Benchmark_Key_SetValue(b *testing.B) {
|
|
||||||
c, _ := Load([]byte(_CONF_DATA))
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
c.Section("").Key("NAME").SetValue("10")
|
|
||||||
}
|
|
||||||
}
|
|
633
Godeps/_workspace/src/gopkg.in/ini.v1/key.go
generated
vendored
Normal file
633
Godeps/_workspace/src/gopkg.in/ini.v1/key.go
generated
vendored
Normal file
@ -0,0 +1,633 @@
|
|||||||
|
// 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 (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Key represents a key under a section.
|
||||||
|
type Key struct {
|
||||||
|
s *Section
|
||||||
|
name string
|
||||||
|
value string
|
||||||
|
isAutoIncrement bool
|
||||||
|
isBooleanType bool
|
||||||
|
|
||||||
|
Comment string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValueMapper represents a mapping function for values, e.g. os.ExpandEnv
|
||||||
|
type ValueMapper func(string) string
|
||||||
|
|
||||||
|
// Name returns name of key.
|
||||||
|
func (k *Key) Name() string {
|
||||||
|
return k.name
|
||||||
|
}
|
||||||
|
|
||||||
|
// Value returns raw value of key for performance purpose.
|
||||||
|
func (k *Key) Value() string {
|
||||||
|
return k.value
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns string representation of value.
|
||||||
|
func (k *Key) String() string {
|
||||||
|
val := k.value
|
||||||
|
if k.s.f.ValueMapper != nil {
|
||||||
|
val = k.s.f.ValueMapper(val)
|
||||||
|
}
|
||||||
|
if strings.Index(val, "%") == -1 {
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < _DEPTH_VALUES; i++ {
|
||||||
|
vr := varPattern.FindString(val)
|
||||||
|
if len(vr) == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// Take off leading '%(' and trailing ')s'.
|
||||||
|
noption := strings.TrimLeft(vr, "%(")
|
||||||
|
noption = strings.TrimRight(noption, ")s")
|
||||||
|
|
||||||
|
// Search in the same section.
|
||||||
|
nk, err := k.s.GetKey(noption)
|
||||||
|
if err != nil {
|
||||||
|
// Search again in default section.
|
||||||
|
nk, _ = k.s.f.Section("").GetKey(noption)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Substitute by new value and take off leading '%(' and trailing ')s'.
|
||||||
|
val = strings.Replace(val, vr, nk.value, -1)
|
||||||
|
}
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate accepts a validate function which can
|
||||||
|
// return modifed result as key value.
|
||||||
|
func (k *Key) Validate(fn func(string) string) string {
|
||||||
|
return fn(k.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseBool returns the boolean value represented by the string.
|
||||||
|
//
|
||||||
|
// It accepts 1, t, T, TRUE, true, True, YES, yes, Yes, y, ON, on, On,
|
||||||
|
// 0, f, F, FALSE, false, False, NO, no, No, n, OFF, off, Off.
|
||||||
|
// Any other value returns an error.
|
||||||
|
func parseBool(str string) (value bool, err error) {
|
||||||
|
switch str {
|
||||||
|
case "1", "t", "T", "true", "TRUE", "True", "YES", "yes", "Yes", "y", "ON", "on", "On":
|
||||||
|
return true, nil
|
||||||
|
case "0", "f", "F", "false", "FALSE", "False", "NO", "no", "No", "n", "OFF", "off", "Off":
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
return false, fmt.Errorf("parsing \"%s\": invalid syntax", str)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bool returns bool type value.
|
||||||
|
func (k *Key) Bool() (bool, error) {
|
||||||
|
return parseBool(k.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Float64 returns float64 type value.
|
||||||
|
func (k *Key) Float64() (float64, error) {
|
||||||
|
return strconv.ParseFloat(k.String(), 64)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Int returns int type value.
|
||||||
|
func (k *Key) Int() (int, error) {
|
||||||
|
return strconv.Atoi(k.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Int64 returns int64 type value.
|
||||||
|
func (k *Key) Int64() (int64, error) {
|
||||||
|
return strconv.ParseInt(k.String(), 10, 64)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Uint returns uint type valued.
|
||||||
|
func (k *Key) Uint() (uint, error) {
|
||||||
|
u, e := strconv.ParseUint(k.String(), 10, 64)
|
||||||
|
return uint(u), e
|
||||||
|
}
|
||||||
|
|
||||||
|
// Uint64 returns uint64 type value.
|
||||||
|
func (k *Key) Uint64() (uint64, error) {
|
||||||
|
return strconv.ParseUint(k.String(), 10, 64)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Duration returns time.Duration type value.
|
||||||
|
func (k *Key) Duration() (time.Duration, error) {
|
||||||
|
return time.ParseDuration(k.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// TimeFormat parses with given format and returns time.Time type value.
|
||||||
|
func (k *Key) TimeFormat(format string) (time.Time, error) {
|
||||||
|
return time.Parse(format, k.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Time parses with RFC3339 format and returns time.Time type value.
|
||||||
|
func (k *Key) Time() (time.Time, error) {
|
||||||
|
return k.TimeFormat(time.RFC3339)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustString returns default value if key value is empty.
|
||||||
|
func (k *Key) MustString(defaultVal string) string {
|
||||||
|
val := k.String()
|
||||||
|
if len(val) == 0 {
|
||||||
|
k.value = defaultVal
|
||||||
|
return defaultVal
|
||||||
|
}
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustBool always returns value without error,
|
||||||
|
// it returns false if error occurs.
|
||||||
|
func (k *Key) MustBool(defaultVal ...bool) bool {
|
||||||
|
val, err := k.Bool()
|
||||||
|
if len(defaultVal) > 0 && err != nil {
|
||||||
|
k.value = strconv.FormatBool(defaultVal[0])
|
||||||
|
return defaultVal[0]
|
||||||
|
}
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustFloat64 always returns value without error,
|
||||||
|
// it returns 0.0 if error occurs.
|
||||||
|
func (k *Key) MustFloat64(defaultVal ...float64) float64 {
|
||||||
|
val, err := k.Float64()
|
||||||
|
if len(defaultVal) > 0 && err != nil {
|
||||||
|
k.value = strconv.FormatFloat(defaultVal[0], 'f', -1, 64)
|
||||||
|
return defaultVal[0]
|
||||||
|
}
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustInt always returns value without error,
|
||||||
|
// it returns 0 if error occurs.
|
||||||
|
func (k *Key) MustInt(defaultVal ...int) int {
|
||||||
|
val, err := k.Int()
|
||||||
|
if len(defaultVal) > 0 && err != nil {
|
||||||
|
k.value = strconv.FormatInt(int64(defaultVal[0]), 10)
|
||||||
|
return defaultVal[0]
|
||||||
|
}
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustInt64 always returns value without error,
|
||||||
|
// it returns 0 if error occurs.
|
||||||
|
func (k *Key) MustInt64(defaultVal ...int64) int64 {
|
||||||
|
val, err := k.Int64()
|
||||||
|
if len(defaultVal) > 0 && err != nil {
|
||||||
|
k.value = strconv.FormatInt(defaultVal[0], 10)
|
||||||
|
return defaultVal[0]
|
||||||
|
}
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustUint always returns value without error,
|
||||||
|
// it returns 0 if error occurs.
|
||||||
|
func (k *Key) MustUint(defaultVal ...uint) uint {
|
||||||
|
val, err := k.Uint()
|
||||||
|
if len(defaultVal) > 0 && err != nil {
|
||||||
|
k.value = strconv.FormatUint(uint64(defaultVal[0]), 10)
|
||||||
|
return defaultVal[0]
|
||||||
|
}
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustUint64 always returns value without error,
|
||||||
|
// it returns 0 if error occurs.
|
||||||
|
func (k *Key) MustUint64(defaultVal ...uint64) uint64 {
|
||||||
|
val, err := k.Uint64()
|
||||||
|
if len(defaultVal) > 0 && err != nil {
|
||||||
|
k.value = strconv.FormatUint(defaultVal[0], 10)
|
||||||
|
return defaultVal[0]
|
||||||
|
}
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustDuration always returns value without error,
|
||||||
|
// it returns zero value if error occurs.
|
||||||
|
func (k *Key) MustDuration(defaultVal ...time.Duration) time.Duration {
|
||||||
|
val, err := k.Duration()
|
||||||
|
if len(defaultVal) > 0 && err != nil {
|
||||||
|
k.value = defaultVal[0].String()
|
||||||
|
return defaultVal[0]
|
||||||
|
}
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustTimeFormat always parses with given format and returns value without error,
|
||||||
|
// it returns zero value if error occurs.
|
||||||
|
func (k *Key) MustTimeFormat(format string, defaultVal ...time.Time) time.Time {
|
||||||
|
val, err := k.TimeFormat(format)
|
||||||
|
if len(defaultVal) > 0 && err != nil {
|
||||||
|
k.value = defaultVal[0].Format(format)
|
||||||
|
return defaultVal[0]
|
||||||
|
}
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustTime always parses with RFC3339 format and returns value without error,
|
||||||
|
// it returns zero value if error occurs.
|
||||||
|
func (k *Key) MustTime(defaultVal ...time.Time) time.Time {
|
||||||
|
return k.MustTimeFormat(time.RFC3339, defaultVal...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// In always returns value without error,
|
||||||
|
// it returns default value if error occurs or doesn't fit into candidates.
|
||||||
|
func (k *Key) In(defaultVal string, candidates []string) string {
|
||||||
|
val := k.String()
|
||||||
|
for _, cand := range candidates {
|
||||||
|
if val == cand {
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return defaultVal
|
||||||
|
}
|
||||||
|
|
||||||
|
// InFloat64 always returns value without error,
|
||||||
|
// it returns default value if error occurs or doesn't fit into candidates.
|
||||||
|
func (k *Key) InFloat64(defaultVal float64, candidates []float64) float64 {
|
||||||
|
val := k.MustFloat64()
|
||||||
|
for _, cand := range candidates {
|
||||||
|
if val == cand {
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return defaultVal
|
||||||
|
}
|
||||||
|
|
||||||
|
// InInt always returns value without error,
|
||||||
|
// it returns default value if error occurs or doesn't fit into candidates.
|
||||||
|
func (k *Key) InInt(defaultVal int, candidates []int) int {
|
||||||
|
val := k.MustInt()
|
||||||
|
for _, cand := range candidates {
|
||||||
|
if val == cand {
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return defaultVal
|
||||||
|
}
|
||||||
|
|
||||||
|
// InInt64 always returns value without error,
|
||||||
|
// it returns default value if error occurs or doesn't fit into candidates.
|
||||||
|
func (k *Key) InInt64(defaultVal int64, candidates []int64) int64 {
|
||||||
|
val := k.MustInt64()
|
||||||
|
for _, cand := range candidates {
|
||||||
|
if val == cand {
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return defaultVal
|
||||||
|
}
|
||||||
|
|
||||||
|
// InUint always returns value without error,
|
||||||
|
// it returns default value if error occurs or doesn't fit into candidates.
|
||||||
|
func (k *Key) InUint(defaultVal uint, candidates []uint) uint {
|
||||||
|
val := k.MustUint()
|
||||||
|
for _, cand := range candidates {
|
||||||
|
if val == cand {
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return defaultVal
|
||||||
|
}
|
||||||
|
|
||||||
|
// InUint64 always returns value without error,
|
||||||
|
// it returns default value if error occurs or doesn't fit into candidates.
|
||||||
|
func (k *Key) InUint64(defaultVal uint64, candidates []uint64) uint64 {
|
||||||
|
val := k.MustUint64()
|
||||||
|
for _, cand := range candidates {
|
||||||
|
if val == cand {
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return defaultVal
|
||||||
|
}
|
||||||
|
|
||||||
|
// InTimeFormat always parses with given format and returns value without error,
|
||||||
|
// it returns default value if error occurs or doesn't fit into candidates.
|
||||||
|
func (k *Key) InTimeFormat(format string, defaultVal time.Time, candidates []time.Time) time.Time {
|
||||||
|
val := k.MustTimeFormat(format)
|
||||||
|
for _, cand := range candidates {
|
||||||
|
if val == cand {
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return defaultVal
|
||||||
|
}
|
||||||
|
|
||||||
|
// InTime always parses with RFC3339 format and returns value without error,
|
||||||
|
// it returns default value if error occurs or doesn't fit into candidates.
|
||||||
|
func (k *Key) InTime(defaultVal time.Time, candidates []time.Time) time.Time {
|
||||||
|
return k.InTimeFormat(time.RFC3339, defaultVal, candidates)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RangeFloat64 checks if value is in given range inclusively,
|
||||||
|
// and returns default value if it's not.
|
||||||
|
func (k *Key) RangeFloat64(defaultVal, min, max float64) float64 {
|
||||||
|
val := k.MustFloat64()
|
||||||
|
if val < min || val > max {
|
||||||
|
return defaultVal
|
||||||
|
}
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
|
||||||
|
// RangeInt checks if value is in given range inclusively,
|
||||||
|
// and returns default value if it's not.
|
||||||
|
func (k *Key) RangeInt(defaultVal, min, max int) int {
|
||||||
|
val := k.MustInt()
|
||||||
|
if val < min || val > max {
|
||||||
|
return defaultVal
|
||||||
|
}
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
|
||||||
|
// RangeInt64 checks if value is in given range inclusively,
|
||||||
|
// and returns default value if it's not.
|
||||||
|
func (k *Key) RangeInt64(defaultVal, min, max int64) int64 {
|
||||||
|
val := k.MustInt64()
|
||||||
|
if val < min || val > max {
|
||||||
|
return defaultVal
|
||||||
|
}
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
|
||||||
|
// RangeTimeFormat checks if value with given format is in given range inclusively,
|
||||||
|
// and returns default value if it's not.
|
||||||
|
func (k *Key) RangeTimeFormat(format string, defaultVal, min, max time.Time) time.Time {
|
||||||
|
val := k.MustTimeFormat(format)
|
||||||
|
if val.Unix() < min.Unix() || val.Unix() > max.Unix() {
|
||||||
|
return defaultVal
|
||||||
|
}
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
|
||||||
|
// RangeTime checks if value with RFC3339 format is in given range inclusively,
|
||||||
|
// and returns default value if it's not.
|
||||||
|
func (k *Key) RangeTime(defaultVal, min, max time.Time) time.Time {
|
||||||
|
return k.RangeTimeFormat(time.RFC3339, defaultVal, min, max)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strings returns list of string divided by given delimiter.
|
||||||
|
func (k *Key) Strings(delim string) []string {
|
||||||
|
str := k.String()
|
||||||
|
if len(str) == 0 {
|
||||||
|
return []string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
vals := strings.Split(str, delim)
|
||||||
|
for i := range vals {
|
||||||
|
vals[i] = strings.TrimSpace(vals[i])
|
||||||
|
}
|
||||||
|
return vals
|
||||||
|
}
|
||||||
|
|
||||||
|
// Float64s returns list of float64 divided by given delimiter. Any invalid input will be treated as zero value.
|
||||||
|
func (k *Key) Float64s(delim string) []float64 {
|
||||||
|
vals, _ := k.getFloat64s(delim, true, false)
|
||||||
|
return vals
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ints returns list of int divided by given delimiter. Any invalid input will be treated as zero value.
|
||||||
|
func (k *Key) Ints(delim string) []int {
|
||||||
|
vals, _ := k.getInts(delim, true, false)
|
||||||
|
return vals
|
||||||
|
}
|
||||||
|
|
||||||
|
// Int64s returns list of int64 divided by given delimiter. Any invalid input will be treated as zero value.
|
||||||
|
func (k *Key) Int64s(delim string) []int64 {
|
||||||
|
vals, _ := k.getInt64s(delim, true, false)
|
||||||
|
return vals
|
||||||
|
}
|
||||||
|
|
||||||
|
// Uints returns list of uint divided by given delimiter. Any invalid input will be treated as zero value.
|
||||||
|
func (k *Key) Uints(delim string) []uint {
|
||||||
|
vals, _ := k.getUints(delim, true, false)
|
||||||
|
return vals
|
||||||
|
}
|
||||||
|
|
||||||
|
// Uint64s returns list of uint64 divided by given delimiter. Any invalid input will be treated as zero value.
|
||||||
|
func (k *Key) Uint64s(delim string) []uint64 {
|
||||||
|
vals, _ := k.getUint64s(delim, true, false)
|
||||||
|
return vals
|
||||||
|
}
|
||||||
|
|
||||||
|
// TimesFormat parses with given format and returns list of time.Time divided by given delimiter.
|
||||||
|
// Any invalid input will be treated as zero value (0001-01-01 00:00:00 +0000 UTC).
|
||||||
|
func (k *Key) TimesFormat(format, delim string) []time.Time {
|
||||||
|
vals, _ := k.getTimesFormat(format, delim, true, false)
|
||||||
|
return vals
|
||||||
|
}
|
||||||
|
|
||||||
|
// Times parses with RFC3339 format and returns list of time.Time divided by given delimiter.
|
||||||
|
// Any invalid input will be treated as zero value (0001-01-01 00:00:00 +0000 UTC).
|
||||||
|
func (k *Key) Times(delim string) []time.Time {
|
||||||
|
return k.TimesFormat(time.RFC3339, delim)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidFloat64s returns list of float64 divided by given delimiter. If some value is not float, then
|
||||||
|
// it will not be included to result list.
|
||||||
|
func (k *Key) ValidFloat64s(delim string) []float64 {
|
||||||
|
vals, _ := k.getFloat64s(delim, false, false)
|
||||||
|
return vals
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidInts returns list of int divided by given delimiter. If some value is not integer, then it will
|
||||||
|
// not be included to result list.
|
||||||
|
func (k *Key) ValidInts(delim string) []int {
|
||||||
|
vals, _ := k.getInts(delim, false, false)
|
||||||
|
return vals
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidInt64s returns list of int64 divided by given delimiter. If some value is not 64-bit integer,
|
||||||
|
// then it will not be included to result list.
|
||||||
|
func (k *Key) ValidInt64s(delim string) []int64 {
|
||||||
|
vals, _ := k.getInt64s(delim, false, false)
|
||||||
|
return vals
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidUints returns list of uint divided by given delimiter. If some value is not unsigned integer,
|
||||||
|
// then it will not be included to result list.
|
||||||
|
func (k *Key) ValidUints(delim string) []uint {
|
||||||
|
vals, _ := k.getUints(delim, false, false)
|
||||||
|
return vals
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidUint64s returns list of uint64 divided by given delimiter. If some value is not 64-bit unsigned
|
||||||
|
// integer, then it will not be included to result list.
|
||||||
|
func (k *Key) ValidUint64s(delim string) []uint64 {
|
||||||
|
vals, _ := k.getUint64s(delim, false, false)
|
||||||
|
return vals
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidTimesFormat parses with given format and returns list of time.Time divided by given delimiter.
|
||||||
|
func (k *Key) ValidTimesFormat(format, delim string) []time.Time {
|
||||||
|
vals, _ := k.getTimesFormat(format, delim, false, false)
|
||||||
|
return vals
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidTimes parses with RFC3339 format and returns list of time.Time divided by given delimiter.
|
||||||
|
func (k *Key) ValidTimes(delim string) []time.Time {
|
||||||
|
return k.ValidTimesFormat(time.RFC3339, delim)
|
||||||
|
}
|
||||||
|
|
||||||
|
// StrictFloat64s returns list of float64 divided by given delimiter or error on first invalid input.
|
||||||
|
func (k *Key) StrictFloat64s(delim string) ([]float64, error) {
|
||||||
|
return k.getFloat64s(delim, false, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// StrictInts returns list of int divided by given delimiter or error on first invalid input.
|
||||||
|
func (k *Key) StrictInts(delim string) ([]int, error) {
|
||||||
|
return k.getInts(delim, false, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// StrictInt64s returns list of int64 divided by given delimiter or error on first invalid input.
|
||||||
|
func (k *Key) StrictInt64s(delim string) ([]int64, error) {
|
||||||
|
return k.getInt64s(delim, false, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// StrictUints returns list of uint divided by given delimiter or error on first invalid input.
|
||||||
|
func (k *Key) StrictUints(delim string) ([]uint, error) {
|
||||||
|
return k.getUints(delim, false, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// StrictUint64s returns list of uint64 divided by given delimiter or error on first invalid input.
|
||||||
|
func (k *Key) StrictUint64s(delim string) ([]uint64, error) {
|
||||||
|
return k.getUint64s(delim, false, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// StrictTimesFormat parses with given format and returns list of time.Time divided by given delimiter
|
||||||
|
// or error on first invalid input.
|
||||||
|
func (k *Key) StrictTimesFormat(format, delim string) ([]time.Time, error) {
|
||||||
|
return k.getTimesFormat(format, delim, false, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// StrictTimes parses with RFC3339 format and returns list of time.Time divided by given delimiter
|
||||||
|
// or error on first invalid input.
|
||||||
|
func (k *Key) StrictTimes(delim string) ([]time.Time, error) {
|
||||||
|
return k.StrictTimesFormat(time.RFC3339, delim)
|
||||||
|
}
|
||||||
|
|
||||||
|
// getFloat64s returns list of float64 divided by given delimiter.
|
||||||
|
func (k *Key) getFloat64s(delim string, addInvalid, returnOnInvalid bool) ([]float64, error) {
|
||||||
|
strs := k.Strings(delim)
|
||||||
|
vals := make([]float64, 0, len(strs))
|
||||||
|
for _, str := range strs {
|
||||||
|
val, err := strconv.ParseFloat(str, 64)
|
||||||
|
if err != nil && returnOnInvalid {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err == nil || addInvalid {
|
||||||
|
vals = append(vals, val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return vals, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getInts returns list of int divided by given delimiter.
|
||||||
|
func (k *Key) getInts(delim string, addInvalid, returnOnInvalid bool) ([]int, error) {
|
||||||
|
strs := k.Strings(delim)
|
||||||
|
vals := make([]int, 0, len(strs))
|
||||||
|
for _, str := range strs {
|
||||||
|
val, err := strconv.Atoi(str)
|
||||||
|
if err != nil && returnOnInvalid {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err == nil || addInvalid {
|
||||||
|
vals = append(vals, val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return vals, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getInt64s returns list of int64 divided by given delimiter.
|
||||||
|
func (k *Key) getInt64s(delim string, addInvalid, returnOnInvalid bool) ([]int64, error) {
|
||||||
|
strs := k.Strings(delim)
|
||||||
|
vals := make([]int64, 0, len(strs))
|
||||||
|
for _, str := range strs {
|
||||||
|
val, err := strconv.ParseInt(str, 10, 64)
|
||||||
|
if err != nil && returnOnInvalid {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err == nil || addInvalid {
|
||||||
|
vals = append(vals, val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return vals, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getUints returns list of uint divided by given delimiter.
|
||||||
|
func (k *Key) getUints(delim string, addInvalid, returnOnInvalid bool) ([]uint, error) {
|
||||||
|
strs := k.Strings(delim)
|
||||||
|
vals := make([]uint, 0, len(strs))
|
||||||
|
for _, str := range strs {
|
||||||
|
val, err := strconv.ParseUint(str, 10, 0)
|
||||||
|
if err != nil && returnOnInvalid {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err == nil || addInvalid {
|
||||||
|
vals = append(vals, uint(val))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return vals, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getUint64s returns list of uint64 divided by given delimiter.
|
||||||
|
func (k *Key) getUint64s(delim string, addInvalid, returnOnInvalid bool) ([]uint64, error) {
|
||||||
|
strs := k.Strings(delim)
|
||||||
|
vals := make([]uint64, 0, len(strs))
|
||||||
|
for _, str := range strs {
|
||||||
|
val, err := strconv.ParseUint(str, 10, 64)
|
||||||
|
if err != nil && returnOnInvalid {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err == nil || addInvalid {
|
||||||
|
vals = append(vals, val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return vals, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getTimesFormat parses with given format and returns list of time.Time divided by given delimiter.
|
||||||
|
func (k *Key) getTimesFormat(format, delim string, addInvalid, returnOnInvalid bool) ([]time.Time, error) {
|
||||||
|
strs := k.Strings(delim)
|
||||||
|
vals := make([]time.Time, 0, len(strs))
|
||||||
|
for _, str := range strs {
|
||||||
|
val, err := time.Parse(format, str)
|
||||||
|
if err != nil && returnOnInvalid {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err == nil || addInvalid {
|
||||||
|
vals = append(vals, val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return vals, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetValue changes key value.
|
||||||
|
func (k *Key) SetValue(v string) {
|
||||||
|
if k.s.f.BlockMode {
|
||||||
|
k.s.f.lock.Lock()
|
||||||
|
defer k.s.f.lock.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
k.value = v
|
||||||
|
k.s.keysHash[k.name] = v
|
||||||
|
}
|
325
Godeps/_workspace/src/gopkg.in/ini.v1/parser.go
generated
vendored
Normal file
325
Godeps/_workspace/src/gopkg.in/ini.v1/parser.go
generated
vendored
Normal file
@ -0,0 +1,325 @@
|
|||||||
|
// 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
|
||||||
|
}
|
206
Godeps/_workspace/src/gopkg.in/ini.v1/section.go
generated
vendored
Normal file
206
Godeps/_workspace/src/gopkg.in/ini.v1/section.go
generated
vendored
Normal file
@ -0,0 +1,206 @@
|
|||||||
|
// 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
285
Godeps/_workspace/src/gopkg.in/ini.v1/struct.go
generated
vendored
285
Godeps/_workspace/src/gopkg.in/ini.v1/struct.go
generated
vendored
@ -15,9 +15,11 @@
|
|||||||
package ini
|
package ini
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
"unicode"
|
"unicode"
|
||||||
)
|
)
|
||||||
@ -75,11 +77,64 @@ func parseDelim(actual string) string {
|
|||||||
|
|
||||||
var reflectTime = reflect.TypeOf(time.Now()).Kind()
|
var reflectTime = reflect.TypeOf(time.Now()).Kind()
|
||||||
|
|
||||||
|
// setSliceWithProperType sets proper values to slice based on its type.
|
||||||
|
func setSliceWithProperType(key *Key, field reflect.Value, delim string) error {
|
||||||
|
strs := key.Strings(delim)
|
||||||
|
numVals := len(strs)
|
||||||
|
if numVals == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var vals interface{}
|
||||||
|
|
||||||
|
sliceOf := field.Type().Elem().Kind()
|
||||||
|
switch sliceOf {
|
||||||
|
case reflect.String:
|
||||||
|
vals = strs
|
||||||
|
case reflect.Int:
|
||||||
|
vals = key.Ints(delim)
|
||||||
|
case reflect.Int64:
|
||||||
|
vals = key.Int64s(delim)
|
||||||
|
case reflect.Uint:
|
||||||
|
vals = key.Uints(delim)
|
||||||
|
case reflect.Uint64:
|
||||||
|
vals = key.Uint64s(delim)
|
||||||
|
case reflect.Float64:
|
||||||
|
vals = key.Float64s(delim)
|
||||||
|
case reflectTime:
|
||||||
|
vals = key.Times(delim)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported type '[]%s'", sliceOf)
|
||||||
|
}
|
||||||
|
|
||||||
|
slice := reflect.MakeSlice(field.Type(), numVals, numVals)
|
||||||
|
for i := 0; i < numVals; i++ {
|
||||||
|
switch sliceOf {
|
||||||
|
case reflect.String:
|
||||||
|
slice.Index(i).Set(reflect.ValueOf(vals.([]string)[i]))
|
||||||
|
case reflect.Int:
|
||||||
|
slice.Index(i).Set(reflect.ValueOf(vals.([]int)[i]))
|
||||||
|
case reflect.Int64:
|
||||||
|
slice.Index(i).Set(reflect.ValueOf(vals.([]int64)[i]))
|
||||||
|
case reflect.Uint:
|
||||||
|
slice.Index(i).Set(reflect.ValueOf(vals.([]uint)[i]))
|
||||||
|
case reflect.Uint64:
|
||||||
|
slice.Index(i).Set(reflect.ValueOf(vals.([]uint64)[i]))
|
||||||
|
case reflect.Float64:
|
||||||
|
slice.Index(i).Set(reflect.ValueOf(vals.([]float64)[i]))
|
||||||
|
case reflectTime:
|
||||||
|
slice.Index(i).Set(reflect.ValueOf(vals.([]time.Time)[i]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
field.Set(slice)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// setWithProperType sets proper value to field based on its type,
|
// setWithProperType sets proper value to field based on its type,
|
||||||
// but it does not return error for failing parsing,
|
// but it does not return error for failing parsing,
|
||||||
// because we want to use default value that is already assigned to strcut.
|
// because we want to use default value that is already assigned to strcut.
|
||||||
func setWithProperType(kind reflect.Kind, key *Key, field reflect.Value, delim string) error {
|
func setWithProperType(t reflect.Type, key *Key, field reflect.Value, delim string) error {
|
||||||
switch kind {
|
switch t.Kind() {
|
||||||
case reflect.String:
|
case reflect.String:
|
||||||
if len(key.String()) == 0 {
|
if len(key.String()) == 0 {
|
||||||
return nil
|
return nil
|
||||||
@ -92,11 +147,33 @@ func setWithProperType(kind reflect.Kind, key *Key, field reflect.Value, delim s
|
|||||||
}
|
}
|
||||||
field.SetBool(boolVal)
|
field.SetBool(boolVal)
|
||||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||||
|
durationVal, err := key.Duration()
|
||||||
|
// Skip zero value
|
||||||
|
if err == nil && int(durationVal) > 0 {
|
||||||
|
field.Set(reflect.ValueOf(durationVal))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
intVal, err := key.Int64()
|
intVal, err := key.Int64()
|
||||||
if err != nil {
|
if err != nil || intVal == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
field.SetInt(intVal)
|
field.SetInt(intVal)
|
||||||
|
// byte is an alias for uint8, so supporting uint8 breaks support for byte
|
||||||
|
case reflect.Uint, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||||
|
durationVal, err := key.Duration()
|
||||||
|
// Skip zero value
|
||||||
|
if err == nil && int(durationVal) > 0 {
|
||||||
|
field.Set(reflect.ValueOf(durationVal))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
uintVal, err := key.Uint64()
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
field.SetUint(uintVal)
|
||||||
|
|
||||||
case reflect.Float64:
|
case reflect.Float64:
|
||||||
floatVal, err := key.Float64()
|
floatVal, err := key.Float64()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -110,31 +187,9 @@ func setWithProperType(kind reflect.Kind, key *Key, field reflect.Value, delim s
|
|||||||
}
|
}
|
||||||
field.Set(reflect.ValueOf(timeVal))
|
field.Set(reflect.ValueOf(timeVal))
|
||||||
case reflect.Slice:
|
case reflect.Slice:
|
||||||
vals := key.Strings(delim)
|
return setSliceWithProperType(key, field, delim)
|
||||||
numVals := len(vals)
|
|
||||||
if numVals == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
sliceOf := field.Type().Elem().Kind()
|
|
||||||
|
|
||||||
var times []time.Time
|
|
||||||
if sliceOf == reflectTime {
|
|
||||||
times = key.Times(delim)
|
|
||||||
}
|
|
||||||
|
|
||||||
slice := reflect.MakeSlice(field.Type(), numVals, numVals)
|
|
||||||
for i := 0; i < numVals; i++ {
|
|
||||||
switch sliceOf {
|
|
||||||
case reflectTime:
|
|
||||||
slice.Index(i).Set(reflect.ValueOf(times[i]))
|
|
||||||
default:
|
|
||||||
slice.Index(i).Set(reflect.ValueOf(vals[i]))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
field.Set(slice)
|
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("unsupported type '%s'", kind)
|
return fmt.Errorf("unsupported type '%s'", t)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -154,20 +209,19 @@ func (s *Section) mapTo(val reflect.Value) error {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
fieldName := s.parseFieldName(tpField.Name, tag)
|
opts := strings.SplitN(tag, ",", 2) // strip off possible omitempty
|
||||||
|
fieldName := s.parseFieldName(tpField.Name, opts[0])
|
||||||
if len(fieldName) == 0 || !field.CanSet() {
|
if len(fieldName) == 0 || !field.CanSet() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if tpField.Type.Kind() == reflect.Struct {
|
isAnonymous := tpField.Type.Kind() == reflect.Ptr && tpField.Anonymous
|
||||||
if sec, err := s.f.GetSection(fieldName); err == nil {
|
isStruct := tpField.Type.Kind() == reflect.Struct
|
||||||
if err = sec.mapTo(field); err != nil {
|
if isAnonymous {
|
||||||
return fmt.Errorf("error mapping field(%s): %v", fieldName, err)
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
} else if tpField.Type.Kind() == reflect.Ptr && tpField.Anonymous {
|
|
||||||
field.Set(reflect.New(tpField.Type.Elem()))
|
field.Set(reflect.New(tpField.Type.Elem()))
|
||||||
|
}
|
||||||
|
|
||||||
|
if isAnonymous || isStruct {
|
||||||
if sec, err := s.f.GetSection(fieldName); err == nil {
|
if sec, err := s.f.GetSection(fieldName); err == nil {
|
||||||
if err = sec.mapTo(field); err != nil {
|
if err = sec.mapTo(field); err != nil {
|
||||||
return fmt.Errorf("error mapping field(%s): %v", fieldName, err)
|
return fmt.Errorf("error mapping field(%s): %v", fieldName, err)
|
||||||
@ -177,7 +231,7 @@ func (s *Section) mapTo(val reflect.Value) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if key, err := s.GetKey(fieldName); err == nil {
|
if key, err := s.GetKey(fieldName); err == nil {
|
||||||
if err = setWithProperType(tpField.Type.Kind(), key, field, parseDelim(tpField.Tag.Get("delim"))); err != nil {
|
if err = setWithProperType(tpField.Type, key, field, parseDelim(tpField.Tag.Get("delim"))); err != nil {
|
||||||
return fmt.Errorf("error mapping field(%s): %v", fieldName, err)
|
return fmt.Errorf("error mapping field(%s): %v", fieldName, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -218,3 +272,160 @@ func MapToWithMapper(v interface{}, mapper NameMapper, source interface{}, other
|
|||||||
func MapTo(v, source interface{}, others ...interface{}) error {
|
func MapTo(v, source interface{}, others ...interface{}) error {
|
||||||
return MapToWithMapper(v, nil, source, others...)
|
return MapToWithMapper(v, nil, source, others...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// reflectSliceWithProperType does the opposite thing as setSliceWithProperType.
|
||||||
|
func reflectSliceWithProperType(key *Key, field reflect.Value, delim string) error {
|
||||||
|
slice := field.Slice(0, field.Len())
|
||||||
|
if field.Len() == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
sliceOf := field.Type().Elem().Kind()
|
||||||
|
for i := 0; i < field.Len(); i++ {
|
||||||
|
switch sliceOf {
|
||||||
|
case reflect.String:
|
||||||
|
buf.WriteString(slice.Index(i).String())
|
||||||
|
case reflect.Int, reflect.Int64:
|
||||||
|
buf.WriteString(fmt.Sprint(slice.Index(i).Int()))
|
||||||
|
case reflect.Uint, reflect.Uint64:
|
||||||
|
buf.WriteString(fmt.Sprint(slice.Index(i).Uint()))
|
||||||
|
case reflect.Float64:
|
||||||
|
buf.WriteString(fmt.Sprint(slice.Index(i).Float()))
|
||||||
|
case reflectTime:
|
||||||
|
buf.WriteString(slice.Index(i).Interface().(time.Time).Format(time.RFC3339))
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported type '[]%s'", sliceOf)
|
||||||
|
}
|
||||||
|
buf.WriteString(delim)
|
||||||
|
}
|
||||||
|
key.SetValue(buf.String()[:buf.Len()-1])
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// reflectWithProperType does the opposite thing as setWithProperType.
|
||||||
|
func reflectWithProperType(t reflect.Type, key *Key, field reflect.Value, delim string) error {
|
||||||
|
switch t.Kind() {
|
||||||
|
case reflect.String:
|
||||||
|
key.SetValue(field.String())
|
||||||
|
case reflect.Bool:
|
||||||
|
key.SetValue(fmt.Sprint(field.Bool()))
|
||||||
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||||
|
key.SetValue(fmt.Sprint(field.Int()))
|
||||||
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||||
|
key.SetValue(fmt.Sprint(field.Uint()))
|
||||||
|
case reflect.Float32, reflect.Float64:
|
||||||
|
key.SetValue(fmt.Sprint(field.Float()))
|
||||||
|
case reflectTime:
|
||||||
|
key.SetValue(fmt.Sprint(field.Interface().(time.Time).Format(time.RFC3339)))
|
||||||
|
case reflect.Slice:
|
||||||
|
return reflectSliceWithProperType(key, field, delim)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported type '%s'", t)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CR: copied from encoding/json/encode.go with modifications of time.Time support.
|
||||||
|
// TODO: add more test coverage.
|
||||||
|
func isEmptyValue(v reflect.Value) bool {
|
||||||
|
switch v.Kind() {
|
||||||
|
case reflect.Array, reflect.Map, reflect.Slice, reflect.String:
|
||||||
|
return v.Len() == 0
|
||||||
|
case reflect.Bool:
|
||||||
|
return !v.Bool()
|
||||||
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||||
|
return v.Int() == 0
|
||||||
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||||
|
return v.Uint() == 0
|
||||||
|
case reflect.Float32, reflect.Float64:
|
||||||
|
return v.Float() == 0
|
||||||
|
case reflectTime:
|
||||||
|
return v.Interface().(time.Time).IsZero()
|
||||||
|
case reflect.Interface, reflect.Ptr:
|
||||||
|
return v.IsNil()
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Section) reflectFrom(val reflect.Value) error {
|
||||||
|
if val.Kind() == reflect.Ptr {
|
||||||
|
val = val.Elem()
|
||||||
|
}
|
||||||
|
typ := val.Type()
|
||||||
|
|
||||||
|
for i := 0; i < typ.NumField(); i++ {
|
||||||
|
field := val.Field(i)
|
||||||
|
tpField := typ.Field(i)
|
||||||
|
|
||||||
|
tag := tpField.Tag.Get("ini")
|
||||||
|
if tag == "-" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
opts := strings.SplitN(tag, ",", 2)
|
||||||
|
if len(opts) == 2 && opts[1] == "omitempty" && isEmptyValue(field) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldName := s.parseFieldName(tpField.Name, opts[0])
|
||||||
|
if len(fieldName) == 0 || !field.CanSet() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tpField.Type.Kind() == reflect.Ptr && tpField.Anonymous) ||
|
||||||
|
(tpField.Type.Kind() == reflect.Struct && tpField.Type.Name() != "Time") {
|
||||||
|
// Note: The only error here is section doesn't exist.
|
||||||
|
sec, err := s.f.GetSection(fieldName)
|
||||||
|
if err != nil {
|
||||||
|
// Note: fieldName can never be empty here, ignore error.
|
||||||
|
sec, _ = s.f.NewSection(fieldName)
|
||||||
|
}
|
||||||
|
if err = sec.reflectFrom(field); err != nil {
|
||||||
|
return fmt.Errorf("error reflecting field (%s): %v", fieldName, err)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: Same reason as secion.
|
||||||
|
key, err := s.GetKey(fieldName)
|
||||||
|
if err != nil {
|
||||||
|
key, _ = s.NewKey(fieldName, "")
|
||||||
|
}
|
||||||
|
if err = reflectWithProperType(tpField.Type, key, field, parseDelim(tpField.Tag.Get("delim"))); err != nil {
|
||||||
|
return fmt.Errorf("error reflecting field (%s): %v", fieldName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReflectFrom reflects secion from given struct.
|
||||||
|
func (s *Section) ReflectFrom(v interface{}) error {
|
||||||
|
typ := reflect.TypeOf(v)
|
||||||
|
val := reflect.ValueOf(v)
|
||||||
|
if typ.Kind() == reflect.Ptr {
|
||||||
|
typ = typ.Elem()
|
||||||
|
val = val.Elem()
|
||||||
|
} else {
|
||||||
|
return errors.New("cannot reflect from non-pointer struct")
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.reflectFrom(val)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReflectFrom reflects file from given struct.
|
||||||
|
func (f *File) ReflectFrom(v interface{}) error {
|
||||||
|
return f.Section("").ReflectFrom(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReflectFrom reflects data sources from given struct with name mapper.
|
||||||
|
func ReflectFromWithMapper(cfg *File, v interface{}, mapper NameMapper) error {
|
||||||
|
cfg.NameMapper = mapper
|
||||||
|
return cfg.ReflectFrom(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReflectFrom reflects data sources from given struct.
|
||||||
|
func ReflectFrom(cfg *File, v interface{}) error {
|
||||||
|
return ReflectFromWithMapper(cfg, v, nil)
|
||||||
|
}
|
||||||
|
181
Godeps/_workspace/src/gopkg.in/ini.v1/struct_test.go
generated
vendored
181
Godeps/_workspace/src/gopkg.in/ini.v1/struct_test.go
generated
vendored
@ -1,181 +0,0 @@
|
|||||||
// 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")
|
|
||||||
})
|
|
||||||
}
|
|
2
Godeps/_workspace/src/gopkg.in/ini.v1/testdata/conf.ini
generated
vendored
2
Godeps/_workspace/src/gopkg.in/ini.v1/testdata/conf.ini
generated
vendored
@ -1,2 +0,0 @@
|
|||||||
[author]
|
|
||||||
E-MAIL = u@gogs.io
|
|
@ -78,7 +78,7 @@ the latest master builds [here](http://grafana.org/download/builds)
|
|||||||
|
|
||||||
### Dependencies
|
### Dependencies
|
||||||
|
|
||||||
- Go 1.5
|
- Go 1.6
|
||||||
- NodeJS v4+
|
- NodeJS v4+
|
||||||
- [Godep](https://github.com/tools/godep)
|
- [Godep](https://github.com/tools/godep)
|
||||||
|
|
||||||
|
2
build.go
2
build.go
@ -34,7 +34,7 @@ var (
|
|||||||
binaries []string = []string{"grafana-server", "grafana-cli"}
|
binaries []string = []string{"grafana-server", "grafana-cli"}
|
||||||
)
|
)
|
||||||
|
|
||||||
const minGoVersion = 1.3
|
const minGoVersion = 1.6
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
log.SetOutput(os.Stdout)
|
log.SetOutput(os.Stdout)
|
||||||
|
@ -31,4 +31,4 @@ deployment:
|
|||||||
branch: master
|
branch: master
|
||||||
owner: grafana
|
owner: grafana
|
||||||
commands:
|
commands:
|
||||||
- ./trigger_grafana_packer.sh ${TRIGGER_GRAFANA_PACKER_CIRCLECI_TOKEN}
|
- ./trigger_grafana_packer.sh ${TRIGGER_GRAFANA_PACKER_CIRCLECI_TOKEN}
|
@ -59,7 +59,7 @@ cert_key =
|
|||||||
|
|
||||||
#################################### Database ####################################
|
#################################### Database ####################################
|
||||||
[database]
|
[database]
|
||||||
# You can configure the database connection by specifying type, host, name, user and password
|
# You can configure the database connection by specifying type, host, name, user and password
|
||||||
# as seperate properties or as on string using the url propertie.
|
# as seperate properties or as on string using the url propertie.
|
||||||
|
|
||||||
# Either "mysql", "postgres" or "sqlite3", it's your choice
|
# Either "mysql", "postgres" or "sqlite3", it's your choice
|
||||||
@ -223,6 +223,19 @@ token_url = https://accounts.google.com/o/oauth2/token
|
|||||||
api_url = https://www.googleapis.com/oauth2/v1/userinfo
|
api_url = https://www.googleapis.com/oauth2/v1/userinfo
|
||||||
allowed_domains =
|
allowed_domains =
|
||||||
|
|
||||||
|
#################################### Generic OAuth ##########################
|
||||||
|
[auth.generic_oauth]
|
||||||
|
enabled = false
|
||||||
|
allow_sign_up = false
|
||||||
|
client_id = some_id
|
||||||
|
client_secret = some_secret
|
||||||
|
scopes = user:email
|
||||||
|
auth_url =
|
||||||
|
token_url =
|
||||||
|
api_url =
|
||||||
|
team_ids =
|
||||||
|
allowed_organizations =
|
||||||
|
|
||||||
#################################### Basic Auth ##########################
|
#################################### Basic Auth ##########################
|
||||||
[auth.basic]
|
[auth.basic]
|
||||||
enabled = true
|
enabled = true
|
||||||
|
@ -61,7 +61,7 @@
|
|||||||
|
|
||||||
#################################### Database ####################################
|
#################################### Database ####################################
|
||||||
[database]
|
[database]
|
||||||
# You can configure the database connection by specifying type, host, name, user and password
|
# You can configure the database connection by specifying type, host, name, user and password
|
||||||
# as seperate properties or as on string using the url propertie.
|
# as seperate properties or as on string using the url propertie.
|
||||||
|
|
||||||
# Either "mysql", "postgres" or "sqlite3", it's your choice
|
# Either "mysql", "postgres" or "sqlite3", it's your choice
|
||||||
@ -205,6 +205,19 @@ check_for_updates = true
|
|||||||
;api_url = https://www.googleapis.com/oauth2/v1/userinfo
|
;api_url = https://www.googleapis.com/oauth2/v1/userinfo
|
||||||
;allowed_domains =
|
;allowed_domains =
|
||||||
|
|
||||||
|
#################################### Generic OAuth ##########################
|
||||||
|
[auth.generic_oauth]
|
||||||
|
;enabled = false
|
||||||
|
;allow_sign_up = false
|
||||||
|
;client_id = some_id
|
||||||
|
;client_secret = some_secret
|
||||||
|
;scopes = user:email,read:org
|
||||||
|
;auth_url = https://foo.bar/login/oauth/authorize
|
||||||
|
;token_url = https://foo.bar/login/oauth/access_token
|
||||||
|
;api_url = https://foo.bar/user
|
||||||
|
;team_ids =
|
||||||
|
;allowed_organizations =
|
||||||
|
|
||||||
#################################### Auth Proxy ##########################
|
#################################### Auth Proxy ##########################
|
||||||
[auth.proxy]
|
[auth.proxy]
|
||||||
;enabled = false
|
;enabled = false
|
||||||
@ -318,7 +331,7 @@ check_for_updates = true
|
|||||||
# \______(_______;;;)__;;;)
|
# \______(_______;;;)__;;;)
|
||||||
|
|
||||||
[alerting]
|
[alerting]
|
||||||
enabled = false
|
;enabled = false
|
||||||
|
|
||||||
#################################### Internal Grafana Metrics ##########################
|
#################################### Internal Grafana Metrics ##########################
|
||||||
# Metrics available at HTTP API Url /api/metrics
|
# Metrics available at HTTP API Url /api/metrics
|
||||||
|
@ -77,7 +77,7 @@ Example dimension queries which will return list of resources for individual AWS
|
|||||||
|
|
||||||
Service | Query
|
Service | Query
|
||||||
------- | -----
|
------- | -----
|
||||||
EBS | `dimension_values(us-east-1,AWS/ELB,RequestCount,LoadBalancerName)`
|
ELB | `dimension_values(us-east-1,AWS/ELB,RequestCount,LoadBalancerName)`
|
||||||
ElastiCache | `dimension_values(us-east-1,AWS/ElastiCache,CPUUtilization,CacheClusterId)`
|
ElastiCache | `dimension_values(us-east-1,AWS/ElastiCache,CPUUtilization,CacheClusterId)`
|
||||||
RedShift | `dimension_values(us-east-1,AWS/Redshift,CPUUtilization,ClusterIdentifier)`
|
RedShift | `dimension_values(us-east-1,AWS/Redshift,CPUUtilization,ClusterIdentifier)`
|
||||||
RDS | `dimension_values(us-east-1,AWS/RDS,CPUUtilization,DBInstanceIdentifier)`
|
RDS | `dimension_values(us-east-1,AWS/RDS,CPUUtilization,DBInstanceIdentifier)`
|
||||||
|
@ -88,6 +88,8 @@ Another way is put a webserver like Nginx or Apache in front of Grafana and have
|
|||||||
|
|
||||||
`http` or `https`
|
`http` or `https`
|
||||||
|
|
||||||
|
> **Note** Grafana versions earlier than 3.0 are vulnerable to [POODLE](https://en.wikipedia.org/wiki/POODLE). So we strongly recommend to upgrade to 3.x or use a reverse proxy for ssl termination.
|
||||||
|
|
||||||
### domain
|
### domain
|
||||||
|
|
||||||
This setting is only used in as a part of the `root_url` setting (see below). Important if you
|
This setting is only used in as a part of the `root_url` setting (see below). Important if you
|
||||||
@ -339,6 +341,23 @@ You may allow users to sign-up via Google authentication by setting the
|
|||||||
user successfully authenticating via Google authentication will be
|
user successfully authenticating via Google authentication will be
|
||||||
automatically signed up.
|
automatically signed up.
|
||||||
|
|
||||||
|
## [auth.generic_oauth]
|
||||||
|
|
||||||
|
This option could be used if have your own oauth service.
|
||||||
|
|
||||||
|
This callback URL must match the full HTTP address that you use in your
|
||||||
|
browser to access Grafana, but with the prefix path of `/login/generic_oauth`.
|
||||||
|
|
||||||
|
[auth.generic_oauth]
|
||||||
|
enabled = true
|
||||||
|
client_id = YOUR_APP_CLIENT_ID
|
||||||
|
client_secret = YOUR_APP_CLIENT_SECRET
|
||||||
|
scopes =
|
||||||
|
auth_url =
|
||||||
|
token_url =
|
||||||
|
allowed_domains = mycompany.com mycompany.org
|
||||||
|
allow_sign_up = false
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
## [auth.basic]
|
## [auth.basic]
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
[Unit]
|
[Unit]
|
||||||
Description=Starts and stops a single grafana instance on this system
|
Description=Grafana instance
|
||||||
Documentation=http://docs.grafana.org
|
Documentation=http://docs.grafana.org
|
||||||
Wants=network-online.target
|
Wants=network-online.target
|
||||||
After=network-online.target
|
After=network-online.target
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
[Unit]
|
[Unit]
|
||||||
Description=Starts and stops a single grafana instance on this system
|
Description=Grafana instance
|
||||||
Documentation=http://docs.grafana.org
|
Documentation=http://docs.grafana.org
|
||||||
Wants=network-online.target
|
Wants=network-online.target
|
||||||
After=network-online.target
|
After=network-online.target
|
||||||
|
@ -8,6 +8,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/middleware"
|
"github.com/grafana/grafana/pkg/middleware"
|
||||||
"github.com/grafana/grafana/pkg/models"
|
"github.com/grafana/grafana/pkg/models"
|
||||||
"github.com/grafana/grafana/pkg/services/alerting"
|
"github.com/grafana/grafana/pkg/services/alerting"
|
||||||
|
"github.com/grafana/grafana/pkg/services/annotations"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ValidateOrgAlert(c *middleware.Context) {
|
func ValidateOrgAlert(c *middleware.Context) {
|
||||||
@ -146,7 +147,7 @@ func DelAlert(c *middleware.Context) Response {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func GetAlertNotifications(c *middleware.Context) Response {
|
func GetAlertNotifications(c *middleware.Context) Response {
|
||||||
query := &models.GetAlertNotificationsQuery{OrgId: c.OrgId}
|
query := &models.GetAllAlertNotificationsQuery{OrgId: c.OrgId}
|
||||||
|
|
||||||
if err := bus.Dispatch(query); err != nil {
|
if err := bus.Dispatch(query); err != nil {
|
||||||
return ApiError(500, "Failed to get alert notifications", err)
|
return ApiError(500, "Failed to get alert notifications", err)
|
||||||
@ -156,11 +157,12 @@ func GetAlertNotifications(c *middleware.Context) Response {
|
|||||||
|
|
||||||
for _, notification := range query.Result {
|
for _, notification := range query.Result {
|
||||||
result = append(result, dtos.AlertNotification{
|
result = append(result, dtos.AlertNotification{
|
||||||
Id: notification.Id,
|
Id: notification.Id,
|
||||||
Name: notification.Name,
|
Name: notification.Name,
|
||||||
Type: notification.Type,
|
Type: notification.Type,
|
||||||
Created: notification.Created,
|
IsDefault: notification.IsDefault,
|
||||||
Updated: notification.Updated,
|
Created: notification.Created,
|
||||||
|
Updated: notification.Updated,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -177,7 +179,7 @@ func GetAlertNotificationById(c *middleware.Context) Response {
|
|||||||
return ApiError(500, "Failed to get alert notifications", err)
|
return ApiError(500, "Failed to get alert notifications", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return Json(200, query.Result[0])
|
return Json(200, query.Result)
|
||||||
}
|
}
|
||||||
|
|
||||||
func CreateAlertNotification(c *middleware.Context, cmd models.CreateAlertNotificationCommand) Response {
|
func CreateAlertNotification(c *middleware.Context, cmd models.CreateAlertNotificationCommand) Response {
|
||||||
@ -212,3 +214,86 @@ func DeleteAlertNotification(c *middleware.Context) Response {
|
|||||||
|
|
||||||
return ApiSuccess("Notification deleted")
|
return ApiSuccess("Notification deleted")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//POST /api/alert-notifications/test
|
||||||
|
func NotificationTest(c *middleware.Context, dto dtos.NotificationTestCommand) Response {
|
||||||
|
cmd := &alerting.NotificationTestCommand{
|
||||||
|
Name: dto.Name,
|
||||||
|
Type: dto.Type,
|
||||||
|
Severity: dto.Severity,
|
||||||
|
Settings: dto.Settings,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := bus.Dispatch(cmd); err != nil {
|
||||||
|
return ApiError(500, "Failed to send alert notifications", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ApiSuccess("Test notification sent")
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetAlertHistory(c *middleware.Context) Response {
|
||||||
|
alertId, err := getAlertIdForRequest(c)
|
||||||
|
if err != nil {
|
||||||
|
return ApiError(400, "Invalid request", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
query := &annotations.ItemQuery{
|
||||||
|
AlertId: alertId,
|
||||||
|
Type: annotations.AlertType,
|
||||||
|
OrgId: c.OrgId,
|
||||||
|
Limit: c.QueryInt64("limit"),
|
||||||
|
}
|
||||||
|
|
||||||
|
repo := annotations.GetRepository()
|
||||||
|
|
||||||
|
items, err := repo.Find(query)
|
||||||
|
if err != nil {
|
||||||
|
return ApiError(500, "Failed to get history for alert", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var result []dtos.AlertHistory
|
||||||
|
for _, item := range items {
|
||||||
|
result = append(result, dtos.AlertHistory{
|
||||||
|
AlertId: item.AlertId,
|
||||||
|
Timestamp: item.Timestamp,
|
||||||
|
Data: item.Data,
|
||||||
|
NewState: item.NewState,
|
||||||
|
Text: item.Text,
|
||||||
|
Metric: item.Metric,
|
||||||
|
Title: item.Title,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return Json(200, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAlertIdForRequest(c *middleware.Context) (int64, error) {
|
||||||
|
alertId := c.QueryInt64("alertId")
|
||||||
|
panelId := c.QueryInt64("panelId")
|
||||||
|
dashboardId := c.QueryInt64("dashboardId")
|
||||||
|
|
||||||
|
if alertId == 0 && dashboardId == 0 && panelId == 0 {
|
||||||
|
return 0, fmt.Errorf("Missing alertId or dashboardId and panelId")
|
||||||
|
}
|
||||||
|
|
||||||
|
if alertId == 0 {
|
||||||
|
//fetch alertId
|
||||||
|
query := models.GetAlertsQuery{
|
||||||
|
OrgId: c.OrgId,
|
||||||
|
DashboardId: dashboardId,
|
||||||
|
PanelId: panelId,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := bus.Dispatch(&query); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(query.Result) != 1 {
|
||||||
|
return 0, fmt.Errorf("PanelId is not unique on dashboard")
|
||||||
|
}
|
||||||
|
|
||||||
|
alertId = query.Result[0].Id
|
||||||
|
}
|
||||||
|
|
||||||
|
return alertId, nil
|
||||||
|
}
|
||||||
|
@ -19,6 +19,9 @@ func Register(r *macaron.Macaron) {
|
|||||||
quota := middleware.Quota
|
quota := middleware.Quota
|
||||||
bind := binding.Bind
|
bind := binding.Bind
|
||||||
|
|
||||||
|
// automatically set HEAD for every GET
|
||||||
|
r.SetAutoHead(true)
|
||||||
|
|
||||||
// not logged in views
|
// not logged in views
|
||||||
r.Get("/", reqSignedIn, Index)
|
r.Get("/", reqSignedIn, Index)
|
||||||
r.Get("/logout", Logout)
|
r.Get("/logout", Logout)
|
||||||
@ -247,14 +250,16 @@ func Register(r *macaron.Macaron) {
|
|||||||
|
|
||||||
r.Group("/alerts", func() {
|
r.Group("/alerts", func() {
|
||||||
r.Post("/test", bind(dtos.AlertTestCommand{}), wrap(AlertTest))
|
r.Post("/test", bind(dtos.AlertTestCommand{}), wrap(AlertTest))
|
||||||
//r.Get("/:alertId/states", wrap(GetAlertStates))
|
|
||||||
r.Get("/:alertId", ValidateOrgAlert, wrap(GetAlert))
|
r.Get("/:alertId", ValidateOrgAlert, wrap(GetAlert))
|
||||||
r.Get("/", wrap(GetAlerts))
|
r.Get("/", wrap(GetAlerts))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
r.Get("/alert-history", wrap(GetAlertHistory))
|
||||||
|
|
||||||
r.Get("/alert-notifications", wrap(GetAlertNotifications))
|
r.Get("/alert-notifications", wrap(GetAlertNotifications))
|
||||||
|
|
||||||
r.Group("/alert-notifications", func() {
|
r.Group("/alert-notifications", func() {
|
||||||
|
r.Post("/test", bind(dtos.NotificationTestCommand{}), wrap(NotificationTest))
|
||||||
r.Post("/", bind(m.CreateAlertNotificationCommand{}), wrap(CreateAlertNotification))
|
r.Post("/", bind(m.CreateAlertNotificationCommand{}), wrap(CreateAlertNotification))
|
||||||
r.Put("/:notificationId", bind(m.UpdateAlertNotificationCommand{}), wrap(UpdateAlertNotification))
|
r.Put("/:notificationId", bind(m.UpdateAlertNotificationCommand{}), wrap(UpdateAlertNotification))
|
||||||
r.Get("/:notificationId", wrap(GetAlertNotificationById))
|
r.Get("/:notificationId", wrap(GetAlertNotificationById))
|
||||||
|
@ -22,7 +22,6 @@ func GetDataSources(c *middleware.Context) {
|
|||||||
|
|
||||||
result := make(dtos.DataSourceList, 0)
|
result := make(dtos.DataSourceList, 0)
|
||||||
for _, ds := range query.Result {
|
for _, ds := range query.Result {
|
||||||
|
|
||||||
dsItem := dtos.DataSource{
|
dsItem := dtos.DataSource{
|
||||||
Id: ds.Id,
|
Id: ds.Id,
|
||||||
OrgId: ds.OrgId,
|
OrgId: ds.OrgId,
|
||||||
@ -35,6 +34,7 @@ func GetDataSources(c *middleware.Context) {
|
|||||||
User: ds.User,
|
User: ds.User,
|
||||||
BasicAuth: ds.BasicAuth,
|
BasicAuth: ds.BasicAuth,
|
||||||
IsDefault: ds.IsDefault,
|
IsDefault: ds.IsDefault,
|
||||||
|
JsonData: ds.JsonData,
|
||||||
}
|
}
|
||||||
|
|
||||||
if plugin, exists := plugins.DataSources[ds.Type]; exists {
|
if plugin, exists := plugins.DataSources[ds.Type]; exists {
|
||||||
|
@ -22,11 +22,12 @@ type AlertRule struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type AlertNotification struct {
|
type AlertNotification struct {
|
||||||
Id int64 `json:"id"`
|
Id int64 `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Created time.Time `json:"created"`
|
IsDefault bool `json:"isDefault"`
|
||||||
Updated time.Time `json:"updated"`
|
Created time.Time `json:"created"`
|
||||||
|
Updated time.Time `json:"updated"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AlertTestCommand struct {
|
type AlertTestCommand struct {
|
||||||
@ -52,3 +53,21 @@ type EvalMatch struct {
|
|||||||
Metric string `json:"metric"`
|
Metric string `json:"metric"`
|
||||||
Value float64 `json:"value"`
|
Value float64 `json:"value"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AlertHistory struct {
|
||||||
|
AlertId int64 `json:"alertId"`
|
||||||
|
NewState string `json:"newState"`
|
||||||
|
Timestamp time.Time `json:"timestamp"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Text string `json:"text"`
|
||||||
|
Metric string `json:"metric"`
|
||||||
|
|
||||||
|
Data *simplejson.Json `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type NotificationTestCommand struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Settings *simplejson.Json `json:"settings"`
|
||||||
|
Severity string `json:"severity"`
|
||||||
|
}
|
||||||
|
@ -27,6 +27,8 @@ func LoginView(c *middleware.Context) {
|
|||||||
|
|
||||||
viewData.Settings["googleAuthEnabled"] = setting.OAuthService.Google
|
viewData.Settings["googleAuthEnabled"] = setting.OAuthService.Google
|
||||||
viewData.Settings["githubAuthEnabled"] = setting.OAuthService.GitHub
|
viewData.Settings["githubAuthEnabled"] = setting.OAuthService.GitHub
|
||||||
|
viewData.Settings["genericOAuthEnabled"] = setting.OAuthService.Generic
|
||||||
|
viewData.Settings["oauthProviderName"] = setting.OAuthService.OAuthProviderName
|
||||||
viewData.Settings["disableUserSignUp"] = !setting.AllowUserSignUp
|
viewData.Settings["disableUserSignUp"] = !setting.AllowUserSignUp
|
||||||
viewData.Settings["loginHint"] = setting.LoginHint
|
viewData.Settings["loginHint"] = setting.LoginHint
|
||||||
viewData.Settings["allowUserPassLogin"] = setting.AllowUserPassLogin
|
viewData.Settings["allowUserPassLogin"] = setting.AllowUserPassLogin
|
||||||
|
@ -14,7 +14,7 @@ func runCommand(command func(commandLine CommandLine) error) func(context *cli.C
|
|||||||
cmd := &contextCommandLine{context}
|
cmd := &contextCommandLine{context}
|
||||||
if err := command(cmd); err != nil {
|
if err := command(cmd); err != nil {
|
||||||
logger.Errorf("\n%s: ", color.RedString("Error"))
|
logger.Errorf("\n%s: ", color.RedString("Error"))
|
||||||
logger.Errorf("%s\n\n", err)
|
logger.Errorf("%s %s\n\n", color.RedString("✗"), err)
|
||||||
|
|
||||||
cmd.ShowHelp()
|
cmd.ShowHelp()
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
|
@ -53,8 +53,16 @@ func upgradeAllCommand(c CommandLine) error {
|
|||||||
for _, p := range pluginsToUpgrade {
|
for _, p := range pluginsToUpgrade {
|
||||||
logger.Infof("Updating %v \n", p.Id)
|
logger.Infof("Updating %v \n", p.Id)
|
||||||
|
|
||||||
s.RemoveInstalledPlugin(pluginsDir, p.Id)
|
var err error
|
||||||
InstallPlugin(p.Id, "", c)
|
err = s.RemoveInstalledPlugin(pluginsDir, p.Id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = InstallPlugin(p.Id, "", c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
@ -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")
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
package imguploader
|
package imguploader
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"reflect"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
@ -27,7 +26,12 @@ func TestImageUploaderFactory(t *testing.T) {
|
|||||||
uploader, err := NewImageUploader()
|
uploader, err := NewImageUploader()
|
||||||
|
|
||||||
So(err, ShouldBeNil)
|
So(err, ShouldBeNil)
|
||||||
So(reflect.TypeOf(uploader), ShouldEqual, reflect.TypeOf(&S3Uploader{}))
|
original, ok := uploader.(*S3Uploader)
|
||||||
|
|
||||||
|
So(ok, ShouldBeTrue)
|
||||||
|
So(original.accessKey, ShouldEqual, "access_key")
|
||||||
|
So(original.secretKey, ShouldEqual, "secret_key")
|
||||||
|
So(original.bucket, ShouldEqual, "bucket_url")
|
||||||
})
|
})
|
||||||
|
|
||||||
Convey("Webdav uploader", func() {
|
Convey("Webdav uploader", func() {
|
||||||
@ -47,7 +51,12 @@ func TestImageUploaderFactory(t *testing.T) {
|
|||||||
uploader, err := NewImageUploader()
|
uploader, err := NewImageUploader()
|
||||||
|
|
||||||
So(err, ShouldBeNil)
|
So(err, ShouldBeNil)
|
||||||
So(reflect.TypeOf(uploader), ShouldEqual, reflect.TypeOf(&WebdavUploader{}))
|
original, ok := uploader.(*WebdavUploader)
|
||||||
|
|
||||||
|
So(ok, ShouldBeTrue)
|
||||||
|
So(original.url, ShouldEqual, "webdavUrl")
|
||||||
|
So(original.username, ShouldEqual, "username")
|
||||||
|
So(original.password, ShouldEqual, "password")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
|||||||
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 {
|
|||||||
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) {
|
|||||||
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) {
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
return fullUrl, nil
|
return imageUrlString, nil
|
||||||
}
|
}
|
||||||
|
23
pkg/components/imguploader/s3uploader_test.go
Normal file
23
pkg/components/imguploader/s3uploader_test.go
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
package imguploader
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
|
. "github.com/smartystreets/goconvey/convey"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestUploadToS3(t *testing.T) {
|
||||||
|
SkipConvey("[Integration test] for external_image_store.webdav", t, func() {
|
||||||
|
setting.NewConfigContext(&setting.CommandLineArgs{
|
||||||
|
HomePath: "../../../",
|
||||||
|
})
|
||||||
|
|
||||||
|
s3Uploader, _ := NewImageUploader()
|
||||||
|
|
||||||
|
path, err := s3Uploader.Upload("../../../public/img/logo_transparent_400x.png")
|
||||||
|
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
So(path, ShouldNotEqual, "")
|
||||||
|
})
|
||||||
|
}
|
@ -9,7 +9,6 @@ import (
|
|||||||
"path"
|
"path"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/log"
|
|
||||||
"github.com/grafana/grafana/pkg/util"
|
"github.com/grafana/grafana/pkg/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -20,7 +19,6 @@ type WebdavUploader struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (u *WebdavUploader) Upload(pa string) (string, error) {
|
func (u *WebdavUploader) Upload(pa string) (string, error) {
|
||||||
log.Error2("Hej")
|
|
||||||
client := http.Client{Timeout: time.Duration(10 * time.Second)}
|
client := http.Client{Timeout: time.Duration(10 * time.Second)}
|
||||||
|
|
||||||
url, _ := url.Parse(u.url)
|
url, _ := url.Parse(u.url)
|
||||||
|
@ -9,34 +9,36 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
M_Instance_Start Counter
|
M_Instance_Start Counter
|
||||||
M_Page_Status_200 Counter
|
M_Page_Status_200 Counter
|
||||||
M_Page_Status_500 Counter
|
M_Page_Status_500 Counter
|
||||||
M_Page_Status_404 Counter
|
M_Page_Status_404 Counter
|
||||||
M_Api_Status_500 Counter
|
M_Api_Status_500 Counter
|
||||||
M_Api_Status_404 Counter
|
M_Api_Status_404 Counter
|
||||||
M_Api_User_SignUpStarted Counter
|
M_Api_User_SignUpStarted Counter
|
||||||
M_Api_User_SignUpCompleted Counter
|
M_Api_User_SignUpCompleted Counter
|
||||||
M_Api_User_SignUpInvite Counter
|
M_Api_User_SignUpInvite Counter
|
||||||
M_Api_Dashboard_Save Timer
|
M_Api_Dashboard_Save Timer
|
||||||
M_Api_Dashboard_Get Timer
|
M_Api_Dashboard_Get Timer
|
||||||
M_Api_Dashboard_Search Timer
|
M_Api_Dashboard_Search Timer
|
||||||
M_Api_Admin_User_Create Counter
|
M_Api_Admin_User_Create Counter
|
||||||
M_Api_Login_Post Counter
|
M_Api_Login_Post Counter
|
||||||
M_Api_Login_OAuth Counter
|
M_Api_Login_OAuth Counter
|
||||||
M_Api_Org_Create Counter
|
M_Api_Org_Create Counter
|
||||||
M_Api_Dashboard_Snapshot_Create Counter
|
M_Api_Dashboard_Snapshot_Create Counter
|
||||||
M_Api_Dashboard_Snapshot_External Counter
|
M_Api_Dashboard_Snapshot_External Counter
|
||||||
M_Api_Dashboard_Snapshot_Get Counter
|
M_Api_Dashboard_Snapshot_Get Counter
|
||||||
M_Models_Dashboard_Insert Counter
|
M_Models_Dashboard_Insert Counter
|
||||||
M_Alerting_Result_Critical Counter
|
M_Alerting_Result_State_Critical Counter
|
||||||
M_Alerting_Result_Warning Counter
|
M_Alerting_Result_State_Warning Counter
|
||||||
M_Alerting_Result_Info Counter
|
M_Alerting_Result_State_Ok Counter
|
||||||
M_Alerting_Result_Ok Counter
|
M_Alerting_Result_State_Paused Counter
|
||||||
M_Alerting_Active_Alerts Counter
|
M_Alerting_Result_State_Unknown Counter
|
||||||
M_Alerting_Notification_Sent_Slack Counter
|
M_Alerting_Result_State_ExecutionError Counter
|
||||||
M_Alerting_Notification_Sent_Email Counter
|
M_Alerting_Active_Alerts Counter
|
||||||
M_Alerting_Notification_Sent_Webhook Counter
|
M_Alerting_Notification_Sent_Slack Counter
|
||||||
|
M_Alerting_Notification_Sent_Email Counter
|
||||||
|
M_Alerting_Notification_Sent_Webhook Counter
|
||||||
|
|
||||||
// Timers
|
// Timers
|
||||||
M_DataSource_ProxyReq_Timer Timer
|
M_DataSource_ProxyReq_Timer Timer
|
||||||
@ -75,10 +77,13 @@ func initMetricVars(settings *MetricSettings) {
|
|||||||
|
|
||||||
M_Models_Dashboard_Insert = RegCounter("models.dashboard.insert")
|
M_Models_Dashboard_Insert = RegCounter("models.dashboard.insert")
|
||||||
|
|
||||||
M_Alerting_Result_Critical = RegCounter("alerting.result", "severity", "critical")
|
M_Alerting_Result_State_Critical = RegCounter("alerting.result", "state", "critical")
|
||||||
M_Alerting_Result_Warning = RegCounter("alerting.result", "severity", "warning")
|
M_Alerting_Result_State_Warning = RegCounter("alerting.result", "state", "warning")
|
||||||
M_Alerting_Result_Info = RegCounter("alerting.result", "severity", "info")
|
M_Alerting_Result_State_Ok = RegCounter("alerting.result", "state", "ok")
|
||||||
M_Alerting_Result_Ok = RegCounter("alerting.result", "severity", "ok")
|
M_Alerting_Result_State_Paused = RegCounter("alerting.result", "state", "paused")
|
||||||
|
M_Alerting_Result_State_Unknown = RegCounter("alerting.result", "state", "unknown")
|
||||||
|
M_Alerting_Result_State_ExecutionError = RegCounter("alerting.result", "state", "execution_error")
|
||||||
|
|
||||||
M_Alerting_Active_Alerts = RegCounter("alerting.active_alerts")
|
M_Alerting_Active_Alerts = RegCounter("alerting.active_alerts")
|
||||||
M_Alerting_Notification_Sent_Slack = RegCounter("alerting.notifications_sent", "type", "slack")
|
M_Alerting_Notification_Sent_Slack = RegCounter("alerting.notifications_sent", "type", "slack")
|
||||||
M_Alerting_Notification_Sent_Email = RegCounter("alerting.notifications_sent", "type", "email")
|
M_Alerting_Notification_Sent_Email = RegCounter("alerting.notifications_sent", "type", "email")
|
||||||
|
@ -10,7 +10,7 @@ type AlertStateType string
|
|||||||
type AlertSeverityType string
|
type AlertSeverityType string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
AlertStatePending AlertStateType = "pending"
|
AlertStateUnknown AlertStateType = "unknown"
|
||||||
AlertStateExeuctionError AlertStateType = "execution_error"
|
AlertStateExeuctionError AlertStateType = "execution_error"
|
||||||
AlertStatePaused AlertStateType = "paused"
|
AlertStatePaused AlertStateType = "paused"
|
||||||
AlertStateCritical AlertStateType = "critical"
|
AlertStateCritical AlertStateType = "critical"
|
||||||
@ -19,7 +19,7 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func (s AlertStateType) IsValid() bool {
|
func (s AlertStateType) IsValid() bool {
|
||||||
return s == AlertStateOK || s == AlertStatePending || s == AlertStateExeuctionError || s == AlertStatePaused || s == AlertStateCritical || s == AlertStateWarning
|
return s == AlertStateOK || s == AlertStateUnknown || s == AlertStateExeuctionError || s == AlertStatePaused || s == AlertStateCritical || s == AlertStateWarning
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -7,29 +7,32 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type AlertNotification struct {
|
type AlertNotification struct {
|
||||||
Id int64 `json:"id"`
|
Id int64 `json:"id"`
|
||||||
OrgId int64 `json:"-"`
|
OrgId int64 `json:"-"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Settings *simplejson.Json `json:"settings"`
|
IsDefault bool `json:"isDefault"`
|
||||||
Created time.Time `json:"created"`
|
Settings *simplejson.Json `json:"settings"`
|
||||||
Updated time.Time `json:"updated"`
|
Created time.Time `json:"created"`
|
||||||
|
Updated time.Time `json:"updated"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type CreateAlertNotificationCommand struct {
|
type CreateAlertNotificationCommand struct {
|
||||||
Name string `json:"name" binding:"Required"`
|
Name string `json:"name" binding:"Required"`
|
||||||
Type string `json:"type" binding:"Required"`
|
Type string `json:"type" binding:"Required"`
|
||||||
Settings *simplejson.Json `json:"settings"`
|
IsDefault bool `json:"isDefault"`
|
||||||
|
Settings *simplejson.Json `json:"settings"`
|
||||||
|
|
||||||
OrgId int64 `json:"-"`
|
OrgId int64 `json:"-"`
|
||||||
Result *AlertNotification
|
Result *AlertNotification
|
||||||
}
|
}
|
||||||
|
|
||||||
type UpdateAlertNotificationCommand struct {
|
type UpdateAlertNotificationCommand struct {
|
||||||
Id int64 `json:"id" binding:"Required"`
|
Id int64 `json:"id" binding:"Required"`
|
||||||
Name string `json:"name" binding:"Required"`
|
Name string `json:"name" binding:"Required"`
|
||||||
Type string `json:"type" binding:"Required"`
|
Type string `json:"type" binding:"Required"`
|
||||||
Settings *simplejson.Json `json:"settings" binding:"Required"`
|
IsDefault bool `json:"isDefault"`
|
||||||
|
Settings *simplejson.Json `json:"settings" binding:"Required"`
|
||||||
|
|
||||||
OrgId int64 `json:"-"`
|
OrgId int64 `json:"-"`
|
||||||
Result *AlertNotification
|
Result *AlertNotification
|
||||||
@ -43,8 +46,20 @@ type DeleteAlertNotificationCommand struct {
|
|||||||
type GetAlertNotificationsQuery struct {
|
type GetAlertNotificationsQuery struct {
|
||||||
Name string
|
Name string
|
||||||
Id int64
|
Id int64
|
||||||
|
OrgId int64
|
||||||
|
|
||||||
|
Result *AlertNotification
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetAlertNotificationsToSendQuery struct {
|
||||||
Ids []int64
|
Ids []int64
|
||||||
OrgId int64
|
OrgId int64
|
||||||
|
|
||||||
Result []*AlertNotification
|
Result []*AlertNotification
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type GetAllAlertNotificationsQuery struct {
|
||||||
|
OrgId int64
|
||||||
|
|
||||||
|
Result []*AlertNotification
|
||||||
|
}
|
||||||
|
@ -6,4 +6,5 @@ const (
|
|||||||
GITHUB OAuthType = iota + 1
|
GITHUB OAuthType = iota + 1
|
||||||
GOOGLE
|
GOOGLE
|
||||||
TWITTER
|
TWITTER
|
||||||
|
GENERIC
|
||||||
)
|
)
|
||||||
|
@ -5,25 +5,21 @@ import (
|
|||||||
|
|
||||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||||
"github.com/grafana/grafana/pkg/services/alerting"
|
"github.com/grafana/grafana/pkg/services/alerting"
|
||||||
"github.com/grafana/grafana/pkg/tsdb"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
defaultTypes []string = []string{"gt", "lt"}
|
defaultTypes []string = []string{"gt", "lt"}
|
||||||
rangedTypes []string = []string{"within_range", "outside_range"}
|
rangedTypes []string = []string{"within_range", "outside_range"}
|
||||||
paramlessTypes []string = []string{"no_value"}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type AlertEvaluator interface {
|
type AlertEvaluator interface {
|
||||||
Eval(timeSeries *tsdb.TimeSeries, reducedValue float64) bool
|
Eval(reducedValue *float64) bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type ParameterlessEvaluator struct {
|
type NoDataEvaluator struct{}
|
||||||
Type string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *ParameterlessEvaluator) Eval(series *tsdb.TimeSeries, reducedValue float64) bool {
|
func (e *NoDataEvaluator) Eval(reducedValue *float64) bool {
|
||||||
return len(series.Points) == 0
|
return reducedValue == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type ThresholdEvaluator struct {
|
type ThresholdEvaluator struct {
|
||||||
@ -47,14 +43,16 @@ func newThresholdEvaludator(typ string, model *simplejson.Json) (*ThresholdEvalu
|
|||||||
return defaultEval, nil
|
return defaultEval, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *ThresholdEvaluator) Eval(series *tsdb.TimeSeries, 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
|
||||||
case "lt":
|
case "lt":
|
||||||
return reducedValue < e.Threshold
|
return *reducedValue < e.Threshold
|
||||||
case "no_value":
|
|
||||||
return len(series.Points) == 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
@ -88,12 +86,16 @@ func newRangedEvaluator(typ string, model *simplejson.Json) (*RangedEvaluator, e
|
|||||||
return rangedEval, nil
|
return rangedEval, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *RangedEvaluator) Eval(series *tsdb.TimeSeries, 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)
|
||||||
case "outside_range":
|
case "outside_range":
|
||||||
return (e.Upper < reducedValue && e.Lower < reducedValue) || (e.Upper > reducedValue && e.Lower > reducedValue)
|
return (e.Upper < *reducedValue && e.Lower < *reducedValue) || (e.Upper > *reducedValue && e.Lower > *reducedValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
@ -113,8 +115,8 @@ func NewAlertEvaluator(model *simplejson.Json) (AlertEvaluator, error) {
|
|||||||
return newRangedEvaluator(typ, model)
|
return newRangedEvaluator(typ, model)
|
||||||
}
|
}
|
||||||
|
|
||||||
if inSlice(typ, paramlessTypes) {
|
if typ == "no_data" {
|
||||||
return &ParameterlessEvaluator{Type: typ}, nil
|
return &NoDataEvaluator{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, alerting.ValidationError{Reason: "Evaludator invalid evaluator type"}
|
return nil, alerting.ValidationError{Reason: "Evaludator invalid evaluator type"}
|
||||||
|
@ -4,7 +4,6 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||||
"github.com/grafana/grafana/pkg/tsdb"
|
|
||||||
. "github.com/smartystreets/goconvey/convey"
|
. "github.com/smartystreets/goconvey/convey"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -15,19 +14,7 @@ func evalutorScenario(json string, reducedValue float64, datapoints ...float64)
|
|||||||
evaluator, err := NewAlertEvaluator(jsonModel)
|
evaluator, err := NewAlertEvaluator(jsonModel)
|
||||||
So(err, ShouldBeNil)
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
var timeserie [][2]float64
|
return evaluator.Eval(reducedValue)
|
||||||
dummieTimestamp := float64(521452145)
|
|
||||||
|
|
||||||
for _, v := range datapoints {
|
|
||||||
timeserie = append(timeserie, [2]float64{v, dummieTimestamp})
|
|
||||||
}
|
|
||||||
|
|
||||||
tsdb := &tsdb.TimeSeries{
|
|
||||||
Name: "test time serie",
|
|
||||||
Points: timeserie,
|
|
||||||
}
|
|
||||||
|
|
||||||
return evaluator.Eval(tsdb, reducedValue)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEvalutors(t *testing.T) {
|
func TestEvalutors(t *testing.T) {
|
||||||
|
@ -40,22 +40,27 @@ func (c *QueryCondition) Eval(context *alerting.EvalContext) {
|
|||||||
|
|
||||||
for _, series := range seriesList {
|
for _, series := range seriesList {
|
||||||
reducedValue := c.Reducer.Reduce(series)
|
reducedValue := c.Reducer.Reduce(series)
|
||||||
evalMatch := c.Evaluator.Eval(series, reducedValue)
|
evalMatch := c.Evaluator.Eval(reducedValue)
|
||||||
|
|
||||||
if context.IsTestRun {
|
if context.IsTestRun {
|
||||||
context.Logs = append(context.Logs, &alerting.ResultLogEntry{
|
context.Logs = append(context.Logs, &alerting.ResultLogEntry{
|
||||||
Message: fmt.Sprintf("Condition[%d]: Eval: %v, Metric: %s, Value: %1.3f", c.Index, evalMatch, series.Name, reducedValue),
|
Message: fmt.Sprintf("Condition[%d]: Eval: %v, Metric: %s, Value: %1.3f", c.Index, evalMatch, series.Name, *reducedValue),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if evalMatch {
|
if evalMatch {
|
||||||
context.EvalMatches = append(context.EvalMatches, &alerting.EvalMatch{
|
context.EvalMatches = append(context.EvalMatches, &alerting.EvalMatch{
|
||||||
Metric: series.Name,
|
Metric: series.Name,
|
||||||
Value: reducedValue,
|
Value: *reducedValue,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
context.Firing = evalMatch
|
context.Firing = evalMatch
|
||||||
|
|
||||||
|
// handle no data scenario
|
||||||
|
if reducedValue == nil {
|
||||||
|
context.NoDataFound = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -106,10 +111,16 @@ func (c *QueryCondition) getRequestForAlertRule(datasource *m.DataSource) *tsdb.
|
|||||||
RefId: "A",
|
RefId: "A",
|
||||||
Query: c.Query.Model.Get("target").MustString(),
|
Query: c.Query.Model.Get("target").MustString(),
|
||||||
DataSource: &tsdb.DataSourceInfo{
|
DataSource: &tsdb.DataSourceInfo{
|
||||||
Id: datasource.Id,
|
Id: datasource.Id,
|
||||||
Name: datasource.Name,
|
Name: datasource.Name,
|
||||||
PluginId: datasource.Type,
|
PluginId: datasource.Type,
|
||||||
Url: datasource.Url,
|
Url: datasource.Url,
|
||||||
|
User: datasource.User,
|
||||||
|
Password: datasource.Password,
|
||||||
|
Database: datasource.Database,
|
||||||
|
BasicAuth: datasource.BasicAuth,
|
||||||
|
BasicAuthUser: datasource.BasicAuthUser,
|
||||||
|
BasicAuthPassword: datasource.BasicAuthPassword,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -1,52 +1,72 @@
|
|||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
type SimpleReducer struct {
|
type SimpleReducer struct {
|
||||||
Type string
|
Type string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SimpleReducer) Reduce(series *tsdb.TimeSeries) float64 {
|
func (s *SimpleReducer) Reduce(series *tsdb.TimeSeries) *float64 {
|
||||||
var value float64 = 0
|
if len(series.Points) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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] {
|
||||||
if value > point[0] {
|
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))
|
||||||
}
|
}
|
||||||
|
|
||||||
return value
|
if allNull {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &value
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSimpleReducer(typ string) *SimpleReducer {
|
func NewSimpleReducer(typ string) *SimpleReducer {
|
||||||
|
@ -26,38 +26,55 @@ type EvalContext struct {
|
|||||||
dashboardSlug string
|
dashboardSlug string
|
||||||
ImagePublicUrl string
|
ImagePublicUrl string
|
||||||
ImageOnDiskPath string
|
ImageOnDiskPath string
|
||||||
|
NoDataFound bool
|
||||||
|
RetryCount int
|
||||||
|
}
|
||||||
|
|
||||||
|
type StateDescription struct {
|
||||||
|
Color string
|
||||||
|
Text string
|
||||||
|
Data string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *EvalContext) GetStateModel() *StateDescription {
|
||||||
|
switch c.Rule.State {
|
||||||
|
case m.AlertStateOK:
|
||||||
|
return &StateDescription{
|
||||||
|
Color: "#36a64f",
|
||||||
|
Text: "OK",
|
||||||
|
}
|
||||||
|
case m.AlertStateUnknown:
|
||||||
|
return &StateDescription{
|
||||||
|
Color: "#888888",
|
||||||
|
Text: "UNKNOWN",
|
||||||
|
}
|
||||||
|
case m.AlertStateExeuctionError:
|
||||||
|
return &StateDescription{
|
||||||
|
Color: "#000",
|
||||||
|
Text: "EXECUTION_ERROR",
|
||||||
|
}
|
||||||
|
case m.AlertStateWarning:
|
||||||
|
return &StateDescription{
|
||||||
|
Color: "#fd821b",
|
||||||
|
Text: "WARNING",
|
||||||
|
}
|
||||||
|
case m.AlertStateCritical:
|
||||||
|
return &StateDescription{
|
||||||
|
Color: "#D63232",
|
||||||
|
Text: "CRITICAL",
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
panic("Unknown rule state " + c.Rule.State)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *EvalContext) GetDurationMs() float64 {
|
func (a *EvalContext) GetDurationMs() float64 {
|
||||||
return float64(a.EndTime.Nanosecond()-a.StartTime.Nanosecond()) / float64(1000000)
|
return float64(a.EndTime.Nanosecond()-a.StartTime.Nanosecond()) / float64(1000000)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *EvalContext) GetColor() string {
|
|
||||||
if !c.Firing {
|
|
||||||
return "#36a64f"
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.Rule.Severity == m.AlertSeverityWarning {
|
|
||||||
return "#fd821b"
|
|
||||||
} else {
|
|
||||||
return "#D63232"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *EvalContext) GetStateText() string {
|
|
||||||
if !c.Firing {
|
|
||||||
return "OK"
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.Rule.Severity == m.AlertSeverityWarning {
|
|
||||||
return "WARNING"
|
|
||||||
} else {
|
|
||||||
return "CRITICAL"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *EvalContext) GetNotificationTitle() string {
|
func (c *EvalContext) GetNotificationTitle() string {
|
||||||
return "[" + c.GetStateText() + "] " + c.Rule.Name
|
return "[" + c.GetStateModel().Text + "] " + c.Rule.Name
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *EvalContext) getDashboardSlug() (string, error) {
|
func (c *EvalContext) getDashboardSlug() (string, error) {
|
||||||
@ -101,5 +118,6 @@ func NewEvalContext(rule *Rule) *EvalContext {
|
|||||||
DoneChan: make(chan bool, 1),
|
DoneChan: make(chan bool, 1),
|
||||||
CancelChan: make(chan bool, 1),
|
CancelChan: make(chan bool, 1),
|
||||||
log: log.New("alerting.evalContext"),
|
log: log.New("alerting.evalContext"),
|
||||||
|
RetryCount: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,10 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/metrics"
|
"github.com/grafana/grafana/pkg/metrics"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
MaxRetries int = 1
|
||||||
|
)
|
||||||
|
|
||||||
type DefaultEvalHandler struct {
|
type DefaultEvalHandler struct {
|
||||||
log log.Logger
|
log log.Logger
|
||||||
alertJobTimeout time.Duration
|
alertJobTimeout time.Duration
|
||||||
@ -21,7 +25,6 @@ func NewEvalHandler() *DefaultEvalHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (e *DefaultEvalHandler) Eval(context *EvalContext) {
|
func (e *DefaultEvalHandler) Eval(context *EvalContext) {
|
||||||
|
|
||||||
go e.eval(context)
|
go e.eval(context)
|
||||||
|
|
||||||
select {
|
select {
|
||||||
@ -29,13 +32,36 @@ func (e *DefaultEvalHandler) Eval(context *EvalContext) {
|
|||||||
context.Error = fmt.Errorf("Timeout")
|
context.Error = fmt.Errorf("Timeout")
|
||||||
context.EndTime = time.Now()
|
context.EndTime = time.Now()
|
||||||
e.log.Debug("Job Execution timeout", "alertId", context.Rule.Id)
|
e.log.Debug("Job Execution timeout", "alertId", context.Rule.Id)
|
||||||
|
e.retry(context)
|
||||||
case <-context.DoneChan:
|
case <-context.DoneChan:
|
||||||
e.log.Debug("Job Execution done", "timeMs", context.GetDurationMs(), "alertId", context.Rule.Id, "firing", context.Firing)
|
e.log.Debug("Job Execution done", "timeMs", context.GetDurationMs(), "alertId", context.Rule.Id, "firing", context.Firing)
|
||||||
}
|
|
||||||
|
|
||||||
|
if context.Error != nil {
|
||||||
|
e.retry(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *DefaultEvalHandler) retry(context *EvalContext) {
|
||||||
|
e.log.Debug("Retrying eval exeuction", "alertId", context.Rule.Id)
|
||||||
|
|
||||||
|
context.RetryCount++
|
||||||
|
if context.RetryCount > MaxRetries {
|
||||||
|
context.DoneChan = make(chan bool, 1)
|
||||||
|
context.CancelChan = make(chan bool, 1)
|
||||||
|
e.Eval(context)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
||||||
|
@ -40,6 +40,5 @@ func TestAlertingExecutor(t *testing.T) {
|
|||||||
handler.eval(context)
|
handler.eval(context)
|
||||||
So(context.Firing, ShouldEqual, false)
|
So(context.Firing, ShouldEqual, false)
|
||||||
})
|
})
|
||||||
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,10 @@
|
|||||||
package alerting
|
package alerting
|
||||||
|
|
||||||
import "time"
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/models"
|
||||||
|
)
|
||||||
|
|
||||||
type EvalHandler interface {
|
type EvalHandler interface {
|
||||||
Eval(context *EvalContext)
|
Eval(context *EvalContext)
|
||||||
@ -15,6 +19,7 @@ type Notifier interface {
|
|||||||
Notify(alertResult *EvalContext)
|
Notify(alertResult *EvalContext)
|
||||||
GetType() string
|
GetType() string
|
||||||
NeedsImage() bool
|
NeedsImage() bool
|
||||||
|
MatchSeverity(result models.AlertSeverityType) bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type Condition interface {
|
type Condition interface {
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
package alerting
|
package alerting
|
||||||
|
|
||||||
type Job struct {
|
type Job struct {
|
||||||
Offset int64
|
Offset int64
|
||||||
Delay bool
|
OffsetWait bool
|
||||||
Running bool
|
Delay bool
|
||||||
Rule *Rule
|
Running bool
|
||||||
|
Rule *Rule
|
||||||
}
|
}
|
||||||
|
|
||||||
type ResultLogEntry struct {
|
type ResultLogEntry struct {
|
||||||
|
@ -28,10 +28,14 @@ func (n *RootNotifier) NeedsImage() bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (n *RootNotifier) MatchSeverity(result m.AlertSeverityType) bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func (n *RootNotifier) Notify(context *EvalContext) {
|
func (n *RootNotifier) Notify(context *EvalContext) {
|
||||||
n.log.Info("Sending notifications for", "ruleId", context.Rule.Id)
|
n.log.Info("Sending notifications for", "ruleId", context.Rule.Id)
|
||||||
|
|
||||||
notifiers, err := n.getNotifiers(context.Rule.OrgId, context.Rule.Notifications)
|
notifiers, err := n.getNotifiers(context.Rule.OrgId, context.Rule.Notifications, context)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
n.log.Error("Failed to read notifications", "error", err)
|
n.log.Error("Failed to read notifications", "error", err)
|
||||||
return
|
return
|
||||||
@ -46,15 +50,17 @@ func (n *RootNotifier) Notify(context *EvalContext) {
|
|||||||
n.log.Error("Failed to upload alert panel image", "error", err)
|
n.log.Error("Failed to upload alert panel image", "error", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
n.sendNotifications(notifiers, context)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *RootNotifier) sendNotifications(notifiers []Notifier, context *EvalContext) {
|
||||||
for _, notifier := range notifiers {
|
for _, notifier := range notifiers {
|
||||||
n.log.Info("Sending notification", "firing", context.Firing, "type", notifier.GetType())
|
n.log.Info("Sending notification", "firing", context.Firing, "type", notifier.GetType())
|
||||||
|
|
||||||
go notifier.Notify(context)
|
go notifier.Notify(context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *RootNotifier) uploadImage(context *EvalContext) error {
|
func (n *RootNotifier) uploadImage(context *EvalContext) error {
|
||||||
|
|
||||||
uploader, _ := imguploader.NewImageUploader()
|
uploader, _ := imguploader.NewImageUploader()
|
||||||
|
|
||||||
imageUrl, err := context.GetImageUrl()
|
imageUrl, err := context.GetImageUrl()
|
||||||
@ -85,29 +91,28 @@ func (n *RootNotifier) uploadImage(context *EvalContext) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *RootNotifier) getNotifiers(orgId int64, notificationIds []int64) ([]Notifier, error) {
|
func (n *RootNotifier) getNotifiers(orgId int64, notificationIds []int64, context *EvalContext) ([]Notifier, error) {
|
||||||
if len(notificationIds) == 0 {
|
query := &m.GetAlertNotificationsToSendQuery{OrgId: orgId, Ids: notificationIds}
|
||||||
return []Notifier{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
query := &m.GetAlertNotificationsQuery{OrgId: orgId, Ids: notificationIds}
|
|
||||||
if err := bus.Dispatch(query); err != nil {
|
if err := bus.Dispatch(query); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var result []Notifier
|
var result []Notifier
|
||||||
for _, notification := range query.Result {
|
for _, notification := range query.Result {
|
||||||
if not, err := n.getNotifierFor(notification); err != nil {
|
if not, err := n.createNotifierFor(notification); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
} else {
|
} else {
|
||||||
result = append(result, not)
|
if shouldUseNotification(not, context) {
|
||||||
|
result = append(result, not)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *RootNotifier) getNotifierFor(model *m.AlertNotification) (Notifier, error) {
|
func (n *RootNotifier) createNotifierFor(model *m.AlertNotification) (Notifier, error) {
|
||||||
factory, found := notifierFactories[model.Type]
|
factory, found := notifierFactories[model.Type]
|
||||||
if !found {
|
if !found {
|
||||||
return nil, errors.New("Unsupported notification type")
|
return nil, errors.New("Unsupported notification type")
|
||||||
@ -116,6 +121,18 @@ func (n *RootNotifier) getNotifierFor(model *m.AlertNotification) (Notifier, err
|
|||||||
return factory(model)
|
return factory(model)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func shouldUseNotification(notifier Notifier, context *EvalContext) bool {
|
||||||
|
if !context.Firing {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if context.Error != nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return notifier.MatchSeverity(context.Rule.Severity)
|
||||||
|
}
|
||||||
|
|
||||||
type NotifierFactory func(notification *m.AlertNotification) (Notifier, error)
|
type NotifierFactory func(notification *m.AlertNotification) (Notifier, error)
|
||||||
|
|
||||||
var notifierFactories map[string]NotifierFactory = make(map[string]NotifierFactory)
|
var notifierFactories map[string]NotifierFactory = make(map[string]NotifierFactory)
|
||||||
|
@ -1,114 +1,82 @@
|
|||||||
package alerting
|
package alerting
|
||||||
|
|
||||||
// func TestAlertNotificationExtraction(t *testing.T) {
|
import (
|
||||||
// Convey("Notifier tests", t, func() {
|
"testing"
|
||||||
// Convey("rules for sending notifications", func() {
|
|
||||||
// dummieNotifier := NotifierImpl{}
|
"fmt"
|
||||||
//
|
|
||||||
// result := &AlertResult{
|
"github.com/grafana/grafana/pkg/models"
|
||||||
// State: alertstates.Critical,
|
. "github.com/smartystreets/goconvey/convey"
|
||||||
// }
|
)
|
||||||
//
|
|
||||||
// notifier := &Notification{
|
type FakeNotifier struct {
|
||||||
// Name: "Test Notifier",
|
FakeMatchResult bool
|
||||||
// Type: "TestType",
|
}
|
||||||
// SendCritical: true,
|
|
||||||
// SendWarning: true,
|
func (fn *FakeNotifier) GetType() string {
|
||||||
// }
|
return "FakeNotifier"
|
||||||
//
|
}
|
||||||
// Convey("Should send notification", func() {
|
|
||||||
// So(dummieNotifier.ShouldDispath(result, notifier), ShouldBeTrue)
|
func (fn *FakeNotifier) NeedsImage() bool {
|
||||||
// })
|
return true
|
||||||
//
|
}
|
||||||
// Convey("warn:false and state:warn should not send", func() {
|
|
||||||
// result.State = alertstates.Warn
|
func (fn *FakeNotifier) Notify(alertResult *EvalContext) {}
|
||||||
// notifier.SendWarning = false
|
|
||||||
// So(dummieNotifier.ShouldDispath(result, notifier), ShouldBeFalse)
|
func (fn *FakeNotifier) MatchSeverity(result models.AlertSeverityType) bool {
|
||||||
// })
|
return fn.FakeMatchResult
|
||||||
// })
|
}
|
||||||
//
|
|
||||||
// Convey("Parsing alert notification from settings", func() {
|
func TestAlertNotificationExtraction(t *testing.T) {
|
||||||
// Convey("Parsing email", func() {
|
|
||||||
// Convey("empty settings should return error", func() {
|
Convey("Notifier tests", t, func() {
|
||||||
// json := `{ }`
|
Convey("none firing alerts", func() {
|
||||||
//
|
ctx := &EvalContext{
|
||||||
// settingsJSON, _ := simplejson.NewJson([]byte(json))
|
Firing: false,
|
||||||
// model := &m.AlertNotification{
|
Rule: &Rule{
|
||||||
// Name: "ops",
|
Severity: models.AlertSeverityCritical,
|
||||||
// Type: "email",
|
},
|
||||||
// Settings: settingsJSON,
|
}
|
||||||
// }
|
notifier := &FakeNotifier{FakeMatchResult: false}
|
||||||
//
|
|
||||||
// _, err := NewNotificationFromDBModel(model)
|
So(shouldUseNotification(notifier, ctx), ShouldBeTrue)
|
||||||
// So(err, ShouldNotBeNil)
|
})
|
||||||
// })
|
|
||||||
//
|
Convey("exeuction error cannot be ignored", func() {
|
||||||
// Convey("from settings", func() {
|
ctx := &EvalContext{
|
||||||
// json := `
|
Firing: true,
|
||||||
// {
|
Error: fmt.Errorf("I used to be a programmer just like you"),
|
||||||
// "to": "ops@grafana.org"
|
Rule: &Rule{
|
||||||
// }`
|
Severity: models.AlertSeverityCritical,
|
||||||
//
|
},
|
||||||
// settingsJSON, _ := simplejson.NewJson([]byte(json))
|
}
|
||||||
// model := &m.AlertNotification{
|
notifier := &FakeNotifier{FakeMatchResult: false}
|
||||||
// Name: "ops",
|
|
||||||
// Type: "email",
|
So(shouldUseNotification(notifier, ctx), ShouldBeTrue)
|
||||||
// Settings: settingsJSON,
|
})
|
||||||
// }
|
|
||||||
//
|
Convey("firing alert that match", func() {
|
||||||
// not, err := NewNotificationFromDBModel(model)
|
ctx := &EvalContext{
|
||||||
//
|
Firing: true,
|
||||||
// So(err, ShouldBeNil)
|
Rule: &Rule{
|
||||||
// So(not.Name, ShouldEqual, "ops")
|
Severity: models.AlertSeverityCritical,
|
||||||
// So(not.Type, ShouldEqual, "email")
|
},
|
||||||
// So(reflect.TypeOf(not.Notifierr).Elem().String(), ShouldEqual, "alerting.EmailNotifier")
|
}
|
||||||
//
|
notifier := &FakeNotifier{FakeMatchResult: true}
|
||||||
// email := not.Notifierr.(*EmailNotifier)
|
|
||||||
// So(email.To, ShouldEqual, "ops@grafana.org")
|
So(shouldUseNotification(notifier, ctx), ShouldBeTrue)
|
||||||
// })
|
})
|
||||||
// })
|
|
||||||
//
|
Convey("firing alert that dont match", func() {
|
||||||
// Convey("Parsing webhook", func() {
|
ctx := &EvalContext{
|
||||||
// Convey("empty settings should return error", func() {
|
Firing: true,
|
||||||
// json := `{ }`
|
Rule: &Rule{
|
||||||
//
|
Severity: models.AlertSeverityCritical,
|
||||||
// settingsJSON, _ := simplejson.NewJson([]byte(json))
|
},
|
||||||
// model := &m.AlertNotification{
|
}
|
||||||
// Name: "ops",
|
notifier := &FakeNotifier{FakeMatchResult: false}
|
||||||
// Type: "webhook",
|
|
||||||
// Settings: settingsJSON,
|
So(shouldUseNotification(notifier, ctx), ShouldBeFalse)
|
||||||
// }
|
})
|
||||||
//
|
})
|
||||||
// _, err := NewNotificationFromDBModel(model)
|
}
|
||||||
// So(err, ShouldNotBeNil)
|
|
||||||
// })
|
|
||||||
//
|
|
||||||
// Convey("from settings", func() {
|
|
||||||
// json := `
|
|
||||||
// {
|
|
||||||
// "url": "http://localhost:3000",
|
|
||||||
// "username": "username",
|
|
||||||
// "password": "password"
|
|
||||||
// }`
|
|
||||||
//
|
|
||||||
// settingsJSON, _ := simplejson.NewJson([]byte(json))
|
|
||||||
// model := &m.AlertNotification{
|
|
||||||
// Name: "slack",
|
|
||||||
// Type: "webhook",
|
|
||||||
// Settings: settingsJSON,
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// not, err := NewNotificationFromDBModel(model)
|
|
||||||
//
|
|
||||||
// So(err, ShouldBeNil)
|
|
||||||
// So(not.Name, ShouldEqual, "slack")
|
|
||||||
// So(not.Type, ShouldEqual, "webhook")
|
|
||||||
// So(reflect.TypeOf(not.Notifierr).Elem().String(), ShouldEqual, "alerting.WebhookNotifier")
|
|
||||||
//
|
|
||||||
// webhook := not.Notifierr.(*WebhookNotifier)
|
|
||||||
// So(webhook.Url, ShouldEqual, "http://localhost:3000")
|
|
||||||
// })
|
|
||||||
// })
|
|
||||||
// })
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
|
@ -1,8 +1,34 @@
|
|||||||
package notifiers
|
package notifiers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||||
|
"github.com/grafana/grafana/pkg/models"
|
||||||
|
)
|
||||||
|
|
||||||
type NotifierBase struct {
|
type NotifierBase struct {
|
||||||
Name string
|
Name string
|
||||||
Type string
|
Type string
|
||||||
|
SeverityFilter models.AlertSeverityType
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewNotifierBase(name, notifierType string, model *simplejson.Json) NotifierBase {
|
||||||
|
base := NotifierBase{Name: name, Type: notifierType}
|
||||||
|
|
||||||
|
severityFilter := models.AlertSeverityType(model.Get("severityFilter").MustString(""))
|
||||||
|
|
||||||
|
if severityFilter == models.AlertSeverityCritical || severityFilter == models.AlertSeverityWarning {
|
||||||
|
base.SeverityFilter = severityFilter
|
||||||
|
}
|
||||||
|
|
||||||
|
return base
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NotifierBase) MatchSeverity(result models.AlertSeverityType) bool {
|
||||||
|
if !n.SeverityFilter.IsValid() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return n.SeverityFilter == result
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *NotifierBase) GetType() string {
|
func (n *NotifierBase) GetType() string {
|
||||||
|
36
pkg/services/alerting/notifiers/base_test.go
Normal file
36
pkg/services/alerting/notifiers/base_test.go
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
package notifiers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||||
|
m "github.com/grafana/grafana/pkg/models"
|
||||||
|
. "github.com/smartystreets/goconvey/convey"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBaseNotifier(t *testing.T) {
|
||||||
|
Convey("Parsing base notification severity", t, func() {
|
||||||
|
|
||||||
|
Convey("matches", func() {
|
||||||
|
json := `
|
||||||
|
{
|
||||||
|
"severityFilter": "critical"
|
||||||
|
}`
|
||||||
|
|
||||||
|
settingsJSON, _ := simplejson.NewJson([]byte(json))
|
||||||
|
not := NewNotifierBase("ops", "email", settingsJSON)
|
||||||
|
So(not.MatchSeverity(m.AlertSeverityCritical), ShouldBeTrue)
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("does not match", func() {
|
||||||
|
json := `
|
||||||
|
{
|
||||||
|
"severityFilter": "critical"
|
||||||
|
}`
|
||||||
|
|
||||||
|
settingsJSON, _ := simplejson.NewJson([]byte(json))
|
||||||
|
not := NewNotifierBase("ops", "email", settingsJSON)
|
||||||
|
So(not.MatchSeverity(m.AlertSeverityWarning), ShouldBeFalse)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
@ -1 +0,0 @@
|
|||||||
package notifiers
|
|
@ -29,12 +29,9 @@ func NewEmailNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &EmailNotifier{
|
return &EmailNotifier{
|
||||||
NotifierBase: NotifierBase{
|
NotifierBase: NewNotifierBase(model.Name, model.Type, model.Settings),
|
||||||
Name: model.Name,
|
Addresses: strings.Split(addressesString, "\n"),
|
||||||
Type: model.Type,
|
log: log.New("alerting.notifier.email"),
|
||||||
},
|
|
||||||
Addresses: strings.Split(addressesString, "\n"),
|
|
||||||
log: log.New("alerting.notifier.email"),
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -54,7 +51,7 @@ func (this *EmailNotifier) Notify(context *alerting.EvalContext) {
|
|||||||
"State": context.Rule.State,
|
"State": context.Rule.State,
|
||||||
"Name": context.Rule.Name,
|
"Name": context.Rule.Name,
|
||||||
"Severity": context.Rule.Severity,
|
"Severity": context.Rule.Severity,
|
||||||
"SeverityColor": context.GetColor(),
|
"SeverityColor": context.GetStateModel().Color,
|
||||||
"Message": context.Rule.Message,
|
"Message": context.Rule.Message,
|
||||||
"RuleUrl": ruleUrl,
|
"RuleUrl": ruleUrl,
|
||||||
"ImageLink": context.ImagePublicUrl,
|
"ImageLink": context.ImagePublicUrl,
|
||||||
|
@ -23,12 +23,9 @@ func NewSlackNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &SlackNotifier{
|
return &SlackNotifier{
|
||||||
NotifierBase: NotifierBase{
|
NotifierBase: NewNotifierBase(model.Name, model.Type, model.Settings),
|
||||||
Name: model.Name,
|
Url: url,
|
||||||
Type: model.Type,
|
log: log.New("alerting.notifier.slack"),
|
||||||
},
|
|
||||||
Url: url,
|
|
||||||
log: log.New("alerting.notifier.slack"),
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -61,13 +58,26 @@ func (this *SlackNotifier) Notify(context *alerting.EvalContext) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if context.Error != nil {
|
||||||
|
fields = append(fields, map[string]interface{}{
|
||||||
|
"title": "Error message",
|
||||||
|
"value": context.Error.Error(),
|
||||||
|
"short": false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
message := ""
|
||||||
|
if context.Rule.State != m.AlertStateOK { //dont add message when going back to alert state ok.
|
||||||
|
message = context.Rule.Message
|
||||||
|
}
|
||||||
|
|
||||||
body := map[string]interface{}{
|
body := map[string]interface{}{
|
||||||
"attachments": []map[string]interface{}{
|
"attachments": []map[string]interface{}{
|
||||||
{
|
{
|
||||||
"color": context.GetColor(),
|
"color": context.GetStateModel().Color,
|
||||||
"title": context.GetNotificationTitle(),
|
"title": context.GetNotificationTitle(),
|
||||||
"title_link": ruleUrl,
|
"title_link": ruleUrl,
|
||||||
"text": context.Rule.Message,
|
"text": message,
|
||||||
"fields": fields,
|
"fields": fields,
|
||||||
"image_url": context.ImagePublicUrl,
|
"image_url": context.ImagePublicUrl,
|
||||||
"footer": "Grafana v" + setting.BuildVersion,
|
"footer": "Grafana v" + setting.BuildVersion,
|
||||||
|
@ -20,14 +20,11 @@ func NewWebHookNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &WebhookNotifier{
|
return &WebhookNotifier{
|
||||||
NotifierBase: NotifierBase{
|
NotifierBase: NewNotifierBase(model.Name, model.Type, model.Settings),
|
||||||
Name: model.Name,
|
Url: url,
|
||||||
Type: model.Type,
|
User: model.Settings.Get("user").MustString(),
|
||||||
},
|
Password: model.Settings.Get("password").MustString(),
|
||||||
Url: url,
|
log: log.New("alerting.notifier.webhook"),
|
||||||
User: model.Settings.Get("user").MustString(),
|
|
||||||
Password: model.Settings.Get("password").MustString(),
|
|
||||||
log: log.New("alerting.notifier.webhook"),
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/bus"
|
"github.com/grafana/grafana/pkg/bus"
|
||||||
|
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||||
"github.com/grafana/grafana/pkg/log"
|
"github.com/grafana/grafana/pkg/log"
|
||||||
"github.com/grafana/grafana/pkg/metrics"
|
"github.com/grafana/grafana/pkg/metrics"
|
||||||
m "github.com/grafana/grafana/pkg/models"
|
m "github.com/grafana/grafana/pkg/models"
|
||||||
@ -30,17 +31,25 @@ func (handler *DefaultResultHandler) Handle(ctx *EvalContext) {
|
|||||||
oldState := ctx.Rule.State
|
oldState := ctx.Rule.State
|
||||||
|
|
||||||
exeuctionError := ""
|
exeuctionError := ""
|
||||||
|
annotationData := simplejson.New()
|
||||||
if ctx.Error != nil {
|
if ctx.Error != nil {
|
||||||
handler.log.Error("Alert Rule Result Error", "ruleId", ctx.Rule.Id, "error", ctx.Error)
|
handler.log.Error("Alert Rule Result Error", "ruleId", ctx.Rule.Id, "error", ctx.Error)
|
||||||
ctx.Rule.State = m.AlertStateExeuctionError
|
ctx.Rule.State = m.AlertStateExeuctionError
|
||||||
exeuctionError = ctx.Error.Error()
|
exeuctionError = ctx.Error.Error()
|
||||||
|
annotationData.Set("errorMessage", exeuctionError)
|
||||||
} else if ctx.Firing {
|
} else if ctx.Firing {
|
||||||
ctx.Rule.State = m.AlertStateType(ctx.Rule.Severity)
|
ctx.Rule.State = m.AlertStateType(ctx.Rule.Severity)
|
||||||
|
annotationData = simplejson.NewFromAny(ctx.EvalMatches)
|
||||||
} else {
|
} else {
|
||||||
ctx.Rule.State = m.AlertStateOK
|
// handle no data case
|
||||||
|
if ctx.NoDataFound {
|
||||||
|
ctx.Rule.State = ctx.Rule.NoDataState
|
||||||
|
} else {
|
||||||
|
ctx.Rule.State = m.AlertStateOK
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
countSeverity(ctx.Rule.Severity)
|
countStateResult(ctx.Rule.State)
|
||||||
if ctx.Rule.State != oldState {
|
if ctx.Rule.State != oldState {
|
||||||
handler.log.Info("New state change", "alertId", ctx.Rule.Id, "newState", ctx.Rule.State, "oldState", oldState)
|
handler.log.Info("New state change", "alertId", ctx.Rule.Id, "newState", ctx.Rule.State, "oldState", oldState)
|
||||||
|
|
||||||
@ -61,10 +70,11 @@ func (handler *DefaultResultHandler) Handle(ctx *EvalContext) {
|
|||||||
Type: annotations.AlertType,
|
Type: annotations.AlertType,
|
||||||
AlertId: ctx.Rule.Id,
|
AlertId: ctx.Rule.Id,
|
||||||
Title: ctx.Rule.Name,
|
Title: ctx.Rule.Name,
|
||||||
Text: ctx.GetStateText(),
|
Text: ctx.GetStateModel().Text,
|
||||||
NewState: string(ctx.Rule.State),
|
NewState: string(ctx.Rule.State),
|
||||||
PrevState: string(oldState),
|
PrevState: string(oldState),
|
||||||
Timestamp: time.Now(),
|
Timestamp: time.Now(),
|
||||||
|
Data: annotationData,
|
||||||
}
|
}
|
||||||
|
|
||||||
annotationRepo := annotations.GetRepository()
|
annotationRepo := annotations.GetRepository()
|
||||||
@ -76,15 +86,19 @@ func (handler *DefaultResultHandler) Handle(ctx *EvalContext) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func countSeverity(state m.AlertSeverityType) {
|
func countStateResult(state m.AlertStateType) {
|
||||||
switch state {
|
switch state {
|
||||||
case m.AlertSeverityOK:
|
case m.AlertStateCritical:
|
||||||
metrics.M_Alerting_Result_Ok.Inc(1)
|
metrics.M_Alerting_Result_State_Critical.Inc(1)
|
||||||
case m.AlertSeverityInfo:
|
case m.AlertStateWarning:
|
||||||
metrics.M_Alerting_Result_Info.Inc(1)
|
metrics.M_Alerting_Result_State_Warning.Inc(1)
|
||||||
case m.AlertSeverityWarning:
|
case m.AlertStateOK:
|
||||||
metrics.M_Alerting_Result_Warning.Inc(1)
|
metrics.M_Alerting_Result_State_Ok.Inc(1)
|
||||||
case m.AlertSeverityCritical:
|
case m.AlertStatePaused:
|
||||||
metrics.M_Alerting_Result_Critical.Inc(1)
|
metrics.M_Alerting_Result_State_Paused.Inc(1)
|
||||||
|
case m.AlertStateUnknown:
|
||||||
|
metrics.M_Alerting_Result_State_Unknown.Inc(1)
|
||||||
|
case m.AlertStateExeuctionError:
|
||||||
|
metrics.M_Alerting_Result_State_ExecutionError.Inc(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,6 +18,7 @@ type Rule struct {
|
|||||||
Frequency int64
|
Frequency int64
|
||||||
Name string
|
Name string
|
||||||
Message string
|
Message string
|
||||||
|
NoDataState m.AlertStateType
|
||||||
State m.AlertStateType
|
State m.AlertStateType
|
||||||
Severity m.AlertSeverityType
|
Severity m.AlertSeverityType
|
||||||
Conditions []Condition
|
Conditions []Condition
|
||||||
@ -67,6 +68,7 @@ func NewRuleFromDBAlert(ruleDef *m.Alert) (*Rule, error) {
|
|||||||
model.Frequency = ruleDef.Frequency
|
model.Frequency = ruleDef.Frequency
|
||||||
model.Severity = ruleDef.Severity
|
model.Severity = ruleDef.Severity
|
||||||
model.State = ruleDef.State
|
model.State = ruleDef.State
|
||||||
|
model.NoDataState = m.AlertStateType(ruleDef.Settings.Get("noDataState").MustString("unknown"))
|
||||||
|
|
||||||
for _, v := range ruleDef.Settings.Get("notifications").MustArray() {
|
for _, v := range ruleDef.Settings.Get("notifications").MustArray() {
|
||||||
jsonModel := simplejson.NewFromAny(v)
|
jsonModel := simplejson.NewFromAny(v)
|
||||||
|
@ -4,7 +4,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||||
"github.com/grafana/grafana/pkg/models"
|
m "github.com/grafana/grafana/pkg/models"
|
||||||
. "github.com/smartystreets/goconvey/convey"
|
. "github.com/smartystreets/goconvey/convey"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -45,6 +45,7 @@ func TestAlertRuleModel(t *testing.T) {
|
|||||||
"name": "name2",
|
"name": "name2",
|
||||||
"description": "desc2",
|
"description": "desc2",
|
||||||
"handler": 0,
|
"handler": 0,
|
||||||
|
"noDataMode": "critical",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"frequency": "60s",
|
"frequency": "60s",
|
||||||
"conditions": [
|
"conditions": [
|
||||||
@ -63,7 +64,7 @@ func TestAlertRuleModel(t *testing.T) {
|
|||||||
alertJSON, jsonErr := simplejson.NewJson([]byte(json))
|
alertJSON, jsonErr := simplejson.NewJson([]byte(json))
|
||||||
So(jsonErr, ShouldBeNil)
|
So(jsonErr, ShouldBeNil)
|
||||||
|
|
||||||
alert := &models.Alert{
|
alert := &m.Alert{
|
||||||
Id: 1,
|
Id: 1,
|
||||||
OrgId: 1,
|
OrgId: 1,
|
||||||
DashboardId: 1,
|
DashboardId: 1,
|
||||||
@ -80,6 +81,10 @@ func TestAlertRuleModel(t *testing.T) {
|
|||||||
Convey("Can read notifications", func() {
|
Convey("Can read notifications", func() {
|
||||||
So(len(alertRule.Notifications), ShouldEqual, 2)
|
So(len(alertRule.Notifications), ShouldEqual, 2)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
Convey("Can read noDataMode", func() {
|
||||||
|
So(len(alertRule.NoDataMode), ShouldEqual, m.AlertStateCritical)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package alerting
|
package alerting
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"math"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/log"
|
"github.com/grafana/grafana/pkg/log"
|
||||||
@ -34,8 +35,8 @@ func (s *SchedulerImpl) Update(rules []*Rule) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
job.Rule = rule
|
job.Rule = rule
|
||||||
job.Offset = int64(i)
|
job.Offset = ((rule.Frequency * 1000) / int64(len(rules))) * int64(i)
|
||||||
|
job.Offset = int64(math.Floor(float64(job.Offset) / 1000))
|
||||||
jobs[rule.Id] = job
|
jobs[rule.Id] = job
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -46,9 +47,27 @@ func (s *SchedulerImpl) Tick(tickTime time.Time, execQueue chan *Job) {
|
|||||||
now := tickTime.Unix()
|
now := tickTime.Unix()
|
||||||
|
|
||||||
for _, job := range s.jobs {
|
for _, job := range s.jobs {
|
||||||
if now%job.Rule.Frequency == 0 && job.Running == false {
|
if job.Running {
|
||||||
s.log.Debug("Scheduler: Putting job on to exec queue", "name", job.Rule.Name)
|
continue
|
||||||
execQueue <- job
|
}
|
||||||
|
|
||||||
|
if job.OffsetWait && now%job.Offset == 0 {
|
||||||
|
job.OffsetWait = false
|
||||||
|
s.enque(job, execQueue)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if now%job.Rule.Frequency == 0 {
|
||||||
|
if job.Offset > 0 {
|
||||||
|
job.OffsetWait = true
|
||||||
|
} else {
|
||||||
|
s.enque(job, execQueue)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *SchedulerImpl) enque(job *Job, execQueue chan *Job) {
|
||||||
|
s.log.Debug("Scheduler: Putting job on to exec queue", "name", job.Rule.Name, "id", job.Rule.Id)
|
||||||
|
execQueue <- job
|
||||||
|
}
|
||||||
|
93
pkg/services/alerting/test_notification.go
Normal file
93
pkg/services/alerting/test_notification.go
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
package alerting
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/grafana/grafana/pkg/bus"
|
||||||
|
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||||
|
"github.com/grafana/grafana/pkg/log"
|
||||||
|
"github.com/grafana/grafana/pkg/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
type NotificationTestCommand struct {
|
||||||
|
Severity string
|
||||||
|
Name string
|
||||||
|
Type string
|
||||||
|
Settings *simplejson.Json
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
bus.AddHandler("alerting", handleNotificationTestCommand)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleNotificationTestCommand(cmd *NotificationTestCommand) error {
|
||||||
|
notifier := NewRootNotifier()
|
||||||
|
|
||||||
|
model := &models.AlertNotification{
|
||||||
|
Name: cmd.Name,
|
||||||
|
Type: cmd.Type,
|
||||||
|
Settings: cmd.Settings,
|
||||||
|
}
|
||||||
|
|
||||||
|
notifiers, err := notifier.createNotifierFor(model)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Error2("Failed to create notifier", "error", err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
severity := models.AlertSeverityType(cmd.Severity)
|
||||||
|
notifier.sendNotifications([]Notifier{notifiers}, createTestEvalContext(severity))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func createTestEvalContext(severity models.AlertSeverityType) *EvalContext {
|
||||||
|
state := models.AlertStateOK
|
||||||
|
firing := false
|
||||||
|
if severity == models.AlertSeverityCritical {
|
||||||
|
state = models.AlertStateCritical
|
||||||
|
firing = true
|
||||||
|
}
|
||||||
|
if severity == models.AlertSeverityWarning {
|
||||||
|
state = models.AlertStateWarning
|
||||||
|
firing = true
|
||||||
|
}
|
||||||
|
|
||||||
|
testRule := &Rule{
|
||||||
|
DashboardId: 1,
|
||||||
|
PanelId: 1,
|
||||||
|
Name: "Test notification",
|
||||||
|
Message: "Someone is testing the alert notification within grafana.",
|
||||||
|
State: state,
|
||||||
|
Severity: severity,
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := NewEvalContext(testRule)
|
||||||
|
ctx.ImagePublicUrl = "http://grafana.org/assets/img/blog/mixed_styles.png"
|
||||||
|
|
||||||
|
ctx.IsTestRun = true
|
||||||
|
ctx.Firing = firing
|
||||||
|
ctx.Error = nil
|
||||||
|
ctx.EvalMatches = evalMatchesBasedOnSeverity(severity)
|
||||||
|
|
||||||
|
return ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
func evalMatchesBasedOnSeverity(severity models.AlertSeverityType) []*EvalMatch {
|
||||||
|
matches := make([]*EvalMatch, 0)
|
||||||
|
if severity == models.AlertSeverityOK {
|
||||||
|
return matches
|
||||||
|
}
|
||||||
|
|
||||||
|
matches = append(matches, &EvalMatch{
|
||||||
|
Metric: "High value",
|
||||||
|
Value: 100,
|
||||||
|
})
|
||||||
|
|
||||||
|
matches = append(matches, &EvalMatch{
|
||||||
|
Metric: "Higher Value",
|
||||||
|
Value: 200,
|
||||||
|
})
|
||||||
|
|
||||||
|
return matches
|
||||||
|
}
|
@ -8,6 +8,15 @@ import (
|
|||||||
|
|
||||||
type Repository interface {
|
type Repository interface {
|
||||||
Save(item *Item) error
|
Save(item *Item) error
|
||||||
|
Find(query *ItemQuery) ([]*Item, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ItemQuery struct {
|
||||||
|
OrgId int64 `json:"orgId"`
|
||||||
|
Type ItemType `json:"type"`
|
||||||
|
AlertId int64 `json:"alertId"`
|
||||||
|
|
||||||
|
Limit int64 `json:"alertId"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var repositoryInstance Repository
|
var repositoryInstance Repository
|
||||||
|
@ -159,7 +159,7 @@ func upsertAlerts(existingAlerts []*m.Alert, cmd *m.SaveAlertsCommand, sess *xor
|
|||||||
} else {
|
} else {
|
||||||
alert.Updated = time.Now()
|
alert.Updated = time.Now()
|
||||||
alert.Created = time.Now()
|
alert.Created = time.Now()
|
||||||
alert.State = m.AlertStatePending
|
alert.State = m.AlertStateUnknown
|
||||||
alert.NewStateDate = time.Now()
|
alert.NewStateDate = time.Now()
|
||||||
|
|
||||||
_, err := sess.Insert(alert)
|
_, err := sess.Insert(alert)
|
||||||
|
@ -16,6 +16,8 @@ func init() {
|
|||||||
bus.AddHandler("sql", CreateAlertNotificationCommand)
|
bus.AddHandler("sql", CreateAlertNotificationCommand)
|
||||||
bus.AddHandler("sql", UpdateAlertNotification)
|
bus.AddHandler("sql", UpdateAlertNotification)
|
||||||
bus.AddHandler("sql", DeleteAlertNotification)
|
bus.AddHandler("sql", DeleteAlertNotification)
|
||||||
|
bus.AddHandler("sql", GetAlertNotificationsToSend)
|
||||||
|
bus.AddHandler("sql", GetAllAlertNotifications)
|
||||||
}
|
}
|
||||||
|
|
||||||
func DeleteAlertNotification(cmd *m.DeleteAlertNotificationCommand) error {
|
func DeleteAlertNotification(cmd *m.DeleteAlertNotificationCommand) error {
|
||||||
@ -32,73 +34,122 @@ func DeleteAlertNotification(cmd *m.DeleteAlertNotificationCommand) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func GetAlertNotifications(query *m.GetAlertNotificationsQuery) error {
|
func GetAlertNotifications(query *m.GetAlertNotificationsQuery) error {
|
||||||
return getAlertNotificationsInternal(query, x.NewSession())
|
return getAlertNotificationInternal(query, x.NewSession())
|
||||||
}
|
}
|
||||||
|
|
||||||
func getAlertNotificationsInternal(query *m.GetAlertNotificationsQuery, sess *xorm.Session) error {
|
func GetAllAlertNotifications(query *m.GetAllAlertNotificationsQuery) error {
|
||||||
|
results := make([]*m.AlertNotification, 0)
|
||||||
|
if err := x.Where("org_id = ?", query.OrgId).Find(&results); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
query.Result = results
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetAlertNotificationsToSend(query *m.GetAlertNotificationsToSendQuery) error {
|
||||||
var sql bytes.Buffer
|
var sql bytes.Buffer
|
||||||
params := make([]interface{}, 0)
|
params := make([]interface{}, 0)
|
||||||
|
|
||||||
sql.WriteString(`SELECT
|
sql.WriteString(`SELECT
|
||||||
alert_notification.id,
|
alert_notification.id,
|
||||||
alert_notification.org_id,
|
alert_notification.org_id,
|
||||||
alert_notification.name,
|
alert_notification.name,
|
||||||
alert_notification.type,
|
alert_notification.type,
|
||||||
alert_notification.created,
|
alert_notification.created,
|
||||||
alert_notification.updated,
|
alert_notification.updated,
|
||||||
alert_notification.settings
|
alert_notification.settings,
|
||||||
FROM alert_notification
|
alert_notification.is_default
|
||||||
`)
|
FROM alert_notification
|
||||||
|
`)
|
||||||
|
|
||||||
sql.WriteString(` WHERE alert_notification.org_id = ?`)
|
sql.WriteString(` WHERE alert_notification.org_id = ?`)
|
||||||
params = append(params, query.OrgId)
|
params = append(params, query.OrgId)
|
||||||
|
|
||||||
if query.Name != "" {
|
sql.WriteString(` AND ((alert_notification.is_default = 1)`)
|
||||||
sql.WriteString(` AND alert_notification.name = ?`)
|
|
||||||
params = append(params, query.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
if query.Id != 0 {
|
|
||||||
sql.WriteString(` AND alert_notification.id = ?`)
|
|
||||||
params = append(params, query.Id)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(query.Ids) > 0 {
|
if len(query.Ids) > 0 {
|
||||||
sql.WriteString(` AND alert_notification.id IN (?` + strings.Repeat(",?", len(query.Ids)-1) + ")")
|
sql.WriteString(` OR alert_notification.id IN (?` + strings.Repeat(",?", len(query.Ids)-1) + ")")
|
||||||
for _, v := range query.Ids {
|
for _, v := range query.Ids {
|
||||||
params = append(params, v)
|
params = append(params, v)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
sql.WriteString(`)`)
|
||||||
|
|
||||||
|
results := make([]*m.AlertNotification, 0)
|
||||||
|
if err := x.Sql(sql.String(), params...).Find(&results); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
query.Result = results
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAlertNotificationInternal(query *m.GetAlertNotificationsQuery, sess *xorm.Session) error {
|
||||||
|
var sql bytes.Buffer
|
||||||
|
params := make([]interface{}, 0)
|
||||||
|
|
||||||
|
sql.WriteString(`SELECT
|
||||||
|
alert_notification.id,
|
||||||
|
alert_notification.org_id,
|
||||||
|
alert_notification.name,
|
||||||
|
alert_notification.type,
|
||||||
|
alert_notification.created,
|
||||||
|
alert_notification.updated,
|
||||||
|
alert_notification.settings,
|
||||||
|
alert_notification.is_default
|
||||||
|
FROM alert_notification
|
||||||
|
`)
|
||||||
|
|
||||||
|
sql.WriteString(` WHERE alert_notification.org_id = ?`)
|
||||||
|
params = append(params, query.OrgId)
|
||||||
|
|
||||||
|
if query.Name != "" || query.Id != 0 {
|
||||||
|
if query.Name != "" {
|
||||||
|
sql.WriteString(` AND alert_notification.name = ?`)
|
||||||
|
params = append(params, query.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if query.Id != 0 {
|
||||||
|
sql.WriteString(` AND alert_notification.id = ?`)
|
||||||
|
params = append(params, query.Id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
results := make([]*m.AlertNotification, 0)
|
results := make([]*m.AlertNotification, 0)
|
||||||
if err := sess.Sql(sql.String(), params...).Find(&results); err != nil {
|
if err := sess.Sql(sql.String(), params...).Find(&results); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
query.Result = results
|
if len(results) == 0 {
|
||||||
|
query.Result = nil
|
||||||
|
} else {
|
||||||
|
query.Result = results[0]
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func CreateAlertNotificationCommand(cmd *m.CreateAlertNotificationCommand) error {
|
func CreateAlertNotificationCommand(cmd *m.CreateAlertNotificationCommand) error {
|
||||||
return inTransaction(func(sess *xorm.Session) error {
|
return inTransaction(func(sess *xorm.Session) error {
|
||||||
existingQuery := &m.GetAlertNotificationsQuery{OrgId: cmd.OrgId, Name: cmd.Name}
|
existingQuery := &m.GetAlertNotificationsQuery{OrgId: cmd.OrgId, Name: cmd.Name}
|
||||||
err := getAlertNotificationsInternal(existingQuery, sess)
|
err := getAlertNotificationInternal(existingQuery, sess)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(existingQuery.Result) > 0 {
|
if existingQuery.Result != nil {
|
||||||
return fmt.Errorf("Alert notification name %s already exists", cmd.Name)
|
return fmt.Errorf("Alert notification name %s already exists", cmd.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
alertNotification := &m.AlertNotification{
|
alertNotification := &m.AlertNotification{
|
||||||
OrgId: cmd.OrgId,
|
OrgId: cmd.OrgId,
|
||||||
Name: cmd.Name,
|
Name: cmd.Name,
|
||||||
Type: cmd.Type,
|
Type: cmd.Type,
|
||||||
Settings: cmd.Settings,
|
Settings: cmd.Settings,
|
||||||
Created: time.Now(),
|
Created: time.Now(),
|
||||||
Updated: time.Now(),
|
Updated: time.Now(),
|
||||||
|
IsDefault: cmd.IsDefault,
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err = sess.Insert(alertNotification); err != nil {
|
if _, err = sess.Insert(alertNotification); err != nil {
|
||||||
@ -120,11 +171,11 @@ func UpdateAlertNotification(cmd *m.UpdateAlertNotificationCommand) error {
|
|||||||
|
|
||||||
// check if name exists
|
// check if name exists
|
||||||
sameNameQuery := &m.GetAlertNotificationsQuery{OrgId: cmd.OrgId, Name: cmd.Name}
|
sameNameQuery := &m.GetAlertNotificationsQuery{OrgId: cmd.OrgId, Name: cmd.Name}
|
||||||
if err := getAlertNotificationsInternal(sameNameQuery, sess); err != nil {
|
if err := getAlertNotificationInternal(sameNameQuery, sess); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(sameNameQuery.Result) > 0 && sameNameQuery.Result[0].Id != current.Id {
|
if sameNameQuery.Result != nil && sameNameQuery.Result.Id != current.Id {
|
||||||
return fmt.Errorf("Alert notification name %s already exists", cmd.Name)
|
return fmt.Errorf("Alert notification name %s already exists", cmd.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -132,6 +183,9 @@ func UpdateAlertNotification(cmd *m.UpdateAlertNotificationCommand) error {
|
|||||||
current.Settings = cmd.Settings
|
current.Settings = cmd.Settings
|
||||||
current.Name = cmd.Name
|
current.Name = cmd.Name
|
||||||
current.Type = cmd.Type
|
current.Type = cmd.Type
|
||||||
|
current.IsDefault = cmd.IsDefault
|
||||||
|
|
||||||
|
sess.UseBool("is_default")
|
||||||
|
|
||||||
if affected, err := sess.Id(cmd.Id).Update(current); err != nil {
|
if affected, err := sess.Id(cmd.Id).Update(current); err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -23,7 +23,7 @@ func TestAlertNotificationSQLAccess(t *testing.T) {
|
|||||||
err := GetAlertNotifications(cmd)
|
err := GetAlertNotifications(cmd)
|
||||||
fmt.Printf("errror %v", err)
|
fmt.Printf("errror %v", err)
|
||||||
So(err, ShouldBeNil)
|
So(err, ShouldBeNil)
|
||||||
So(len(cmd.Result), ShouldEqual, 0)
|
So(cmd.Result, ShouldBeNil)
|
||||||
})
|
})
|
||||||
|
|
||||||
Convey("Can save Alert Notification", func() {
|
Convey("Can save Alert Notification", func() {
|
||||||
@ -63,20 +63,35 @@ func TestAlertNotificationSQLAccess(t *testing.T) {
|
|||||||
cmd1 := m.CreateAlertNotificationCommand{Name: "nagios", Type: "webhook", OrgId: 1, Settings: simplejson.New()}
|
cmd1 := m.CreateAlertNotificationCommand{Name: "nagios", Type: "webhook", OrgId: 1, Settings: simplejson.New()}
|
||||||
cmd2 := m.CreateAlertNotificationCommand{Name: "slack", Type: "webhook", OrgId: 1, Settings: simplejson.New()}
|
cmd2 := m.CreateAlertNotificationCommand{Name: "slack", Type: "webhook", OrgId: 1, Settings: simplejson.New()}
|
||||||
cmd3 := m.CreateAlertNotificationCommand{Name: "ops2", Type: "email", OrgId: 1, Settings: simplejson.New()}
|
cmd3 := m.CreateAlertNotificationCommand{Name: "ops2", Type: "email", OrgId: 1, Settings: simplejson.New()}
|
||||||
|
cmd4 := m.CreateAlertNotificationCommand{IsDefault: true, Name: "default", Type: "email", OrgId: 1, Settings: simplejson.New()}
|
||||||
|
|
||||||
|
otherOrg := m.CreateAlertNotificationCommand{Name: "default", Type: "email", OrgId: 2, Settings: simplejson.New()}
|
||||||
|
|
||||||
So(CreateAlertNotificationCommand(&cmd1), ShouldBeNil)
|
So(CreateAlertNotificationCommand(&cmd1), ShouldBeNil)
|
||||||
So(CreateAlertNotificationCommand(&cmd2), ShouldBeNil)
|
So(CreateAlertNotificationCommand(&cmd2), ShouldBeNil)
|
||||||
So(CreateAlertNotificationCommand(&cmd3), ShouldBeNil)
|
So(CreateAlertNotificationCommand(&cmd3), ShouldBeNil)
|
||||||
|
So(CreateAlertNotificationCommand(&cmd4), ShouldBeNil)
|
||||||
|
So(CreateAlertNotificationCommand(&otherOrg), ShouldBeNil)
|
||||||
|
|
||||||
Convey("search", func() {
|
Convey("search", func() {
|
||||||
query := &m.GetAlertNotificationsQuery{
|
query := &m.GetAlertNotificationsToSendQuery{
|
||||||
Ids: []int64{cmd1.Result.Id, cmd2.Result.Id, 112341231},
|
Ids: []int64{cmd1.Result.Id, cmd2.Result.Id, 112341231},
|
||||||
OrgId: 1,
|
OrgId: 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
err := GetAlertNotifications(query)
|
err := GetAlertNotificationsToSend(query)
|
||||||
So(err, ShouldBeNil)
|
So(err, ShouldBeNil)
|
||||||
So(len(query.Result), ShouldEqual, 2)
|
So(len(query.Result), ShouldEqual, 3)
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("all", func() {
|
||||||
|
query := &m.GetAllAlertNotificationsQuery{
|
||||||
|
OrgId: 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := GetAllAlertNotifications(query)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
So(len(query.Result), ShouldEqual, 4)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
package sqlstore
|
package sqlstore
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"github.com/go-xorm/xorm"
|
"github.com/go-xorm/xorm"
|
||||||
"github.com/grafana/grafana/pkg/services/annotations"
|
"github.com/grafana/grafana/pkg/services/annotations"
|
||||||
)
|
)
|
||||||
@ -17,5 +20,39 @@ func (r *SqlAnnotationRepo) Save(item *annotations.Item) error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SqlAnnotationRepo) Find(query *annotations.ItemQuery) ([]*annotations.Item, error) {
|
||||||
|
var sql bytes.Buffer
|
||||||
|
params := make([]interface{}, 0)
|
||||||
|
|
||||||
|
sql.WriteString(`SELECT *
|
||||||
|
from annotation
|
||||||
|
`)
|
||||||
|
|
||||||
|
sql.WriteString(`WHERE org_id = ?`)
|
||||||
|
params = append(params, query.OrgId)
|
||||||
|
|
||||||
|
if query.AlertId != 0 {
|
||||||
|
sql.WriteString(` AND alert_id = ?`)
|
||||||
|
params = append(params, query.AlertId)
|
||||||
|
}
|
||||||
|
|
||||||
|
if query.Type != "" {
|
||||||
|
sql.WriteString(` AND type = ?`)
|
||||||
|
params = append(params, string(query.Type))
|
||||||
|
}
|
||||||
|
|
||||||
|
if query.Limit == 0 {
|
||||||
|
query.Limit = 10
|
||||||
|
}
|
||||||
|
|
||||||
|
sql.WriteString(fmt.Sprintf("ORDER BY timestamp DESC LIMIT %v", query.Limit))
|
||||||
|
|
||||||
|
items := make([]*annotations.Item, 0)
|
||||||
|
if err := x.Sql(sql.String(), params...).Find(&items); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return items, nil
|
||||||
}
|
}
|
||||||
|
@ -62,5 +62,9 @@ func addAlertMigrations(mg *Migrator) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
mg.AddMigration("create alert_notification table v1", NewAddTableMigration(alert_notification))
|
mg.AddMigration("create alert_notification table v1", NewAddTableMigration(alert_notification))
|
||||||
|
mg.AddMigration("Add column is_default", NewAddColumnMigration(alert_notification, &Column{
|
||||||
|
Name: "is_default", Type: DB_Bool, Nullable: false, Default: "0",
|
||||||
|
}))
|
||||||
mg.AddMigration("add index alert_notification org_id & name", NewAddIndexMigration(alert_notification, alert_notification.Indices[0]))
|
mg.AddMigration("add index alert_notification org_id & name", NewAddIndexMigration(alert_notification, alert_notification.Indices[0]))
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -190,7 +190,7 @@ func ToAbsUrl(relativeUrl string) string {
|
|||||||
|
|
||||||
func shouldRedactKey(s string) bool {
|
func shouldRedactKey(s string) bool {
|
||||||
uppercased := strings.ToUpper(s)
|
uppercased := strings.ToUpper(s)
|
||||||
return strings.Contains(uppercased, "PASSWORD") || strings.Contains(uppercased, "SECRET")
|
return strings.Contains(uppercased, "PASSWORD") || strings.Contains(uppercased, "SECRET") || strings.Contains(uppercased, "PROVIDER_CONFIG")
|
||||||
}
|
}
|
||||||
|
|
||||||
func shouldRedactURLKey(s string) bool {
|
func shouldRedactURLKey(s string) bool {
|
||||||
|
@ -11,8 +11,9 @@ type OAuthInfo struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type OAuther struct {
|
type OAuther struct {
|
||||||
GitHub, Google, Twitter bool
|
GitHub, Google, Twitter, Generic bool
|
||||||
OAuthInfos map[string]*OAuthInfo
|
OAuthInfos map[string]*OAuthInfo
|
||||||
|
OAuthProviderName string
|
||||||
}
|
}
|
||||||
|
|
||||||
var OAuthService *OAuther
|
var OAuthService *OAuther
|
||||||
|
20
pkg/social/common.go
Normal file
20
pkg/social/common.go
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
package social
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func isEmailAllowed(email string, allowedDomains []string) bool {
|
||||||
|
if len(allowedDomains) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
valid := false
|
||||||
|
for _, domain := range allowedDomains {
|
||||||
|
emailSuffix := fmt.Sprintf("@%s", domain)
|
||||||
|
valid = valid || strings.HasSuffix(email, emailSuffix)
|
||||||
|
}
|
||||||
|
|
||||||
|
return valid
|
||||||
|
}
|
205
pkg/social/generic_oauth.go
Normal file
205
pkg/social/generic_oauth.go
Normal file
@ -0,0 +1,205 @@
|
|||||||
|
package social
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/models"
|
||||||
|
|
||||||
|
"golang.org/x/oauth2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type GenericOAuth struct {
|
||||||
|
*oauth2.Config
|
||||||
|
allowedDomains []string
|
||||||
|
allowedOrganizations []string
|
||||||
|
apiUrl string
|
||||||
|
allowSignup bool
|
||||||
|
teamIds []int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *GenericOAuth) Type() int {
|
||||||
|
return int(models.GENERIC)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *GenericOAuth) IsEmailAllowed(email string) bool {
|
||||||
|
return isEmailAllowed(email, s.allowedDomains)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *GenericOAuth) IsSignupAllowed() bool {
|
||||||
|
return s.allowSignup
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *GenericOAuth) IsTeamMember(client *http.Client) bool {
|
||||||
|
if len(s.teamIds) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
teamMemberships, err := s.FetchTeamMemberships(client)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, teamId := range s.teamIds {
|
||||||
|
for _, membershipId := range teamMemberships {
|
||||||
|
if teamId == membershipId {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *GenericOAuth) IsOrganizationMember(client *http.Client) bool {
|
||||||
|
if len(s.allowedOrganizations) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
organizations, err := s.FetchOrganizations(client)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, allowedOrganization := range s.allowedOrganizations {
|
||||||
|
for _, organization := range organizations {
|
||||||
|
if organization == allowedOrganization {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *GenericOAuth) FetchPrivateEmail(client *http.Client) (string, error) {
|
||||||
|
type Record struct {
|
||||||
|
Email string `json:"email"`
|
||||||
|
Primary bool `json:"primary"`
|
||||||
|
Verified bool `json:"verified"`
|
||||||
|
}
|
||||||
|
|
||||||
|
emailsUrl := fmt.Sprintf(s.apiUrl + "/emails")
|
||||||
|
r, err := client.Get(emailsUrl)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer r.Body.Close()
|
||||||
|
|
||||||
|
var records []Record
|
||||||
|
|
||||||
|
if err = json.NewDecoder(r.Body).Decode(&records); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
var email = ""
|
||||||
|
for _, record := range records {
|
||||||
|
if record.Primary {
|
||||||
|
email = record.Email
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return email, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *GenericOAuth) FetchTeamMemberships(client *http.Client) ([]int, error) {
|
||||||
|
type Record struct {
|
||||||
|
Id int `json:"id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
membershipUrl := fmt.Sprintf(s.apiUrl + "/teams")
|
||||||
|
r, err := client.Get(membershipUrl)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer r.Body.Close()
|
||||||
|
|
||||||
|
var records []Record
|
||||||
|
|
||||||
|
if err = json.NewDecoder(r.Body).Decode(&records); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var ids = make([]int, len(records))
|
||||||
|
for i, record := range records {
|
||||||
|
ids[i] = record.Id
|
||||||
|
}
|
||||||
|
|
||||||
|
return ids, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *GenericOAuth) FetchOrganizations(client *http.Client) ([]string, error) {
|
||||||
|
type Record struct {
|
||||||
|
Login string `json:"login"`
|
||||||
|
}
|
||||||
|
|
||||||
|
url := fmt.Sprintf(s.apiUrl + "/orgs")
|
||||||
|
r, err := client.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer r.Body.Close()
|
||||||
|
|
||||||
|
var records []Record
|
||||||
|
|
||||||
|
if err = json.NewDecoder(r.Body).Decode(&records); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var logins = make([]string, len(records))
|
||||||
|
for i, record := range records {
|
||||||
|
logins[i] = record.Login
|
||||||
|
}
|
||||||
|
|
||||||
|
return logins, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *GenericOAuth) UserInfo(token *oauth2.Token) (*BasicUserInfo, error) {
|
||||||
|
var data struct {
|
||||||
|
Id int `json:"id"`
|
||||||
|
Name string `json:"login"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
client := s.Client(oauth2.NoContext, token)
|
||||||
|
r, err := client.Get(s.apiUrl)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer r.Body.Close()
|
||||||
|
|
||||||
|
if err = json.NewDecoder(r.Body).Decode(&data); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
userInfo := &BasicUserInfo{
|
||||||
|
Identity: strconv.Itoa(data.Id),
|
||||||
|
Name: data.Name,
|
||||||
|
Email: data.Email,
|
||||||
|
}
|
||||||
|
|
||||||
|
if !s.IsTeamMember(client) {
|
||||||
|
return nil, errors.New("User not a member of one of the required teams")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !s.IsOrganizationMember(client) {
|
||||||
|
return nil, errors.New("User not a member of one of the required organizations")
|
||||||
|
}
|
||||||
|
|
||||||
|
if userInfo.Email == "" {
|
||||||
|
userInfo.Email, err = s.FetchPrivateEmail(client)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return userInfo, nil
|
||||||
|
}
|
213
pkg/social/github_oauth.go
Normal file
213
pkg/social/github_oauth.go
Normal file
@ -0,0 +1,213 @@
|
|||||||
|
package social
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/models"
|
||||||
|
|
||||||
|
"golang.org/x/oauth2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SocialGithub struct {
|
||||||
|
*oauth2.Config
|
||||||
|
allowedDomains []string
|
||||||
|
allowedOrganizations []string
|
||||||
|
apiUrl string
|
||||||
|
allowSignup bool
|
||||||
|
teamIds []int
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrMissingTeamMembership = errors.New("User not a member of one of the required teams")
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrMissingOrganizationMembership = errors.New("User not a member of one of the required organizations")
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *SocialGithub) Type() int {
|
||||||
|
return int(models.GITHUB)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SocialGithub) IsEmailAllowed(email string) bool {
|
||||||
|
return isEmailAllowed(email, s.allowedDomains)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SocialGithub) IsSignupAllowed() bool {
|
||||||
|
return s.allowSignup
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SocialGithub) IsTeamMember(client *http.Client) bool {
|
||||||
|
if len(s.teamIds) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
teamMemberships, err := s.FetchTeamMemberships(client)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, teamId := range s.teamIds {
|
||||||
|
for _, membershipId := range teamMemberships {
|
||||||
|
if teamId == membershipId {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SocialGithub) IsOrganizationMember(client *http.Client) bool {
|
||||||
|
if len(s.allowedOrganizations) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
organizations, err := s.FetchOrganizations(client)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, allowedOrganization := range s.allowedOrganizations {
|
||||||
|
for _, organization := range organizations {
|
||||||
|
if organization == allowedOrganization {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SocialGithub) FetchPrivateEmail(client *http.Client) (string, error) {
|
||||||
|
type Record struct {
|
||||||
|
Email string `json:"email"`
|
||||||
|
Primary bool `json:"primary"`
|
||||||
|
Verified bool `json:"verified"`
|
||||||
|
}
|
||||||
|
|
||||||
|
emailsUrl := fmt.Sprintf(s.apiUrl + "/emails")
|
||||||
|
r, err := client.Get(emailsUrl)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer r.Body.Close()
|
||||||
|
|
||||||
|
var records []Record
|
||||||
|
|
||||||
|
if err = json.NewDecoder(r.Body).Decode(&records); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
var email = ""
|
||||||
|
for _, record := range records {
|
||||||
|
if record.Primary {
|
||||||
|
email = record.Email
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return email, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SocialGithub) FetchTeamMemberships(client *http.Client) ([]int, error) {
|
||||||
|
type Record struct {
|
||||||
|
Id int `json:"id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
membershipUrl := fmt.Sprintf(s.apiUrl + "/teams")
|
||||||
|
r, err := client.Get(membershipUrl)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer r.Body.Close()
|
||||||
|
|
||||||
|
var records []Record
|
||||||
|
|
||||||
|
if err = json.NewDecoder(r.Body).Decode(&records); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var ids = make([]int, len(records))
|
||||||
|
for i, record := range records {
|
||||||
|
ids[i] = record.Id
|
||||||
|
}
|
||||||
|
|
||||||
|
return ids, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SocialGithub) FetchOrganizations(client *http.Client) ([]string, error) {
|
||||||
|
type Record struct {
|
||||||
|
Login string `json:"login"`
|
||||||
|
}
|
||||||
|
|
||||||
|
url := fmt.Sprintf(s.apiUrl + "/orgs")
|
||||||
|
r, err := client.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer r.Body.Close()
|
||||||
|
|
||||||
|
var records []Record
|
||||||
|
|
||||||
|
if err = json.NewDecoder(r.Body).Decode(&records); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var logins = make([]string, len(records))
|
||||||
|
for i, record := range records {
|
||||||
|
logins[i] = record.Login
|
||||||
|
}
|
||||||
|
|
||||||
|
return logins, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SocialGithub) UserInfo(token *oauth2.Token) (*BasicUserInfo, error) {
|
||||||
|
var data struct {
|
||||||
|
Id int `json:"id"`
|
||||||
|
Name string `json:"login"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
client := s.Client(oauth2.NoContext, token)
|
||||||
|
r, err := client.Get(s.apiUrl)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer r.Body.Close()
|
||||||
|
|
||||||
|
if err = json.NewDecoder(r.Body).Decode(&data); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
userInfo := &BasicUserInfo{
|
||||||
|
Identity: strconv.Itoa(data.Id),
|
||||||
|
Name: data.Name,
|
||||||
|
Email: data.Email,
|
||||||
|
}
|
||||||
|
|
||||||
|
if !s.IsTeamMember(client) {
|
||||||
|
return nil, ErrMissingTeamMembership
|
||||||
|
}
|
||||||
|
|
||||||
|
if !s.IsOrganizationMember(client) {
|
||||||
|
return nil, ErrMissingOrganizationMembership
|
||||||
|
}
|
||||||
|
|
||||||
|
if userInfo.Email == "" {
|
||||||
|
userInfo.Email, err = s.FetchPrivateEmail(client)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return userInfo, nil
|
||||||
|
}
|
52
pkg/social/google_oauth.go
Normal file
52
pkg/social/google_oauth.go
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
package social
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/models"
|
||||||
|
|
||||||
|
"golang.org/x/oauth2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SocialGoogle struct {
|
||||||
|
*oauth2.Config
|
||||||
|
allowedDomains []string
|
||||||
|
apiUrl string
|
||||||
|
allowSignup bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SocialGoogle) Type() int {
|
||||||
|
return int(models.GOOGLE)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SocialGoogle) IsEmailAllowed(email string) bool {
|
||||||
|
return isEmailAllowed(email, s.allowedDomains)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SocialGoogle) IsSignupAllowed() bool {
|
||||||
|
return s.allowSignup
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SocialGoogle) UserInfo(token *oauth2.Token) (*BasicUserInfo, error) {
|
||||||
|
var data struct {
|
||||||
|
Id string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
|
||||||
|
client := s.Client(oauth2.NoContext, token)
|
||||||
|
r, err := client.Get(s.apiUrl)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer r.Body.Close()
|
||||||
|
if err = json.NewDecoder(r.Body).Decode(&data); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &BasicUserInfo{
|
||||||
|
Identity: data.Id,
|
||||||
|
Name: data.Name,
|
||||||
|
Email: data.Email,
|
||||||
|
}, nil
|
||||||
|
}
|
@ -1,14 +1,8 @@
|
|||||||
package social
|
package social
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/models"
|
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
"golang.org/x/net/context"
|
"golang.org/x/net/context"
|
||||||
|
|
||||||
@ -42,7 +36,7 @@ func NewOAuthService() {
|
|||||||
setting.OAuthService = &setting.OAuther{}
|
setting.OAuthService = &setting.OAuther{}
|
||||||
setting.OAuthService.OAuthInfos = make(map[string]*setting.OAuthInfo)
|
setting.OAuthService.OAuthInfos = make(map[string]*setting.OAuthInfo)
|
||||||
|
|
||||||
allOauthes := []string{"github", "google"}
|
allOauthes := []string{"github", "google", "generic_oauth"}
|
||||||
|
|
||||||
for _, name := range allOauthes {
|
for _, name := range allOauthes {
|
||||||
sec := setting.Cfg.Section("auth." + name)
|
sec := setting.Cfg.Section("auth." + name)
|
||||||
@ -98,269 +92,21 @@ func NewOAuthService() {
|
|||||||
allowSignup: info.AllowSignup,
|
allowSignup: info.AllowSignup,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func isEmailAllowed(email string, allowedDomains []string) bool {
|
// Generic - Uses the same scheme as Github.
|
||||||
if len(allowedDomains) == 0 {
|
if name == "generic_oauth" {
|
||||||
return true
|
setting.OAuthService.Generic = true
|
||||||
}
|
setting.OAuthService.OAuthProviderName = sec.Key("oauth_provider_name").String()
|
||||||
|
teamIds := sec.Key("team_ids").Ints(",")
|
||||||
valid := false
|
allowedOrganizations := sec.Key("allowed_organizations").Strings(" ")
|
||||||
for _, domain := range allowedDomains {
|
SocialMap["generic_oauth"] = &GenericOAuth{
|
||||||
emailSuffix := fmt.Sprintf("@%s", domain)
|
Config: &config,
|
||||||
valid = valid || strings.HasSuffix(email, emailSuffix)
|
allowedDomains: info.AllowedDomains,
|
||||||
}
|
apiUrl: info.ApiUrl,
|
||||||
|
allowSignup: info.AllowSignup,
|
||||||
return valid
|
teamIds: teamIds,
|
||||||
}
|
allowedOrganizations: allowedOrganizations,
|
||||||
|
|
||||||
type SocialGithub struct {
|
|
||||||
*oauth2.Config
|
|
||||||
allowedDomains []string
|
|
||||||
allowedOrganizations []string
|
|
||||||
apiUrl string
|
|
||||||
allowSignup bool
|
|
||||||
teamIds []int
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
ErrMissingTeamMembership = errors.New("User not a member of one of the required teams")
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
ErrMissingOrganizationMembership = errors.New("User not a member of one of the required organizations")
|
|
||||||
)
|
|
||||||
|
|
||||||
func (s *SocialGithub) Type() int {
|
|
||||||
return int(models.GITHUB)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SocialGithub) IsEmailAllowed(email string) bool {
|
|
||||||
return isEmailAllowed(email, s.allowedDomains)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SocialGithub) IsSignupAllowed() bool {
|
|
||||||
return s.allowSignup
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SocialGithub) IsTeamMember(client *http.Client) bool {
|
|
||||||
if len(s.teamIds) == 0 {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
teamMemberships, err := s.FetchTeamMemberships(client)
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, teamId := range s.teamIds {
|
|
||||||
for _, membershipId := range teamMemberships {
|
|
||||||
if teamId == membershipId {
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SocialGithub) IsOrganizationMember(client *http.Client) bool {
|
|
||||||
if len(s.allowedOrganizations) == 0 {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
organizations, err := s.FetchOrganizations(client)
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, allowedOrganization := range s.allowedOrganizations {
|
|
||||||
for _, organization := range organizations {
|
|
||||||
if organization == allowedOrganization {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SocialGithub) FetchPrivateEmail(client *http.Client) (string, error) {
|
|
||||||
type Record struct {
|
|
||||||
Email string `json:"email"`
|
|
||||||
Primary bool `json:"primary"`
|
|
||||||
Verified bool `json:"verified"`
|
|
||||||
}
|
|
||||||
|
|
||||||
emailsUrl := fmt.Sprintf(s.apiUrl + "/emails")
|
|
||||||
r, err := client.Get(emailsUrl)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
defer r.Body.Close()
|
|
||||||
|
|
||||||
var records []Record
|
|
||||||
|
|
||||||
if err = json.NewDecoder(r.Body).Decode(&records); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
var email = ""
|
|
||||||
for _, record := range records {
|
|
||||||
if record.Primary {
|
|
||||||
email = record.Email
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return email, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SocialGithub) FetchTeamMemberships(client *http.Client) ([]int, error) {
|
|
||||||
type Record struct {
|
|
||||||
Id int `json:"id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
membershipUrl := fmt.Sprintf(s.apiUrl + "/teams")
|
|
||||||
r, err := client.Get(membershipUrl)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
defer r.Body.Close()
|
|
||||||
|
|
||||||
var records []Record
|
|
||||||
|
|
||||||
if err = json.NewDecoder(r.Body).Decode(&records); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var ids = make([]int, len(records))
|
|
||||||
for i, record := range records {
|
|
||||||
ids[i] = record.Id
|
|
||||||
}
|
|
||||||
|
|
||||||
return ids, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SocialGithub) FetchOrganizations(client *http.Client) ([]string, error) {
|
|
||||||
type Record struct {
|
|
||||||
Login string `json:"login"`
|
|
||||||
}
|
|
||||||
|
|
||||||
url := fmt.Sprintf(s.apiUrl + "/orgs")
|
|
||||||
r, err := client.Get(url)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
defer r.Body.Close()
|
|
||||||
|
|
||||||
var records []Record
|
|
||||||
|
|
||||||
if err = json.NewDecoder(r.Body).Decode(&records); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var logins = make([]string, len(records))
|
|
||||||
for i, record := range records {
|
|
||||||
logins[i] = record.Login
|
|
||||||
}
|
|
||||||
|
|
||||||
return logins, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SocialGithub) UserInfo(token *oauth2.Token) (*BasicUserInfo, error) {
|
|
||||||
var data struct {
|
|
||||||
Id int `json:"id"`
|
|
||||||
Name string `json:"login"`
|
|
||||||
Email string `json:"email"`
|
|
||||||
}
|
|
||||||
|
|
||||||
var err error
|
|
||||||
client := s.Client(oauth2.NoContext, token)
|
|
||||||
r, err := client.Get(s.apiUrl)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
defer r.Body.Close()
|
|
||||||
|
|
||||||
if err = json.NewDecoder(r.Body).Decode(&data); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
userInfo := &BasicUserInfo{
|
|
||||||
Identity: strconv.Itoa(data.Id),
|
|
||||||
Name: data.Name,
|
|
||||||
Email: data.Email,
|
|
||||||
}
|
|
||||||
|
|
||||||
if !s.IsTeamMember(client) {
|
|
||||||
return nil, ErrMissingTeamMembership
|
|
||||||
}
|
|
||||||
|
|
||||||
if !s.IsOrganizationMember(client) {
|
|
||||||
return nil, ErrMissingOrganizationMembership
|
|
||||||
}
|
|
||||||
|
|
||||||
if userInfo.Email == "" {
|
|
||||||
userInfo.Email, err = s.FetchPrivateEmail(client)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return userInfo, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ________ .__
|
|
||||||
// / _____/ ____ ____ ____ | | ____
|
|
||||||
// / \ ___ / _ \ / _ \ / ___\| | _/ __ \
|
|
||||||
// \ \_\ ( <_> | <_> ) /_/ > |_\ ___/
|
|
||||||
// \______ /\____/ \____/\___ /|____/\___ >
|
|
||||||
// \/ /_____/ \/
|
|
||||||
|
|
||||||
type SocialGoogle struct {
|
|
||||||
*oauth2.Config
|
|
||||||
allowedDomains []string
|
|
||||||
apiUrl string
|
|
||||||
allowSignup bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SocialGoogle) Type() int {
|
|
||||||
return int(models.GOOGLE)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SocialGoogle) IsEmailAllowed(email string) bool {
|
|
||||||
return isEmailAllowed(email, s.allowedDomains)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SocialGoogle) IsSignupAllowed() bool {
|
|
||||||
return s.allowSignup
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SocialGoogle) UserInfo(token *oauth2.Token) (*BasicUserInfo, error) {
|
|
||||||
var data struct {
|
|
||||||
Id string `json:"id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Email string `json:"email"`
|
|
||||||
}
|
|
||||||
var err error
|
|
||||||
|
|
||||||
client := s.Client(oauth2.NoContext, token)
|
|
||||||
r, err := client.Get(s.apiUrl)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer r.Body.Close()
|
|
||||||
if err = json.NewDecoder(r.Body).Decode(&data); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &BasicUserInfo{
|
|
||||||
Identity: data.Id,
|
|
||||||
Name: data.Name,
|
|
||||||
Email: data.Email,
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
@ -2,16 +2,23 @@ package graphite
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/log"
|
"github.com/grafana/grafana/pkg/log"
|
||||||
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
"github.com/grafana/grafana/pkg/tsdb"
|
"github.com/grafana/grafana/pkg/tsdb"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
HttpClient = http.Client{Timeout: time.Duration(10 * time.Second)}
|
||||||
|
)
|
||||||
|
|
||||||
type GraphiteExecutor struct {
|
type GraphiteExecutor struct {
|
||||||
*tsdb.DataSourceInfo
|
*tsdb.DataSourceInfo
|
||||||
}
|
}
|
||||||
@ -30,7 +37,7 @@ func init() {
|
|||||||
func (e *GraphiteExecutor) Execute(queries tsdb.QuerySlice, context *tsdb.QueryContext) *tsdb.BatchResult {
|
func (e *GraphiteExecutor) Execute(queries tsdb.QuerySlice, context *tsdb.QueryContext) *tsdb.BatchResult {
|
||||||
result := &tsdb.BatchResult{}
|
result := &tsdb.BatchResult{}
|
||||||
|
|
||||||
params := url.Values{
|
formData := url.Values{
|
||||||
"from": []string{"-" + formatTimeRange(context.TimeRange.From)},
|
"from": []string{"-" + formatTimeRange(context.TimeRange.From)},
|
||||||
"until": []string{formatTimeRange(context.TimeRange.To)},
|
"until": []string{formatTimeRange(context.TimeRange.To)},
|
||||||
"format": []string{"json"},
|
"format": []string{"json"},
|
||||||
@ -38,28 +45,26 @@ func (e *GraphiteExecutor) Execute(queries tsdb.QuerySlice, context *tsdb.QueryC
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, query := range queries {
|
for _, query := range queries {
|
||||||
params["target"] = []string{query.Query}
|
formData["target"] = []string{query.Query}
|
||||||
glog.Debug("Graphite request", "query", query.Query)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
client := http.Client{Timeout: time.Duration(10 * time.Second)}
|
if setting.Env == setting.DEV {
|
||||||
res, err := client.PostForm(e.Url+"/render?", params)
|
glog.Debug("Graphite request", "params", formData)
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := e.createRequest(formData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
result.Error = err
|
result.Error = err
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
defer res.Body.Close()
|
res, err := HttpClient.Do(req)
|
||||||
|
|
||||||
body, err := ioutil.ReadAll(res.Body)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
result.Error = err
|
result.Error = err
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
var data []TargetResponseDTO
|
data, err := e.parseResponse(res)
|
||||||
err = json.Unmarshal(body, &data)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
glog.Info("Failed to unmarshal graphite response", "error", err, "body", string(body))
|
|
||||||
result.Error = err
|
result.Error = err
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
@ -71,12 +76,56 @@ func (e *GraphiteExecutor) Execute(queries tsdb.QuerySlice, context *tsdb.QueryC
|
|||||||
Name: series.Target,
|
Name: series.Target,
|
||||||
Points: series.DataPoints,
|
Points: series.DataPoints,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if setting.Env == setting.DEV {
|
||||||
|
glog.Debug("Graphite response", "target", series.Target, "datapoints", len(series.DataPoints))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
result.QueryResults["A"] = queryRes
|
result.QueryResults["A"] = queryRes
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e *GraphiteExecutor) parseResponse(res *http.Response) ([]TargetResponseDTO, error) {
|
||||||
|
body, err := ioutil.ReadAll(res.Body)
|
||||||
|
defer res.Body.Close()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if res.StatusCode == http.StatusUnauthorized {
|
||||||
|
glog.Info("Request is Unauthorized", "status", res.Status, "body", string(body))
|
||||||
|
return nil, fmt.Errorf("Request is Unauthorized status: %v body: %s", res.Status, string(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
var data []TargetResponseDTO
|
||||||
|
err = json.Unmarshal(body, &data)
|
||||||
|
if err != nil {
|
||||||
|
glog.Info("Failed to unmarshal graphite response", "error", err, "status", res.Status, "body", string(body))
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *GraphiteExecutor) createRequest(data url.Values) (*http.Request, error) {
|
||||||
|
u, _ := url.Parse(e.Url)
|
||||||
|
u.Path = path.Join(u.Path, "render")
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodPost, u.String(), strings.NewReader(data.Encode()))
|
||||||
|
if err != nil {
|
||||||
|
glog.Info("Failed to create request", "error", err)
|
||||||
|
return nil, fmt.Errorf("Failed to create request. error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
if e.BasicAuth {
|
||||||
|
req.SetBasicAuth(e.BasicAuthUser, e.BasicAuthPassword)
|
||||||
|
}
|
||||||
|
|
||||||
|
return req, err
|
||||||
|
}
|
||||||
|
|
||||||
func formatTimeRange(input string) string {
|
func formatTimeRange(input string) string {
|
||||||
if input == "now" {
|
if input == "now" {
|
||||||
return input
|
return input
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
package graphite
|
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"`
|
||||||
}
|
}
|
||||||
|
@ -46,13 +46,13 @@ 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,
|
||||||
|
@ -17,8 +17,10 @@ function (angular, coreModule, config) {
|
|||||||
|
|
||||||
$scope.googleAuthEnabled = config.googleAuthEnabled;
|
$scope.googleAuthEnabled = config.googleAuthEnabled;
|
||||||
$scope.githubAuthEnabled = config.githubAuthEnabled;
|
$scope.githubAuthEnabled = config.githubAuthEnabled;
|
||||||
$scope.oauthEnabled = config.githubAuthEnabled || config.googleAuthEnabled;
|
$scope.oauthEnabled = config.githubAuthEnabled || config.googleAuthEnabled || config.genericOAuthEnabled;
|
||||||
$scope.allowUserPassLogin = config.allowUserPassLogin;
|
$scope.allowUserPassLogin = config.allowUserPassLogin;
|
||||||
|
$scope.genericOAuthEnabled = config.genericOAuthEnabled;
|
||||||
|
$scope.oauthProviderName = config.oauthProviderName;
|
||||||
$scope.disableUserSignUp = config.disableUserSignUp;
|
$scope.disableUserSignUp = config.disableUserSignUp;
|
||||||
$scope.loginHint = config.loginHint;
|
$scope.loginHint = config.loginHint;
|
||||||
|
|
||||||
|
@ -113,7 +113,7 @@ function (_, $, coreModule) {
|
|||||||
if (str[0] === '/') { str = str.substring(1); }
|
if (str[0] === '/') { str = str.substring(1); }
|
||||||
if (str[str.length - 1] === '/') { str = str.substring(0, str.length-1); }
|
if (str[str.length - 1] === '/') { str = str.substring(0, str.length-1); }
|
||||||
try {
|
try {
|
||||||
return item.toLowerCase().match(str);
|
return item.toLowerCase().match(str.toLowerCase());
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -80,28 +80,27 @@ export class AlertSrv {
|
|||||||
showConfirmModal(payload) {
|
showConfirmModal(payload) {
|
||||||
var scope = this.$rootScope.$new();
|
var scope = this.$rootScope.$new();
|
||||||
|
|
||||||
scope.title = payload.title;
|
|
||||||
scope.text = payload.text;
|
|
||||||
scope.text2 = payload.text2;
|
|
||||||
scope.confirmTextRequired = payload.confirmText !== undefined && payload.confirmText !== "";
|
|
||||||
|
|
||||||
scope.onConfirm = function() {
|
scope.onConfirm = function() {
|
||||||
if (!scope.confirmTextRequired || (scope.confirmTextRequired && scope.confirmTextValid)) {
|
payload.onConfirm();
|
||||||
payload.onConfirm();
|
scope.dismiss();
|
||||||
scope.dismiss();
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
scope.updateConfirmText = function(value) {
|
scope.updateConfirmText = function(value) {
|
||||||
scope.confirmTextValid = payload.confirmText.toLowerCase() === value.toLowerCase();
|
scope.confirmTextValid = payload.confirmText.toLowerCase() === value.toLowerCase();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
scope.title = payload.title;
|
||||||
|
scope.text = payload.text;
|
||||||
|
scope.text2 = payload.text2;
|
||||||
|
scope.confirmText = payload.confirmText;
|
||||||
|
|
||||||
scope.onConfirm = payload.onConfirm;
|
scope.onConfirm = payload.onConfirm;
|
||||||
scope.onAltAction = payload.onAltAction;
|
scope.onAltAction = payload.onAltAction;
|
||||||
scope.altActionText = payload.altActionText;
|
scope.altActionText = payload.altActionText;
|
||||||
scope.icon = payload.icon || "fa-check";
|
scope.icon = payload.icon || "fa-check";
|
||||||
scope.yesText = payload.yesText || "Yes";
|
scope.yesText = payload.yesText || "Yes";
|
||||||
scope.noText = payload.noText || "Cancel";
|
scope.noText = payload.noText || "Cancel";
|
||||||
|
scope.confirmTextValid = scope.confirmText ? false : true;
|
||||||
|
|
||||||
var confirmModal = this.$modal({
|
var confirmModal = this.$modal({
|
||||||
template: 'public/app/partials/confirm_modal.html',
|
template: 'public/app/partials/confirm_modal.html',
|
||||||
|
@ -36,6 +36,13 @@ var reducerTypes = [
|
|||||||
{text: 'count()', value: 'count'},
|
{text: 'count()', value: 'count'},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
var noDataModes = [
|
||||||
|
{text: 'OK', value: 'ok'},
|
||||||
|
{text: 'Critical', value: 'critical'},
|
||||||
|
{text: 'Warning', value: 'warning'},
|
||||||
|
{text: 'Unknown', value: 'unknown'},
|
||||||
|
];
|
||||||
|
|
||||||
function createReducerPart(model) {
|
function createReducerPart(model) {
|
||||||
var def = new QueryPartDef({type: model.type, defaultParams: []});
|
var def = new QueryPartDef({type: model.type, defaultParams: []});
|
||||||
return new QueryPart(model, def);
|
return new QueryPart(model, def);
|
||||||
@ -69,9 +76,9 @@ function getStateDisplayModel(state) {
|
|||||||
stateClass: 'alert-state-warning'
|
stateClass: 'alert-state-warning'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case 'pending': {
|
case 'unknown': {
|
||||||
return {
|
return {
|
||||||
text: 'PENDING',
|
text: 'UNKNOWN',
|
||||||
iconClass: "fa fa-question",
|
iconClass: "fa fa-question",
|
||||||
stateClass: 'alert-state-warning'
|
stateClass: 'alert-state-warning'
|
||||||
};
|
};
|
||||||
@ -100,6 +107,7 @@ export default {
|
|||||||
conditionTypes: conditionTypes,
|
conditionTypes: conditionTypes,
|
||||||
evalFunctions: evalFunctions,
|
evalFunctions: evalFunctions,
|
||||||
severityLevels: severityLevels,
|
severityLevels: severityLevels,
|
||||||
|
noDataModes: noDataModes,
|
||||||
reducerTypes: reducerTypes,
|
reducerTypes: reducerTypes,
|
||||||
createReducerPart: createReducerPart,
|
createReducerPart: createReducerPart,
|
||||||
};
|
};
|
||||||
|
@ -13,7 +13,7 @@ export class AlertListCtrl {
|
|||||||
stateFilters = [
|
stateFilters = [
|
||||||
{text: 'All', value: null},
|
{text: 'All', value: null},
|
||||||
{text: 'OK', value: 'ok'},
|
{text: 'OK', value: 'ok'},
|
||||||
{text: 'Pending', value: 'pending'},
|
{text: 'Unknown', value: 'unknown'},
|
||||||
{text: 'Warning', value: 'warning'},
|
{text: 'Warning', value: 'warning'},
|
||||||
{text: 'Critical', value: 'critical'},
|
{text: 'Critical', value: 'critical'},
|
||||||
{text: 'Execution Error', value: 'execution_error'},
|
{text: 'Execution Error', value: 'execution_error'},
|
||||||
|
@ -5,6 +5,7 @@ import {ThresholdMapper} from './threshold_mapper';
|
|||||||
import {QueryPart} from 'app/core/components/query_part/query_part';
|
import {QueryPart} from 'app/core/components/query_part/query_part';
|
||||||
import alertDef from './alert_def';
|
import alertDef from './alert_def';
|
||||||
import config from 'app/core/config';
|
import config from 'app/core/config';
|
||||||
|
import moment from 'moment';
|
||||||
|
|
||||||
export class AlertTabCtrl {
|
export class AlertTabCtrl {
|
||||||
panel: any;
|
panel: any;
|
||||||
@ -17,11 +18,13 @@ export class AlertTabCtrl {
|
|||||||
conditionModels: any;
|
conditionModels: any;
|
||||||
evalFunctions: any;
|
evalFunctions: any;
|
||||||
severityLevels: any;
|
severityLevels: any;
|
||||||
|
noDataModes: any;
|
||||||
addNotificationSegment;
|
addNotificationSegment;
|
||||||
notifications;
|
notifications;
|
||||||
alertNotifications;
|
alertNotifications;
|
||||||
error: string;
|
error: string;
|
||||||
appSubUrl: string;
|
appSubUrl: string;
|
||||||
|
alertHistory: any;
|
||||||
|
|
||||||
/** @ngInject */
|
/** @ngInject */
|
||||||
constructor(private $scope,
|
constructor(private $scope,
|
||||||
@ -39,6 +42,7 @@ export class AlertTabCtrl {
|
|||||||
this.evalFunctions = alertDef.evalFunctions;
|
this.evalFunctions = alertDef.evalFunctions;
|
||||||
this.conditionTypes = alertDef.conditionTypes;
|
this.conditionTypes = alertDef.conditionTypes;
|
||||||
this.severityLevels = alertDef.severityLevels;
|
this.severityLevels = alertDef.severityLevels;
|
||||||
|
this.noDataModes = alertDef.noDataModes;
|
||||||
this.appSubUrl = config.appSubUrl;
|
this.appSubUrl = config.appSubUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -60,6 +64,7 @@ export class AlertTabCtrl {
|
|||||||
// build notification model
|
// build notification model
|
||||||
this.notifications = [];
|
this.notifications = [];
|
||||||
this.alertNotifications = [];
|
this.alertNotifications = [];
|
||||||
|
this.alertHistory = [];
|
||||||
|
|
||||||
return this.backendSrv.get('/api/alert-notifications').then(res => {
|
return this.backendSrv.get('/api/alert-notifications').then(res => {
|
||||||
this.notifications = res;
|
this.notifications = res;
|
||||||
@ -74,6 +79,21 @@ export class AlertTabCtrl {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getAlertHistory() {
|
||||||
|
this.backendSrv.get(`/api/alert-history?dashboardId=${this.panelCtrl.dashboard.id}&panelId=${this.panel.id}`).then(res => {
|
||||||
|
this.alertHistory = _.map(res, ah => {
|
||||||
|
ah.time = moment(ah.timestamp).format('MMM D, YYYY HH:mm:ss');
|
||||||
|
ah.stateModel = alertDef.getStateDisplayModel(ah.newState);
|
||||||
|
|
||||||
|
ah.metrics = _.map(ah.data, ev=> {
|
||||||
|
return ev.Metric + "=" + ev.Value;
|
||||||
|
}).join(', ');
|
||||||
|
|
||||||
|
return ah;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
getNotificationIcon(type) {
|
getNotificationIcon(type) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case "email": return "fa fa-envelope";
|
case "email": return "fa fa-envelope";
|
||||||
@ -88,6 +108,13 @@ export class AlertTabCtrl {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
changeTabIndex(newTabIndex) {
|
||||||
|
this.subTabIndex = newTabIndex;
|
||||||
|
|
||||||
|
if (this.subTabIndex === 2) {
|
||||||
|
this.getAlertHistory();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
notificationAdded() {
|
notificationAdded() {
|
||||||
var model = _.findWhere(this.notifications, {name: this.addNotificationSegment.value});
|
var model = _.findWhere(this.notifications, {name: this.addNotificationSegment.value});
|
||||||
@ -109,13 +136,18 @@ export class AlertTabCtrl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
initModel() {
|
initModel() {
|
||||||
var alert = this.alert = this.panel.alert = this.panel.alert || {};
|
var alert = this.alert = this.panel.alert = this.panel.alert || {enabled: false};
|
||||||
|
|
||||||
|
if (!this.alert.enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
alert.conditions = alert.conditions || [];
|
alert.conditions = alert.conditions || [];
|
||||||
if (alert.conditions.length === 0) {
|
if (alert.conditions.length === 0) {
|
||||||
alert.conditions.push(this.buildDefaultCondition());
|
alert.conditions.push(this.buildDefaultCondition());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
alert.noDataState = alert.noDataState || 'unknown';
|
||||||
alert.severity = alert.severity || 'critical';
|
alert.severity = alert.severity || 'critical';
|
||||||
alert.frequency = alert.frequency || '60s';
|
alert.frequency = alert.frequency || '60s';
|
||||||
alert.handler = alert.handler || 1;
|
alert.handler = alert.handler || 1;
|
||||||
@ -129,11 +161,9 @@ export class AlertTabCtrl {
|
|||||||
return memo;
|
return memo;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (this.alert.enabled) {
|
|
||||||
this.panelCtrl.editingThresholds = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
ThresholdMapper.alertToGraphThresholds(this.panel);
|
ThresholdMapper.alertToGraphThresholds(this.panel);
|
||||||
|
|
||||||
|
this.panelCtrl.editingThresholds = true;
|
||||||
this.panelCtrl.render();
|
this.panelCtrl.render();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -157,6 +187,10 @@ export class AlertTabCtrl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
validateModel() {
|
validateModel() {
|
||||||
|
if (!this.alert.enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let firstTarget;
|
let firstTarget;
|
||||||
var fixed = false;
|
var fixed = false;
|
||||||
let foundTarget = null;
|
let foundTarget = null;
|
||||||
@ -192,6 +226,8 @@ export class AlertTabCtrl {
|
|||||||
this.error = 'Currently the alerting backend only supports Graphite queries';
|
this.error = 'Currently the alerting backend only supports Graphite queries';
|
||||||
} else if (this.templateSrv.variableExists(foundTarget.target)) {
|
} else if (this.templateSrv.variableExists(foundTarget.target)) {
|
||||||
this.error = 'Template variables are not supported in alert queries';
|
this.error = 'Template variables are not supported in alert queries';
|
||||||
|
} else {
|
||||||
|
this.error = '';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,8 @@ import config from 'app/core/config';
|
|||||||
|
|
||||||
export class AlertNotificationEditCtrl {
|
export class AlertNotificationEditCtrl {
|
||||||
model: any;
|
model: any;
|
||||||
|
showTest: boolean = false;
|
||||||
|
testSeverity: string = "critical";
|
||||||
|
|
||||||
/** @ngInject */
|
/** @ngInject */
|
||||||
constructor(private $routeParams, private backendSrv, private $scope, private $location) {
|
constructor(private $routeParams, private backendSrv, private $scope, private $location) {
|
||||||
@ -15,7 +17,10 @@ export class AlertNotificationEditCtrl {
|
|||||||
} else {
|
} else {
|
||||||
this.model = {
|
this.model = {
|
||||||
type: 'email',
|
type: 'email',
|
||||||
settings: {}
|
settings: {
|
||||||
|
severityFilter: 'none'
|
||||||
|
},
|
||||||
|
isDefault: false
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -38,8 +43,8 @@ export class AlertNotificationEditCtrl {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.backendSrv.post(`/api/alert-notifications`, this.model).then(res => {
|
this.backendSrv.post(`/api/alert-notifications`, this.model).then(res => {
|
||||||
this.$location.path('alerting/notification/' + res.id + '/edit');
|
|
||||||
this.$scope.appEvent('alert-success', ['Notification created', '']);
|
this.$scope.appEvent('alert-success', ['Notification created', '']);
|
||||||
|
this.$location.path('alerting/notifications');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -47,6 +52,24 @@ export class AlertNotificationEditCtrl {
|
|||||||
typeChanged() {
|
typeChanged() {
|
||||||
this.model.settings = {};
|
this.model.settings = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toggleTest() {
|
||||||
|
this.showTest = !this.showTest;
|
||||||
|
}
|
||||||
|
|
||||||
|
testNotification() {
|
||||||
|
var payload = {
|
||||||
|
name: this.model.name,
|
||||||
|
type: this.model.type,
|
||||||
|
settings: this.model.settings,
|
||||||
|
severity: this.testSeverity
|
||||||
|
};
|
||||||
|
|
||||||
|
this.backendSrv.post(`/api/alert-notifications/test`, payload)
|
||||||
|
.then(res => {
|
||||||
|
this.$scope.appEvent('alert-succes', ['Test notification sent', '']);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
coreModule.controller('AlertNotificationEditCtrl', AlertNotificationEditCtrl);
|
coreModule.controller('AlertNotificationEditCtrl', AlertNotificationEditCtrl);
|
||||||
|
@ -2,15 +2,15 @@
|
|||||||
<aside class="edit-sidemenu-aside">
|
<aside class="edit-sidemenu-aside">
|
||||||
<ul class="edit-sidemenu">
|
<ul class="edit-sidemenu">
|
||||||
<li ng-class="{active: ctrl.subTabIndex === 0}">
|
<li ng-class="{active: ctrl.subTabIndex === 0}">
|
||||||
<a ng-click="ctrl.subTabIndex = 0">Alert Config</a>
|
<a ng-click="ctrl.changeTabIndex(0)">Alert Config</a>
|
||||||
</li>
|
</li>
|
||||||
<li ng-class="{active: ctrl.subTabIndex === 1}">
|
<li ng-class="{active: ctrl.subTabIndex === 1}">
|
||||||
<a ng-click="ctrl.subTabIndex = 1">
|
<a ng-click="ctrl.changeTabIndex(1)">
|
||||||
Notifications <span class="muted">({{ctrl.alert.notifications.length}})</span>
|
Notifications <span class="muted">({{ctrl.alert.notifications.length}})</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li ng-class="{active: ctrl.subTabIndex === 2}">
|
<li ng-class="{active: ctrl.subTabIndex === 2}">
|
||||||
<a ng-click="ctrl.subTabIndex = 2">Alert History</a>
|
<a ng-click="ctrl.changeTabIndex(2)">Alert History</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a ng-click="ctrl.delete()">Delete</a>
|
<a ng-click="ctrl.delete()">Delete</a>
|
||||||
@ -52,20 +52,20 @@
|
|||||||
<span class="gf-form-label query-keyword width-5" ng-if="$index">AND</span>
|
<span class="gf-form-label query-keyword width-5" ng-if="$index">AND</span>
|
||||||
<span class="gf-form-label query-keyword width-5" ng-if="$index===0">WHEN</span>
|
<span class="gf-form-label query-keyword width-5" ng-if="$index===0">WHEN</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="gf-form">
|
<div class="gf-form">
|
||||||
<query-part-editor class="gf-form-label query-part" part="conditionModel.queryPart" handle-event="ctrl.handleQueryPartEvent(conditionModel, $event)">
|
<query-part-editor class="gf-form-label query-part" part="conditionModel.reducerPart" handle-event="ctrl.handleReducerPartEvent(conditionModel, $event)">
|
||||||
</query-part-editor>
|
</query-part-editor>
|
||||||
|
<span class="gf-form-label query-keyword">OF</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="gf-form">
|
<div class="gf-form">
|
||||||
<span class="gf-form-label">Reducer</span>
|
<query-part-editor class="gf-form-label query-part" part="conditionModel.queryPart" handle-event="ctrl.handleQueryPartEvent(conditionModel, $event)">
|
||||||
<query-part-editor class="gf-form-label query-part" part="conditionModel.reducerPart" handle-event="ctrl.handleReducerPartEvent(conditionModel, $event)">
|
|
||||||
</query-part-editor>
|
</query-part-editor>
|
||||||
</div>
|
</div>
|
||||||
<div class="gf-form">
|
<div class="gf-form">
|
||||||
<metric-segment-model property="conditionModel.evaluator.type" options="ctrl.evalFunctions" custom="false" css-class="query-keyword" on-change="ctrl.evaluatorTypeChanged(conditionModel.evaluator)"></metric-segment-model>
|
<metric-segment-model property="conditionModel.evaluator.type" options="ctrl.evalFunctions" custom="false" css-class="query-keyword" on-change="ctrl.evaluatorTypeChanged(conditionModel.evaluator)"></metric-segment-model>
|
||||||
<input class="gf-form-input max-width-7" type="number" ng-hide="conditionModel.evaluator.params.length === 0" ng-model="conditionModel.evaluator.params[0]" ng-change="ctrl.evaluatorParamsChanged()"></input>
|
<input class="gf-form-input max-width-7" type="number" ng-hide="conditionModel.evaluator.params.length === 0" ng-model="conditionModel.evaluator.params[0]" ng-change="ctrl.evaluatorParamsChanged()"></input>
|
||||||
<label class="gf-form-label query-keyword" ng-show="conditionModel.evaluator.params.length === 2">TO</label>
|
<label class="gf-form-label query-keyword" ng-show="conditionModel.evaluator.params.length === 2">TO</label>
|
||||||
<input class="gf-form-input max-width-7" type="number" ng-if="conditionModel.evaluator.params.length === 2" ng-model="conditionModel.evaluator.params[1]" ng-change="ctrl.evaluatorParamsChanged()"></input>
|
<input class="gf-form-input max-width-7" type="number" ng-if="conditionModel.evaluator.params.length === 2" ng-model="conditionModel.evaluator.params[1]" ng-change="ctrl.evaluatorParamsChanged()"></input>
|
||||||
</div>
|
</div>
|
||||||
<div class="gf-form">
|
<div class="gf-form">
|
||||||
<label class="gf-form-label">
|
<label class="gf-form-label">
|
||||||
@ -89,6 +89,18 @@
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="gf-form-group">
|
||||||
|
<div class="gf-form">
|
||||||
|
<span class="gf-form-label">If no data points or all values are null</span>
|
||||||
|
<span class="gf-form-label query-keyword">SET STATE TO</span>
|
||||||
|
<div class="gf-form-select-wrapper">
|
||||||
|
<select class="gf-form-input" ng-model="ctrl.alert.noDataState" ng-options="f.value as f.text for f in ctrl.noDataModes">
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="gf-form-button-row">
|
<div class="gf-form-button-row">
|
||||||
<button class="btn btn-inverse" ng-click="ctrl.test()">
|
<button class="btn btn-inverse" ng-click="ctrl.test()">
|
||||||
Test Rule
|
Test Rule
|
||||||
@ -122,6 +134,31 @@
|
|||||||
<textarea class="gf-form-input width-20" rows="10" ng-model="ctrl.alert.message" placeholder="Notification message details..."></textarea>
|
<textarea class="gf-form-input width-20" rows="10" ng-model="ctrl.alert.message" placeholder="Notification message details..."></textarea>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="gf-form-group" style="max-width: 720px;" ng-if="ctrl.subTabIndex === 2">
|
||||||
|
<h5 class="section-heading">Alert history</h5>
|
||||||
|
<section class="card-section card-list-layout-list">
|
||||||
|
<ol class="card-list" >
|
||||||
|
<li class="card-item-wrapper" ng-repeat="ah in ctrl.alertHistory">
|
||||||
|
<div class="card-item card-item--alert">
|
||||||
|
<div class="card-item-body">
|
||||||
|
<div class="card-item-details">
|
||||||
|
<div class="card-item-sub-name">
|
||||||
|
<span class="alert-list-item-state {{ah.stateModel.stateClass}}">
|
||||||
|
<i class="{{ah.stateModel.iconClass}}"></i>
|
||||||
|
{{ah.stateModel.text}}
|
||||||
|
</span> {{ah.metrics}}
|
||||||
|
</div>
|
||||||
|
<div class="card-item-sub-name">
|
||||||
|
{{ah.time}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -12,11 +12,11 @@
|
|||||||
|
|
||||||
<div class="gf-form-group">
|
<div class="gf-form-group">
|
||||||
<div class="gf-form">
|
<div class="gf-form">
|
||||||
<span class="gf-form-label width-8">Name</span>
|
<span class="gf-form-label width-12">Name</span>
|
||||||
<input type="text" class="gf-form-input max-width-15" ng-model="ctrl.model.name" required></input>
|
<input type="text" class="gf-form-input max-width-15" ng-model="ctrl.model.name" required></input>
|
||||||
</div>
|
</div>
|
||||||
<div class="gf-form">
|
<div class="gf-form">
|
||||||
<span class="gf-form-label width-8">Type</span>
|
<span class="gf-form-label width-12">Type</span>
|
||||||
<div class="gf-form-select-wrapper width-15">
|
<div class="gf-form-select-wrapper width-15">
|
||||||
<select class="gf-form-input"
|
<select class="gf-form-input"
|
||||||
ng-model="ctrl.model.type"
|
ng-model="ctrl.model.type"
|
||||||
@ -25,6 +25,24 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="gf-form">
|
||||||
|
<span class="gf-form-label width-12">Severity filter</span>
|
||||||
|
<div class="gf-form-select-wrapper width-15">
|
||||||
|
<select class="gf-form-input"
|
||||||
|
ng-model="ctrl.model.settings.severityFilter"
|
||||||
|
ng-options="t for t in ['none', 'critical', 'warning']">
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="gf-form">
|
||||||
|
<gf-form-switch
|
||||||
|
class="gf-form"
|
||||||
|
label="Send on all alerts"
|
||||||
|
label-class="width-12"
|
||||||
|
checked="ctrl.model.isDefault"
|
||||||
|
tooltip="Use this notification for all alerts">
|
||||||
|
</gf-form-switch>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="gf-form-group" ng-show="ctrl.model.type === 'webhook'">
|
<div class="gf-form-group" ng-show="ctrl.model.type === 'webhook'">
|
||||||
@ -60,7 +78,27 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="gf-form-button-row">
|
<div class="gf-form-group">
|
||||||
<button ng-click="ctrl.save()" class="btn btn-success">Save</button>
|
<div class="gf-form-inline">
|
||||||
</div>
|
<div class="gf-form width-6">
|
||||||
|
<button ng-click="ctrl.save()" class="btn btn-success">Save</button>
|
||||||
|
</div>
|
||||||
|
<div class="gf-form width-8">
|
||||||
|
<button ng-click="ctrl.toggleTest()" class="btn btn-secondary">Test</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="gf-form width-20" ng-show="ctrl.showTest">
|
||||||
|
<span class="gf-form-label width-13">Severity for test notification</span>
|
||||||
|
<div class="gf-form-select-wrapper width-7">
|
||||||
|
<select class="gf-form-input"
|
||||||
|
ng-model="ctrl.testSeverity"
|
||||||
|
ng-options="t for t in ['critical', 'warning', 'ok']">
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="gf-form" ng-show="ctrl.showTest">
|
||||||
|
<button ng-click="ctrl.testNotification()" class="btn btn-secondary">Send</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<table class="grafana-options-table" style="/*width: 600px;*/">
|
<table class="grafana-options-table">
|
||||||
<thead>
|
<thead>
|
||||||
<th style="min-width: 200px"><strong>Name</strong></th>
|
<th style="min-width: 200px"><strong>Name</strong></th>
|
||||||
<th style="min-width: 100px">Type</th>
|
<th style="min-width: 100px">Type</th>
|
||||||
@ -25,7 +25,10 @@
|
|||||||
<td>
|
<td>
|
||||||
{{notification.type}}
|
{{notification.type}}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td class="text-right">
|
||||||
|
<span class="btn btn-secondary btn-small" ng-show="notification.isDefault == true">
|
||||||
|
default
|
||||||
|
</span>
|
||||||
<a href="alerting/notification/{{notification.id}}/edit" class="btn btn-inverse btn-small">
|
<a href="alerting/notification/{{notification.id}}/edit" class="btn btn-inverse btn-small">
|
||||||
<i class="fa fa-edit"></i>
|
<i class="fa fa-edit"></i>
|
||||||
edit
|
edit
|
||||||
|
@ -158,18 +158,13 @@ export class DashNavCtrl {
|
|||||||
$scope.deleteDashboard = function() {
|
$scope.deleteDashboard = function() {
|
||||||
var confirmText = "";
|
var confirmText = "";
|
||||||
var text2 = $scope.dashboard.title;
|
var text2 = $scope.dashboard.title;
|
||||||
var alerts = 0;
|
var alerts = $scope.dashboard.rows.reduce((memo, row) => {
|
||||||
|
memo += row.panels.filter(panel => panel.alert && panel.alert.enabled).length;
|
||||||
_.each($scope.dashboard.rows, row => {
|
return memo;
|
||||||
_.each(row.panels, panel => {
|
}, 0);
|
||||||
if (panel.alerting && panel.alerting.queryRef !== '- select query -') {
|
|
||||||
alerts += 1;
|
|
||||||
};
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
if (alerts > 0) {
|
if (alerts > 0) {
|
||||||
confirmText = $scope.dashboard.title;
|
confirmText = 'DELETE';
|
||||||
text2 = `This dashboad contains ${alerts} alerts. Deleting this dashboad will also delete those alerts`;
|
text2 = `This dashboad contains ${alerts} alerts. Deleting this dashboad will also delete those alerts`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -36,7 +36,7 @@
|
|||||||
|
|
||||||
<div class="gf-form-group">
|
<div class="gf-form-group">
|
||||||
<div class="gf-form">
|
<div class="gf-form">
|
||||||
<textarea rows="7" data-share-panel-url="" class="gf-form-input" ng-ctrl="ctrl.jsonText"></textarea>
|
<textarea rows="7" data-share-panel-url="" class="gf-form-input" ng-model="ctrl.jsonText"></textarea>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="btn btn-secondary" ng-click="ctrl.loadJsonText()">
|
<button type="button" class="btn btn-secondary" ng-click="ctrl.loadJsonText()">
|
||||||
<i class="fa fa-paste"></i>
|
<i class="fa fa-paste"></i>
|
||||||
|
@ -12,6 +12,14 @@ function (angular) {
|
|||||||
$scope.clone.id = null;
|
$scope.clone.id = null;
|
||||||
$scope.clone.editable = true;
|
$scope.clone.editable = true;
|
||||||
$scope.clone.title = $scope.clone.title + " Copy";
|
$scope.clone.title = $scope.clone.title + " Copy";
|
||||||
|
|
||||||
|
// remove alerts
|
||||||
|
$scope.clone.rows.forEach(function(row) {
|
||||||
|
row.panels.forEach(function(panel) {
|
||||||
|
delete panel.alert;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// remove auto update
|
// remove auto update
|
||||||
delete $scope.clone.autoUpdate;
|
delete $scope.clone.autoUpdate;
|
||||||
};
|
};
|
||||||
|
@ -22,14 +22,13 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div class="modal-content-confirm-text" ng-if="confirmTextRequired">
|
<div class="modal-content-confirm-text" ng-if="confirmText">
|
||||||
<span><i class="fa fa-warning"></i> Please type in the name of the dashboard to confirm.</span>
|
<input type="text" class="gf-form-input width-16" style="display: inline-block;" placeholder="Type {{confirmText}} to confirm" ng-model="confirmInput" ng-change="updateConfirmText(confirmInput)">
|
||||||
<input type="text" class="gf-form-input width-16" style="display: inline-block;" ng-model="confirmInput" ng-change="updateConfirmText(confirmInput)">
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="confirm-modal-buttons">
|
<div class="confirm-modal-buttons">
|
||||||
<button type="button" class="btn btn-inverse" ng-click="dismiss()">{{noText}}</button>
|
<button type="button" class="btn btn-inverse" ng-click="dismiss()">{{noText}}</button>
|
||||||
<button type="button" class="btn btn-danger" ng-click="onConfirm();dismiss();">{{yesText}}</button>
|
<button type="button" class="btn btn-danger" ng-click="onConfirm();dismiss();" ng-disabled="!confirmTextValid">{{yesText}}</button>
|
||||||
<button ng-show="onAltAction" type="button" class="btn btn-success" ng-click="dismiss();onAltAction();">{{altActionText}}</button>
|
<button ng-show="onAltAction" type="button" class="btn btn-success" ng-click="dismiss();onAltAction();">{{altActionText}}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -59,6 +59,10 @@
|
|||||||
<i class="fa fa-github"></i>
|
<i class="fa fa-github"></i>
|
||||||
with Github
|
with Github
|
||||||
</a>
|
</a>
|
||||||
|
<a class="btn btn-large btn-generic-oauth" href="login/generic_oauth" target="_self" ng-if="genericOAuthEnabled">
|
||||||
|
<i class="fa fa-gear"></i>
|
||||||
|
with {{oauthProviderName || "OAuth 2"}}
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -254,6 +254,15 @@ register({
|
|||||||
renderer: functionRenderer,
|
renderer: functionRenderer,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
register({
|
||||||
|
type: 'elapsed',
|
||||||
|
addStrategy: addTransformationStrategy,
|
||||||
|
category: categories.Transformations,
|
||||||
|
params: [{ name: "duration", type: "interval", options: ['1s', '10s', '1m', '5m', '10m', '15m', '1h']}],
|
||||||
|
defaultParams: ['10s'],
|
||||||
|
renderer: functionRenderer,
|
||||||
|
});
|
||||||
|
|
||||||
// Selectors
|
// Selectors
|
||||||
register({
|
register({
|
||||||
type: 'bottom',
|
type: 'bottom',
|
||||||
|
@ -61,7 +61,6 @@ function (angular, $, moment, _, kbn, GraphTooltip, thresholdManExports) {
|
|||||||
ctrl.events.on('render', function(renderData) {
|
ctrl.events.on('render', function(renderData) {
|
||||||
data = renderData || data;
|
data = renderData || data;
|
||||||
if (!data) {
|
if (!data) {
|
||||||
ctrl.refresh();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
annotations = data.annotations || annotations;
|
annotations = data.annotations || annotations;
|
||||||
|
@ -39,7 +39,6 @@ $brand-primary: $orange;
|
|||||||
$brand-success: $green;
|
$brand-success: $green;
|
||||||
$brand-warning: $brand-primary;
|
$brand-warning: $brand-primary;
|
||||||
$brand-danger: $red;
|
$brand-danger: $red;
|
||||||
$brand-text-highlight: #f7941d;
|
|
||||||
|
|
||||||
// Status colors
|
// Status colors
|
||||||
// -------------------------
|
// -------------------------
|
||||||
|
@ -44,7 +44,6 @@ $brand-primary: $orange;
|
|||||||
$brand-success: $green;
|
$brand-success: $green;
|
||||||
$brand-warning: $orange;
|
$brand-warning: $orange;
|
||||||
$brand-danger: $red;
|
$brand-danger: $red;
|
||||||
$brand-text-highlight: #f7941d;
|
|
||||||
|
|
||||||
// Status colors
|
// Status colors
|
||||||
// -------------------------
|
// -------------------------
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user