mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge branch 'master' of https://github.com/grafana/grafana
This commit is contained in:
commit
0000065053
2
.github/CONTRIBUTING.md
vendored
2
.github/CONTRIBUTING.md
vendored
@ -12,7 +12,7 @@ grunt karma:dev
|
||||
|
||||
### Run tests for backend assets before commit
|
||||
```
|
||||
test -z "$(gofmt -s -l . | grep -v vendor/src/ | tee /dev/stderr)"
|
||||
test -z "$(gofmt -s -l . | grep -v -E 'vendor/(github.com|golang.org|gopkg.in)' | tee /dev/stderr)"
|
||||
```
|
||||
|
||||
### Run tests for frontend assets before commit
|
||||
|
@ -12,6 +12,10 @@
|
||||
* **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)
|
||||
* **OAuth**: Add support for generic oauth, closes [#4718](https://github.com/grafana/grafana/pull/4718)
|
||||
* **Cloudwatch**: Add support to expand multi select template variable, closes [#5003](https://github.com/grafana/grafana/pull/5003)
|
||||
* **Graph Panel**: Now supports flexible lower/upper bounds on Y-Max and Y-Min, PR [#5720](https://github.com/grafana/grafana/pull/5720)
|
||||
* **Background Tasks**: Now support automatic purging of old snapshots, closes [#4087](https://github.com/grafana/grafana/issues/4087)
|
||||
* **Background Tasks**: Now support automatic purging of old rendered images, closes [#2172](https://github.com/grafana/grafana/issues/2172)
|
||||
|
||||
### Breaking changes
|
||||
* **SystemD**: Change systemd description, closes [#5971](https://github.com/grafana/grafana/pull/5971)
|
||||
@ -19,6 +23,11 @@
|
||||
|
||||
### Bugfixes
|
||||
* **Table Panel**: Fixed problem when switching to Mixed datasource in metrics tab, fixes [#5999](https://github.com/grafana/grafana/pull/5999)
|
||||
* **Playlist**: Fixed problem with play order not matching order defined in playlist, fixes [#5467](https://github.com/grafana/grafana/pull/5467)
|
||||
* **Graph panel**: Fixed problem with auto decimals on y axis when datamin=datamax, fixes [#6070](https://github.com/grafana/grafana/pull/6070)
|
||||
* **Snapshot**: Can view embedded panels/png rendered panels in snapshots without login, fixes [#3769](https://github.com/grafana/grafana/pull/3769)
|
||||
* **Elasticsearch**: Fix for query template variable when looking up terms without query, no longer relies on elasticsearch default field, fixes [#3887](https://github.com/grafana/grafana/pull/3887)
|
||||
* **PNG Rendering**: Fix for server side rendering when using auth proxy, fixes [#5906](https://github.com/grafana/grafana/pull/5906)
|
||||
|
||||
# 3.1.2 (unreleased)
|
||||
* **Templating**: Fixed issue when combining row & panel repeats, fixes [#5790](https://github.com/grafana/grafana/issues/5790)
|
||||
|
@ -9,7 +9,6 @@ module.exports = function (grunt) {
|
||||
genDir: 'public_gen',
|
||||
destDir: 'dist',
|
||||
tempDir: 'tmp',
|
||||
arch: os.arch(),
|
||||
platform: process.platform.replace('win32', 'windows'),
|
||||
};
|
||||
|
||||
@ -17,6 +16,10 @@ module.exports = function (grunt) {
|
||||
config.arch = process.env.hasOwnProperty('ProgramFiles(x86)') ? 'x64' : 'x86';
|
||||
}
|
||||
|
||||
config.arch = grunt.option('arch') || os.arch();
|
||||
|
||||
config.phjs = grunt.option('phjsToRelease');
|
||||
|
||||
config.pkg.version = grunt.option('pkgVer') || config.pkg.version;
|
||||
console.log('Version', config.pkg.version);
|
||||
|
||||
|
@ -96,7 +96,7 @@ easily the grafana repository you want to build.
|
||||
```bash
|
||||
go get github.com/*your_account*/grafana
|
||||
mkdir $GOPATH/src/github.com/grafana
|
||||
ln -s github.com/*your_account*/grafana $GOPATH/src/github.com/grafana/grafana
|
||||
ln -s $GOPATH/src/github.com/*your_account*/grafana $GOPATH/src/github.com/grafana/grafana
|
||||
```
|
||||
|
||||
### Building the backend
|
||||
|
44
build.go
44
build.go
@ -25,11 +25,16 @@ var (
|
||||
versionRe = regexp.MustCompile(`-[0-9]{1,3}-g[0-9a-f]{5,10}`)
|
||||
goarch string
|
||||
goos string
|
||||
gocc string
|
||||
gocxx string
|
||||
cgo string
|
||||
pkgArch string
|
||||
version string = "v1"
|
||||
// deb & rpm does not support semver so have to handle their version a little differently
|
||||
linuxPackageVersion string = "v1"
|
||||
linuxPackageIteration string = ""
|
||||
race bool
|
||||
phjsToRelease string
|
||||
workingDir string
|
||||
binaries []string = []string{"grafana-server", "grafana-cli"}
|
||||
)
|
||||
@ -47,6 +52,11 @@ func main() {
|
||||
|
||||
flag.StringVar(&goarch, "goarch", runtime.GOARCH, "GOARCH")
|
||||
flag.StringVar(&goos, "goos", runtime.GOOS, "GOOS")
|
||||
flag.StringVar(&gocc, "cc", "", "CC")
|
||||
flag.StringVar(&gocxx, "cxx", "", "CXX")
|
||||
flag.StringVar(&cgo, "cgo-enabled", "", "CGO_ENABLED")
|
||||
flag.StringVar(&pkgArch, "pkg-arch", "", "PKG ARCH")
|
||||
flag.StringVar(&phjsToRelease, "phjs", "", "PhantomJS binary")
|
||||
flag.BoolVar(&race, "race", race, "Use race detector")
|
||||
flag.Parse()
|
||||
|
||||
@ -73,15 +83,15 @@ func main() {
|
||||
grunt("test")
|
||||
|
||||
case "package":
|
||||
grunt("release", fmt.Sprintf("--pkgVer=%v-%v", linuxPackageVersion, linuxPackageIteration))
|
||||
grunt(gruntBuildArg("release")...)
|
||||
createLinuxPackages()
|
||||
|
||||
case "pkg-rpm":
|
||||
grunt("release")
|
||||
grunt(gruntBuildArg("release")...)
|
||||
createRpmPackages()
|
||||
|
||||
case "pkg-deb":
|
||||
grunt("release")
|
||||
grunt(gruntBuildArg("release")...)
|
||||
createDebPackages()
|
||||
|
||||
case "latest":
|
||||
@ -258,6 +268,10 @@ func createPackage(options linuxPackageOptions) {
|
||||
"-p", "./dist",
|
||||
}
|
||||
|
||||
if pkgArch != "" {
|
||||
args = append(args, "-a", pkgArch)
|
||||
}
|
||||
|
||||
if linuxPackageIteration != "" {
|
||||
args = append(args, "--iteration", linuxPackageIteration)
|
||||
}
|
||||
@ -307,11 +321,20 @@ func grunt(params ...string) {
|
||||
runPrint("./node_modules/.bin/grunt", params...)
|
||||
}
|
||||
|
||||
func gruntBuildArg(task string) []string {
|
||||
args := []string{task, fmt.Sprintf("--pkgVer=%v-%v", linuxPackageVersion, linuxPackageIteration)}
|
||||
if pkgArch != "" {
|
||||
args = append(args, fmt.Sprintf("--arch=%v", pkgArch))
|
||||
}
|
||||
if phjsToRelease != "" {
|
||||
args = append(args, fmt.Sprintf("--phjsToRelease=%v", phjsToRelease))
|
||||
}
|
||||
return args
|
||||
}
|
||||
|
||||
func setup() {
|
||||
runPrint("go", "get", "-v", "github.com/kardianos/govendor")
|
||||
runPrint("go", "get", "-v", "github.com/blang/semver")
|
||||
runPrint("go", "get", "-v", "github.com/mattn/go-sqlite3")
|
||||
runPrint("go", "install", "-v", "github.com/mattn/go-sqlite3")
|
||||
runPrint("go", "install", "-v", "./pkg/cmd/grafana-server")
|
||||
}
|
||||
|
||||
func test(pkg string) {
|
||||
@ -382,6 +405,15 @@ func setBuildEnv() {
|
||||
if goarch == "386" {
|
||||
os.Setenv("GO386", "387")
|
||||
}
|
||||
if cgo != "" {
|
||||
os.Setenv("CGO_ENABLED", cgo)
|
||||
}
|
||||
if gocc != "" {
|
||||
os.Setenv("CC", gocc)
|
||||
}
|
||||
if gocxx != "" {
|
||||
os.Setenv("CXX", gocxx)
|
||||
}
|
||||
}
|
||||
|
||||
func getGitSha() string {
|
||||
|
@ -9,7 +9,7 @@ app_mode = production
|
||||
# instance name, defaults to HOSTNAME environment variable value or hostname if HOSTNAME var is empty
|
||||
instance_name = ${HOSTNAME}
|
||||
|
||||
#################################### Paths ####################################
|
||||
#################################### Paths ###############################
|
||||
[paths]
|
||||
# Path to where grafana can store temp files, sessions, and the sqlite3 db (if that is used)
|
||||
#
|
||||
@ -23,7 +23,7 @@ logs = data/log
|
||||
#
|
||||
plugins = data/plugins
|
||||
|
||||
#################################### Server ####################################
|
||||
#################################### Server ##############################
|
||||
[server]
|
||||
# Protocol (http or https)
|
||||
protocol = http
|
||||
@ -57,7 +57,7 @@ enable_gzip = false
|
||||
cert_file =
|
||||
cert_key =
|
||||
|
||||
#################################### Database ####################################
|
||||
#################################### Database ############################
|
||||
[database]
|
||||
# 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.
|
||||
@ -84,7 +84,7 @@ server_cert_name =
|
||||
# For "sqlite3" only, path relative to data_path setting
|
||||
path = grafana.db
|
||||
|
||||
#################################### Session ####################################
|
||||
#################################### Session #############################
|
||||
[session]
|
||||
# Either "memory", "file", "redis", "mysql", "postgres", "memcache", default is "file"
|
||||
provider = file
|
||||
@ -112,7 +112,7 @@ cookie_secure = false
|
||||
session_life_time = 86400
|
||||
gc_interval_time = 86400
|
||||
|
||||
#################################### Analytics ####################################
|
||||
#################################### Analytics ###########################
|
||||
[analytics]
|
||||
# Server reporting, sends usage counters to stats.grafana.org every 24 hours.
|
||||
# No ip addresses are being tracked, only simple counters to track
|
||||
@ -133,7 +133,7 @@ google_analytics_ua_id =
|
||||
# Google Tag Manager ID, only enabled if you specify an id here
|
||||
google_tag_manager_id =
|
||||
|
||||
#################################### Security ####################################
|
||||
#################################### Security ############################
|
||||
[security]
|
||||
# default admin user, created on startup
|
||||
admin_user = admin
|
||||
@ -161,6 +161,12 @@ external_enabled = true
|
||||
external_snapshot_url = https://snapshots-origin.raintank.io
|
||||
external_snapshot_name = Publish to snapshot.raintank.io
|
||||
|
||||
# remove expired snapshot
|
||||
snapshot_remove_expired = true
|
||||
|
||||
# remove snapshots after 90 days
|
||||
snapshot_TTL_days = 90
|
||||
|
||||
#################################### Users ####################################
|
||||
[users]
|
||||
# disable user signup / registration
|
||||
@ -184,10 +190,11 @@ login_hint = email or username
|
||||
# Default UI theme ("dark" or "light")
|
||||
default_theme = dark
|
||||
|
||||
# Allow users to sign in using username and password
|
||||
allow_user_pass_login = true
|
||||
[auth]
|
||||
# Set to true to disable (hide) the login form, useful if you use OAuth
|
||||
disable_login_form = false
|
||||
|
||||
#################################### Anonymous Auth ##########################
|
||||
#################################### Anonymous Auth ######################
|
||||
[auth.anonymous]
|
||||
# enable anonymous access
|
||||
enabled = false
|
||||
@ -198,7 +205,7 @@ org_name = Main Org.
|
||||
# specify role for unauthenticated users
|
||||
org_role = Viewer
|
||||
|
||||
#################################### Github Auth ##########################
|
||||
#################################### Github Auth #########################
|
||||
[auth.github]
|
||||
enabled = false
|
||||
allow_sign_up = false
|
||||
@ -211,7 +218,7 @@ api_url = https://api.github.com/user
|
||||
team_ids =
|
||||
allowed_organizations =
|
||||
|
||||
#################################### Google Auth ##########################
|
||||
#################################### Google Auth #########################
|
||||
[auth.google]
|
||||
enabled = false
|
||||
allow_sign_up = false
|
||||
@ -223,7 +230,16 @@ token_url = https://accounts.google.com/o/oauth2/token
|
||||
api_url = https://www.googleapis.com/oauth2/v1/userinfo
|
||||
allowed_domains =
|
||||
|
||||
#################################### Generic OAuth ##########################
|
||||
#################################### Grafana.net Auth ####################
|
||||
[auth.grafananet]
|
||||
enabled = false
|
||||
allow_sign_up = false
|
||||
client_id = some_id
|
||||
client_secret = some_secret
|
||||
scopes = user:email
|
||||
allowed_organizations =
|
||||
|
||||
#################################### Generic OAuth #######################
|
||||
[auth.generic_oauth]
|
||||
enabled = false
|
||||
allow_sign_up = false
|
||||
@ -247,12 +263,12 @@ header_name = X-WEBAUTH-USER
|
||||
header_property = username
|
||||
auto_sign_up = true
|
||||
|
||||
#################################### Auth LDAP ##########################
|
||||
#################################### Auth LDAP ###########################
|
||||
[auth.ldap]
|
||||
enabled = false
|
||||
config_file = /etc/grafana/ldap.toml
|
||||
|
||||
#################################### SMTP / Emailing ##########################
|
||||
#################################### SMTP / Emailing #####################
|
||||
[smtp]
|
||||
enabled = false
|
||||
host = localhost:25
|
||||
@ -322,18 +338,18 @@ facility =
|
||||
tag =
|
||||
|
||||
|
||||
#################################### AMQP Event Publisher ##########################
|
||||
#################################### AMQP Event Publisher ################
|
||||
[event_publisher]
|
||||
enabled = false
|
||||
rabbitmq_url = amqp://localhost/
|
||||
exchange = grafana_events
|
||||
|
||||
#################################### Dashboard JSON files ##########################
|
||||
#################################### Dashboard JSON files ################
|
||||
[dashboards.json]
|
||||
enabled = false
|
||||
path = /var/lib/grafana/dashboards
|
||||
|
||||
#################################### Usage Quotas ##########################
|
||||
#################################### Usage Quotas ########################
|
||||
[quota]
|
||||
enabled = false
|
||||
|
||||
@ -368,7 +384,7 @@ global_api_key = -1
|
||||
# global limit on number of logged in users.
|
||||
global_session = -1
|
||||
|
||||
#################################### Alerting ######################################
|
||||
#################################### Alerting ############################
|
||||
# docs about alerting can be found in /docs/sources/alerting/
|
||||
# __.-/|
|
||||
# \`o_O'
|
||||
@ -387,7 +403,7 @@ global_session = -1
|
||||
[alerting]
|
||||
enabled = true
|
||||
|
||||
#################################### Internal Grafana Metrics ##########################
|
||||
#################################### Internal Grafana Metrics ############
|
||||
# Metrics available at HTTP API Url /api/metrics
|
||||
[metrics]
|
||||
enabled = true
|
||||
@ -402,9 +418,9 @@ prefix = prod.grafana.%(instance_name)s.
|
||||
[grafana_net]
|
||||
url = https://grafana.net
|
||||
|
||||
#################################### External image storage ##########################
|
||||
#################################### External Image Storage ##############
|
||||
[external_image_storage]
|
||||
# You can choose between (s3, webdav or internal)
|
||||
# You can choose between (s3, webdav)
|
||||
provider = s3
|
||||
|
||||
[external_image_storage.s3]
|
||||
|
@ -116,7 +116,7 @@
|
||||
# in some UI views to notify that grafana or plugin update exists
|
||||
# This option does not cause any auto updates, nor send any information
|
||||
# only a GET request to http://grafana.net to get latest versions
|
||||
check_for_updates = true
|
||||
;check_for_updates = true
|
||||
|
||||
# Google Analytics universal tracking code, only enabled if you specify an id here
|
||||
;google_analytics_ua_id =
|
||||
@ -149,6 +149,12 @@ check_for_updates = true
|
||||
;external_snapshot_url = https://snapshots-origin.raintank.io
|
||||
;external_snapshot_name = Publish to snapshot.raintank.io
|
||||
|
||||
# remove expired snapshot
|
||||
;snapshot_remove_expired = true
|
||||
|
||||
# remove snapshots after 90 days
|
||||
;snapshot_TTL_days = 90
|
||||
|
||||
#################################### Users ####################################
|
||||
[users]
|
||||
# disable user signup / registration
|
||||
@ -169,6 +175,10 @@ check_for_updates = true
|
||||
# Default UI theme ("dark" or "light")
|
||||
;default_theme = dark
|
||||
|
||||
[auth]
|
||||
# Set to true to disable (hide) the login form, useful if you use OAuth, defaults to false
|
||||
;disable_login_form = false
|
||||
|
||||
#################################### Anonymous Auth ##########################
|
||||
[auth.anonymous]
|
||||
# enable anonymous access
|
||||
@ -218,6 +228,15 @@ check_for_updates = true
|
||||
;team_ids =
|
||||
;allowed_organizations =
|
||||
|
||||
#################################### Grafana.net Auth ####################
|
||||
[auth.grafananet]
|
||||
;enabled = false
|
||||
;allow_sign_up = false
|
||||
;client_id = some_id
|
||||
;client_secret = some_secret
|
||||
;scopes = user:email
|
||||
;allowed_organizations =
|
||||
|
||||
#################################### Auth Proxy ##########################
|
||||
[auth.proxy]
|
||||
;enabled = false
|
||||
|
@ -6,11 +6,7 @@ page_keywords: grafana, influxdb, metrics, query, documentation
|
||||
|
||||
# InfluxDB
|
||||
|
||||
There are currently two separate datasources for InfluxDB in Grafana: InfluxDB 0.8.x and InfluxDB 0.9.x.
|
||||
The API and capabilities of InfluxDB 0.9.x are completely different from InfluxDB 0.8.x which is why Grafana handles
|
||||
them as different data sources.
|
||||
|
||||
InfluxDB 0.9 is rapidly evolving and we continue to track its API. InfluxDB 0.8 is no longer maintained by InfluxDB Inc, but we provide support as a convenience to existing users.
|
||||
Grafana ships with very a feature data source plugin for InfluxDB. Supporting a feature rich query editor, annotation and templating queries.
|
||||
|
||||
## Adding the data source
|
||||

|
||||
|
@ -6,6 +6,10 @@ page_keywords: grafana, admin, http, api, documentation
|
||||
|
||||
# Admin API
|
||||
|
||||
The admin http API does not currently work with an api token. Api Token's are currently only linked to an organization and organization role. They cannot given
|
||||
the permission of server admin, only user's can be given that permission. So in order to use these API calls you will have to use basic auth and Grafana user
|
||||
with Grafana admin permission.
|
||||
|
||||
## Settings
|
||||
|
||||
`GET /api/admin/settings`
|
||||
@ -15,7 +19,6 @@ page_keywords: grafana, admin, http, api, documentation
|
||||
GET /api/admin/settings
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
|
||||
**Example Response**:
|
||||
|
||||
@ -171,7 +174,6 @@ page_keywords: grafana, admin, http, api, documentation
|
||||
GET /api/admin/stats
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
|
||||
**Example Response**:
|
||||
|
||||
@ -201,7 +203,6 @@ Create new user
|
||||
POST /api/admin/users HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
|
||||
{
|
||||
"name":"User",
|
||||
@ -228,7 +229,6 @@ Change password for specific user
|
||||
PUT /api/admin/users/2/password HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
|
||||
**Example Response**:
|
||||
|
||||
@ -246,7 +246,6 @@ Change password for specific user
|
||||
PUT /api/admin/users/2/permissions HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
|
||||
**Example Response**:
|
||||
|
||||
@ -264,7 +263,6 @@ Change password for specific user
|
||||
DELETE /api/admin/users/2 HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
|
||||
**Example Response**:
|
||||
|
||||
|
@ -238,6 +238,14 @@ options are `Admin` and `Editor` and `Read-Only Editor`.
|
||||
|
||||
<hr>
|
||||
|
||||
## [auth]
|
||||
|
||||
### disable_login_form
|
||||
|
||||
Set to true to disable (hide) the login form, useful if you use OAuth, defaults to false.
|
||||
|
||||
<hr>
|
||||
|
||||
## [auth.anonymous]
|
||||
|
||||
### enabled
|
||||
@ -484,6 +492,33 @@ Grafana backend index those json dashboards which will make them appear in regul
|
||||
### path
|
||||
The full path to a directory containing your json dashboards.
|
||||
|
||||
## [smtp]
|
||||
Email server settings.
|
||||
|
||||
### enabled
|
||||
defaults to false
|
||||
|
||||
### host
|
||||
defaults to localhost:25
|
||||
|
||||
### user
|
||||
In case of SMTP auth, defaults to `empty`
|
||||
|
||||
### password
|
||||
In case of SMTP auth, defaults to `empty`
|
||||
|
||||
### cert_file
|
||||
File path to a cert file, defaults to `empty`
|
||||
|
||||
### key_file
|
||||
File path to a key file, defaults to `empty`
|
||||
|
||||
### skip_verify
|
||||
Verify SSL for smtp server? defaults to `false`
|
||||
|
||||
### from_address
|
||||
Address used when sending out emails, defaults to `admin@grafana.localhost`
|
||||
|
||||
## [log]
|
||||
|
||||
### mode
|
||||
@ -525,3 +560,9 @@ Set root url to a Grafana instance where you want to publish external snapshots
|
||||
|
||||
### external_snapshot_name
|
||||
Set name for external snapshot button. Defaults to `Publish to snapshot.raintank.io`
|
||||
|
||||
### remove expired snapshot
|
||||
Enabled to automatically remove expired snapshots
|
||||
|
||||
### remove snapshots after 90 days
|
||||
Time to live for snapshots.
|
||||
|
@ -25,6 +25,25 @@ func ValidateOrgAlert(c *middleware.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
func GetAlertStatesForDashboard(c *middleware.Context) Response {
|
||||
dashboardId := c.QueryInt64("dashboardId")
|
||||
|
||||
if dashboardId == 0 {
|
||||
return ApiError(400, "Missing query parameter dashboardId", nil)
|
||||
}
|
||||
|
||||
query := models.GetAlertStatesForDashboardQuery{
|
||||
OrgId: c.OrgId,
|
||||
DashboardId: c.QueryInt64("dashboardId"),
|
||||
}
|
||||
|
||||
if err := bus.Dispatch(&query); err != nil {
|
||||
return ApiError(500, "Failed to fetch alert states", err)
|
||||
}
|
||||
|
||||
return Json(200, query.Result)
|
||||
}
|
||||
|
||||
// GET /api/alerts
|
||||
func GetAlerts(c *middleware.Context) Response {
|
||||
query := models.GetAlertsQuery{
|
||||
|
@ -58,6 +58,7 @@ func Register(r *macaron.Macaron) {
|
||||
r.Get("/plugins/:id/page/:page", reqSignedIn, Index)
|
||||
|
||||
r.Get("/dashboard/*", reqSignedIn, Index)
|
||||
r.Get("/dashboard-solo/snapshot/*", Index)
|
||||
r.Get("/dashboard-solo/*", reqSignedIn, Index)
|
||||
r.Get("/import/dashboard", reqSignedIn, Index)
|
||||
r.Get("/dashboards/*", reqSignedIn, Index)
|
||||
@ -202,9 +203,9 @@ func Register(r *macaron.Macaron) {
|
||||
|
||||
r.Get("/plugins", wrap(GetPluginList))
|
||||
r.Get("/plugins/:pluginId/settings", wrap(GetPluginSettingById))
|
||||
r.Get("/plugins/:pluginId/readme", wrap(GetPluginReadme))
|
||||
|
||||
r.Group("/plugins", func() {
|
||||
r.Get("/:pluginId/readme", wrap(GetPluginReadme))
|
||||
r.Get("/:pluginId/dashboards/", wrap(GetPluginDashboards))
|
||||
r.Post("/:pluginId/settings", bind(m.UpdatePluginSettingCmd{}), wrap(UpdatePluginSetting))
|
||||
}, reqOrgAdmin)
|
||||
@ -243,7 +244,8 @@ func Register(r *macaron.Macaron) {
|
||||
r.Get("/search/", Search)
|
||||
|
||||
// metrics
|
||||
r.Get("/metrics/test", wrap(GetTestMetrics))
|
||||
r.Post("/tsdb/query", bind(dtos.MetricRequest{}), wrap(QueryMetrics))
|
||||
r.Get("/tsdb/testdata/scenarios", wrap(GetTestDataScenarios))
|
||||
|
||||
// metrics
|
||||
r.Get("/metrics", wrap(GetInternalMetrics))
|
||||
@ -252,6 +254,7 @@ func Register(r *macaron.Macaron) {
|
||||
r.Post("/test", bind(dtos.AlertTestCommand{}), wrap(AlertTest))
|
||||
r.Get("/:alertId", ValidateOrgAlert, wrap(GetAlert))
|
||||
r.Get("/", wrap(GetAlerts))
|
||||
r.Get("/states-for-dashboard", wrap(GetAlertStatesForDashboard))
|
||||
})
|
||||
|
||||
r.Get("/alert-notifications", wrap(GetAlertNotifications))
|
||||
|
@ -96,13 +96,10 @@ func (slice DataSourceList) Swap(i, j int) {
|
||||
slice[i], slice[j] = slice[j], slice[i]
|
||||
}
|
||||
|
||||
type MetricQueryResultDto struct {
|
||||
Data []MetricQueryResultDataDto `json:"data"`
|
||||
}
|
||||
|
||||
type MetricQueryResultDataDto struct {
|
||||
Target string `json:"target"`
|
||||
DataPoints [][2]float64 `json:"datapoints"`
|
||||
type MetricRequest struct {
|
||||
From string `json:"from"`
|
||||
To string `json:"to"`
|
||||
Queries []*simplejson.Json `json:"queries"`
|
||||
}
|
||||
|
||||
type UserStars struct {
|
||||
|
23
pkg/api/dtos/playlist.go
Normal file
23
pkg/api/dtos/playlist.go
Normal file
@ -0,0 +1,23 @@
|
||||
package dtos
|
||||
|
||||
type PlaylistDashboard struct {
|
||||
Id int64 `json:"id"`
|
||||
Slug string `json:"slug"`
|
||||
Title string `json:"title"`
|
||||
Uri string `json:"uri"`
|
||||
Order int `json:"order"`
|
||||
}
|
||||
|
||||
type PlaylistDashboardsSlice []PlaylistDashboard
|
||||
|
||||
func (slice PlaylistDashboardsSlice) Len() int {
|
||||
return len(slice)
|
||||
}
|
||||
|
||||
func (slice PlaylistDashboardsSlice) Less(i, j int) bool {
|
||||
return slice[i].Order < slice[j].Order
|
||||
}
|
||||
|
||||
func (slice PlaylistDashboardsSlice) Swap(i, j int) {
|
||||
slice[i], slice[j] = slice[j], slice[i]
|
||||
}
|
@ -38,7 +38,7 @@ func getFrontendSettingsMap(c *middleware.Context) (map[string]interface{}, erro
|
||||
url := ds.Url
|
||||
|
||||
if ds.Access == m.DS_ACCESS_PROXY {
|
||||
url = setting.AppSubUrl + "/api/datasources/proxy/" + strconv.FormatInt(ds.Id, 10)
|
||||
url = "/api/datasources/proxy/" + strconv.FormatInt(ds.Id, 10)
|
||||
}
|
||||
|
||||
var dsMap = map[string]interface{}{
|
||||
|
@ -1,6 +1,7 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
@ -32,6 +33,16 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
|
||||
locale = parts[0]
|
||||
}
|
||||
|
||||
appUrl := setting.AppUrl
|
||||
appSubUrl := setting.AppSubUrl
|
||||
|
||||
// special case when doing localhost call from phantomjs
|
||||
if c.IsRenderCall {
|
||||
appUrl = fmt.Sprintf("%s://localhost:%s", setting.Protocol, setting.HttpPort)
|
||||
appSubUrl = ""
|
||||
settings["appSubUrl"] = ""
|
||||
}
|
||||
|
||||
var data = dtos.IndexViewData{
|
||||
User: &dtos.CurrentUser{
|
||||
Id: c.UserId,
|
||||
@ -49,8 +60,8 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
|
||||
Locale: locale,
|
||||
},
|
||||
Settings: settings,
|
||||
AppUrl: setting.AppUrl,
|
||||
AppSubUrl: setting.AppSubUrl,
|
||||
AppUrl: appUrl,
|
||||
AppSubUrl: appSubUrl,
|
||||
GoogleAnalyticsId: setting.GoogleAnalyticsId,
|
||||
GoogleTagManagerId: setting.GoogleTagManagerId,
|
||||
BuildVersion: setting.BuildVersion,
|
||||
@ -154,7 +165,7 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
|
||||
}
|
||||
}
|
||||
|
||||
if c.OrgRole == m.ROLE_ADMIN {
|
||||
if len(appLink.Children) > 0 && c.OrgRole == m.ROLE_ADMIN {
|
||||
appLink.Children = append(appLink.Children, &dtos.NavLink{Divider: true})
|
||||
appLink.Children = append(appLink.Children, &dtos.NavLink{Text: "Plugin Config", Icon: "fa fa-cog", Url: setting.AppSubUrl + "/plugins/" + plugin.Id + "/edit"})
|
||||
}
|
||||
|
@ -25,13 +25,15 @@ func LoginView(c *middleware.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
viewData.Settings["googleAuthEnabled"] = setting.OAuthService.Google
|
||||
viewData.Settings["githubAuthEnabled"] = setting.OAuthService.GitHub
|
||||
viewData.Settings["genericOAuthEnabled"] = setting.OAuthService.Generic
|
||||
viewData.Settings["oauthProviderName"] = setting.OAuthService.OAuthProviderName
|
||||
enabledOAuths := make(map[string]interface{})
|
||||
for key, oauth := range setting.OAuthService.OAuthInfos {
|
||||
enabledOAuths[key] = map[string]string{"name": oauth.Name}
|
||||
}
|
||||
|
||||
viewData.Settings["oauth"] = enabledOAuths
|
||||
viewData.Settings["disableUserSignUp"] = !setting.AllowUserSignUp
|
||||
viewData.Settings["loginHint"] = setting.LoginHint
|
||||
viewData.Settings["allowUserPassLogin"] = setting.AllowUserPassLogin
|
||||
viewData.Settings["disableLoginForm"] = setting.DisableLoginForm
|
||||
|
||||
if !tryLoginUsingRememberCookie(c) {
|
||||
c.HTML(200, VIEW_INDEX, viewData)
|
||||
|
@ -3,7 +3,6 @@ package api
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
@ -46,9 +45,9 @@ func OAuthLogin(ctx *middleware.Context) {
|
||||
userInfo, err := connect.UserInfo(token)
|
||||
if err != nil {
|
||||
if err == social.ErrMissingTeamMembership {
|
||||
ctx.Redirect(setting.AppSubUrl + "/login?failedMsg=" + url.QueryEscape("Required Github team membership not fulfilled"))
|
||||
ctx.Redirect(setting.AppSubUrl + "/login?failCode=1000")
|
||||
} else if err == social.ErrMissingOrganizationMembership {
|
||||
ctx.Redirect(setting.AppSubUrl + "/login?failedMsg=" + url.QueryEscape("Required Github organization membership not fulfilled"))
|
||||
ctx.Redirect(setting.AppSubUrl + "/login?failCode=1001")
|
||||
} else {
|
||||
ctx.Handle(500, fmt.Sprintf("login.OAuthLogin(get info from %s)", name), err)
|
||||
}
|
||||
@ -60,7 +59,7 @@ func OAuthLogin(ctx *middleware.Context) {
|
||||
// validate that the email is allowed to login to grafana
|
||||
if !connect.IsEmailAllowed(userInfo.Email) {
|
||||
ctx.Logger.Info("OAuth login attempt with unallowed email", "email", userInfo.Email)
|
||||
ctx.Redirect(setting.AppSubUrl + "/login?failedMsg=" + url.QueryEscape("Required email domain not fulfilled"))
|
||||
ctx.Redirect(setting.AppSubUrl + "/login?failCode=1002")
|
||||
return
|
||||
}
|
||||
|
||||
@ -83,10 +82,11 @@ func OAuthLogin(ctx *middleware.Context) {
|
||||
return
|
||||
}
|
||||
cmd := m.CreateUserCommand{
|
||||
Login: userInfo.Email,
|
||||
Email: userInfo.Email,
|
||||
Name: userInfo.Name,
|
||||
Company: userInfo.Company,
|
||||
Login: userInfo.Email,
|
||||
Email: userInfo.Email,
|
||||
Name: userInfo.Name,
|
||||
Company: userInfo.Company,
|
||||
DefaultOrgRole: userInfo.Role,
|
||||
}
|
||||
|
||||
if err = bus.Dispatch(&cmd); err != nil {
|
||||
|
@ -2,39 +2,54 @@ package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
"github.com/grafana/grafana/pkg/metrics"
|
||||
"github.com/grafana/grafana/pkg/middleware"
|
||||
"github.com/grafana/grafana/pkg/tsdb"
|
||||
"github.com/grafana/grafana/pkg/tsdb/testdata"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
|
||||
func GetTestMetrics(c *middleware.Context) Response {
|
||||
from := c.QueryInt64("from")
|
||||
to := c.QueryInt64("to")
|
||||
maxDataPoints := c.QueryInt64("maxDataPoints")
|
||||
stepInSeconds := (to - from) / maxDataPoints
|
||||
// POST /api/tsdb/query
|
||||
func QueryMetrics(c *middleware.Context, reqDto dtos.MetricRequest) Response {
|
||||
timeRange := tsdb.NewTimeRange(reqDto.From, reqDto.To)
|
||||
|
||||
result := dtos.MetricQueryResultDto{}
|
||||
result.Data = make([]dtos.MetricQueryResultDataDto, 1)
|
||||
request := &tsdb.Request{TimeRange: timeRange}
|
||||
|
||||
for seriesIndex := range result.Data {
|
||||
points := make([][2]float64, maxDataPoints)
|
||||
walker := rand.Float64() * 100
|
||||
time := from
|
||||
for _, query := range reqDto.Queries {
|
||||
request.Queries = append(request.Queries, &tsdb.Query{
|
||||
RefId: query.Get("refId").MustString("A"),
|
||||
MaxDataPoints: query.Get("maxDataPoints").MustInt64(100),
|
||||
IntervalMs: query.Get("intervalMs").MustInt64(1000),
|
||||
Model: query,
|
||||
DataSource: &tsdb.DataSourceInfo{
|
||||
Name: "Grafana TestDataDB",
|
||||
PluginId: "grafana-testdata-datasource",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
for i := range points {
|
||||
points[i][0] = walker
|
||||
points[i][1] = float64(time)
|
||||
walker += rand.Float64() - 0.5
|
||||
time += stepInSeconds
|
||||
}
|
||||
resp, err := tsdb.HandleRequest(request)
|
||||
if err != nil {
|
||||
return ApiError(500, "Metric request error", err)
|
||||
}
|
||||
|
||||
result.Data[seriesIndex].Target = "test-series-" + strconv.Itoa(seriesIndex)
|
||||
result.Data[seriesIndex].DataPoints = points
|
||||
return Json(200, &resp)
|
||||
}
|
||||
|
||||
// GET /api/tsdb/testdata/scenarios
|
||||
func GetTestDataScenarios(c *middleware.Context) Response {
|
||||
result := make([]interface{}, 0)
|
||||
|
||||
for _, scenario := range testdata.ScenarioRegistry {
|
||||
result = append(result, map[string]interface{}{
|
||||
"id": scenario.Id,
|
||||
"name": scenario.Name,
|
||||
"description": scenario.Description,
|
||||
"stringInput": scenario.StringInput,
|
||||
})
|
||||
}
|
||||
|
||||
return Json(200, &result)
|
||||
|
@ -1,16 +1,18 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strconv"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
_ "github.com/grafana/grafana/pkg/log"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/search"
|
||||
)
|
||||
|
||||
func populateDashboardsById(dashboardByIds []int64) ([]m.PlaylistDashboardDto, error) {
|
||||
result := make([]m.PlaylistDashboardDto, 0)
|
||||
func populateDashboardsById(dashboardByIds []int64, dashboardIdOrder map[int64]int) (dtos.PlaylistDashboardsSlice, error) {
|
||||
result := make(dtos.PlaylistDashboardsSlice, 0)
|
||||
|
||||
if len(dashboardByIds) > 0 {
|
||||
dashboardQuery := m.GetDashboardsQuery{DashboardIds: dashboardByIds}
|
||||
@ -19,11 +21,12 @@ func populateDashboardsById(dashboardByIds []int64) ([]m.PlaylistDashboardDto, e
|
||||
}
|
||||
|
||||
for _, item := range dashboardQuery.Result {
|
||||
result = append(result, m.PlaylistDashboardDto{
|
||||
result = append(result, dtos.PlaylistDashboard{
|
||||
Id: item.Id,
|
||||
Slug: item.Slug,
|
||||
Title: item.Title,
|
||||
Uri: "db/" + item.Slug,
|
||||
Order: dashboardIdOrder[item.Id],
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -31,8 +34,8 @@ func populateDashboardsById(dashboardByIds []int64) ([]m.PlaylistDashboardDto, e
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func populateDashboardsByTag(orgId, userId int64, dashboardByTag []string) []m.PlaylistDashboardDto {
|
||||
result := make([]m.PlaylistDashboardDto, 0)
|
||||
func populateDashboardsByTag(orgId, userId int64, dashboardByTag []string, dashboardTagOrder map[string]int) dtos.PlaylistDashboardsSlice {
|
||||
result := make(dtos.PlaylistDashboardsSlice, 0)
|
||||
|
||||
if len(dashboardByTag) > 0 {
|
||||
for _, tag := range dashboardByTag {
|
||||
@ -47,10 +50,11 @@ func populateDashboardsByTag(orgId, userId int64, dashboardByTag []string) []m.P
|
||||
|
||||
if err := bus.Dispatch(&searchQuery); err == nil {
|
||||
for _, item := range searchQuery.Result {
|
||||
result = append(result, m.PlaylistDashboardDto{
|
||||
result = append(result, dtos.PlaylistDashboard{
|
||||
Id: item.Id,
|
||||
Title: item.Title,
|
||||
Uri: item.Uri,
|
||||
Order: dashboardTagOrder[tag],
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -60,28 +64,33 @@ func populateDashboardsByTag(orgId, userId int64, dashboardByTag []string) []m.P
|
||||
return result
|
||||
}
|
||||
|
||||
func LoadPlaylistDashboards(orgId, userId, playlistId int64) ([]m.PlaylistDashboardDto, error) {
|
||||
func LoadPlaylistDashboards(orgId, userId, playlistId int64) (dtos.PlaylistDashboardsSlice, error) {
|
||||
playlistItems, _ := LoadPlaylistItems(playlistId)
|
||||
|
||||
dashboardByIds := make([]int64, 0)
|
||||
dashboardByTag := make([]string, 0)
|
||||
dashboardIdOrder := make(map[int64]int)
|
||||
dashboardTagOrder := make(map[string]int)
|
||||
|
||||
for _, i := range playlistItems {
|
||||
if i.Type == "dashboard_by_id" {
|
||||
dashboardId, _ := strconv.ParseInt(i.Value, 10, 64)
|
||||
dashboardByIds = append(dashboardByIds, dashboardId)
|
||||
dashboardIdOrder[dashboardId] = i.Order
|
||||
}
|
||||
|
||||
if i.Type == "dashboard_by_tag" {
|
||||
dashboardByTag = append(dashboardByTag, i.Value)
|
||||
dashboardTagOrder[i.Value] = i.Order
|
||||
}
|
||||
}
|
||||
|
||||
result := make([]m.PlaylistDashboardDto, 0)
|
||||
result := make(dtos.PlaylistDashboardsSlice, 0)
|
||||
|
||||
var k, _ = populateDashboardsById(dashboardByIds)
|
||||
var k, _ = populateDashboardsById(dashboardByIds, dashboardIdOrder)
|
||||
result = append(result, k...)
|
||||
result = append(result, populateDashboardsByTag(orgId, userId, dashboardByTag)...)
|
||||
result = append(result, populateDashboardsByTag(orgId, userId, dashboardByTag, dashboardTagOrder)...)
|
||||
|
||||
sort.Sort(sort.Reverse(result))
|
||||
return result, nil
|
||||
}
|
||||
|
@ -6,35 +6,21 @@ import (
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/renderer"
|
||||
"github.com/grafana/grafana/pkg/middleware"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
|
||||
func RenderToPng(c *middleware.Context) {
|
||||
queryReader := util.NewUrlQueryReader(c.Req.URL)
|
||||
queryParams := fmt.Sprintf("?%s", c.Req.URL.RawQuery)
|
||||
sessionId := c.Session.ID()
|
||||
|
||||
// Handle api calls authenticated without session
|
||||
if sessionId == "" && c.ApiKeyId != 0 {
|
||||
c.Session.Start(c)
|
||||
c.Session.Set(middleware.SESS_KEY_APIKEY, c.ApiKeyId)
|
||||
// release will make sure the new session is persisted before
|
||||
// we spin up phantomjs
|
||||
c.Session.Release()
|
||||
// cleanup session after render is complete
|
||||
defer func() { c.Session.Destory(c) }()
|
||||
}
|
||||
|
||||
renderOpts := &renderer.RenderOpts{
|
||||
Url: c.Params("*") + queryParams,
|
||||
Width: queryReader.Get("width", "800"),
|
||||
Height: queryReader.Get("height", "400"),
|
||||
SessionId: c.Session.ID(),
|
||||
Timeout: queryReader.Get("timeout", "30"),
|
||||
Path: c.Params("*") + queryParams,
|
||||
Width: queryReader.Get("width", "800"),
|
||||
Height: queryReader.Get("height", "400"),
|
||||
OrgId: c.OrgId,
|
||||
Timeout: queryReader.Get("timeout", "30"),
|
||||
}
|
||||
|
||||
renderOpts.Url = setting.ToAbsUrl(renderOpts.Url)
|
||||
pngPath, err := renderer.RenderToPng(renderOpts)
|
||||
|
||||
if err != nil {
|
||||
|
@ -141,8 +141,6 @@ func createRequest(repoUrl string, subPaths ...string) ([]byte, error) {
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, u.String(), nil)
|
||||
|
||||
logger.Info("grafanaVersion ", grafanaVersion)
|
||||
|
||||
req.Header.Set("grafana-version", grafanaVersion)
|
||||
req.Header.Set("User-Agent", "grafana "+grafanaVersion)
|
||||
|
||||
|
@ -13,16 +13,15 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
"github.com/grafana/grafana/pkg/login"
|
||||
"github.com/grafana/grafana/pkg/metrics"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
alertingInit "github.com/grafana/grafana/pkg/services/alerting/init"
|
||||
"github.com/grafana/grafana/pkg/services/eventpublisher"
|
||||
"github.com/grafana/grafana/pkg/services/notifications"
|
||||
"github.com/grafana/grafana/pkg/services/search"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/social"
|
||||
|
||||
_ "github.com/grafana/grafana/pkg/services/alerting/conditions"
|
||||
_ "github.com/grafana/grafana/pkg/services/alerting/notifiers"
|
||||
_ "github.com/grafana/grafana/pkg/tsdb/graphite"
|
||||
_ "github.com/grafana/grafana/pkg/tsdb/prometheus"
|
||||
_ "github.com/grafana/grafana/pkg/tsdb/testdata"
|
||||
)
|
||||
|
||||
var version = "3.1.0"
|
||||
@ -56,26 +55,8 @@ func main() {
|
||||
setting.BuildCommit = commit
|
||||
setting.BuildStamp = buildstampInt64
|
||||
|
||||
go listenToSystemSignels()
|
||||
|
||||
flag.Parse()
|
||||
writePIDFile()
|
||||
initRuntime()
|
||||
metrics.Init()
|
||||
|
||||
search.Init()
|
||||
login.Init()
|
||||
social.NewOAuthService()
|
||||
eventpublisher.Init()
|
||||
plugins.Init()
|
||||
alertingInit.Init()
|
||||
|
||||
if err := notifications.Init(); err != nil {
|
||||
log.Fatal(3, "Notification service failed to initialize", err)
|
||||
}
|
||||
|
||||
StartServer()
|
||||
exitChan <- 0
|
||||
server := NewGrafanaServer()
|
||||
server.Start()
|
||||
}
|
||||
|
||||
func initRuntime() {
|
||||
@ -93,7 +74,9 @@ func initRuntime() {
|
||||
logger.Info("Starting Grafana", "version", version, "commit", commit, "compiled", time.Unix(setting.BuildStamp, 0))
|
||||
|
||||
setting.LogConfigurationInfo()
|
||||
}
|
||||
|
||||
func initSql() {
|
||||
sqlstore.NewEngine()
|
||||
sqlstore.EnsureAdminUser()
|
||||
}
|
||||
@ -116,7 +99,7 @@ func writePIDFile() {
|
||||
}
|
||||
}
|
||||
|
||||
func listenToSystemSignels() {
|
||||
func listenToSystemSignals(server models.GrafanaServer) {
|
||||
signalChan := make(chan os.Signal, 1)
|
||||
code := 0
|
||||
|
||||
@ -124,16 +107,8 @@ func listenToSystemSignels() {
|
||||
|
||||
select {
|
||||
case sig := <-signalChan:
|
||||
log.Info("Received signal %s. shutting down", sig)
|
||||
server.Shutdown(0, fmt.Sprintf("system signal: %s", sig))
|
||||
case code = <-exitChan:
|
||||
switch code {
|
||||
case 0:
|
||||
log.Info("Shutting down")
|
||||
default:
|
||||
log.Warn("Shutting down")
|
||||
}
|
||||
server.Shutdown(code, "startup error")
|
||||
}
|
||||
|
||||
log.Close()
|
||||
os.Exit(code)
|
||||
}
|
||||
|
128
pkg/cmd/grafana-server/server.go
Normal file
128
pkg/cmd/grafana-server/server.go
Normal file
@ -0,0 +1,128 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api"
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
"github.com/grafana/grafana/pkg/login"
|
||||
"github.com/grafana/grafana/pkg/metrics"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/services/alerting"
|
||||
"github.com/grafana/grafana/pkg/services/cleanup"
|
||||
"github.com/grafana/grafana/pkg/services/eventpublisher"
|
||||
"github.com/grafana/grafana/pkg/services/notifications"
|
||||
"github.com/grafana/grafana/pkg/services/search"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/social"
|
||||
)
|
||||
|
||||
func NewGrafanaServer() models.GrafanaServer {
|
||||
rootCtx, shutdownFn := context.WithCancel(context.Background())
|
||||
childRoutines, childCtx := errgroup.WithContext(rootCtx)
|
||||
|
||||
return &GrafanaServerImpl{
|
||||
context: childCtx,
|
||||
shutdownFn: shutdownFn,
|
||||
childRoutines: childRoutines,
|
||||
log: log.New("server"),
|
||||
}
|
||||
}
|
||||
|
||||
type GrafanaServerImpl struct {
|
||||
context context.Context
|
||||
shutdownFn context.CancelFunc
|
||||
childRoutines *errgroup.Group
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
func (g *GrafanaServerImpl) Start() {
|
||||
go listenToSystemSignals(g)
|
||||
|
||||
writePIDFile()
|
||||
initRuntime()
|
||||
initSql()
|
||||
metrics.Init()
|
||||
search.Init()
|
||||
login.Init()
|
||||
social.NewOAuthService()
|
||||
eventpublisher.Init()
|
||||
plugins.Init()
|
||||
|
||||
// init alerting
|
||||
if setting.AlertingEnabled {
|
||||
engine := alerting.NewEngine()
|
||||
g.childRoutines.Go(func() error { return engine.Run(g.context) })
|
||||
}
|
||||
|
||||
// cleanup service
|
||||
cleanUpService := cleanup.NewCleanUpService()
|
||||
g.childRoutines.Go(func() error { return cleanUpService.Run(g.context) })
|
||||
|
||||
if err := notifications.Init(); err != nil {
|
||||
g.log.Error("Notification service failed to initialize", "erro", err)
|
||||
g.Shutdown(1, "Startup failed")
|
||||
return
|
||||
}
|
||||
|
||||
g.startHttpServer()
|
||||
}
|
||||
|
||||
func (g *GrafanaServerImpl) startHttpServer() {
|
||||
logger = log.New("http.server")
|
||||
|
||||
var err error
|
||||
m := newMacaron()
|
||||
api.Register(m)
|
||||
|
||||
listenAddr := fmt.Sprintf("%s:%s", setting.HttpAddr, setting.HttpPort)
|
||||
g.log.Info("Initializing HTTP Server", "address", listenAddr, "protocol", setting.Protocol, "subUrl", setting.AppSubUrl)
|
||||
|
||||
switch setting.Protocol {
|
||||
case setting.HTTP:
|
||||
err = http.ListenAndServe(listenAddr, m)
|
||||
case setting.HTTPS:
|
||||
err = http.ListenAndServeTLS(listenAddr, setting.CertFile, setting.KeyFile, m)
|
||||
default:
|
||||
g.log.Error("Invalid protocol", "protocol", setting.Protocol)
|
||||
g.Shutdown(1, "Startup failed")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
g.log.Error("Fail to start server", "error", err)
|
||||
g.Shutdown(1, "Startup failed")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (g *GrafanaServerImpl) Shutdown(code int, reason string) {
|
||||
g.log.Info("Shutdown started", "code", code, "reason", reason)
|
||||
|
||||
g.shutdownFn()
|
||||
err := g.childRoutines.Wait()
|
||||
|
||||
g.log.Info("Shutdown completed", "reason", err)
|
||||
log.Close()
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
// implement context.Context
|
||||
func (g *GrafanaServerImpl) Deadline() (deadline time.Time, ok bool) {
|
||||
return g.context.Deadline()
|
||||
}
|
||||
func (g *GrafanaServerImpl) Done() <-chan struct{} {
|
||||
return g.context.Done()
|
||||
}
|
||||
func (g *GrafanaServerImpl) Err() error {
|
||||
return g.context.Err()
|
||||
}
|
||||
func (g *GrafanaServerImpl) Value(key interface{}) interface{} {
|
||||
return g.context.Value(key)
|
||||
}
|
@ -6,7 +6,6 @@ package main
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"gopkg.in/macaron.v1"
|
||||
@ -79,7 +78,7 @@ func mapStatic(m *macaron.Macaron, rootDir string, dir string, prefix string) {
|
||||
))
|
||||
}
|
||||
|
||||
func StartServer() {
|
||||
func StartServer() int {
|
||||
logger = log.New("server")
|
||||
|
||||
var err error
|
||||
@ -95,11 +94,13 @@ func StartServer() {
|
||||
err = http.ListenAndServeTLS(listenAddr, setting.CertFile, setting.KeyFile, m)
|
||||
default:
|
||||
logger.Error("Invalid protocol", "protocol", setting.Protocol)
|
||||
os.Exit(1)
|
||||
return 1
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
logger.Error("Fail to start server", "error", err)
|
||||
os.Exit(1)
|
||||
return 1
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
@ -12,36 +12,51 @@ import (
|
||||
"strconv"
|
||||
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
"github.com/grafana/grafana/pkg/middleware"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
|
||||
type RenderOpts struct {
|
||||
Url string
|
||||
Width string
|
||||
Height string
|
||||
SessionId string
|
||||
Timeout string
|
||||
Path string
|
||||
Width string
|
||||
Height string
|
||||
Timeout string
|
||||
OrgId int64
|
||||
}
|
||||
|
||||
var rendererLog log.Logger = log.New("png-renderer")
|
||||
|
||||
func RenderToPng(params *RenderOpts) (string, error) {
|
||||
rendererLog.Info("Rendering", "url", params.Url)
|
||||
rendererLog.Info("Rendering", "path", params.Path)
|
||||
|
||||
var executable = "phantomjs"
|
||||
if runtime.GOOS == "windows" {
|
||||
executable = executable + ".exe"
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s://localhost:%s/%s", setting.Protocol, setting.HttpPort, params.Path)
|
||||
|
||||
binPath, _ := filepath.Abs(filepath.Join(setting.PhantomDir, executable))
|
||||
scriptPath, _ := filepath.Abs(filepath.Join(setting.PhantomDir, "render.js"))
|
||||
pngPath, _ := filepath.Abs(filepath.Join(setting.ImagesDir, util.GetRandomString(20)))
|
||||
pngPath = pngPath + ".png"
|
||||
|
||||
cmd := exec.Command(binPath, "--ignore-ssl-errors=true", scriptPath, "url="+params.Url, "width="+params.Width,
|
||||
"height="+params.Height, "png="+pngPath, "cookiename="+setting.SessionOptions.CookieName,
|
||||
"domain="+setting.Domain, "sessionid="+params.SessionId)
|
||||
renderKey := middleware.AddRenderAuthKey(params.OrgId)
|
||||
defer middleware.RemoveRenderAuthKey(renderKey)
|
||||
|
||||
cmdArgs := []string{
|
||||
"--ignore-ssl-errors=true",
|
||||
scriptPath,
|
||||
"url=" + url,
|
||||
"width=" + params.Width,
|
||||
"height=" + params.Height,
|
||||
"png=" + pngPath,
|
||||
"domain=" + setting.Domain,
|
||||
"renderKey=" + renderKey,
|
||||
}
|
||||
|
||||
cmd := exec.Command(binPath, cmdArgs...)
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
|
||||
if err != nil {
|
||||
|
@ -32,11 +32,25 @@ func New(logger string, ctx ...interface{}) Logger {
|
||||
}
|
||||
|
||||
func Trace(format string, v ...interface{}) {
|
||||
Root.Debug(fmt.Sprintf(format, v))
|
||||
var message string
|
||||
if len(v) > 0 {
|
||||
message = fmt.Sprintf(format, v)
|
||||
} else {
|
||||
message = format
|
||||
}
|
||||
|
||||
Root.Debug(message)
|
||||
}
|
||||
|
||||
func Debug(format string, v ...interface{}) {
|
||||
Root.Debug(fmt.Sprintf(format, v))
|
||||
var message string
|
||||
if len(v) > 0 {
|
||||
message = fmt.Sprintf(format, v)
|
||||
} else {
|
||||
message = format
|
||||
}
|
||||
|
||||
Root.Debug(message)
|
||||
}
|
||||
|
||||
func Debug2(message string, v ...interface{}) {
|
||||
@ -44,7 +58,14 @@ func Debug2(message string, v ...interface{}) {
|
||||
}
|
||||
|
||||
func Info(format string, v ...interface{}) {
|
||||
Root.Info(fmt.Sprintf(format, v))
|
||||
var message string
|
||||
if len(v) > 0 {
|
||||
message = fmt.Sprintf(format, v)
|
||||
} else {
|
||||
message = format
|
||||
}
|
||||
|
||||
Root.Info(message)
|
||||
}
|
||||
|
||||
func Info2(message string, v ...interface{}) {
|
||||
@ -52,7 +73,14 @@ func Info2(message string, v ...interface{}) {
|
||||
}
|
||||
|
||||
func Warn(format string, v ...interface{}) {
|
||||
Root.Warn(fmt.Sprintf(format, v))
|
||||
var message string
|
||||
if len(v) > 0 {
|
||||
message = fmt.Sprintf(format, v)
|
||||
} else {
|
||||
message = format
|
||||
}
|
||||
|
||||
Root.Warn(message)
|
||||
}
|
||||
|
||||
func Warn2(message string, v ...interface{}) {
|
||||
|
@ -24,10 +24,10 @@ func NewGauge(meta *MetricMeta) Gauge {
|
||||
}
|
||||
}
|
||||
|
||||
func RegGauge(meta *MetricMeta) Gauge {
|
||||
g := NewGauge(meta)
|
||||
MetricStats.Register(g)
|
||||
return g
|
||||
func RegGauge(name string, tagStrings ...string) Gauge {
|
||||
tr := NewGauge(NewMetricMeta(name, tagStrings))
|
||||
MetricStats.Register(tr)
|
||||
return tr
|
||||
}
|
||||
|
||||
// GaugeSnapshot is a read-only copy of another Gauge.
|
||||
|
@ -63,6 +63,8 @@ func (this *GraphitePublisher) Publish(metrics []Metric) {
|
||||
switch metric := m.(type) {
|
||||
case Counter:
|
||||
this.addCount(buf, metricName+".count", metric.Count(), now)
|
||||
case Gauge:
|
||||
this.addCount(buf, metricName, metric.Value(), now)
|
||||
case Timer:
|
||||
percentiles := metric.Percentiles([]float64{0.25, 0.75, 0.90, 0.99})
|
||||
this.addCount(buf, metricName+".count", metric.Count(), now)
|
||||
|
@ -49,6 +49,12 @@ var (
|
||||
// Timers
|
||||
M_DataSource_ProxyReq_Timer Timer
|
||||
M_Alerting_Exeuction_Time Timer
|
||||
|
||||
// StatTotals
|
||||
M_StatTotal_Dashboards Gauge
|
||||
M_StatTotal_Users Gauge
|
||||
M_StatTotal_Orgs Gauge
|
||||
M_StatTotal_Playlists Gauge
|
||||
)
|
||||
|
||||
func initMetricVars(settings *MetricSettings) {
|
||||
@ -105,4 +111,10 @@ func initMetricVars(settings *MetricSettings) {
|
||||
// Timers
|
||||
M_DataSource_ProxyReq_Timer = RegTimer("api.dataproxy.request.all")
|
||||
M_Alerting_Exeuction_Time = RegTimer("alerting.execution_time")
|
||||
|
||||
// StatTotals
|
||||
M_StatTotal_Dashboards = RegGauge("stat_totals", "stat", "dashboards")
|
||||
M_StatTotal_Users = RegGauge("stat_totals", "stat", "users")
|
||||
M_StatTotal_Orgs = RegGauge("stat_totals", "stat", "orgs")
|
||||
M_StatTotal_Playlists = RegGauge("stat_totals", "stat", "playlists")
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ import (
|
||||
)
|
||||
|
||||
var metricsLogger log.Logger = log.New("metrics")
|
||||
var metricPublishCounter int64 = 0
|
||||
|
||||
func Init() {
|
||||
settings := readSettings()
|
||||
@ -45,12 +46,33 @@ func sendMetrics(settings *MetricSettings) {
|
||||
return
|
||||
}
|
||||
|
||||
updateTotalStats()
|
||||
|
||||
metrics := MetricStats.GetSnapshots()
|
||||
for _, publisher := range settings.Publishers {
|
||||
publisher.Publish(metrics)
|
||||
}
|
||||
}
|
||||
|
||||
func updateTotalStats() {
|
||||
|
||||
// every interval also publish totals
|
||||
metricPublishCounter++
|
||||
if metricPublishCounter%10 == 0 {
|
||||
// get stats
|
||||
statsQuery := m.GetSystemStatsQuery{}
|
||||
if err := bus.Dispatch(&statsQuery); err != nil {
|
||||
metricsLogger.Error("Failed to get system stats", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
M_StatTotal_Dashboards.Update(statsQuery.Result.DashboardCount)
|
||||
M_StatTotal_Users.Update(statsQuery.Result.UserCount)
|
||||
M_StatTotal_Playlists.Update(statsQuery.Result.PlaylistCount)
|
||||
M_StatTotal_Orgs.Update(statsQuery.Result.OrgCount)
|
||||
}
|
||||
}
|
||||
|
||||
func sendUsageStats() {
|
||||
if !setting.ReportingEnabled {
|
||||
return
|
||||
|
@ -22,6 +22,7 @@ type Context struct {
|
||||
Session SessionStore
|
||||
|
||||
IsSignedIn bool
|
||||
IsRenderCall bool
|
||||
AllowAnonymous bool
|
||||
Logger log.Logger
|
||||
}
|
||||
@ -42,11 +43,11 @@ func GetContextHandler() macaron.Handler {
|
||||
// then init session and look for userId in session
|
||||
// then look for api key in session (special case for render calls via api)
|
||||
// then test if anonymous access is enabled
|
||||
if initContextWithApiKey(ctx) ||
|
||||
if initContextWithRenderAuth(ctx) ||
|
||||
initContextWithApiKey(ctx) ||
|
||||
initContextWithBasicAuth(ctx) ||
|
||||
initContextWithAuthProxy(ctx) ||
|
||||
initContextWithUserSessionCookie(ctx) ||
|
||||
initContextWithApiKeyFromSession(ctx) ||
|
||||
initContextWithAnonymousUser(ctx) {
|
||||
}
|
||||
|
||||
@ -176,29 +177,6 @@ func initContextWithBasicAuth(ctx *Context) bool {
|
||||
}
|
||||
}
|
||||
|
||||
// special case for panel render calls with api key
|
||||
func initContextWithApiKeyFromSession(ctx *Context) bool {
|
||||
keyId := ctx.Session.Get(SESS_KEY_APIKEY)
|
||||
if keyId == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
keyQuery := m.GetApiKeyByIdQuery{ApiKeyId: keyId.(int64)}
|
||||
if err := bus.Dispatch(&keyQuery); err != nil {
|
||||
ctx.Logger.Error("Failed to get api key by id", "id", keyId, "error", err)
|
||||
return false
|
||||
} else {
|
||||
apikey := keyQuery.Result
|
||||
|
||||
ctx.IsSignedIn = true
|
||||
ctx.SignedInUser = &m.SignedInUser{}
|
||||
ctx.OrgRole = apikey.Role
|
||||
ctx.ApiKeyId = apikey.Id
|
||||
ctx.OrgId = apikey.OrgId
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Handle handles and logs error by given status.
|
||||
func (ctx *Context) Handle(status int, title string, err error) {
|
||||
if err != nil {
|
||||
|
55
pkg/middleware/render_auth.go
Normal file
55
pkg/middleware/render_auth.go
Normal file
@ -0,0 +1,55 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
|
||||
var renderKeysLock sync.Mutex
|
||||
var renderKeys map[string]*m.SignedInUser = make(map[string]*m.SignedInUser)
|
||||
|
||||
func initContextWithRenderAuth(ctx *Context) bool {
|
||||
key := ctx.GetCookie("renderKey")
|
||||
if key == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
renderKeysLock.Lock()
|
||||
defer renderKeysLock.Unlock()
|
||||
|
||||
if renderUser, exists := renderKeys[key]; !exists {
|
||||
ctx.JsonApiErr(401, "Invalid Render Key", nil)
|
||||
return true
|
||||
} else {
|
||||
|
||||
ctx.IsSignedIn = true
|
||||
ctx.SignedInUser = renderUser
|
||||
ctx.IsRenderCall = true
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
type renderContextFunc func(key string) (string, error)
|
||||
|
||||
func AddRenderAuthKey(orgId int64) string {
|
||||
renderKeysLock.Lock()
|
||||
|
||||
key := util.GetRandomString(32)
|
||||
|
||||
renderKeys[key] = &m.SignedInUser{
|
||||
OrgId: orgId,
|
||||
OrgRole: m.ROLE_VIEWER,
|
||||
}
|
||||
|
||||
renderKeysLock.Unlock()
|
||||
|
||||
return key
|
||||
}
|
||||
|
||||
func RemoveRenderAuthKey(key string) {
|
||||
renderKeysLock.Lock()
|
||||
delete(renderKeys, key)
|
||||
renderKeysLock.Unlock()
|
||||
}
|
@ -13,7 +13,6 @@ import (
|
||||
|
||||
const (
|
||||
SESS_KEY_USERID = "uid"
|
||||
SESS_KEY_APIKEY = "apikey_id" // used fror render requests with api keys
|
||||
)
|
||||
|
||||
var sessionManager *session.Manager
|
||||
|
@ -135,3 +135,18 @@ type GetAlertByIdQuery struct {
|
||||
|
||||
Result *Alert
|
||||
}
|
||||
|
||||
type GetAlertStatesForDashboardQuery struct {
|
||||
OrgId int64
|
||||
DashboardId int64
|
||||
|
||||
Result []*AlertStateInfoDTO
|
||||
}
|
||||
|
||||
type AlertStateInfoDTO struct {
|
||||
Id int64 `json:"id"`
|
||||
DashboardId int64 `json:"dashboardId"`
|
||||
PanelId int64 `json:"panelId"`
|
||||
State AlertStateType `json:"state"`
|
||||
NewStateDate time.Time `json:"newStateDate"`
|
||||
}
|
||||
|
@ -63,6 +63,9 @@ type DeleteDashboardSnapshotCommand struct {
|
||||
DeleteKey string `json:"-"`
|
||||
}
|
||||
|
||||
type DeleteExpiredSnapshotsCommand struct {
|
||||
}
|
||||
|
||||
type GetDashboardSnapshotQuery struct {
|
||||
Key string
|
||||
|
||||
|
@ -7,4 +7,5 @@ const (
|
||||
GOOGLE
|
||||
TWITTER
|
||||
GENERIC
|
||||
GRAFANANET
|
||||
)
|
||||
|
@ -57,17 +57,6 @@ func (this PlaylistDashboard) TableName() string {
|
||||
type Playlists []*Playlist
|
||||
type PlaylistDashboards []*PlaylistDashboard
|
||||
|
||||
//
|
||||
// DTOS
|
||||
//
|
||||
|
||||
type PlaylistDashboardDto struct {
|
||||
Id int64 `json:"id"`
|
||||
Slug string `json:"slug"`
|
||||
Title string `json:"title"`
|
||||
Uri string `json:"uri"`
|
||||
}
|
||||
|
||||
//
|
||||
// COMMANDS
|
||||
//
|
||||
|
10
pkg/models/server.go
Normal file
10
pkg/models/server.go
Normal file
@ -0,0 +1,10 @@
|
||||
package models
|
||||
|
||||
import "context"
|
||||
|
||||
type GrafanaServer interface {
|
||||
context.Context
|
||||
|
||||
Start()
|
||||
Shutdown(code int, reason string)
|
||||
}
|
@ -1,10 +1,10 @@
|
||||
package models
|
||||
|
||||
type SystemStats struct {
|
||||
DashboardCount int
|
||||
UserCount int
|
||||
OrgCount int
|
||||
PlaylistCount int
|
||||
DashboardCount int64
|
||||
UserCount int64
|
||||
OrgCount int64
|
||||
PlaylistCount int64
|
||||
}
|
||||
|
||||
type DataSourceStats struct {
|
||||
|
@ -44,15 +44,16 @@ func (u *User) NameOrFallback() string {
|
||||
// COMMANDS
|
||||
|
||||
type CreateUserCommand struct {
|
||||
Email string
|
||||
Login string
|
||||
Name string
|
||||
Company string
|
||||
OrgName string
|
||||
Password string
|
||||
EmailVerified bool
|
||||
IsAdmin bool
|
||||
SkipOrgSetup bool
|
||||
Email string
|
||||
Login string
|
||||
Name string
|
||||
Company string
|
||||
OrgName string
|
||||
Password string
|
||||
EmailVerified bool
|
||||
IsAdmin bool
|
||||
SkipOrgSetup bool
|
||||
DefaultOrgRole string
|
||||
|
||||
Result User
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ type DataSourcePlugin struct {
|
||||
FrontendPluginBase
|
||||
Annotations bool `json:"annotations"`
|
||||
Metrics bool `json:"metrics"`
|
||||
Alerting bool `json:"alerting"`
|
||||
BuiltIn bool `json:"builtIn"`
|
||||
Mixed bool `json:"mixed"`
|
||||
App string `json:"app"`
|
||||
|
@ -43,7 +43,12 @@ func (fp *FrontendPluginBase) setPathsBasedOnApp(app *AppPlugin) {
|
||||
appSubPath := strings.Replace(fp.PluginDir, app.PluginDir, "", 1)
|
||||
fp.IncludedInAppId = app.Id
|
||||
fp.BaseUrl = app.BaseUrl
|
||||
fp.Module = util.JoinUrlFragments("plugins/"+app.Id, appSubPath) + "/module"
|
||||
|
||||
if isExternalPlugin(app.PluginDir) {
|
||||
fp.Module = util.JoinUrlFragments("plugins/"+app.Id, appSubPath) + "/module"
|
||||
} else {
|
||||
fp.Module = util.JoinUrlFragments("app/plugins/app/"+app.Id, appSubPath) + "/module"
|
||||
}
|
||||
}
|
||||
|
||||
func (fp *FrontendPluginBase) handleModuleDefaults() {
|
||||
|
@ -9,6 +9,11 @@ import (
|
||||
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/hashicorp/go-version"
|
||||
)
|
||||
|
||||
var (
|
||||
httpClient http.Client = http.Client{Timeout: time.Duration(10 * time.Second)}
|
||||
)
|
||||
|
||||
type GrafanaNetPlugin struct {
|
||||
@ -39,26 +44,23 @@ func StartPluginUpdateChecker() {
|
||||
}
|
||||
|
||||
func getAllExternalPluginSlugs() string {
|
||||
str := ""
|
||||
|
||||
var result []string
|
||||
for _, plug := range Plugins {
|
||||
if plug.IsCorePlugin {
|
||||
continue
|
||||
}
|
||||
|
||||
str += plug.Id + ","
|
||||
result = append(result, plug.Id)
|
||||
}
|
||||
|
||||
return str
|
||||
return strings.Join(result, ",")
|
||||
}
|
||||
|
||||
func checkForUpdates() {
|
||||
log.Trace("Checking for updates")
|
||||
|
||||
client := http.Client{Timeout: time.Duration(5 * time.Second)}
|
||||
|
||||
pluginSlugs := getAllExternalPluginSlugs()
|
||||
resp, err := client.Get("https://grafana.net/api/plugins/versioncheck?slugIn=" + pluginSlugs + "&grafanaVersion=" + setting.BuildVersion)
|
||||
resp, err := httpClient.Get("https://grafana.net/api/plugins/versioncheck?slugIn=" + pluginSlugs + "&grafanaVersion=" + setting.BuildVersion)
|
||||
|
||||
if err != nil {
|
||||
log.Trace("Failed to get plugins repo from grafana.net, %v", err.Error())
|
||||
@ -84,12 +86,20 @@ func checkForUpdates() {
|
||||
for _, gplug := range gNetPlugins {
|
||||
if gplug.Slug == plug.Id {
|
||||
plug.GrafanaNetVersion = gplug.Version
|
||||
plug.GrafanaNetHasUpdate = plug.Info.Version != plug.GrafanaNetVersion
|
||||
|
||||
plugVersion, err1 := version.NewVersion(plug.Info.Version)
|
||||
gplugVersion, err2 := version.NewVersion(gplug.Version)
|
||||
|
||||
if err1 != nil || err2 != nil {
|
||||
plug.GrafanaNetHasUpdate = plug.Info.Version != plug.GrafanaNetVersion
|
||||
} else {
|
||||
plug.GrafanaNetHasUpdate = plugVersion.LessThan(gplugVersion)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resp2, err := client.Get("https://raw.githubusercontent.com/grafana/grafana/master/latest.json")
|
||||
resp2, err := httpClient.Get("https://raw.githubusercontent.com/grafana/grafana/master/latest.json")
|
||||
if err != nil {
|
||||
log.Trace("Failed to get latest.json repo from github: %v", err.Error())
|
||||
return
|
||||
@ -116,4 +126,11 @@ func checkForUpdates() {
|
||||
GrafanaLatestVersion = githubLatest.Stable
|
||||
GrafanaHasUpdate = githubLatest.Stable != setting.BuildVersion
|
||||
}
|
||||
|
||||
currVersion, err1 := version.NewVersion(setting.BuildVersion)
|
||||
latestVersion, err2 := version.NewVersion(GrafanaLatestVersion)
|
||||
|
||||
if err1 == nil && err2 == nil {
|
||||
GrafanaHasUpdate = currVersion.LessThan(latestVersion)
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import (
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/services/alerting"
|
||||
"gopkg.in/guregu/null.v3"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -13,13 +14,13 @@ var (
|
||||
)
|
||||
|
||||
type AlertEvaluator interface {
|
||||
Eval(reducedValue *float64) bool
|
||||
Eval(reducedValue null.Float) bool
|
||||
}
|
||||
|
||||
type NoDataEvaluator struct{}
|
||||
|
||||
func (e *NoDataEvaluator) Eval(reducedValue *float64) bool {
|
||||
return reducedValue == nil
|
||||
func (e *NoDataEvaluator) Eval(reducedValue null.Float) bool {
|
||||
return reducedValue.Valid == false
|
||||
}
|
||||
|
||||
type ThresholdEvaluator struct {
|
||||
@ -43,16 +44,16 @@ func newThresholdEvaludator(typ string, model *simplejson.Json) (*ThresholdEvalu
|
||||
return defaultEval, nil
|
||||
}
|
||||
|
||||
func (e *ThresholdEvaluator) Eval(reducedValue *float64) bool {
|
||||
if reducedValue == nil {
|
||||
func (e *ThresholdEvaluator) Eval(reducedValue null.Float) bool {
|
||||
if reducedValue.Valid == false {
|
||||
return false
|
||||
}
|
||||
|
||||
switch e.Type {
|
||||
case "gt":
|
||||
return *reducedValue > e.Threshold
|
||||
return reducedValue.Float64 > e.Threshold
|
||||
case "lt":
|
||||
return *reducedValue < e.Threshold
|
||||
return reducedValue.Float64 < e.Threshold
|
||||
}
|
||||
|
||||
return false
|
||||
@ -86,16 +87,18 @@ func newRangedEvaluator(typ string, model *simplejson.Json) (*RangedEvaluator, e
|
||||
return rangedEval, nil
|
||||
}
|
||||
|
||||
func (e *RangedEvaluator) Eval(reducedValue *float64) bool {
|
||||
if reducedValue == nil {
|
||||
func (e *RangedEvaluator) Eval(reducedValue null.Float) bool {
|
||||
if reducedValue.Valid == false {
|
||||
return false
|
||||
}
|
||||
|
||||
floatValue := reducedValue.Float64
|
||||
|
||||
switch e.Type {
|
||||
case "within_range":
|
||||
return (e.Lower < *reducedValue && e.Upper > *reducedValue) || (e.Upper < *reducedValue && e.Lower > *reducedValue)
|
||||
return (e.Lower < floatValue && e.Upper > floatValue) || (e.Upper < floatValue && e.Lower > floatValue)
|
||||
case "outside_range":
|
||||
return (e.Upper < *reducedValue && e.Lower < *reducedValue) || (e.Upper > *reducedValue && e.Lower > *reducedValue)
|
||||
return (e.Upper < floatValue && e.Lower < floatValue) || (e.Upper > floatValue && e.Lower > floatValue)
|
||||
}
|
||||
|
||||
return false
|
||||
|
@ -3,6 +3,8 @@ package conditions
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"gopkg.in/guregu/null.v3"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
@ -14,7 +16,7 @@ func evalutorScenario(json string, reducedValue float64, datapoints ...float64)
|
||||
evaluator, err := NewAlertEvaluator(jsonModel)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
return evaluator.Eval(&reducedValue)
|
||||
return evaluator.Eval(null.FloatFrom(reducedValue))
|
||||
}
|
||||
|
||||
func TestEvalutors(t *testing.T) {
|
||||
@ -51,6 +53,6 @@ func TestEvalutors(t *testing.T) {
|
||||
evaluator, err := NewAlertEvaluator(jsonModel)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(evaluator.Eval(nil), ShouldBeTrue)
|
||||
So(evaluator.Eval(null.FloatFromPtr(nil)), ShouldBeTrue)
|
||||
})
|
||||
}
|
||||
|
@ -2,6 +2,8 @@ package conditions
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
@ -32,7 +34,8 @@ type AlertQuery struct {
|
||||
}
|
||||
|
||||
func (c *QueryCondition) Eval(context *alerting.EvalContext) {
|
||||
seriesList, err := c.executeQuery(context)
|
||||
timeRange := tsdb.NewTimeRange(c.Query.From, c.Query.To)
|
||||
seriesList, err := c.executeQuery(context, timeRange)
|
||||
if err != nil {
|
||||
context.Error = err
|
||||
return
|
||||
@ -43,21 +46,21 @@ func (c *QueryCondition) Eval(context *alerting.EvalContext) {
|
||||
reducedValue := c.Reducer.Reduce(series)
|
||||
evalMatch := c.Evaluator.Eval(reducedValue)
|
||||
|
||||
if reducedValue == nil {
|
||||
if reducedValue.Valid == false {
|
||||
emptySerieCount++
|
||||
continue
|
||||
}
|
||||
|
||||
if context.IsTestRun {
|
||||
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.Float64),
|
||||
})
|
||||
}
|
||||
|
||||
if evalMatch {
|
||||
context.EvalMatches = append(context.EvalMatches, &alerting.EvalMatch{
|
||||
Metric: series.Name,
|
||||
Value: *reducedValue,
|
||||
Value: reducedValue.Float64,
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -66,7 +69,7 @@ func (c *QueryCondition) Eval(context *alerting.EvalContext) {
|
||||
context.Firing = len(context.EvalMatches) > 0
|
||||
}
|
||||
|
||||
func (c *QueryCondition) executeQuery(context *alerting.EvalContext) (tsdb.TimeSeriesSlice, error) {
|
||||
func (c *QueryCondition) executeQuery(context *alerting.EvalContext, timeRange *tsdb.TimeRange) (tsdb.TimeSeriesSlice, error) {
|
||||
getDsInfo := &m.GetDataSourceByIdQuery{
|
||||
Id: c.Query.DatasourceId,
|
||||
OrgId: context.Rule.OrgId,
|
||||
@ -76,7 +79,7 @@ func (c *QueryCondition) executeQuery(context *alerting.EvalContext) (tsdb.TimeS
|
||||
return nil, fmt.Errorf("Could not find datasource")
|
||||
}
|
||||
|
||||
req := c.getRequestForAlertRule(getDsInfo.Result)
|
||||
req := c.getRequestForAlertRule(getDsInfo.Result, timeRange)
|
||||
result := make(tsdb.TimeSeriesSlice, 0)
|
||||
|
||||
resp, err := c.HandleRequest(req)
|
||||
@ -102,16 +105,13 @@ func (c *QueryCondition) executeQuery(context *alerting.EvalContext) (tsdb.TimeS
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (c *QueryCondition) getRequestForAlertRule(datasource *m.DataSource) *tsdb.Request {
|
||||
func (c *QueryCondition) getRequestForAlertRule(datasource *m.DataSource, timeRange *tsdb.TimeRange) *tsdb.Request {
|
||||
req := &tsdb.Request{
|
||||
TimeRange: tsdb.TimeRange{
|
||||
From: c.Query.From,
|
||||
To: c.Query.To,
|
||||
},
|
||||
TimeRange: timeRange,
|
||||
Queries: []*tsdb.Query{
|
||||
{
|
||||
RefId: "A",
|
||||
Query: c.Query.Model.Get("target").MustString(),
|
||||
Model: c.Query.Model,
|
||||
DataSource: &tsdb.DataSourceInfo{
|
||||
Id: datasource.Id,
|
||||
Name: datasource.Name,
|
||||
@ -141,6 +141,15 @@ func NewQueryCondition(model *simplejson.Json, index int) (*QueryCondition, erro
|
||||
condition.Query.Model = queryJson.Get("model")
|
||||
condition.Query.From = queryJson.Get("params").MustArray()[1].(string)
|
||||
condition.Query.To = queryJson.Get("params").MustArray()[2].(string)
|
||||
|
||||
if err := validateFromValue(condition.Query.From); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := validateToValue(condition.Query.To); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
condition.Query.DatasourceId = queryJson.Get("datasourceId").MustInt64()
|
||||
|
||||
reducerJson := model.Get("reducer")
|
||||
@ -155,3 +164,26 @@ func NewQueryCondition(model *simplejson.Json, index int) (*QueryCondition, erro
|
||||
condition.Evaluator = evaluator
|
||||
return &condition, nil
|
||||
}
|
||||
|
||||
func validateFromValue(from string) error {
|
||||
fromRaw := strings.Replace(from, "now-", "", 1)
|
||||
|
||||
_, err := time.ParseDuration("-" + fromRaw)
|
||||
return err
|
||||
}
|
||||
|
||||
func validateToValue(to string) error {
|
||||
if to == "now" {
|
||||
return nil
|
||||
} else if strings.HasPrefix(to, "now-") {
|
||||
withoutNow := strings.Replace(to, "now-", "", 1)
|
||||
|
||||
_, err := time.ParseDuration("-" + withoutNow)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
_, err := time.ParseDuration(to)
|
||||
return err
|
||||
}
|
||||
|
@ -3,6 +3,8 @@ package conditions
|
||||
import (
|
||||
"testing"
|
||||
|
||||
null "gopkg.in/guregu/null.v3"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
@ -41,9 +43,8 @@ func TestQueryCondition(t *testing.T) {
|
||||
})
|
||||
|
||||
Convey("should fire when avg is above 100", func() {
|
||||
one := float64(120)
|
||||
two := float64(0)
|
||||
ctx.series = tsdb.TimeSeriesSlice{tsdb.NewTimeSeries("test1", [][2]*float64{{&one, &two}})}
|
||||
points := tsdb.NewTimeSeriesPointsFromArgs(120, 0)
|
||||
ctx.series = tsdb.TimeSeriesSlice{tsdb.NewTimeSeries("test1", points)}
|
||||
ctx.exec()
|
||||
|
||||
So(ctx.result.Error, ShouldBeNil)
|
||||
@ -51,9 +52,8 @@ func TestQueryCondition(t *testing.T) {
|
||||
})
|
||||
|
||||
Convey("Should not fire when avg is below 100", func() {
|
||||
one := float64(90)
|
||||
two := float64(0)
|
||||
ctx.series = tsdb.TimeSeriesSlice{tsdb.NewTimeSeries("test1", [][2]*float64{{&one, &two}})}
|
||||
points := tsdb.NewTimeSeriesPointsFromArgs(90, 0)
|
||||
ctx.series = tsdb.TimeSeriesSlice{tsdb.NewTimeSeries("test1", points)}
|
||||
ctx.exec()
|
||||
|
||||
So(ctx.result.Error, ShouldBeNil)
|
||||
@ -61,11 +61,9 @@ func TestQueryCondition(t *testing.T) {
|
||||
})
|
||||
|
||||
Convey("Should fire if only first serie matches", func() {
|
||||
one := float64(120)
|
||||
two := float64(0)
|
||||
ctx.series = tsdb.TimeSeriesSlice{
|
||||
tsdb.NewTimeSeries("test1", [][2]*float64{{&one, &two}}),
|
||||
tsdb.NewTimeSeries("test2", [][2]*float64{{&two, &two}}),
|
||||
tsdb.NewTimeSeries("test1", tsdb.NewTimeSeriesPointsFromArgs(120, 0)),
|
||||
tsdb.NewTimeSeries("test2", tsdb.NewTimeSeriesPointsFromArgs(0, 0)),
|
||||
}
|
||||
ctx.exec()
|
||||
|
||||
@ -76,8 +74,8 @@ func TestQueryCondition(t *testing.T) {
|
||||
Convey("Empty series", func() {
|
||||
Convey("Should set NoDataFound both series are empty", func() {
|
||||
ctx.series = tsdb.TimeSeriesSlice{
|
||||
tsdb.NewTimeSeries("test1", [][2]*float64{}),
|
||||
tsdb.NewTimeSeries("test2", [][2]*float64{}),
|
||||
tsdb.NewTimeSeries("test1", tsdb.NewTimeSeriesPointsFromArgs()),
|
||||
tsdb.NewTimeSeries("test2", tsdb.NewTimeSeriesPointsFromArgs()),
|
||||
}
|
||||
ctx.exec()
|
||||
|
||||
@ -86,10 +84,9 @@ func TestQueryCondition(t *testing.T) {
|
||||
})
|
||||
|
||||
Convey("Should set NoDataFound both series contains null", func() {
|
||||
one := float64(120)
|
||||
ctx.series = tsdb.TimeSeriesSlice{
|
||||
tsdb.NewTimeSeries("test1", [][2]*float64{{nil, &one}}),
|
||||
tsdb.NewTimeSeries("test2", [][2]*float64{{nil, &one}}),
|
||||
tsdb.NewTimeSeries("test1", tsdb.TimeSeriesPoints{tsdb.TimePoint{null.FloatFromPtr(nil), null.FloatFrom(0)}}),
|
||||
tsdb.NewTimeSeries("test2", tsdb.TimeSeriesPoints{tsdb.TimePoint{null.FloatFromPtr(nil), null.FloatFrom(0)}}),
|
||||
}
|
||||
ctx.exec()
|
||||
|
||||
@ -98,11 +95,9 @@ func TestQueryCondition(t *testing.T) {
|
||||
})
|
||||
|
||||
Convey("Should not set NoDataFound if one serie is empty", func() {
|
||||
one := float64(120)
|
||||
two := float64(0)
|
||||
ctx.series = tsdb.TimeSeriesSlice{
|
||||
tsdb.NewTimeSeries("test1", [][2]*float64{}),
|
||||
tsdb.NewTimeSeries("test2", [][2]*float64{{&one, &two}}),
|
||||
tsdb.NewTimeSeries("test1", tsdb.NewTimeSeriesPointsFromArgs()),
|
||||
tsdb.NewTimeSeries("test2", tsdb.NewTimeSeriesPointsFromArgs(120, 0)),
|
||||
}
|
||||
ctx.exec()
|
||||
|
||||
|
@ -4,19 +4,20 @@ import (
|
||||
"math"
|
||||
|
||||
"github.com/grafana/grafana/pkg/tsdb"
|
||||
"gopkg.in/guregu/null.v3"
|
||||
)
|
||||
|
||||
type QueryReducer interface {
|
||||
Reduce(timeSeries *tsdb.TimeSeries) *float64
|
||||
Reduce(timeSeries *tsdb.TimeSeries) null.Float
|
||||
}
|
||||
|
||||
type SimpleReducer struct {
|
||||
Type string
|
||||
}
|
||||
|
||||
func (s *SimpleReducer) Reduce(series *tsdb.TimeSeries) *float64 {
|
||||
func (s *SimpleReducer) Reduce(series *tsdb.TimeSeries) null.Float {
|
||||
if len(series.Points) == 0 {
|
||||
return nil
|
||||
return null.FloatFromPtr(nil)
|
||||
}
|
||||
|
||||
value := float64(0)
|
||||
@ -25,36 +26,36 @@ func (s *SimpleReducer) Reduce(series *tsdb.TimeSeries) *float64 {
|
||||
switch s.Type {
|
||||
case "avg":
|
||||
for _, point := range series.Points {
|
||||
if point[0] != nil {
|
||||
value += *point[0]
|
||||
if point[0].Valid {
|
||||
value += point[0].Float64
|
||||
allNull = false
|
||||
}
|
||||
}
|
||||
value = value / float64(len(series.Points))
|
||||
case "sum":
|
||||
for _, point := range series.Points {
|
||||
if point[0] != nil {
|
||||
value += *point[0]
|
||||
if point[0].Valid {
|
||||
value += point[0].Float64
|
||||
allNull = false
|
||||
}
|
||||
}
|
||||
case "min":
|
||||
value = math.MaxFloat64
|
||||
for _, point := range series.Points {
|
||||
if point[0] != nil {
|
||||
if point[0].Valid {
|
||||
allNull = false
|
||||
if value > *point[0] {
|
||||
value = *point[0]
|
||||
if value > point[0].Float64 {
|
||||
value = point[0].Float64
|
||||
}
|
||||
}
|
||||
}
|
||||
case "max":
|
||||
value = -math.MaxFloat64
|
||||
for _, point := range series.Points {
|
||||
if point[0] != nil {
|
||||
if point[0].Valid {
|
||||
allNull = false
|
||||
if value < *point[0] {
|
||||
value = *point[0]
|
||||
if value < point[0].Float64 {
|
||||
value = point[0].Float64
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -64,10 +65,10 @@ func (s *SimpleReducer) Reduce(series *tsdb.TimeSeries) *float64 {
|
||||
}
|
||||
|
||||
if allNull {
|
||||
return nil
|
||||
return null.FloatFromPtr(nil)
|
||||
}
|
||||
|
||||
return &value
|
||||
return null.FloatFrom(value)
|
||||
}
|
||||
|
||||
func NewSimpleReducer(typ string) *SimpleReducer {
|
||||
|
@ -10,44 +10,41 @@ import (
|
||||
func TestSimpleReducer(t *testing.T) {
|
||||
Convey("Test simple reducer by calculating", t, func() {
|
||||
Convey("avg", func() {
|
||||
result := *testReducer("avg", 1, 2, 3)
|
||||
result := testReducer("avg", 1, 2, 3)
|
||||
So(result, ShouldEqual, float64(2))
|
||||
})
|
||||
|
||||
Convey("sum", func() {
|
||||
result := *testReducer("sum", 1, 2, 3)
|
||||
result := testReducer("sum", 1, 2, 3)
|
||||
So(result, ShouldEqual, float64(6))
|
||||
})
|
||||
|
||||
Convey("min", func() {
|
||||
result := *testReducer("min", 3, 2, 1)
|
||||
result := testReducer("min", 3, 2, 1)
|
||||
So(result, ShouldEqual, float64(1))
|
||||
})
|
||||
|
||||
Convey("max", func() {
|
||||
result := *testReducer("max", 1, 2, 3)
|
||||
result := testReducer("max", 1, 2, 3)
|
||||
So(result, ShouldEqual, float64(3))
|
||||
})
|
||||
|
||||
Convey("count", func() {
|
||||
result := *testReducer("count", 1, 2, 3000)
|
||||
result := testReducer("count", 1, 2, 3000)
|
||||
So(result, ShouldEqual, float64(3))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func testReducer(typ string, datapoints ...float64) *float64 {
|
||||
func testReducer(typ string, datapoints ...float64) float64 {
|
||||
reducer := NewSimpleReducer(typ)
|
||||
var timeserie [][2]*float64
|
||||
dummieTimestamp := float64(521452145)
|
||||
series := &tsdb.TimeSeries{
|
||||
Name: "test time serie",
|
||||
}
|
||||
|
||||
for idx := range datapoints {
|
||||
timeserie = append(timeserie, [2]*float64{&datapoints[idx], &dummieTimestamp})
|
||||
series.Points = append(series.Points, tsdb.NewTimePoint(datapoints[idx], 1234134))
|
||||
}
|
||||
|
||||
tsdb := &tsdb.TimeSeries{
|
||||
Name: "test time serie",
|
||||
Points: timeserie,
|
||||
}
|
||||
return reducer.Reduce(tsdb)
|
||||
return reducer.Reduce(series).Float64
|
||||
}
|
||||
|
@ -1,10 +1,12 @@
|
||||
package alerting
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/benbjohnson/clock"
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
type Engine struct {
|
||||
@ -34,12 +36,19 @@ func NewEngine() *Engine {
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *Engine) Start() {
|
||||
e.log.Info("Starting Alerting Engine")
|
||||
func (e *Engine) Run(ctx context.Context) error {
|
||||
e.log.Info("Initializing Alerting")
|
||||
|
||||
go e.alertingTicker()
|
||||
go e.execDispatcher()
|
||||
go e.resultDispatcher()
|
||||
g, ctx := errgroup.WithContext(ctx)
|
||||
|
||||
g.Go(func() error { return e.alertingTicker(ctx) })
|
||||
g.Go(func() error { return e.execDispatcher(ctx) })
|
||||
g.Go(func() error { return e.resultDispatcher(ctx) })
|
||||
|
||||
err := g.Wait()
|
||||
|
||||
e.log.Info("Stopped Alerting", "reason", err)
|
||||
return err
|
||||
}
|
||||
|
||||
func (e *Engine) Stop() {
|
||||
@ -47,7 +56,7 @@ func (e *Engine) Stop() {
|
||||
close(e.resultQueue)
|
||||
}
|
||||
|
||||
func (e *Engine) alertingTicker() {
|
||||
func (e *Engine) alertingTicker(grafanaCtx context.Context) error {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
e.log.Error("Scheduler Panic: stopping alertingTicker", "error", err, "stack", log.Stack(1))
|
||||
@ -58,6 +67,8 @@ func (e *Engine) alertingTicker() {
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-grafanaCtx.Done():
|
||||
return grafanaCtx.Err()
|
||||
case tick := <-e.ticker.C:
|
||||
// TEMP SOLUTION update rules ever tenth tick
|
||||
if tickIndex%10 == 0 {
|
||||
@ -70,37 +81,69 @@ func (e *Engine) alertingTicker() {
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Engine) execDispatcher() {
|
||||
for job := range e.execQueue {
|
||||
e.log.Debug("Starting executing alert rule", "alert id", job.Rule.Id)
|
||||
go e.executeJob(job)
|
||||
func (e *Engine) execDispatcher(grafanaCtx context.Context) error {
|
||||
for {
|
||||
select {
|
||||
case <-grafanaCtx.Done():
|
||||
close(e.resultQueue)
|
||||
return grafanaCtx.Err()
|
||||
case job := <-e.execQueue:
|
||||
go e.executeJob(grafanaCtx, job)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Engine) executeJob(job *Job) {
|
||||
func (e *Engine) executeJob(grafanaCtx context.Context, job *Job) error {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
e.log.Error("Execute Alert Panic", "error", err, "stack", log.Stack(1))
|
||||
}
|
||||
}()
|
||||
|
||||
job.Running = true
|
||||
context := NewEvalContext(job.Rule)
|
||||
e.evalHandler.Eval(context)
|
||||
job.Running = false
|
||||
done := make(chan *EvalContext, 1)
|
||||
go func() {
|
||||
job.Running = true
|
||||
context := NewEvalContext(job.Rule)
|
||||
e.evalHandler.Eval(context)
|
||||
job.Running = false
|
||||
done <- context
|
||||
close(done)
|
||||
}()
|
||||
|
||||
e.resultQueue <- context
|
||||
select {
|
||||
|
||||
case <-grafanaCtx.Done():
|
||||
return grafanaCtx.Err()
|
||||
case evalContext := <-done:
|
||||
e.resultQueue <- evalContext
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *Engine) resultDispatcher() {
|
||||
func (e *Engine) resultDispatcher(grafanaCtx context.Context) error {
|
||||
for {
|
||||
select {
|
||||
case <-grafanaCtx.Done():
|
||||
//handle all responses before shutting down.
|
||||
for result := range e.resultQueue {
|
||||
e.handleResponse(result)
|
||||
}
|
||||
|
||||
return grafanaCtx.Err()
|
||||
case result := <-e.resultQueue:
|
||||
e.handleResponse(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Engine) handleResponse(result *EvalContext) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
e.log.Error("Panic in resultDispatcher", "error", err, "stack", log.Stack(1))
|
||||
}
|
||||
}()
|
||||
|
||||
for result := range e.resultQueue {
|
||||
e.log.Debug("Alert Rule Result", "ruleId", result.Rule.Id, "firing", result.Firing)
|
||||
e.resultHandler.Handle(result)
|
||||
}
|
||||
e.log.Debug("Alert Rule Result", "ruleId", result.Rule.Id, "firing", result.Firing)
|
||||
e.resultHandler.Handle(result)
|
||||
}
|
||||
|
@ -71,7 +71,7 @@ func (c *EvalContext) GetNotificationTitle() string {
|
||||
return "[" + c.GetStateModel().Text + "] " + c.Rule.Name
|
||||
}
|
||||
|
||||
func (c *EvalContext) getDashboardSlug() (string, error) {
|
||||
func (c *EvalContext) GetDashboardSlug() (string, error) {
|
||||
if c.dashboardSlug != "" {
|
||||
return c.dashboardSlug, nil
|
||||
}
|
||||
@ -86,7 +86,7 @@ func (c *EvalContext) getDashboardSlug() (string, error) {
|
||||
}
|
||||
|
||||
func (c *EvalContext) GetRuleUrl() (string, error) {
|
||||
if slug, err := c.getDashboardSlug(); err != nil {
|
||||
if slug, err := c.GetDashboardSlug(); err != nil {
|
||||
return "", err
|
||||
} else {
|
||||
ruleUrl := fmt.Sprintf("%sdashboard/db/%s?fullscreen&edit&tab=alert&panelId=%d", setting.AppUrl, slug, c.Rule.PanelId)
|
||||
@ -94,15 +94,6 @@ func (c *EvalContext) GetRuleUrl() (string, error) {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *EvalContext) GetImageUrl() (string, error) {
|
||||
if slug, err := c.getDashboardSlug(); err != nil {
|
||||
return "", err
|
||||
} else {
|
||||
ruleUrl := fmt.Sprintf("%sdashboard-solo/db/%s?&panelId=%d", setting.AppUrl, slug, c.Rule.PanelId)
|
||||
return ruleUrl, nil
|
||||
}
|
||||
}
|
||||
|
||||
func NewEvalContext(rule *Rule) *EvalContext {
|
||||
return &EvalContext{
|
||||
StartTime: time.Now(),
|
||||
|
@ -20,7 +20,7 @@ type DefaultEvalHandler struct {
|
||||
func NewEvalHandler() *DefaultEvalHandler {
|
||||
return &DefaultEvalHandler{
|
||||
log: log.New("alerting.evalHandler"),
|
||||
alertJobTimeout: time.Second * 10,
|
||||
alertJobTimeout: time.Second * 15,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -74,9 +74,9 @@ func (e *DashAlertExtractor) GetAlerts() ([]*m.Alert, error) {
|
||||
continue
|
||||
}
|
||||
|
||||
// backward compatability check, can be removed later
|
||||
enabled, hasEnabled := jsonAlert.CheckGet("enabled")
|
||||
|
||||
if !hasEnabled || !enabled.MustBool() {
|
||||
if hasEnabled && enabled.MustBool() == false {
|
||||
continue
|
||||
}
|
||||
|
||||
|
@ -42,7 +42,6 @@ func TestAlertRuleExtraction(t *testing.T) {
|
||||
"name": "name1",
|
||||
"message": "desc1",
|
||||
"handler": 1,
|
||||
"enabled": true,
|
||||
"frequency": "60s",
|
||||
"conditions": [
|
||||
{
|
||||
@ -66,7 +65,6 @@ func TestAlertRuleExtraction(t *testing.T) {
|
||||
"name": "name2",
|
||||
"message": "desc2",
|
||||
"handler": 0,
|
||||
"enabled": true,
|
||||
"frequency": "60s",
|
||||
"severity": "warning",
|
||||
"conditions": [
|
||||
|
@ -1,20 +0,0 @@
|
||||
package init
|
||||
|
||||
import (
|
||||
"github.com/grafana/grafana/pkg/services/alerting"
|
||||
_ "github.com/grafana/grafana/pkg/services/alerting/conditions"
|
||||
_ "github.com/grafana/grafana/pkg/services/alerting/notifiers"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
_ "github.com/grafana/grafana/pkg/tsdb/graphite"
|
||||
)
|
||||
|
||||
var engine *alerting.Engine
|
||||
|
||||
func Init() {
|
||||
if !setting.AlertingEnabled {
|
||||
return
|
||||
}
|
||||
|
||||
engine = alerting.NewEngine()
|
||||
engine.Start()
|
||||
}
|
@ -2,6 +2,7 @@ package alerting
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/components/imguploader"
|
||||
@ -60,20 +61,23 @@ func (n *RootNotifier) sendNotifications(notifiers []Notifier, context *EvalCont
|
||||
}
|
||||
}
|
||||
|
||||
func (n *RootNotifier) uploadImage(context *EvalContext) error {
|
||||
uploader, _ := imguploader.NewImageUploader()
|
||||
|
||||
imageUrl, err := context.GetImageUrl()
|
||||
func (n *RootNotifier) uploadImage(context *EvalContext) (err error) {
|
||||
uploader, err := imguploader.NewImageUploader()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
renderOpts := &renderer.RenderOpts{
|
||||
Url: imageUrl,
|
||||
Width: "800",
|
||||
Height: "400",
|
||||
SessionId: "123",
|
||||
Timeout: "10",
|
||||
Width: "800",
|
||||
Height: "400",
|
||||
Timeout: "30",
|
||||
OrgId: context.Rule.OrgId,
|
||||
}
|
||||
|
||||
if slug, err := context.GetDashboardSlug(); err != nil {
|
||||
return err
|
||||
} else {
|
||||
renderOpts.Path = fmt.Sprintf("dashboard-solo/db/%s?&panelId=%d", slug, context.Rule.PanelId)
|
||||
}
|
||||
|
||||
if imagePath, err := renderer.RenderToPng(renderOpts); err != nil {
|
||||
|
@ -52,9 +52,8 @@ func (this *WebhookNotifier) Notify(context *alerting.EvalContext) {
|
||||
bodyJSON.Set("rule_url", ruleUrl)
|
||||
}
|
||||
|
||||
imageUrl, err := context.GetImageUrl()
|
||||
if err == nil {
|
||||
bodyJSON.Set("image_url", imageUrl)
|
||||
if context.ImagePublicUrl != "" {
|
||||
bodyJSON.Set("image_url", context.ImagePublicUrl)
|
||||
}
|
||||
|
||||
body, _ := bodyJSON.MarshalJSON()
|
||||
|
85
pkg/services/cleanup/cleanup.go
Normal file
85
pkg/services/cleanup/cleanup.go
Normal file
@ -0,0 +1,85 @@
|
||||
package cleanup
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
type CleanUpService struct {
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
func NewCleanUpService() *CleanUpService {
|
||||
return &CleanUpService{
|
||||
log: log.New("cleanup"),
|
||||
}
|
||||
}
|
||||
|
||||
func (service *CleanUpService) Run(ctx context.Context) error {
|
||||
service.log.Info("Initializing CleanUpService")
|
||||
|
||||
g, _ := errgroup.WithContext(ctx)
|
||||
g.Go(func() error { return service.start(ctx) })
|
||||
|
||||
err := g.Wait()
|
||||
service.log.Info("Stopped CleanUpService", "reason", err)
|
||||
return err
|
||||
}
|
||||
|
||||
func (service *CleanUpService) start(ctx context.Context) error {
|
||||
service.cleanUpTmpFiles()
|
||||
|
||||
ticker := time.NewTicker(time.Hour * 1)
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
service.cleanUpTmpFiles()
|
||||
service.deleteExpiredSnapshots()
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (service *CleanUpService) cleanUpTmpFiles() {
|
||||
if _, err := os.Stat(setting.ImagesDir); os.IsNotExist(err) {
|
||||
return
|
||||
}
|
||||
|
||||
files, err := ioutil.ReadDir(setting.ImagesDir)
|
||||
if err != nil {
|
||||
service.log.Error("Problem reading image dir", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
var toDelete []os.FileInfo
|
||||
for _, file := range files {
|
||||
if file.ModTime().AddDate(0, 0, 1).Before(time.Now()) {
|
||||
toDelete = append(toDelete, file)
|
||||
}
|
||||
}
|
||||
|
||||
for _, file := range toDelete {
|
||||
fullPath := path.Join(setting.ImagesDir, file.Name())
|
||||
err := os.Remove(fullPath)
|
||||
if err != nil {
|
||||
service.log.Error("Failed to delete temp file", "file", file.Name(), "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
service.log.Debug("Found old rendered image to delete", "deleted", len(toDelete), "keept", len(files))
|
||||
}
|
||||
|
||||
func (service *CleanUpService) deleteExpiredSnapshots() {
|
||||
bus.Dispatch(&m.DeleteExpiredSnapshotsCommand{})
|
||||
}
|
@ -12,6 +12,7 @@ import (
|
||||
"net/smtp"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
@ -66,7 +67,7 @@ func sendToSmtpServer(recipients []string, msgContent []byte) error {
|
||||
tlsconfig.Certificates = []tls.Certificate{cert}
|
||||
}
|
||||
|
||||
conn, err := net.Dial("tcp", net.JoinHostPort(host, port))
|
||||
conn, err := net.DialTimeout("tcp", net.JoinHostPort(host, port), time.Second*10)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -44,7 +44,7 @@ func sendWebRequest(webhook *Webhook) error {
|
||||
webhookLog.Debug("Sending webhook", "url", webhook.Url)
|
||||
|
||||
client := http.Client{
|
||||
Timeout: time.Duration(3 * time.Second),
|
||||
Timeout: time.Duration(10 * time.Second),
|
||||
}
|
||||
|
||||
request, err := http.NewRequest("POST", webhook.Url, bytes.NewReader([]byte(webhook.Body)))
|
||||
|
@ -17,6 +17,7 @@ func init() {
|
||||
bus.AddHandler("sql", DeleteAlertById)
|
||||
bus.AddHandler("sql", GetAllAlertQueryHandler)
|
||||
bus.AddHandler("sql", SetAlertState)
|
||||
bus.AddHandler("sql", GetAlertStatesForDashboard)
|
||||
}
|
||||
|
||||
func GetAlertById(query *m.GetAlertByIdQuery) error {
|
||||
@ -92,7 +93,7 @@ func HandleAlertsQuery(query *m.GetAlertsQuery) error {
|
||||
params = append(params, query.Limit)
|
||||
}
|
||||
|
||||
sql.WriteString("ORDER BY name ASC")
|
||||
sql.WriteString(" ORDER BY name ASC")
|
||||
|
||||
alerts := make([]*m.Alert, 0)
|
||||
if err := x.Sql(sql.String(), params...).Find(&alerts); err != nil {
|
||||
@ -241,3 +242,19 @@ func SetAlertState(cmd *m.SetAlertStateCommand) error {
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func GetAlertStatesForDashboard(query *m.GetAlertStatesForDashboardQuery) error {
|
||||
var rawSql = `SELECT
|
||||
id,
|
||||
dashboard_id,
|
||||
panel_id,
|
||||
state,
|
||||
new_state_date
|
||||
FROM alert
|
||||
WHERE org_id = ? AND dashboard_id = ?`
|
||||
|
||||
query.Result = make([]*m.AlertStateInfoDTO, 0)
|
||||
err := x.Sql(rawSql, query.OrgId, query.DashboardId).Find(&query.Result)
|
||||
|
||||
return err
|
||||
}
|
||||
|
@ -66,7 +66,8 @@ func GetAlertNotificationsToSend(query *m.GetAlertNotificationsToSendQuery) erro
|
||||
sql.WriteString(` WHERE alert_notification.org_id = ?`)
|
||||
params = append(params, query.OrgId)
|
||||
|
||||
sql.WriteString(` AND ((alert_notification.is_default = 1)`)
|
||||
sql.WriteString(` AND ((alert_notification.is_default = ?)`)
|
||||
params = append(params, dialect.BooleanStr(true))
|
||||
if len(query.Ids) > 0 {
|
||||
sql.WriteString(` OR alert_notification.id IN (?` + strings.Repeat(",?", len(query.Ids)-1) + ")")
|
||||
for _, v := range query.Ids {
|
||||
|
@ -75,7 +75,7 @@ func (r *SqlAnnotationRepo) Find(query *annotations.ItemQuery) ([]*annotations.I
|
||||
query.Limit = 10
|
||||
}
|
||||
|
||||
sql.WriteString(fmt.Sprintf("ORDER BY epoch DESC LIMIT %v", query.Limit))
|
||||
sql.WriteString(fmt.Sprintf(" ORDER BY epoch DESC LIMIT %v", query.Limit))
|
||||
|
||||
items := make([]*annotations.Item, 0)
|
||||
if err := x.Sql(sql.String(), params...).Find(&items); err != nil {
|
||||
|
@ -6,6 +6,7 @@ import (
|
||||
"github.com/go-xorm/xorm"
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@ -13,6 +14,25 @@ func init() {
|
||||
bus.AddHandler("sql", GetDashboardSnapshot)
|
||||
bus.AddHandler("sql", DeleteDashboardSnapshot)
|
||||
bus.AddHandler("sql", SearchDashboardSnapshots)
|
||||
bus.AddHandler("sql", DeleteExpiredSnapshots)
|
||||
}
|
||||
|
||||
func DeleteExpiredSnapshots(cmd *m.DeleteExpiredSnapshotsCommand) error {
|
||||
return inTransaction(func(sess *xorm.Session) error {
|
||||
var expiredCount int64 = 0
|
||||
|
||||
if setting.SnapShotRemoveExpired {
|
||||
deleteExpiredSql := "DELETE FROM dashboard_snapshot WHERE expires < ?"
|
||||
expiredResponse, err := x.Exec(deleteExpiredSql, time.Now)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
expiredCount, _ = expiredResponse.RowsAffected()
|
||||
}
|
||||
|
||||
sqlog.Debug("Deleted old/expired snaphots", "expired", expiredCount)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func CreateDashboardSnapshot(cmd *m.CreateDashboardSnapshotCommand) error {
|
||||
|
@ -120,4 +120,9 @@ func addDashboardMigration(mg *Migrator) {
|
||||
mg.AddMigration("Add index for plugin_id in dashboard", NewAddIndexMigration(dashboardV2, &Index{
|
||||
Cols: []string{"org_id", "plugin_id"}, Type: IndexType,
|
||||
}))
|
||||
|
||||
// dashboard_id index for dashboard_tag table
|
||||
mg.AddMigration("Add index for dashboard_id in dashboard_tag", NewAddIndexMigration(dashboardTagV1, &Index{
|
||||
Cols: []string{"dashboard_id"}, Type: IndexType,
|
||||
}))
|
||||
}
|
||||
|
@ -18,6 +18,7 @@ type Dialect interface {
|
||||
SupportEngine() bool
|
||||
LikeStr() string
|
||||
Default(col *Column) string
|
||||
BooleanStr(bool) string
|
||||
|
||||
CreateIndexSql(tableName string, index *Index) string
|
||||
CreateTableSql(table *Table) string
|
||||
|
@ -92,44 +92,45 @@ func (mg *Migrator) Start() error {
|
||||
|
||||
mg.Logger.Debug("Executing", "sql", sql)
|
||||
|
||||
if err := mg.exec(m); err != nil {
|
||||
mg.Logger.Error("Exec failed", "error", err, "sql", sql)
|
||||
record.Error = err.Error()
|
||||
mg.x.Insert(&record)
|
||||
err := mg.inTransaction(func(sess *xorm.Session) error {
|
||||
|
||||
if err := mg.exec(m, sess); err != nil {
|
||||
mg.Logger.Error("Exec failed", "error", err, "sql", sql)
|
||||
record.Error = err.Error()
|
||||
sess.Insert(&record)
|
||||
return err
|
||||
} else {
|
||||
record.Success = true
|
||||
sess.Insert(&record)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
} else {
|
||||
record.Success = true
|
||||
mg.x.Insert(&record)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (mg *Migrator) exec(m Migration) error {
|
||||
func (mg *Migrator) exec(m Migration, sess *xorm.Session) error {
|
||||
mg.Logger.Info("Executing migration", "id", m.Id())
|
||||
|
||||
err := mg.inTransaction(func(sess *xorm.Session) error {
|
||||
|
||||
condition := m.GetCondition()
|
||||
if condition != nil {
|
||||
sql, args := condition.Sql(mg.dialect)
|
||||
results, err := sess.Query(sql, args...)
|
||||
if err != nil || len(results) == 0 {
|
||||
mg.Logger.Info("Skipping migration condition not fulfilled", "id", m.Id())
|
||||
return sess.Rollback()
|
||||
}
|
||||
condition := m.GetCondition()
|
||||
if condition != nil {
|
||||
sql, args := condition.Sql(mg.dialect)
|
||||
results, err := sess.Query(sql, args...)
|
||||
if err != nil || len(results) == 0 {
|
||||
mg.Logger.Info("Skipping migration condition not fulfilled", "id", m.Id())
|
||||
return sess.Rollback()
|
||||
}
|
||||
}
|
||||
|
||||
_, err := sess.Exec(m.Sql(mg.dialect))
|
||||
if err != nil {
|
||||
mg.Logger.Error("Executing migration failed", "id", m.Id(), "error", err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
_, err := sess.Exec(m.Sql(mg.dialect))
|
||||
if err != nil {
|
||||
mg.Logger.Error("Executing migration failed", "id", m.Id(), "error", err)
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -29,6 +29,10 @@ func (db *Mysql) AutoIncrStr() string {
|
||||
return "AUTO_INCREMENT"
|
||||
}
|
||||
|
||||
func (db *Mysql) BooleanStr(value bool) string {
|
||||
return strconv.FormatBool(value)
|
||||
}
|
||||
|
||||
func (db *Mysql) SqlType(c *Column) string {
|
||||
var res string
|
||||
switch c.Type {
|
||||
|
@ -36,6 +36,10 @@ func (db *Postgres) AutoIncrStr() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (db *Postgres) BooleanStr(value bool) string {
|
||||
return strconv.FormatBool(value)
|
||||
}
|
||||
|
||||
func (b *Postgres) Default(col *Column) string {
|
||||
if col.Type == DB_Bool {
|
||||
if col.Default == "0" {
|
||||
|
@ -29,6 +29,13 @@ func (db *Sqlite3) AutoIncrStr() string {
|
||||
return "AUTOINCREMENT"
|
||||
}
|
||||
|
||||
func (db *Sqlite3) BooleanStr(value bool) string {
|
||||
if value {
|
||||
return "1"
|
||||
}
|
||||
return "0"
|
||||
}
|
||||
|
||||
func (db *Sqlite3) SqlType(c *Column) string {
|
||||
switch c.Type {
|
||||
case DB_Date, DB_DateTime, DB_TimeStamp, DB_Time:
|
||||
|
@ -128,7 +128,11 @@ func CreateUser(cmd *m.CreateUserCommand) error {
|
||||
}
|
||||
|
||||
if setting.AutoAssignOrg && !user.IsAdmin {
|
||||
orgUser.Role = m.RoleType(setting.AutoAssignOrgRole)
|
||||
if len(cmd.DefaultOrgRole) > 0 {
|
||||
orgUser.Role = m.RoleType(cmd.DefaultOrgRole)
|
||||
} else {
|
||||
orgUser.Role = m.RoleType(setting.AutoAssignOrgRole)
|
||||
}
|
||||
}
|
||||
|
||||
if _, err = sess.Insert(&orgUser); err != nil {
|
||||
|
@ -78,9 +78,11 @@ var (
|
||||
DataProxyWhiteList map[string]bool
|
||||
|
||||
// Snapshots
|
||||
ExternalSnapshotUrl string
|
||||
ExternalSnapshotName string
|
||||
ExternalEnabled bool
|
||||
ExternalSnapshotUrl string
|
||||
ExternalSnapshotName string
|
||||
ExternalEnabled bool
|
||||
SnapShotTTLDays int
|
||||
SnapShotRemoveExpired bool
|
||||
|
||||
// User settings
|
||||
AllowUserSignUp bool
|
||||
@ -90,7 +92,7 @@ var (
|
||||
VerifyEmailEnabled bool
|
||||
LoginHint string
|
||||
DefaultTheme string
|
||||
AllowUserPassLogin bool
|
||||
DisableLoginForm bool
|
||||
|
||||
// Http auth
|
||||
AdminUser string
|
||||
@ -495,6 +497,8 @@ func NewConfigContext(args *CommandLineArgs) error {
|
||||
ExternalSnapshotUrl = snapshots.Key("external_snapshot_url").String()
|
||||
ExternalSnapshotName = snapshots.Key("external_snapshot_name").String()
|
||||
ExternalEnabled = snapshots.Key("external_enabled").MustBool(true)
|
||||
SnapShotRemoveExpired = snapshots.Key("snapshot_remove_expired").MustBool(true)
|
||||
SnapShotTTLDays = snapshots.Key("snapshot_TTL_days").MustInt(90)
|
||||
|
||||
// read data source proxy white list
|
||||
DataProxyWhiteList = make(map[string]bool)
|
||||
@ -514,7 +518,10 @@ func NewConfigContext(args *CommandLineArgs) error {
|
||||
VerifyEmailEnabled = users.Key("verify_email_enabled").MustBool(false)
|
||||
LoginHint = users.Key("login_hint").String()
|
||||
DefaultTheme = users.Key("default_theme").String()
|
||||
AllowUserPassLogin = users.Key("allow_user_pass_login").MustBool(true)
|
||||
|
||||
// auth
|
||||
auth := Cfg.Section("auth")
|
||||
DisableLoginForm = auth.Key("disable_login_form").MustBool(false)
|
||||
|
||||
// anonymous access
|
||||
AnonymousEnabled = Cfg.Section("auth.anonymous").Key("enabled").MustBool(false)
|
||||
@ -556,7 +563,7 @@ func NewConfigContext(args *CommandLineArgs) error {
|
||||
log.Warn("require_email_validation is enabled but smpt is disabled")
|
||||
}
|
||||
|
||||
GrafanaNetUrl = Cfg.Section("grafana.net").Key("url").MustString("https://grafana.net")
|
||||
GrafanaNetUrl = Cfg.Section("grafana_net").Key("url").MustString("https://grafana.net")
|
||||
|
||||
imageUploadingSection := Cfg.Section("external_image_storage")
|
||||
ImageUploadProvider = imageUploadingSection.Key("provider").MustString("internal")
|
||||
|
@ -8,12 +8,11 @@ type OAuthInfo struct {
|
||||
AllowedDomains []string
|
||||
ApiUrl string
|
||||
AllowSignup bool
|
||||
Name string
|
||||
}
|
||||
|
||||
type OAuther struct {
|
||||
GitHub, Google, Twitter, Generic bool
|
||||
OAuthInfos map[string]*OAuthInfo
|
||||
OAuthProviderName string
|
||||
OAuthInfos map[string]*OAuthInfo
|
||||
}
|
||||
|
||||
var OAuthService *OAuther
|
||||
|
114
pkg/social/grafananet_oauth.go
Normal file
114
pkg/social/grafananet_oauth.go
Normal file
@ -0,0 +1,114 @@
|
||||
package social
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
type SocialGrafanaNet struct {
|
||||
*oauth2.Config
|
||||
url string
|
||||
allowedOrganizations []string
|
||||
allowSignup bool
|
||||
}
|
||||
|
||||
func (s *SocialGrafanaNet) Type() int {
|
||||
return int(models.GRAFANANET)
|
||||
}
|
||||
|
||||
func (s *SocialGrafanaNet) IsEmailAllowed(email string) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *SocialGrafanaNet) IsSignupAllowed() bool {
|
||||
return s.allowSignup
|
||||
}
|
||||
|
||||
func (s *SocialGrafanaNet) 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 *SocialGrafanaNet) FetchOrganizations(client *http.Client) ([]string, error) {
|
||||
type Record struct {
|
||||
Login string `json:"login"`
|
||||
}
|
||||
|
||||
url := fmt.Sprintf(s.url + "/api/oauth2/user/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 *SocialGrafanaNet) UserInfo(token *oauth2.Token) (*BasicUserInfo, error) {
|
||||
var data struct {
|
||||
Id int `json:"id"`
|
||||
Name string `json:"login"`
|
||||
Email string `json:"email"`
|
||||
Role string `json:"role"`
|
||||
}
|
||||
|
||||
var err error
|
||||
client := s.Client(oauth2.NoContext, token)
|
||||
r, err := client.Get(s.url + "/api/oauth2/user")
|
||||
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,
|
||||
Role: data.Role,
|
||||
}
|
||||
|
||||
if !s.IsOrganizationMember(client) {
|
||||
return nil, ErrMissingOrganizationMembership
|
||||
}
|
||||
|
||||
return userInfo, nil
|
||||
}
|
@ -15,6 +15,7 @@ type BasicUserInfo struct {
|
||||
Email string
|
||||
Login string
|
||||
Company string
|
||||
Role string
|
||||
}
|
||||
|
||||
type SocialConnector interface {
|
||||
@ -36,7 +37,7 @@ func NewOAuthService() {
|
||||
setting.OAuthService = &setting.OAuther{}
|
||||
setting.OAuthService.OAuthInfos = make(map[string]*setting.OAuthInfo)
|
||||
|
||||
allOauthes := []string{"github", "google", "generic_oauth"}
|
||||
allOauthes := []string{"github", "google", "generic_oauth", "grafananet"}
|
||||
|
||||
for _, name := range allOauthes {
|
||||
sec := setting.Cfg.Section("auth." + name)
|
||||
@ -50,6 +51,7 @@ func NewOAuthService() {
|
||||
Enabled: sec.Key("enabled").MustBool(),
|
||||
AllowedDomains: sec.Key("allowed_domains").Strings(" "),
|
||||
AllowSignup: sec.Key("allow_sign_up").MustBool(),
|
||||
Name: sec.Key("name").MustString(name),
|
||||
}
|
||||
|
||||
if !info.Enabled {
|
||||
@ -70,22 +72,18 @@ func NewOAuthService() {
|
||||
|
||||
// GitHub.
|
||||
if name == "github" {
|
||||
setting.OAuthService.GitHub = true
|
||||
teamIds := sec.Key("team_ids").Ints(",")
|
||||
allowedOrganizations := sec.Key("allowed_organizations").Strings(" ")
|
||||
SocialMap["github"] = &SocialGithub{
|
||||
Config: &config,
|
||||
allowedDomains: info.AllowedDomains,
|
||||
apiUrl: info.ApiUrl,
|
||||
allowSignup: info.AllowSignup,
|
||||
teamIds: teamIds,
|
||||
allowedOrganizations: allowedOrganizations,
|
||||
teamIds: sec.Key("team_ids").Ints(","),
|
||||
allowedOrganizations: sec.Key("allowed_organizations").Strings(" "),
|
||||
}
|
||||
}
|
||||
|
||||
// Google.
|
||||
if name == "google" {
|
||||
setting.OAuthService.Google = true
|
||||
SocialMap["google"] = &SocialGoogle{
|
||||
Config: &config, allowedDomains: info.AllowedDomains,
|
||||
apiUrl: info.ApiUrl,
|
||||
@ -95,17 +93,33 @@ func NewOAuthService() {
|
||||
|
||||
// Generic - Uses the same scheme as Github.
|
||||
if name == "generic_oauth" {
|
||||
setting.OAuthService.Generic = true
|
||||
setting.OAuthService.OAuthProviderName = sec.Key("oauth_provider_name").String()
|
||||
teamIds := sec.Key("team_ids").Ints(",")
|
||||
allowedOrganizations := sec.Key("allowed_organizations").Strings(" ")
|
||||
SocialMap["generic_oauth"] = &GenericOAuth{
|
||||
Config: &config,
|
||||
allowedDomains: info.AllowedDomains,
|
||||
apiUrl: info.ApiUrl,
|
||||
allowSignup: info.AllowSignup,
|
||||
teamIds: teamIds,
|
||||
allowedOrganizations: allowedOrganizations,
|
||||
teamIds: sec.Key("team_ids").Ints(","),
|
||||
allowedOrganizations: sec.Key("allowed_organizations").Strings(" "),
|
||||
}
|
||||
}
|
||||
|
||||
if name == "grafananet" {
|
||||
config := oauth2.Config{
|
||||
ClientID: info.ClientId,
|
||||
ClientSecret: info.ClientSecret,
|
||||
Endpoint: oauth2.Endpoint{
|
||||
AuthURL: setting.GrafanaNetUrl + "/oauth2/authorize",
|
||||
TokenURL: setting.GrafanaNetUrl + "/api/oauth2/token",
|
||||
},
|
||||
RedirectURL: strings.TrimSuffix(setting.AppUrl, "/") + SocialBaseUrl + name,
|
||||
Scopes: info.Scopes,
|
||||
}
|
||||
|
||||
SocialMap["grafananet"] = &SocialGrafanaNet{
|
||||
Config: &config,
|
||||
url: setting.GrafanaNetUrl,
|
||||
allowSignup: info.AllowSignup,
|
||||
allowedOrganizations: sec.Key("allowed_organizations").Strings(" "),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -26,7 +26,7 @@ func (bg *Batch) process(context *QueryContext) {
|
||||
if executor == nil {
|
||||
bg.Done = true
|
||||
result := &BatchResult{
|
||||
Error: errors.New("Could not find executor for data source type " + bg.Queries[0].DataSource.PluginId),
|
||||
Error: errors.New("Could not find executor for data source type: " + bg.Queries[0].DataSource.PluginId),
|
||||
QueryResults: make(map[string]*QueryResult),
|
||||
}
|
||||
for _, query := range bg.Queries {
|
||||
|
@ -38,7 +38,7 @@ func init() {
|
||||
}
|
||||
|
||||
HttpClient = http.Client{
|
||||
Timeout: time.Duration(10 * time.Second),
|
||||
Timeout: time.Duration(15 * time.Second),
|
||||
Transport: tr,
|
||||
}
|
||||
}
|
||||
@ -54,7 +54,7 @@ func (e *GraphiteExecutor) Execute(queries tsdb.QuerySlice, context *tsdb.QueryC
|
||||
}
|
||||
|
||||
for _, query := range queries {
|
||||
formData["target"] = []string{query.Query}
|
||||
formData["target"] = []string{query.Model.Get("target").MustString()}
|
||||
}
|
||||
|
||||
if setting.Env == setting.DEV {
|
||||
@ -79,7 +79,8 @@ func (e *GraphiteExecutor) Execute(queries tsdb.QuerySlice, context *tsdb.QueryC
|
||||
}
|
||||
|
||||
result.QueryResults = make(map[string]*tsdb.QueryResult)
|
||||
queryRes := &tsdb.QueryResult{}
|
||||
queryRes := tsdb.NewQueryResult()
|
||||
|
||||
for _, series := range data {
|
||||
queryRes.Series = append(queryRes.Series, &tsdb.TimeSeries{
|
||||
Name: series.Target,
|
||||
@ -102,9 +103,9 @@ func (e *GraphiteExecutor) parseResponse(res *http.Response) ([]TargetResponseDT
|
||||
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))
|
||||
if res.StatusCode/100 != 2 {
|
||||
glog.Info("Request failed", "status", res.Status, "body", string(body))
|
||||
return nil, fmt.Errorf("Request failed status: %v", res.Status)
|
||||
}
|
||||
|
||||
var data []TargetResponseDTO
|
||||
|
@ -1,23 +1 @@
|
||||
package graphite
|
||||
|
||||
// func TestGraphite(t *testing.T) {
|
||||
//
|
||||
// Convey("When executing graphite query", t, func() {
|
||||
// executor := NewGraphiteExecutor(&tsdb.DataSourceInfo{
|
||||
// Url: "http://localhost:8080",
|
||||
// })
|
||||
//
|
||||
// queries := tsdb.QuerySlice{
|
||||
// &tsdb.Query{Query: "{\"target\": \"apps.backend.*.counters.requests.count\"}"},
|
||||
// }
|
||||
//
|
||||
// context := tsdb.NewQueryContext(queries, tsdb.TimeRange{})
|
||||
// result := executor.Execute(queries, context)
|
||||
// So(result.Error, ShouldBeNil)
|
||||
//
|
||||
// Convey("Should return series", func() {
|
||||
// So(result.QueryResults, ShouldNotBeEmpty)
|
||||
// })
|
||||
// })
|
||||
//
|
||||
// }
|
||||
|
@ -1,6 +1,8 @@
|
||||
package graphite
|
||||
|
||||
import "github.com/grafana/grafana/pkg/tsdb"
|
||||
|
||||
type TargetResponseDTO struct {
|
||||
Target string `json:"target"`
|
||||
DataPoints [][2]*float64 `json:"datapoints"`
|
||||
Target string `json:"target"`
|
||||
DataPoints tsdb.TimeSeriesPoints `json:"datapoints"`
|
||||
}
|
||||
|
@ -1,19 +1,31 @@
|
||||
package tsdb
|
||||
|
||||
type TimeRange struct {
|
||||
From string
|
||||
To string
|
||||
import (
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"gopkg.in/guregu/null.v3"
|
||||
)
|
||||
|
||||
type Query struct {
|
||||
RefId string
|
||||
Model *simplejson.Json
|
||||
Depends []string
|
||||
DataSource *DataSourceInfo
|
||||
Results []*TimeSeries
|
||||
Exclude bool
|
||||
MaxDataPoints int64
|
||||
IntervalMs int64
|
||||
}
|
||||
|
||||
type QuerySlice []*Query
|
||||
|
||||
type Request struct {
|
||||
TimeRange TimeRange
|
||||
MaxDataPoints int
|
||||
Queries QuerySlice
|
||||
TimeRange *TimeRange
|
||||
Queries QuerySlice
|
||||
}
|
||||
|
||||
type Response struct {
|
||||
BatchTimings []*BatchTiming
|
||||
Results map[string]*QueryResult
|
||||
BatchTimings []*BatchTiming `json:"timings"`
|
||||
Results map[string]*QueryResult `json:"results"`
|
||||
}
|
||||
|
||||
type DataSourceInfo struct {
|
||||
@ -40,19 +52,41 @@ type BatchResult struct {
|
||||
}
|
||||
|
||||
type QueryResult struct {
|
||||
Error error
|
||||
RefId string
|
||||
Series TimeSeriesSlice
|
||||
Error error `json:"error"`
|
||||
RefId string `json:"refId"`
|
||||
Series TimeSeriesSlice `json:"series"`
|
||||
}
|
||||
|
||||
type TimeSeries struct {
|
||||
Name string `json:"name"`
|
||||
Points [][2]*float64 `json:"points"`
|
||||
Name string `json:"name"`
|
||||
Points TimeSeriesPoints `json:"points"`
|
||||
}
|
||||
|
||||
type TimePoint [2]null.Float
|
||||
type TimeSeriesPoints []TimePoint
|
||||
type TimeSeriesSlice []*TimeSeries
|
||||
|
||||
func NewTimeSeries(name string, points [][2]*float64) *TimeSeries {
|
||||
func NewQueryResult() *QueryResult {
|
||||
return &QueryResult{
|
||||
Series: make(TimeSeriesSlice, 0),
|
||||
}
|
||||
}
|
||||
|
||||
func NewTimePoint(value float64, timestamp float64) TimePoint {
|
||||
return TimePoint{null.FloatFrom(value), null.FloatFrom(timestamp)}
|
||||
}
|
||||
|
||||
func NewTimeSeriesPointsFromArgs(values ...float64) TimeSeriesPoints {
|
||||
points := make(TimeSeriesPoints, 0)
|
||||
|
||||
for i := 0; i < len(values); i += 2 {
|
||||
points = append(points, NewTimePoint(values[i], values[i+1]))
|
||||
}
|
||||
|
||||
return points
|
||||
}
|
||||
|
||||
func NewTimeSeries(name string, points TimeSeriesPoints) *TimeSeries {
|
||||
return &TimeSeries{
|
||||
Name: name,
|
||||
Points: points,
|
||||
|
161
pkg/tsdb/prometheus/prometheus.go
Normal file
161
pkg/tsdb/prometheus/prometheus.go
Normal file
@ -0,0 +1,161 @@
|
||||
package prometheus
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
"github.com/grafana/grafana/pkg/tsdb"
|
||||
"github.com/prometheus/client_golang/api/prometheus"
|
||||
pmodel "github.com/prometheus/common/model"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
type PrometheusExecutor struct {
|
||||
*tsdb.DataSourceInfo
|
||||
}
|
||||
|
||||
func NewPrometheusExecutor(dsInfo *tsdb.DataSourceInfo) tsdb.Executor {
|
||||
return &PrometheusExecutor{dsInfo}
|
||||
}
|
||||
|
||||
var (
|
||||
plog log.Logger
|
||||
HttpClient http.Client
|
||||
)
|
||||
|
||||
func init() {
|
||||
plog = log.New("tsdb.prometheus")
|
||||
tsdb.RegisterExecutor("prometheus", NewPrometheusExecutor)
|
||||
}
|
||||
|
||||
func (e *PrometheusExecutor) getClient() (prometheus.QueryAPI, error) {
|
||||
cfg := prometheus.Config{
|
||||
Address: e.DataSourceInfo.Url,
|
||||
}
|
||||
|
||||
client, err := prometheus.New(cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return prometheus.NewQueryAPI(client), nil
|
||||
}
|
||||
|
||||
func (e *PrometheusExecutor) Execute(queries tsdb.QuerySlice, queryContext *tsdb.QueryContext) *tsdb.BatchResult {
|
||||
result := &tsdb.BatchResult{}
|
||||
|
||||
client, err := e.getClient()
|
||||
if err != nil {
|
||||
return resultWithError(result, err)
|
||||
}
|
||||
|
||||
query, err := parseQuery(queries, queryContext)
|
||||
if err != nil {
|
||||
return resultWithError(result, err)
|
||||
}
|
||||
|
||||
timeRange := prometheus.Range{
|
||||
Start: query.Start,
|
||||
End: query.End,
|
||||
Step: query.Step,
|
||||
}
|
||||
|
||||
value, err := client.QueryRange(context.Background(), query.Expr, timeRange)
|
||||
|
||||
if err != nil {
|
||||
return resultWithError(result, err)
|
||||
}
|
||||
|
||||
queryResult, err := parseResponse(value, query)
|
||||
if err != nil {
|
||||
return resultWithError(result, err)
|
||||
}
|
||||
result.QueryResults = queryResult
|
||||
return result
|
||||
}
|
||||
|
||||
func formatLegend(metric pmodel.Metric, query *PrometheusQuery) string {
|
||||
reg, _ := regexp.Compile(`\{\{\s*(.+?)\s*\}\}`)
|
||||
|
||||
result := reg.ReplaceAllFunc([]byte(query.LegendFormat), func(in []byte) []byte {
|
||||
ind := strings.Replace(strings.Replace(string(in), "{{", "", 1), "}}", "", 1)
|
||||
if val, exists := metric[pmodel.LabelName(ind)]; exists {
|
||||
return []byte(val)
|
||||
}
|
||||
|
||||
return in
|
||||
})
|
||||
|
||||
return string(result)
|
||||
}
|
||||
|
||||
func parseQuery(queries tsdb.QuerySlice, queryContext *tsdb.QueryContext) (*PrometheusQuery, error) {
|
||||
queryModel := queries[0]
|
||||
|
||||
expr, err := queryModel.Model.Get("expr").String()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
step, err := queryModel.Model.Get("step").Int64()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
format, err := queryModel.Model.Get("legendFormat").String()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
start, err := queryContext.TimeRange.ParseFrom()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
end, err := queryContext.TimeRange.ParseTo()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &PrometheusQuery{
|
||||
Expr: expr,
|
||||
Step: time.Second * time.Duration(step),
|
||||
LegendFormat: format,
|
||||
Start: start,
|
||||
End: end,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func parseResponse(value pmodel.Value, query *PrometheusQuery) (map[string]*tsdb.QueryResult, error) {
|
||||
queryResults := make(map[string]*tsdb.QueryResult)
|
||||
queryRes := tsdb.NewQueryResult()
|
||||
|
||||
data, ok := value.(pmodel.Matrix)
|
||||
if !ok {
|
||||
return queryResults, fmt.Errorf("Unsupported result format: %s", value.Type().String())
|
||||
}
|
||||
|
||||
for _, v := range data {
|
||||
series := tsdb.TimeSeries{
|
||||
Name: formatLegend(v.Metric, query),
|
||||
}
|
||||
|
||||
for _, k := range v.Values {
|
||||
series.Points = append(series.Points, tsdb.NewTimePoint(float64(k.Value), float64(k.Timestamp.Unix()*1000)))
|
||||
}
|
||||
|
||||
queryRes.Series = append(queryRes.Series, &series)
|
||||
}
|
||||
|
||||
queryResults["A"] = queryRes
|
||||
return queryResults, nil
|
||||
}
|
||||
|
||||
func resultWithError(result *tsdb.BatchResult, err error) *tsdb.BatchResult {
|
||||
result.Error = err
|
||||
return result
|
||||
}
|
26
pkg/tsdb/prometheus/prometheus_test.go
Normal file
26
pkg/tsdb/prometheus/prometheus_test.go
Normal file
@ -0,0 +1,26 @@
|
||||
package prometheus
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
p "github.com/prometheus/common/model"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestPrometheus(t *testing.T) {
|
||||
Convey("Prometheus", t, func() {
|
||||
|
||||
Convey("converting metric name", func() {
|
||||
metric := map[p.LabelName]p.LabelValue{
|
||||
p.LabelName("app"): p.LabelValue("backend"),
|
||||
p.LabelName("device"): p.LabelValue("mobile"),
|
||||
}
|
||||
|
||||
query := &PrometheusQuery{
|
||||
LegendFormat: "legend {{app}} {{device}} {{broken}}",
|
||||
}
|
||||
|
||||
So(formatLegend(metric, query), ShouldEqual, "legend backend mobile {{broken}}")
|
||||
})
|
||||
})
|
||||
}
|
11
pkg/tsdb/prometheus/types.go
Normal file
11
pkg/tsdb/prometheus/types.go
Normal file
@ -0,0 +1,11 @@
|
||||
package prometheus
|
||||
|
||||
import "time"
|
||||
|
||||
type PrometheusQuery struct {
|
||||
Expr string
|
||||
Step time.Duration
|
||||
LegendFormat string
|
||||
Start time.Time
|
||||
End time.Time
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
package tsdb
|
||||
|
||||
type Query struct {
|
||||
RefId string
|
||||
Query string
|
||||
Depends []string
|
||||
DataSource *DataSourceInfo
|
||||
Results []*TimeSeries
|
||||
Exclude bool
|
||||
}
|
||||
|
||||
type QuerySlice []*Query
|
@ -3,7 +3,7 @@ package tsdb
|
||||
import "sync"
|
||||
|
||||
type QueryContext struct {
|
||||
TimeRange TimeRange
|
||||
TimeRange *TimeRange
|
||||
Queries QuerySlice
|
||||
Results map[string]*QueryResult
|
||||
ResultsChan chan *BatchResult
|
||||
@ -11,7 +11,7 @@ type QueryContext struct {
|
||||
BatchWaits sync.WaitGroup
|
||||
}
|
||||
|
||||
func NewQueryContext(queries QuerySlice, timeRange TimeRange) *QueryContext {
|
||||
func NewQueryContext(queries QuerySlice, timeRange *TimeRange) *QueryContext {
|
||||
return &QueryContext{
|
||||
TimeRange: timeRange,
|
||||
Queries: queries,
|
||||
|
130
pkg/tsdb/testdata/scenarios.go
vendored
Normal file
130
pkg/tsdb/testdata/scenarios.go
vendored
Normal file
@ -0,0 +1,130 @@
|
||||
package testdata
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
"github.com/grafana/grafana/pkg/tsdb"
|
||||
)
|
||||
|
||||
type ScenarioHandler func(query *tsdb.Query, context *tsdb.QueryContext) *tsdb.QueryResult
|
||||
|
||||
type Scenario struct {
|
||||
Id string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
StringInput string `json:"stringOption"`
|
||||
Description string `json:"description"`
|
||||
Handler ScenarioHandler `json:"-"`
|
||||
}
|
||||
|
||||
var ScenarioRegistry map[string]*Scenario
|
||||
|
||||
func init() {
|
||||
ScenarioRegistry = make(map[string]*Scenario)
|
||||
logger := log.New("tsdb.testdata")
|
||||
|
||||
logger.Debug("Initializing TestData Scenario")
|
||||
|
||||
registerScenario(&Scenario{
|
||||
Id: "random_walk",
|
||||
Name: "Random Walk",
|
||||
|
||||
Handler: func(query *tsdb.Query, context *tsdb.QueryContext) *tsdb.QueryResult {
|
||||
timeWalkerMs := context.TimeRange.GetFromAsMsEpoch()
|
||||
to := context.TimeRange.GetToAsMsEpoch()
|
||||
|
||||
series := newSeriesForQuery(query)
|
||||
|
||||
points := make(tsdb.TimeSeriesPoints, 0)
|
||||
walker := rand.Float64() * 100
|
||||
|
||||
for i := int64(0); i < 10000 && timeWalkerMs < to; i++ {
|
||||
points = append(points, tsdb.NewTimePoint(walker, float64(timeWalkerMs)))
|
||||
|
||||
walker += rand.Float64() - 0.5
|
||||
timeWalkerMs += query.IntervalMs
|
||||
}
|
||||
|
||||
series.Points = points
|
||||
|
||||
queryRes := tsdb.NewQueryResult()
|
||||
queryRes.Series = append(queryRes.Series, series)
|
||||
return queryRes
|
||||
},
|
||||
})
|
||||
|
||||
registerScenario(&Scenario{
|
||||
Id: "no_data_points",
|
||||
Name: "No Data Points",
|
||||
Handler: func(query *tsdb.Query, context *tsdb.QueryContext) *tsdb.QueryResult {
|
||||
return tsdb.NewQueryResult()
|
||||
},
|
||||
})
|
||||
|
||||
registerScenario(&Scenario{
|
||||
Id: "datapoints_outside_range",
|
||||
Name: "Datapoints Outside Range",
|
||||
Handler: func(query *tsdb.Query, context *tsdb.QueryContext) *tsdb.QueryResult {
|
||||
queryRes := tsdb.NewQueryResult()
|
||||
|
||||
series := newSeriesForQuery(query)
|
||||
outsideTime := context.TimeRange.MustGetFrom().Add(-1*time.Hour).Unix() * 1000
|
||||
|
||||
series.Points = append(series.Points, tsdb.NewTimePoint(10, float64(outsideTime)))
|
||||
queryRes.Series = append(queryRes.Series, series)
|
||||
|
||||
return queryRes
|
||||
},
|
||||
})
|
||||
|
||||
registerScenario(&Scenario{
|
||||
Id: "csv_metric_values",
|
||||
Name: "CSV Metric Values",
|
||||
StringInput: "1,20,90,30,5,0",
|
||||
Handler: func(query *tsdb.Query, context *tsdb.QueryContext) *tsdb.QueryResult {
|
||||
queryRes := tsdb.NewQueryResult()
|
||||
|
||||
stringInput := query.Model.Get("stringInput").MustString()
|
||||
values := []float64{}
|
||||
for _, strVal := range strings.Split(stringInput, ",") {
|
||||
if val, err := strconv.ParseFloat(strVal, 64); err == nil {
|
||||
values = append(values, val)
|
||||
}
|
||||
}
|
||||
|
||||
if len(values) == 0 {
|
||||
return queryRes
|
||||
}
|
||||
|
||||
series := newSeriesForQuery(query)
|
||||
startTime := context.TimeRange.GetFromAsMsEpoch()
|
||||
endTime := context.TimeRange.GetToAsMsEpoch()
|
||||
step := (endTime - startTime) / int64(len(values)-1)
|
||||
|
||||
for _, val := range values {
|
||||
series.Points = append(series.Points, tsdb.NewTimePoint(val, float64(startTime)))
|
||||
startTime += step
|
||||
}
|
||||
|
||||
queryRes.Series = append(queryRes.Series, series)
|
||||
|
||||
return queryRes
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func registerScenario(scenario *Scenario) {
|
||||
ScenarioRegistry[scenario.Id] = scenario
|
||||
}
|
||||
|
||||
func newSeriesForQuery(query *tsdb.Query) *tsdb.TimeSeries {
|
||||
alias := query.Model.Get("alias").MustString("")
|
||||
if alias == "" {
|
||||
alias = query.RefId + "-series"
|
||||
}
|
||||
|
||||
return &tsdb.TimeSeries{Name: alias}
|
||||
}
|
39
pkg/tsdb/testdata/testdata.go
vendored
Normal file
39
pkg/tsdb/testdata/testdata.go
vendored
Normal file
@ -0,0 +1,39 @@
|
||||
package testdata
|
||||
|
||||
import (
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
"github.com/grafana/grafana/pkg/tsdb"
|
||||
)
|
||||
|
||||
type TestDataExecutor struct {
|
||||
*tsdb.DataSourceInfo
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
func NewTestDataExecutor(dsInfo *tsdb.DataSourceInfo) tsdb.Executor {
|
||||
return &TestDataExecutor{
|
||||
DataSourceInfo: dsInfo,
|
||||
log: log.New("tsdb.testdata"),
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
tsdb.RegisterExecutor("grafana-testdata-datasource", NewTestDataExecutor)
|
||||
}
|
||||
|
||||
func (e *TestDataExecutor) Execute(queries tsdb.QuerySlice, context *tsdb.QueryContext) *tsdb.BatchResult {
|
||||
result := &tsdb.BatchResult{}
|
||||
result.QueryResults = make(map[string]*tsdb.QueryResult)
|
||||
|
||||
for _, query := range queries {
|
||||
scenarioId := query.Model.Get("scenarioId").MustString("random_walk")
|
||||
if scenario, exist := ScenarioRegistry[scenarioId]; exist {
|
||||
result.QueryResults[query.RefId] = scenario.Handler(query, context)
|
||||
result.QueryResults[query.RefId].RefId = query.RefId
|
||||
} else {
|
||||
e.log.Error("Scenario not found", "scenarioId", scenarioId)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
90
pkg/tsdb/time_range.go
Normal file
90
pkg/tsdb/time_range.go
Normal file
@ -0,0 +1,90 @@
|
||||
package tsdb
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func NewTimeRange(from, to string) *TimeRange {
|
||||
return &TimeRange{
|
||||
From: from,
|
||||
To: to,
|
||||
Now: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
type TimeRange struct {
|
||||
From string
|
||||
To string
|
||||
Now time.Time
|
||||
}
|
||||
|
||||
func (tr *TimeRange) GetFromAsMsEpoch() int64 {
|
||||
return tr.MustGetFrom().UnixNano() / int64(time.Millisecond)
|
||||
}
|
||||
|
||||
func (tr *TimeRange) GetToAsMsEpoch() int64 {
|
||||
return tr.MustGetTo().UnixNano() / int64(time.Millisecond)
|
||||
}
|
||||
|
||||
func (tr *TimeRange) MustGetFrom() time.Time {
|
||||
if res, err := tr.ParseFrom(); err != nil {
|
||||
return time.Unix(0, 0)
|
||||
} else {
|
||||
return res
|
||||
}
|
||||
}
|
||||
|
||||
func (tr *TimeRange) MustGetTo() time.Time {
|
||||
if res, err := tr.ParseTo(); err != nil {
|
||||
return time.Unix(0, 0)
|
||||
} else {
|
||||
return res
|
||||
}
|
||||
}
|
||||
|
||||
func tryParseUnixMsEpoch(val string) (time.Time, bool) {
|
||||
if val, err := strconv.ParseInt(val, 10, 64); err == nil {
|
||||
seconds := val / 1000
|
||||
nano := (val - seconds*1000) * 1000000
|
||||
return time.Unix(seconds, nano), true
|
||||
}
|
||||
return time.Time{}, false
|
||||
}
|
||||
|
||||
func (tr *TimeRange) ParseFrom() (time.Time, error) {
|
||||
if res, ok := tryParseUnixMsEpoch(tr.From); ok {
|
||||
return res, nil
|
||||
}
|
||||
|
||||
fromRaw := strings.Replace(tr.From, "now-", "", 1)
|
||||
diff, err := time.ParseDuration("-" + fromRaw)
|
||||
if err != nil {
|
||||
return time.Time{}, err
|
||||
}
|
||||
|
||||
return tr.Now.Add(diff), nil
|
||||
}
|
||||
|
||||
func (tr *TimeRange) ParseTo() (time.Time, error) {
|
||||
if tr.To == "now" {
|
||||
return tr.Now, nil
|
||||
} else if strings.HasPrefix(tr.To, "now-") {
|
||||
withoutNow := strings.Replace(tr.To, "now-", "", 1)
|
||||
|
||||
diff, err := time.ParseDuration("-" + withoutNow)
|
||||
if err != nil {
|
||||
return time.Time{}, nil
|
||||
}
|
||||
|
||||
return tr.Now.Add(diff), nil
|
||||
}
|
||||
|
||||
if res, ok := tryParseUnixMsEpoch(tr.To); ok {
|
||||
return res, nil
|
||||
}
|
||||
|
||||
return time.Time{}, fmt.Errorf("cannot parse to value %s", tr.To)
|
||||
}
|
95
pkg/tsdb/time_range_test.go
Normal file
95
pkg/tsdb/time_range_test.go
Normal file
@ -0,0 +1,95 @@
|
||||
package tsdb
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestTimeRange(t *testing.T) {
|
||||
Convey("Time range", t, func() {
|
||||
|
||||
now := time.Now()
|
||||
|
||||
Convey("Can parse 5m, now", func() {
|
||||
tr := TimeRange{
|
||||
From: "5m",
|
||||
To: "now",
|
||||
Now: now,
|
||||
}
|
||||
|
||||
Convey("5m ago ", func() {
|
||||
fiveMinAgo, _ := time.ParseDuration("-5m")
|
||||
expected := now.Add(fiveMinAgo)
|
||||
|
||||
res, err := tr.ParseFrom()
|
||||
So(err, ShouldBeNil)
|
||||
So(res.Unix(), ShouldEqual, expected.Unix())
|
||||
})
|
||||
|
||||
Convey("now ", func() {
|
||||
res, err := tr.ParseTo()
|
||||
So(err, ShouldBeNil)
|
||||
So(res.Unix(), ShouldEqual, now.Unix())
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Can parse 5h, now-10m", func() {
|
||||
tr := TimeRange{
|
||||
From: "5h",
|
||||
To: "now-10m",
|
||||
Now: now,
|
||||
}
|
||||
|
||||
Convey("5h ago ", func() {
|
||||
fiveHourAgo, _ := time.ParseDuration("-5h")
|
||||
expected := now.Add(fiveHourAgo)
|
||||
|
||||
res, err := tr.ParseFrom()
|
||||
So(err, ShouldBeNil)
|
||||
So(res.Unix(), ShouldEqual, expected.Unix())
|
||||
})
|
||||
|
||||
Convey("now-10m ", func() {
|
||||
fiveMinAgo, _ := time.ParseDuration("-10m")
|
||||
expected := now.Add(fiveMinAgo)
|
||||
res, err := tr.ParseTo()
|
||||
So(err, ShouldBeNil)
|
||||
So(res.Unix(), ShouldEqual, expected.Unix())
|
||||
})
|
||||
})
|
||||
|
||||
Convey("can parse unix epocs", func() {
|
||||
var err error
|
||||
tr := TimeRange{
|
||||
From: "1474973725473",
|
||||
To: "1474975757930",
|
||||
Now: now,
|
||||
}
|
||||
|
||||
res, err := tr.ParseFrom()
|
||||
So(err, ShouldBeNil)
|
||||
So(res.UnixNano()/int64(time.Millisecond), ShouldEqual, 1474973725473)
|
||||
|
||||
res, err = tr.ParseTo()
|
||||
So(err, ShouldBeNil)
|
||||
So(res.UnixNano()/int64(time.Millisecond), ShouldEqual, 1474975757930)
|
||||
})
|
||||
|
||||
Convey("Cannot parse asdf", func() {
|
||||
var err error
|
||||
tr := TimeRange{
|
||||
From: "asdf",
|
||||
To: "asdf",
|
||||
Now: now,
|
||||
}
|
||||
|
||||
_, err = tr.ParseFrom()
|
||||
So(err, ShouldNotBeNil)
|
||||
|
||||
_, err = tr.ParseTo()
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
})
|
||||
}
|
@ -14,9 +14,9 @@ func TestMetricQuery(t *testing.T) {
|
||||
Convey("Given 3 queries for 2 data sources", func() {
|
||||
request := &Request{
|
||||
Queries: QuerySlice{
|
||||
{RefId: "A", Query: "asd", DataSource: &DataSourceInfo{Id: 1}},
|
||||
{RefId: "B", Query: "asd", DataSource: &DataSourceInfo{Id: 1}},
|
||||
{RefId: "C", Query: "asd", DataSource: &DataSourceInfo{Id: 2}},
|
||||
{RefId: "A", DataSource: &DataSourceInfo{Id: 1}},
|
||||
{RefId: "B", DataSource: &DataSourceInfo{Id: 1}},
|
||||
{RefId: "C", DataSource: &DataSourceInfo{Id: 2}},
|
||||
},
|
||||
}
|
||||
|
||||
@ -31,9 +31,9 @@ func TestMetricQuery(t *testing.T) {
|
||||
Convey("Given query 2 depends on query 1", func() {
|
||||
request := &Request{
|
||||
Queries: QuerySlice{
|
||||
{RefId: "A", Query: "asd", DataSource: &DataSourceInfo{Id: 1}},
|
||||
{RefId: "B", Query: "asd", DataSource: &DataSourceInfo{Id: 2}},
|
||||
{RefId: "C", Query: "#A / #B", DataSource: &DataSourceInfo{Id: 3}, Depends: []string{"A", "B"}},
|
||||
{RefId: "A", DataSource: &DataSourceInfo{Id: 1}},
|
||||
{RefId: "B", DataSource: &DataSourceInfo{Id: 2}},
|
||||
{RefId: "C", DataSource: &DataSourceInfo{Id: 3}, Depends: []string{"A", "B"}},
|
||||
},
|
||||
}
|
||||
|
||||
@ -55,7 +55,7 @@ func TestMetricQuery(t *testing.T) {
|
||||
Convey("When executing request with one query", t, func() {
|
||||
req := &Request{
|
||||
Queries: QuerySlice{
|
||||
{RefId: "A", Query: "asd", DataSource: &DataSourceInfo{Id: 1, PluginId: "test"}},
|
||||
{RefId: "A", DataSource: &DataSourceInfo{Id: 1, PluginId: "test"}},
|
||||
},
|
||||
}
|
||||
|
||||
@ -74,8 +74,8 @@ func TestMetricQuery(t *testing.T) {
|
||||
Convey("When executing one request with two queries from same data source", t, func() {
|
||||
req := &Request{
|
||||
Queries: QuerySlice{
|
||||
{RefId: "A", Query: "asd", DataSource: &DataSourceInfo{Id: 1, PluginId: "test"}},
|
||||
{RefId: "B", Query: "asd", DataSource: &DataSourceInfo{Id: 1, PluginId: "test"}},
|
||||
{RefId: "A", DataSource: &DataSourceInfo{Id: 1, PluginId: "test"}},
|
||||
{RefId: "B", DataSource: &DataSourceInfo{Id: 1, PluginId: "test"}},
|
||||
},
|
||||
}
|
||||
|
||||
@ -100,9 +100,9 @@ func TestMetricQuery(t *testing.T) {
|
||||
Convey("When executing one request with three queries from different datasources", t, func() {
|
||||
req := &Request{
|
||||
Queries: QuerySlice{
|
||||
{RefId: "A", Query: "asd", DataSource: &DataSourceInfo{Id: 1, PluginId: "test"}},
|
||||
{RefId: "B", Query: "asd", DataSource: &DataSourceInfo{Id: 1, PluginId: "test"}},
|
||||
{RefId: "C", Query: "asd", DataSource: &DataSourceInfo{Id: 2, PluginId: "test"}},
|
||||
{RefId: "A", DataSource: &DataSourceInfo{Id: 1, PluginId: "test"}},
|
||||
{RefId: "B", DataSource: &DataSourceInfo{Id: 1, PluginId: "test"}},
|
||||
{RefId: "C", DataSource: &DataSourceInfo{Id: 2, PluginId: "test"}},
|
||||
},
|
||||
}
|
||||
|
||||
@ -117,7 +117,7 @@ func TestMetricQuery(t *testing.T) {
|
||||
Convey("When query uses data source of unknown type", t, func() {
|
||||
req := &Request{
|
||||
Queries: QuerySlice{
|
||||
{RefId: "A", Query: "asd", DataSource: &DataSourceInfo{Id: 1, PluginId: "asdasdas"}},
|
||||
{RefId: "A", DataSource: &DataSourceInfo{Id: 1, PluginId: "asdasdas"}},
|
||||
},
|
||||
}
|
||||
|
||||
@ -129,10 +129,10 @@ func TestMetricQuery(t *testing.T) {
|
||||
req := &Request{
|
||||
Queries: QuerySlice{
|
||||
{
|
||||
RefId: "A", Query: "asd", DataSource: &DataSourceInfo{Id: 1, PluginId: "test"},
|
||||
RefId: "A", DataSource: &DataSourceInfo{Id: 1, PluginId: "test"},
|
||||
},
|
||||
{
|
||||
RefId: "B", Query: "#A / 2", DataSource: &DataSourceInfo{Id: 2, PluginId: "test"}, Depends: []string{"A"},
|
||||
RefId: "B", DataSource: &DataSourceInfo{Id: 2, PluginId: "test"}, Depends: []string{"A"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -1,11 +1,18 @@
|
||||
define([
|
||||
'angular',
|
||||
'lodash',
|
||||
'../core_module',
|
||||
'app/core/config',
|
||||
],
|
||||
function (angular, coreModule, config) {
|
||||
function (angular, _, coreModule, config) {
|
||||
'use strict';
|
||||
|
||||
var failCodes = {
|
||||
"1000": "Required team membership not fulfilled",
|
||||
"1001": "Required organization membership not fulfilled",
|
||||
"1002": "Required email domain not fulfilled",
|
||||
};
|
||||
|
||||
coreModule.default.controller('LoginCtrl', function($scope, backendSrv, contextSrv, $location) {
|
||||
$scope.formModel = {
|
||||
user: '',
|
||||
@ -15,12 +22,10 @@ function (angular, coreModule, config) {
|
||||
|
||||
contextSrv.sidemenu = false;
|
||||
|
||||
$scope.googleAuthEnabled = config.googleAuthEnabled;
|
||||
$scope.githubAuthEnabled = config.githubAuthEnabled;
|
||||
$scope.oauthEnabled = config.githubAuthEnabled || config.googleAuthEnabled || config.genericOAuthEnabled;
|
||||
$scope.allowUserPassLogin = config.allowUserPassLogin;
|
||||
$scope.genericOAuthEnabled = config.genericOAuthEnabled;
|
||||
$scope.oauthProviderName = config.oauthProviderName;
|
||||
$scope.oauth = config.oauth;
|
||||
$scope.oauthEnabled = _.keys(config.oauth).length > 0;
|
||||
|
||||
$scope.disableLoginForm = config.disableLoginForm;
|
||||
$scope.disableUserSignUp = config.disableUserSignUp;
|
||||
$scope.loginHint = config.loginHint;
|
||||
|
||||
@ -31,8 +36,8 @@ function (angular, coreModule, config) {
|
||||
$scope.$watch("loginMode", $scope.loginModeChanged);
|
||||
|
||||
var params = $location.search();
|
||||
if (params.failedMsg) {
|
||||
$scope.appEvent('alert-warning', ['Login Failed', params.failedMsg]);
|
||||
if (params.failCode) {
|
||||
$scope.appEvent('alert-warning', ['Login Failed', failCodes[params.failCode]]);
|
||||
delete params.failedMsg;
|
||||
$location.search(params);
|
||||
}
|
||||
|
@ -41,6 +41,7 @@ import 'app/core/routes/routes';
|
||||
import './filters/filters';
|
||||
import coreModule from './core_module';
|
||||
import appEvents from './app_events';
|
||||
import colors from './utils/colors';
|
||||
|
||||
|
||||
export {
|
||||
@ -60,4 +61,5 @@ export {
|
||||
dashboardSelector,
|
||||
queryPartEditorDirective,
|
||||
WizardFlow,
|
||||
colors,
|
||||
};
|
||||
|
@ -23,10 +23,10 @@ function (_, $, coreModule) {
|
||||
getOptions: "&",
|
||||
onChange: "&",
|
||||
},
|
||||
link: function($scope, elem, attrs) {
|
||||
link: function($scope, elem) {
|
||||
var $input = $(inputTemplate);
|
||||
var $button = $(attrs.styleMode === 'select' ? selectTemplate : linkTemplate);
|
||||
var segment = $scope.segment;
|
||||
var $button = $(segment.selectMode ? selectTemplate : linkTemplate);
|
||||
var options = null;
|
||||
var cancelBlur = null;
|
||||
var linkMode = true;
|
||||
@ -136,7 +136,7 @@ function (_, $, coreModule) {
|
||||
|
||||
$button.click(function() {
|
||||
options = null;
|
||||
$input.css('width', ($button.width() + 16) + 'px');
|
||||
$input.css('width', (Math.max($button.width(), 80) + 16) + 'px');
|
||||
|
||||
$button.hide();
|
||||
$input.show();
|
||||
@ -170,6 +170,7 @@ function (_, $, coreModule) {
|
||||
},
|
||||
link: {
|
||||
pre: function postLink($scope, elem, attrs) {
|
||||
var cachedOptions;
|
||||
|
||||
$scope.valueToSegment = function(value) {
|
||||
var option = _.find($scope.options, {value: value});
|
||||
@ -177,7 +178,9 @@ function (_, $, coreModule) {
|
||||
cssClass: attrs.cssClass,
|
||||
custom: attrs.custom,
|
||||
value: option ? option.text : value,
|
||||
selectMode: attrs.selectMode,
|
||||
};
|
||||
|
||||
return uiSegmentSrv.newSegment(segment);
|
||||
};
|
||||
|
||||
@ -188,13 +191,20 @@ function (_, $, coreModule) {
|
||||
});
|
||||
return $q.when(optionSegments);
|
||||
} else {
|
||||
return $scope.getOptions();
|
||||
return $scope.getOptions().then(function(options) {
|
||||
cachedOptions = options;
|
||||
return _.map(options, function(option) {
|
||||
return uiSegmentSrv.newSegment({value: option.text});
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.onSegmentChange = function() {
|
||||
if ($scope.options) {
|
||||
var option = _.find($scope.options, {text: $scope.segment.value});
|
||||
var options = $scope.options || cachedOptions;
|
||||
|
||||
if (options) {
|
||||
var option = _.find(options, {text: $scope.segment.value});
|
||||
if (option && option.value !== $scope.property) {
|
||||
$scope.property = option.value;
|
||||
} else if (attrs.custom !== 'false') {
|
||||
|
@ -236,7 +236,7 @@ function (angular, _, coreModule) {
|
||||
var inputEl = elem.find('input');
|
||||
|
||||
function openDropdown() {
|
||||
inputEl.css('width', Math.max(linkEl.width(), 30) + 'px');
|
||||
inputEl.css('width', Math.max(linkEl.width(), 80) + 'px');
|
||||
|
||||
inputEl.show();
|
||||
linkEl.hide();
|
||||
|
@ -114,6 +114,10 @@ export class BackendSrv {
|
||||
var requestIsLocal = options.url.indexOf('/') === 0;
|
||||
var firstAttempt = options.retry === 0;
|
||||
|
||||
if (requestIsLocal && !options.hasSubUrl && options.retry === 0) {
|
||||
options.url = config.appSubUrl + options.url;
|
||||
}
|
||||
|
||||
if (requestIsLocal && options.headers && options.headers.Authorization) {
|
||||
options.headers['X-DS-Authorization'] = options.headers.Authorization;
|
||||
delete options.headers.Authorization;
|
||||
|
@ -9,6 +9,7 @@ export class User {
|
||||
isGrafanaAdmin: any;
|
||||
isSignedIn: any;
|
||||
orgRole: any;
|
||||
timezone: string;
|
||||
|
||||
constructor() {
|
||||
if (config.bootData.user) {
|
||||
|
@ -28,6 +28,7 @@ function (angular, _, coreModule) {
|
||||
this.type = options.type;
|
||||
this.fake = options.fake;
|
||||
this.value = options.value;
|
||||
this.selectMode = options.selectMode;
|
||||
this.type = options.type;
|
||||
this.expandable = options.expandable;
|
||||
this.html = options.html || $sce.trustAsHtml(templateSrv.highlightVariablesAsHtml(this.value));
|
||||
|
@ -31,6 +31,8 @@ export default class TimeSeries {
|
||||
allIsZero: boolean;
|
||||
decimals: number;
|
||||
scaledDecimals: number;
|
||||
hasMsResolution: boolean;
|
||||
isOutsideRange: boolean;
|
||||
|
||||
lines: any;
|
||||
bars: any;
|
||||
@ -54,6 +56,7 @@ export default class TimeSeries {
|
||||
this.stats = {};
|
||||
this.legend = true;
|
||||
this.unit = opts.unit;
|
||||
this.hasMsResolution = this.isMsResolutionNeeded();
|
||||
}
|
||||
|
||||
applySeriesOverrides(overrides) {
|
||||
|
12
public/app/core/utils/colors.ts
Normal file
12
public/app/core/utils/colors.ts
Normal file
@ -0,0 +1,12 @@
|
||||
|
||||
|
||||
export default [
|
||||
"#7EB26D","#EAB839","#6ED0E0","#EF843C","#E24D42","#1F78C1","#BA43A9","#705DA0",
|
||||
"#508642","#CCA300","#447EBC","#C15C17","#890F02","#0A437C","#6D1F62","#584477",
|
||||
"#B7DBAB","#F4D598","#70DBED","#F9BA8F","#F29191","#82B5D8","#E5A8E2","#AEA2E0",
|
||||
"#629E51","#E5AC0E","#64B0C8","#E0752D","#BF1B00","#0A50A1","#962D82","#614D93",
|
||||
"#9AC48A","#F2C96D","#65C5DB","#F9934E","#EA6460","#5195CE","#D683CE","#806EB7",
|
||||
"#3F6833","#967302","#2F575E","#99440A","#58140C","#052B51","#511749","#3F2B5B",
|
||||
"#E0F9D7","#FCEACA","#CFFAFF","#F9E2D2","#FCE2DE","#BADFF4","#F9D9F9","#DEDAF7"
|
||||
];
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user