Merge remote-tracking branch 'upstream/master'

Conflicts:
	public/app/plugins/datasource/opentsdb/datasource.js
This commit is contained in:
Mike Kobyakov 2015-07-10 15:55:45 -07:00
commit 76c18e50a4
496 changed files with 50012 additions and 9311 deletions

3
.bowerrc Normal file
View File

@ -0,0 +1,3 @@
{
"directory": "public/vendor/"
}

1
.gitignore vendored
View File

@ -26,4 +26,5 @@ public/css/*.min.css
conf/custom.ini conf/custom.ini
fig.yml fig.yml
profile.cov

View File

@ -1,3 +1,52 @@
# 2.1.0 (unreleased - master branch)
**Data sources**
- [Issue #1525](https://github.com/grafana/grafana/issues/1525). InfluxDB: Full support for InfluxDB 0.9 with new adapted query editor
- [Issue #2191](https://github.com/grafana/grafana/issues/2191). KariosDB: Grafana now ships with a KariosDB data source plugin, thx @masaori335
- [Issue #1177](https://github.com/grafana/grafana/issues/1177). OpenTSDB: Limit tags by metric, OpenTSDB config option tsd.core.meta.enable_realtime_ts must enabled for OpenTSDB lookup api
- [Issue #1250](https://github.com/grafana/grafana/issues/1250). OpenTSDB: Support for template variable values lookup queries
**New dashboard features**
- [Issue #1144](https://github.com/grafana/grafana/issues/1144). Templating: You can now select multiple template variables values at the same time.
- [Issue #1922](https://github.com/grafana/grafana/issues/1922). Templating: Specify multiple variable values via URL params.
- [Issue #1888](https://github.com/grafana/grafana/issues/1144). Templating: Repeat panel or row for each selected template variable value
- [Issue #1888](https://github.com/grafana/grafana/issues/1944). Dashboard: Custom Navigation links & dynamic links to related dashboards
- [Issue #590](https://github.com/grafana/grafana/issues/590). Graph: Define series color using regex rule
- [Issue #2162](https://github.com/grafana/grafana/issues/2162). Graph: New series style override, negative-y transform and stack groups
- [Issue #2096](https://github.com/grafana/grafana/issues/2096). Dashboard list panel: Now supports search by multiple tags
- [Issue #2203](https://github.com/grafana/grafana/issues/2203). Singlestat: Now support string values
**User or Organization admin**
- [Issue #1899](https://github.com/grafana/grafana/issues/1899). Organization: You can now update the organization user role directly (without removing and readding the organization user).
- [Issue #2088](https://github.com/grafana/grafana/issues/2088). Roles: New user role `Read Only Editor` that replaces the old `Viewer` role behavior
**Backend**
- [Issue #2218](https://github.com/grafana/grafana/issues/2218). Auth: You can now authenicate against api with username / password using basic auth
- [Issue #2095](https://github.com/grafana/grafana/issues/2095). Search: Search now supports filtering by multiple dashboard tags
- [Issue #1905](https://github.com/grafana/grafana/issues/1905). Github OAuth: You can now configure a Github team membership requirement, thx @dewski
- [Issue #2052](https://github.com/grafana/grafana/issues/2052). Github OAuth: You can now configure a Github organization requirement, thx @indrekj
- [Issue #1891](https://github.com/grafana/grafana/issues/1891). Security: New config option to disable the use of gravatar for profile images
- [Issue #1921](https://github.com/grafana/grafana/issues/1921). Auth: Support for user authentication via reverse proxy header (like X-Authenticated-User, or X-WEBAUTH-USER)
- [Issue #960](https://github.com/grafana/grafana/issues/960). Search: Backend can now index a folder with json files, will be available in search (saving back to folder is not supported, this feature is meant for static generated json dashboards)
**Breaking changes**
- [Issue #1826](https://github.com/grafana/grafana/issues/1826). User role 'Viewer' are now prohibited from entering edit mode (and doing other transient dashboard edits). A new role `Read Only Editor` will replace the old Viewer behavior
- [Issue #1928](https://github.com/grafana/grafana/issues/1928). HTTP API: GET /api/dashboards/db/:slug response changed property `model` to `dashboard` to match the POST request nameing
- Backend render URL changed from `/render/dashboard/solo` `render/dashboard-solo/` (in order to have consistent dashboard url `/dashboard/:type/:slug`)
- Search HTTP API response has changed (simplified), tags list moved to seperate HTTP resource URI
- Datasource HTTP api breaking change, ADD datasource is now POST /api/datasources/, update is now PUT /api/datasources/:id
**Fixes**
- [Issue #2185](https://github.com/grafana/grafana/issues/2185). Graph: fixed PNG rendering of panels with legend table to the right
- [Issue #2163](https://github.com/grafana/grafana/issues/2163). Backend: Load dashboards with capital letters in the dashboard url slug (url id)
# 2.0.3 (unreleased - 2.0.x branch)
**Fixes**
- [Issue #1872](https://github.com/grafana/grafana/issues/1872). Firefox/IE issue, invisible text in dashboard search fixed
- [Issue #1857](https://github.com/grafana/grafana/issues/1857). /api/login/ping Fix for issue when behind reverse proxy and subpath
- [Issue #1863](https://github.com/grafana/grafana/issues/1863). MySQL: Dashboard.data column type changed to mediumtext (sql migration added)
# 2.0.2 (2015-04-22) # 2.0.2 (2015-04-22)
**Fixes** **Fixes**

14
Godeps/Godeps.json generated
View File

@ -29,7 +29,7 @@
}, },
{ {
"ImportPath": "github.com/gosimple/slug", "ImportPath": "github.com/gosimple/slug",
"Rev": "a2392a4a87fa0366cbff131d3fd421f83f52492f" "Rev": "8d258463b4459f161f51d6a357edacd3eef9d663"
}, },
{ {
"ImportPath": "github.com/jtolds/gls", "ImportPath": "github.com/jtolds/gls",
@ -52,6 +52,10 @@
"ImportPath": "github.com/mattn/go-sqlite3", "ImportPath": "github.com/mattn/go-sqlite3",
"Rev": "e28cd440fabdd39b9520344bc26829f61db40ece" "Rev": "e28cd440fabdd39b9520344bc26829f61db40ece"
}, },
{
"ImportPath": "github.com/rainycape/unidecode",
"Rev": "836ef0a715aedf08a12d595ed73ec8ed5b288cac"
},
{ {
"ImportPath": "github.com/smartystreets/goconvey/convey", "ImportPath": "github.com/smartystreets/goconvey/convey",
"Comment": "1.5.0-356-gfbc0a1c", "Comment": "1.5.0-356-gfbc0a1c",
@ -83,14 +87,6 @@
"ImportPath": "gopkg.in/redis.v2", "ImportPath": "gopkg.in/redis.v2",
"Comment": "v2.3.2", "Comment": "v2.3.2",
"Rev": "e6179049628164864e6e84e973cfb56335748dea" "Rev": "e6179049628164864e6e84e973cfb56335748dea"
},
{
"ImportPath": "gopkgs.com/pool.v1",
"Rev": "c850f092aad1780cbffff25f471c5cc32097932a"
},
{
"ImportPath": "gopkgs.com/unidecode.v1",
"Rev": "4deae2c05236b41cc39f8144ac87a837ba974d40"
} }
] ]
} }

View File

@ -6,9 +6,10 @@
package slug package slug
import ( import (
"gopkgs.com/unidecode.v1"
"regexp" "regexp"
"strings" "strings"
"github.com/rainycape/unidecode"
) )
var ( var (

View File

@ -0,0 +1,6 @@
unidecode
=========
Unicode transliterator in Golang - Replaces non-ASCII characters with their ASCII approximations.
[![GoDoc](https://godoc.org/github.com/rainycape/unidecode?status.svg)](https://godoc.org/github.com/rainycape/unidecode)

View File

@ -5,12 +5,9 @@ import (
"encoding/binary" "encoding/binary"
"io" "io"
"strings" "strings"
"sync"
) )
var ( var (
decoded = false
mutex sync.Mutex
transliterations [65536][]rune transliterations [65536][]rune
transCount = rune(len(transliterations)) transCount = rune(len(transliterations))
getUint16 = binary.LittleEndian.Uint16 getUint16 = binary.LittleEndian.Uint16

View File

@ -4,15 +4,15 @@
package unidecode package unidecode
import ( import (
"sync"
"unicode" "unicode"
"gopkgs.com/pool.v1"
) )
const pooledCapacity = 64 const pooledCapacity = 64
var ( var (
slicePool = pool.New(0) slicePool sync.Pool
decodingOnce sync.Once
) )
// Unidecode implements a unicode transliterator, which // Unidecode implements a unicode transliterator, which
@ -23,14 +23,7 @@ var (
// with their closest ASCII counterparts. // with their closest ASCII counterparts.
// e.g. Unicode("áéíóú") => "aeiou" // e.g. Unicode("áéíóú") => "aeiou"
func Unidecode(s string) string { func Unidecode(s string) string {
if !decoded { decodingOnce.Do(decodeTransliterations)
mutex.Lock()
if !decoded {
decodeTransliterations()
decoded = true
}
mutex.Unlock()
}
l := len(s) l := len(s)
var r []rune var r []rune
if l > pooledCapacity { if l > pooledCapacity {

View File

@ -1,13 +0,0 @@
pool
====
sync.Pool compatibility layer for for Go - falls back to a channel based pool in Go < 1.3
Please, use the following import path to ensure a stable API:
```go
import "gopkgs.com/pool.v1"
```
View other available versions, documentation and examples at http://gopkgs.com/pool

View File

@ -1,3 +0,0 @@
// Package pool provides a sync.Pool compatibility layer, which
// falls back to a channel based pool on Go < 1.3.
package pool

View File

@ -1,23 +0,0 @@
package pool_test
import (
"fmt"
"gopkgs.com/pool.v1"
)
func ExamplePool() {
p := pool.New(0)
p.Put("Hello")
fmt.Println(p.Get())
// OutPut: Hello
}
func ExamplePoolNew() {
p := pool.New(0)
p.New = func() interface{} {
return "World!"
}
fmt.Println(p.Get())
// OutPut: World!
}

View File

@ -1,24 +0,0 @@
package pool
import (
"fmt"
"reflect"
)
// gopkgs.go: v1
// NOTE: This file is autogenerated by gopkgs.com.
const (
goPkgsSrcPath = "github.com/rainycape/pool"
goPkgsName = "pool"
goPkgsErrFmt = "invalid import path %s - please use gopkgs.com/%s.v1 or see http://gopkgs.com/%s"
)
type goPkgsCheck struct{}
func init() {
typ := reflect.TypeOf(goPkgsCheck{})
if typ.PkgPath() == goPkgsSrcPath {
panic(fmt.Errorf(goPkgsErrFmt, typ.PkgPath(), goPkgsName, goPkgsName))
}
}

View File

@ -1,37 +0,0 @@
// +build go1.3,!appengine
package pool
import (
"sync"
)
// Pool is a thin compatibility type to allow Go
// libraries to use the new sync.Pool in Go 1.3,
// while remaining compatible with lower Go versions.
// For more information, see the sync.Pool type.
type Pool sync.Pool
// New returns a new Pool. The size argument is
// ignored on Go >= 1.3. In Go < 1.3, if size is
// zero, it's set to runtime.GOMAXPROCS(0) * 2.
func New(size int) *Pool {
return &Pool{}
}
// Get returns an arbitrary previously Put value, removing
// it from the pool, or nil if there are no such values. Note
// that callers should not assume anything about the Get return
// value, since the runtime might decide to collect the elements
// from the pool at any time.
//
// If there are no elements to return and the New() field is non-nil,
// Get returns the result of calling it.
func (p *Pool) Get() interface{} {
return (*sync.Pool)(p).Get()
}
// Put adds x to the pool.
func (p *Pool) Put(x interface{}) {
(*sync.Pool)(p).Put(x)
}

View File

@ -1,57 +0,0 @@
// +build !go1.3 appengine
package pool
import (
"runtime"
)
// Pool is a thin compatibility type to allow Go
// libraries to use the new sync.Pool in Go 1.3,
// while remaining compatible with lower Go versions.
// For more information, see the sync.Pool type.
type Pool struct {
ch chan interface{}
// New specifies a function to generate
// a new value, when Get would otherwise
// return nil.
New func() interface{}
}
// New returns a new Pool. The size argument is
// ignored on Go >= 1.3. In Go < 1.3, if size is
// zero, it's set to runtime.GOMAXPROCS(0) * 2.
func New(size int) *Pool {
if size == 0 {
size = runtime.GOMAXPROCS(0) * 2
}
return &Pool{ch: make(chan interface{}, size)}
}
// Get returns an arbitrary previously Put value, removing
// it from the pool, or nil if there are no such values. Note
// that callers should not assume anything about the Get return
// value, since the runtime might decide to collect the elements
// from the pool at any time.
//
// If there are no elements to return and the New() field is non-nil,
// Get returns the result of calling it.
func (p *Pool) Get() interface{} {
select {
case x := <-p.ch:
return x
default:
}
if p.New != nil {
return p.New()
}
return nil
}
// Put adds x to the pool.
func (p *Pool) Put(x interface{}) {
select {
case p.ch <- x:
default:
}
}

View File

@ -1,23 +0,0 @@
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a
*.so
# Folders
_obj
_test
# Architecture specific extensions/prefixes
*.[568vq]
[568vq].out
*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*
_testmain.go
*.exe
*.test

View File

@ -1,201 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright {yyyy} {name of copyright owner}
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -1,12 +0,0 @@
unidecode
=========
Unicode transliterator in Golang - Replaces non-ASCII characters with their ASCII approximations.
Please, use the following import path to ensure a stable API:
```go
import "gopkgs.com/unidecode.v1"
```
View other available versions, documentation and examples at http://gopkgs.com/unidecode

View File

@ -1,24 +0,0 @@
package unidecode
import (
"fmt"
"reflect"
)
// gopkgs.go: v1
// NOTE: This file is autogenerated by gopkgs.com.
const (
goPkgsSrcPath = "github.com/rainycape/unidecode"
goPkgsName = "unidecode"
goPkgsErrFmt = "invalid import path %s - please use gopkgs.com/%s.v1 or see http://gopkgs.com/%s"
)
type goPkgsCheck struct{}
func init() {
typ := reflect.TypeOf(goPkgsCheck{})
if typ.PkgPath() == goPkgsSrcPath {
panic(fmt.Errorf(goPkgsErrFmt, typ.PkgPath(), goPkgsName, goPkgsName))
}
}

View File

@ -87,7 +87,7 @@ go get github.com/grafana/grafana
``` ```
cd $GOPATH/src/github.com/grafana/grafana cd $GOPATH/src/github.com/grafana/grafana
go run build.go setup (only needed once to install godep) go run build.go setup (only needed once to install godep)
godep restore (will pull down all golang lib dependecies in your current GOPATH) godep restore (will pull down all golang lib dependencies in your current GOPATH)
go build . go build .
``` ```
@ -125,8 +125,9 @@ You only need to add the options you want to override. Config files are applied
2. dev.ini (if found) 2. dev.ini (if found)
3. custom.ini 3. custom.ini
## Create a pull requests ## Create a pull request
Before or after your create a pull requests, sign the [contributor license aggrement](/docs/contributing/cla.html).## Contribute Before or after you create a pull request, sign the [contributor license agreement](http://grafana.org/docs/contributing/cla.html).
## Contribute
If you have any idea for an improvement or found a bug do not hesitate to open an issue. If you have any idea for an improvement or found a bug do not hesitate to open an issue.
And if you have time clone this repo and submit a pull request and help me make Grafana And if you have time clone this repo and submit a pull request and help me make Grafana
the kickass metrics & devops dashboard we all dream about! the kickass metrics & devops dashboard we all dream about!

26
bower.json Normal file
View File

@ -0,0 +1,26 @@
{
"name": "grafana",
"version": "2.0.2",
"homepage": "https://github.com/grafana/grafana",
"authors": [],
"license": "Apache 2.0",
"ignore": [
"**/.*",
"node_modules",
"bower_components",
"public/vendor/",
"test",
"tests"
],
"dependencies": {
"jquery": "~2.1.4",
"angular": "~1.4.0",
"angular-route": "~1.4.0",
"angular-mocks": "~1.4.0",
"angular-sanitize": "~1.4.0",
"angular-native-dragdrop": "~1.1.0",
"angular-bindonce": "~0.3.3",
"requirejs": "~2.1.18",
"requirejs-text": "~2.0.14"
}
}

View File

@ -22,3 +22,4 @@ test:
- godep go test -v ./pkg/... - godep go test -v ./pkg/...
# js tests # js tests
- ./node_modules/grunt-cli/bin/grunt test - ./node_modules/grunt-cli/bin/grunt test
- npm run coveralls

View File

@ -7,7 +7,7 @@ app_mode = production
#################################### Paths #################################### #################################### Paths ####################################
[paths] [paths]
# Path to where grafana can store temp files, sessions, and the sqlite3 db (if that is useD) # Path to where grafana can store temp files, sessions, and the sqlite3 db (if that is used)
# #
data = data data = data
# #
@ -29,6 +29,10 @@ http_port = 3000
# The public facing domain name used to access grafana from a browser # The public facing domain name used to access grafana from a browser
domain = localhost domain = localhost
# Redirect to correct domain if host header does not match domain
# Prevents DNS rebinding attacks
enforce_domain = false
# The full public facing url # The full public facing url
root_url = %(protocol)s://%(domain)s:%(http_port)s/ root_url = %(protocol)s://%(domain)s:%(http_port)s/
@ -62,14 +66,16 @@ path = grafana.db
#################################### Session #################################### #################################### Session ####################################
[session] [session]
# Either "memory", "file", "redis", "mysql", default is "memory" # Either "memory", "file", "redis", "mysql", "postgresql", default is "file"
provider = file provider = file
# Provider config options # Provider config options
# memory: not have any config yet # memory: not have any config yet
# file: session dir path, is relative to grafana data_path # file: session dir path, is relative to grafana data_path
# redis: config like redis server addr, poolSize, password, e.g. `127.0.0.1:6379,100,grafana` # redis: config like redis server e.g. `addr=127.0.0.1:6379,pool_size=100,db=grafana`
# mysql: go-sql-driver/mysql dsn config string, e.g. `user:password@tcp(127.0.0.1)/database_name` # postgres: user=a password=b host=localhost port=5432 dbname=c sslmode=disable
# mysql: go-sql-driver/mysql dsn config string, e.g. `user:password@tcp(127.0.0.1:3306)/database_name`
provider_config = sessions provider_config = sessions
# Session cookie name # Session cookie name
@ -108,6 +114,9 @@ login_remember_days = 7
cookie_username = grafana_user cookie_username = grafana_user
cookie_remember_name = grafana_remember cookie_remember_name = grafana_remember
# disable gravatar profile images
disable_gravatar = false
#################################### Users #################################### #################################### Users ####################################
[users] [users]
# disable user signup / registration # disable user signup / registration
@ -136,18 +145,21 @@ org_role = Viewer
#################################### Github Auth ########################## #################################### Github Auth ##########################
[auth.github] [auth.github]
enabled = false enabled = false
allow_sign_up = false
client_id = some_id client_id = some_id
client_secret = some_secret client_secret = some_secret
scopes = user:email scopes = user:email
auth_url = https://github.com/login/oauth/authorize auth_url = https://github.com/login/oauth/authorize
token_url = https://github.com/login/oauth/access_token token_url = https://github.com/login/oauth/access_token
api_url = https://api.github.com/user api_url = https://api.github.com/user
team_ids =
allowed_domains = allowed_domains =
allow_sign_up = false allowed_organizations =
#################################### Google Auth ########################## #################################### Google Auth ##########################
[auth.google] [auth.google]
enabled = false enabled = false
allow_sign_up = false
client_id = some_client_id client_id = some_client_id
client_secret = some_client_secret client_secret = some_client_secret
scopes = https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email scopes = https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email
@ -155,7 +167,32 @@ auth_url = https://accounts.google.com/o/oauth2/auth
token_url = https://accounts.google.com/o/oauth2/token token_url = https://accounts.google.com/o/oauth2/token
api_url = https://www.googleapis.com/oauth2/v1/userinfo api_url = https://www.googleapis.com/oauth2/v1/userinfo
allowed_domains = allowed_domains =
allow_sign_up = false
#################################### Basic Auth ##########################
[auth.basic]
enabled = true
#################################### Auth Proxy ##########################
[auth.proxy]
enabled = false
header_name = X-WEBAUTH-USER
header_property = username
auto_sign_up = true
#################################### SMTP / Emailing ##########################
[smtp]
enabled = false
host = localhost:25
user =
password =
cert_file =
key_file =
skip_verify = false
from_address = admin@grafana.localhost
[emails]
welcome_email_on_sign_up = false
templates_pattern = emails/*.html
#################################### Logging ########################## #################################### Logging ##########################
[log] [log]
@ -196,3 +233,10 @@ max_days = 7
enabled = false enabled = false
rabbitmq_url = amqp://localhost/ rabbitmq_url = amqp://localhost/
exchange = grafana_events exchange = grafana_events
#################################### Dashboard JSON files ##########################
[dashboards.json]
enabled = false
path = /var/lib/grafana/dashboards

View File

@ -7,7 +7,7 @@
#################################### Paths #################################### #################################### Paths ####################################
[paths] [paths]
# Path to where grafana can store temp files, sessions, and the sqlite3 db (if that is useD) # Path to where grafana can store temp files, sessions, and the sqlite3 db (if that is used)
# #
;data = /var/lib/grafana ;data = /var/lib/grafana
# #
@ -29,6 +29,10 @@
# The public facing domain name used to access grafana from a browser # The public facing domain name used to access grafana from a browser
;domain = localhost ;domain = localhost
# Redirect to correct domain if host header does not match domain
# Prevents DNS rebinding attacks
;enforce_domain = false
# The full public facing url # The full public facing url
;root_url = %(protocol)s://%(domain)s:%(http_port)s/ ;root_url = %(protocol)s://%(domain)s:%(http_port)s/
@ -62,14 +66,15 @@
#################################### Session #################################### #################################### Session ####################################
[session] [session]
# Either "memory", "file", "redis", "mysql", default is "memory" # Either "memory", "file", "redis", "mysql", "postgresql", default is "file"
;provider = file ;provider = file
# Provider config options # Provider config options
# memory: not have any config yet # memory: not have any config yet
# file: session dir path, is relative to grafana data_path # file: session dir path, is relative to grafana data_path
# redis: config like redis server addr, poolSize, password, e.g. `127.0.0.1:6379,100,grafana` # redis: config like redis server e.g. `addr=127.0.0.1:6379,pool_size=100,db=grafana`
# mysql: go-sql-driver/mysql dsn config string, e.g. `user:password@tcp(127.0.0.1)/database_name` # mysql: go-sql-driver/mysql dsn config string, e.g. `user:password@tcp(127.0.0.1:3306)/database_name`
# postgres: user=a password=b host=localhost port=5432 dbname=c sslmode=disable
;provider_config = sessions ;provider_config = sessions
# Session cookie name # Session cookie name
@ -108,6 +113,9 @@
;cookie_username = grafana_user ;cookie_username = grafana_user
;cookie_remember_name = grafana_remember ;cookie_remember_name = grafana_remember
# disable gravatar profile images
;disable_gravatar = false
#################################### Users #################################### #################################### Users ####################################
[users] [users]
# disable user signup / registration # disable user signup / registration
@ -136,26 +144,53 @@
#################################### Github Auth ########################## #################################### Github Auth ##########################
[auth.github] [auth.github]
;enabled = false ;enabled = false
;allow_sign_up = false
;client_id = some_id ;client_id = some_id
;client_secret = some_secret ;client_secret = some_secret
;scopes = user:email ;scopes = user:email,read:org
;auth_url = https://github.com/login/oauth/authorize ;auth_url = https://github.com/login/oauth/authorize
;token_url = https://github.com/login/oauth/access_token ;token_url = https://github.com/login/oauth/access_token
;api_url = https://api.github.com/user ;api_url = https://api.github.com/user
# Uncomment bellow to only allow specific email domains ;team_ids =
; allowed_domains = mycompany.com othercompany.com ;allowed_domains =
;allowed_organizations =
#################################### Google Auth ########################## #################################### Google Auth ##########################
[auth.google] [auth.google]
;enabled = false ;enabled = false
;allow_sign_up = false
;client_id = some_client_id ;client_id = some_client_id
;client_secret = some_client_secret ;client_secret = some_client_secret
;scopes = https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email ;scopes = https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email
;auth_url = https://accounts.google.com/o/oauth2/auth ;auth_url = https://accounts.google.com/o/oauth2/auth
;token_url = https://accounts.google.com/o/oauth2/token ;token_url = https://accounts.google.com/o/oauth2/token
;api_url = https://www.googleapis.com/oauth2/v1/userinfo ;api_url = https://www.googleapis.com/oauth2/v1/userinfo
# Uncomment bellow to only allow specific email domains ;allowed_domains =
; allowed_domains = mycompany.com othercompany.com
#################################### Auth Proxy ##########################
[auth.proxy]
;enabled = false
;header_name = X-WEBAUTH-USER
;header_property = username
;auto_sign_up = true
#################################### Basic Auth ##########################
[auth.basic]
;enabled = true
#################################### SMTP / Emailing ##########################
[smtp]
;enabled = false
;host = localhost:25
;user =
;password =
;cert_file =
;key_file =
;skip_verify = false
;from_address = admin@grafana.localhost
[emails]
;welcome_email_on_sign_up = false
#################################### Logging ########################## #################################### Logging ##########################
[log] [log]
@ -196,3 +231,11 @@
;enabled = false ;enabled = false
;rabbitmq_url = amqp://localhost/ ;rabbitmq_url = amqp://localhost/
;exchange = grafana_events ;exchange = grafana_events
;#################################### Dashboard JSON files ##########################
[dashboards.json]
;enabled = false
;path = /var/lib/grafana/dashboards

View File

@ -0,0 +1,13 @@
FROM centos:centos7
MAINTAINER Przemyslaw Ozgo <linux@ozgo.info>
RUN \
yum update -y && \
yum install -y net-snmp net-snmp-utils && \
yum clean all
COPY bootstrap.sh /tmp/bootstrap.sh
EXPOSE 161
ENTRYPOINT ["/tmp/bootstrap.sh"]

27
docker/blocks/smtp/bootstrap.sh Executable file
View File

@ -0,0 +1,27 @@
#!/bin/sh
set -u
# User params
USER_PARAMS=$@
# Internal params
RUN_CMD="snmpd -f ${USER_PARAMS}"
#######################################
# Echo/log function
# Arguments:
# String: value to log
#######################################
log() {
if [[ "$@" ]]; then echo "[`date +'%Y-%m-%d %T'`] $@";
else echo; fi
}
# Launch
log $RUN_CMD
$RUN_CMD
# Exit immidiately in case of any errors or when we have interactive terminal
if [[ $? != 0 ]] || test -t 0; then exit $?; fi
log

4
docker/blocks/smtp/fig Normal file
View File

@ -0,0 +1,4 @@
snmpd:
build: blocks/snmpd
ports:
- "161:161"

View File

@ -1,6 +0,0 @@
FROM centos:latest
RUN yum install -y initscripts
ADD *.rpm /tmp/

View File

@ -44,7 +44,7 @@ docs-test: docs-build
$(DOCKER_RUN_DOCS) "$(DOCKER_DOCS_IMAGE)" ./test.sh $(DOCKER_RUN_DOCS) "$(DOCKER_DOCS_IMAGE)" ./test.sh
docs-build: docs-build:
git fetch https://github.com/grafana/grafana.git docs-2.0 && git diff --name-status FETCH_HEAD...HEAD -- . > changed-files git fetch https://github.com/grafana/grafana.git docs-1.x && git diff --name-status FETCH_HEAD...HEAD -- . > changed-files
echo "$(GIT_BRANCH)" > GIT_BRANCH echo "$(GIT_BRANCH)" > GIT_BRANCH
echo "$(GITCOMMIT)" > GITCOMMIT echo "$(GITCOMMIT)" > GITCOMMIT
docker build -t "$(DOCKER_DOCS_IMAGE)" . docker build -t "$(DOCKER_DOCS_IMAGE)" .

View File

@ -1 +1 @@
2.0.0-beta 2.1.0

View File

@ -45,7 +45,7 @@ pages:
- ['reference/graph.md', 'Reference', 'Graph Panel'] - ['reference/graph.md', 'Reference', 'Graph Panel']
- ['reference/singlestat.md', 'Reference', 'Singlestat Panel'] - ['reference/singlestat.md', 'Reference', 'Singlestat Panel']
- ['reference/dashlist.md', 'Reference', 'Dashlist Panel'] - ['reference/dashlist.md', 'Reference', 'Dashboard list Panel']
- ['reference/sharing.md', 'Reference', 'Sharing'] - ['reference/sharing.md', 'Reference', 'Sharing']
- ['reference/annotations.md', 'Reference', 'Annotations'] - ['reference/annotations.md', 'Reference', 'Annotations']
- ['reference/timerange.md', 'Reference', 'Time range controls'] - ['reference/timerange.md', 'Reference', 'Time range controls']
@ -60,8 +60,9 @@ pages:
- ['datasources/graphite.md', 'Data Sources', 'Graphite'] - ['datasources/graphite.md', 'Data Sources', 'Graphite']
- ['datasources/influxdb.md', 'Data Sources', 'InfluxDB'] - ['datasources/influxdb.md', 'Data Sources', 'InfluxDB']
- ['datasources/opentsdb.md', 'Data Sources', 'OpenTSDB'] - ['datasources/opentsdb.md', 'Data Sources', 'OpenTSDB']
- ['datasources/kairosdb.md', 'Data Sources', 'KairosDB']
- ['project/building_from_source.md', 'Project', 'Building from souce'] - ['project/building_from_source.md', 'Project', 'Building from source']
- ['project/cla.md', 'Project', 'Contributor License Agreement'] - ['project/cla.md', 'Project', 'Contributor License Agreement']
- ['jsearch.md', '**HIDDEN**'] - ['jsearch.md', '**HIDDEN**']

View File

@ -6,9 +6,9 @@ page_keywords: grafana, graphite, metrics, query, documentation
# Graphite # Graphite
Grafana has an advanced graphite query editor that lets you quickly navigate the metric space, add functions. Grafana has an advanced Graphite query editor that lets you quickly navigate the metric space, add functions,
Change function paramaters and much more. The editor cannot handle all types of queries yet. change function parameters and much more. The editor can handle all types of graphite queries. It can even handle complex nested
To switch to a regular text box click the pen icon to the right. queries through the use of query references.
## Adding the data source to Grafana ## Adding the data source to Grafana
Open the side menu by clicking the the Grafana icon in the top header. In the side menu under the `Dashboards` link you Open the side menu by clicking the the Grafana icon in the top header. In the side menu under the `Dashboards` link you
@ -52,8 +52,21 @@ Some functions like aliasByNode support an optional second argument. To add this
## Point consolidation ## Point consolidation
All graphite metrics are consolidated so that graphite doesn't return more data points than there are pixels in the graph. By default All Graphite metrics are consolidated so that Graphite doesn't return more data points than there are pixels in the graph. By default
this consolidation is done using `avg` function. You can how graphite consolidates metrics by adding the Graphite consolidateBy function. this consolidation is done using `avg` function. You can how Graphite consolidates metrics by adding the Graphite consolidateBy function.
> *Notice* This means that legend summary values (max, min, total) cannot be all correct at the same time. They are calculated > *Notice* This means that legend summary values (max, min, total) cannot be all correct at the same time. They are calculated
> client side by Grafana. And depending on your consolidation function only one or two can be correct at the same time. > client side by Grafana. And depending on your consolidation function only one or two can be correct at the same time.
## Templating
You can create a template variable in Grafana and have that variable filled with values from any Graphite metric exploration query.
You can then use this variable in your Graphite queries, either as part of a metric path or as arguments to functions.
For example a query like `prod.servers.*` will fill the variable with all possible
values that exists in the wildcard position.
You can also create nested variables that use other variables in their definition. For example
`apps.$app.servers.*` uses the variable `$app` in its query definition.
![](/img/v2/templated_variable_parameter.png)

View File

@ -4,10 +4,11 @@ page_description: InfluxDB query guide
page_keywords: grafana, influxdb, metrics, query, documentation page_keywords: grafana, influxdb, metrics, query, documentation
--- ---
# InfluxDB # 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. InfluxDB 0.9.x data source support is provided on an experimental basis. 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.
## Adding the data source to Grafana ## Adding the data source to Grafana
Open the side menu by clicking the the Grafana icon in the top header. In the side menu under the `Dashboards` link you Open the side menu by clicking the the Grafana icon in the top header. In the side menu under the `Dashboards` link you
@ -31,37 +32,73 @@ Password | Database user's password
> *Note* When using Proxy access mode the InfluxDB database, user and password will be hidden from the browser/frontend. When > *Note* When using Proxy access mode the InfluxDB database, user and password will be hidden from the browser/frontend. When
> using direct access mode all users will be able to see the database user & password. > using direct access mode all users will be able to see the database user & password.
## InfluxDB 0.9.x query editor ## InfluxDB 0.9.x
This editor & data source is not compatible with InfluxDB 0.8.x, please use the right data source for you InfluxDB version. ![](/img/influxdb/InfluxDB_09_editor.png)
The InfluxDB 0.9.x editor is currently under development and is not yet fully usable.
## InfluxDB 0.8.x query editor You find the InfluxDB editor in the metrics tab in Graph or Singlestat panel's edit mode. You enter edit mode by clicking the
panel title, then edit. The editor allows you to select metrics and tags.
### Editor tag filters
To add a tag filter click the plus icon to the right of the `WHERE` condition. You can remove tag filters by clicking on
the tag key and select `--remove tag filter--`.
### Regex matching
You can type in regex patterns for metric names or tag filter values, be sure to wrap the regex pattern in forward slashes (`/`). Grafana
will automaticallay adjust the filter tag condition to use the InfluxDB regex match condition operator (`=~`).
### Editor group by
To group by a tag click the plus icon after the `GROUP BY ($interval)` text. Pick a tag from the dropdown that appears.
You can remove the group by by clicking on the tag and then select `--remove group by--` from the dropdown.
### Editor RAW Query
You can switch to raw query mode by pressing the pen icon.
> If you use Raw Query be sure your query at minimum have `WHERE $timeFilter` clause and ends with `order by asc`.
> Also please always have a group by time and an aggregation function, otherwise InfluxDB can easily return hundreds of thousands
> of data points that will hang the browser.
### Alias patterns
- $m = replaced with measurement name
- $measurement = replaced with measurement name
- $tag_hostname = replaced with the value of the hostname tag
- You can also use [[tag_hostname]] pattern replacement syntax
### Templating
You can create a template variable in Grafana and have that variable filled with values from any InfluxDB metric exploration query.
You can then use this variable in your InfluxDB metric queries.
For example you can have a variable that contains all values for tag `hostname` if you specify a query like this
in the templating edit view.
```sql
SHOW TAG VALUES WITH KEY = "hostname"
```
You can also create nested variables. For example if you had another variable, for example `region`. Then you could have
the hosts variable only show hosts from the current selected region with a query like this:
```sql
SHOW TAG VALUES WITH KEY = "hostname" WHERE region =~ /$region/
```
> Always you `regex values` or `regex wildcard` for All format or multi select format.
![](/img/influxdb/templating_simple_ex1.png)
### Annotations
Annotations allows you to overlay rich event information on top of graphs.
An example query:
```SQL
SELECT title, description from events WHERE $timeFilter order asc
```
### InfluxDB 0.8.x
![](/img/v1/influxdb_editor.png) ![](/img/v1/influxdb_editor.png)
When you add an InfluxDB query you can specify series name (can be regex), value column and a function. Group by time can be specified or if left blank will be automatically set depending on how long the current time span is. It will translate to a InfluxDB query that looks like this:
```sql
select [[func]]([[column]]) from [[series]] where [[timeFilter]] group by time([[interval]]) order asc
```
To write the complete query yourself click the cog wheel icon to the right and select ``Raw query mode``.
## InfluxDB 0.9 Filters & Templates queries
The InfluxDB 0.9 data source does not currently support filters or templates.
## InfluxDB 0.8 Filters & Templated queries
![](/img/animated_gifs/influxdb_templated_query.gif)
Use a distinct influxdb query in the filter query input box:
```sql
select distinct(host) from app.status
```

View File

@ -0,0 +1,47 @@
---
page_title: KairosDB Guide
page_description: KairosDB guide for Grafana
page_keywords: grafana, kairosdb, documentation
---
# KairosDB Guide
## Adding the data source to Grafana
Open the side menu by clicking the the Grafana icon in the top header. In the side menu under the `Dashboards` link you
should find a link named `Data Sources`. If this link is missing in the side menu it means that your current
user does not have the `Admin` role for the current organization.
<!-- ![](/img/v2/add_datasource_kairosdb.png) -->
Now click the `Add new` link in the top header.
Name | Description
------------ | -------------
Name | The data source name, important that this is the same as in Grafana v1.x if you plan to import old dashboards.
Default | Default data source means that it will be pre-selected for new panels.
Url | The http protocol, ip and port of your kairosdb server (default port is usually 8080)
Access | Proxy = access via Grafana backend, Direct = access directory from browser.
## Query editor
Open a graph in edit mode by click the title.
<!-- ![](/img/v2/kairosdb_query_editor.png) -->
For details on KairosDB metric queries checkout the offical.
- [Query Metrics - KairosDB 0.9.4 documentation](http://kairosdb.github.io/kairosdocs/restapi/QueryMetrics.html).
## Templated queries
KairosDB Datasource Plugin provides following functions in `Variables values query` field in Templating Editor to query `metric names`, `tag names`, and `tag values` to kairosdb server.
Name | Description
---- | ----
`metrics(query)` | Returns a list of metric names. If nothing is given, returns a list of all metric names.
`tag_names(query)` | Returns a list of tag names. If nothing is given, returns a list of all tag names.
`tag_values(query)` | Returns a list of tag values. If nothing is given, returns a list of all tag values.
For details of `metric names`, `tag names`, and `tag values`, please refer to the KairosDB documentations.
- [List Metric Names - KairosDB 0.9.4 documentation](http://kairosdb.github.io/kairosdocs/restapi/ListMetricNames.html)
- [List Tag Names - KairosDB 0.9.4 documentation](http://kairosdb.github.io/kairosdocs/restapi/ListTagNames.html)
- [List Tag Values - KairosDB 0.9.4 documentation](http://kairosdb.github.io/kairosdocs/restapi/ListTagValues.html)

View File

@ -27,7 +27,19 @@ Open a graph in edit mode by click the title.
![](/img/v2/opentsdb_query_editor.png) ![](/img/v2/opentsdb_query_editor.png)
For details on opentsdb metric queries checkout the offical [OpenTSDB documentation](http://opentsdb.net/docs/build/html/index.html) ### Auto complete suggestions
You should get auto complete suggestions for tags and tag values. If you do not you need to enable `tsd.core.meta.enable_realtime_ts` in
the OpentSDB server settings. This is required for the OpenTSDB `lookup` api to work.
## Templating queries
When using OpenTSDB with a template variable of `query` type you can use following syntax for lookup.
metrics() // returns metric names
tag_names(cpu) // return tag names (i.e. keys) for a specific cpu metric
tag_values(cpu, hostname) // return tag values for metric cpu and tag key hostname
For details on opentsdb metric queries checkout the official [OpenTSDB documentation](http://opentsdb.net/docs/build/html/index.html)

View File

@ -19,7 +19,7 @@ The image above shows you the top header for a dashboard.
1. Side menubar toggle: This toggles the side menu, allowing you to focus on the data presented in the dashboard. The side menu provides access to features unrelated to a Dashboard such as Users, Organizations, and Data Sources. 1. Side menubar toggle: This toggles the side menu, allowing you to focus on the data presented in the dashboard. The side menu provides access to features unrelated to a Dashboard such as Users, Organizations, and Data Sources.
2. Dashboard dropdown: This dropdown shows you which Dashboard you are currently viewing, and allows you to easily switch to a new Dashboard. From here you can also create a new Dashboard, Import existing Dashboards, and manage Dashboard playlists. 2. Dashboard dropdown: This dropdown shows you which Dashboard you are currently viewing, and allows you to easily switch to a new Dashboard. From here you can also create a new Dashboard, Import existing Dashboards, and manage Dashboard playlists.
3. Star Dashboard: Star (or unstar) the current Dashboar. Starred Dashboards will show up on your own Home Dashboard by default, and are a convenient way to mark Dashboards that you're interested in. 3. Star Dashboard: Star (or unstar) the current Dashboard. Starred Dashboards will show up on your own Home Dashboard by default, and are a convenient way to mark Dashboards that you're interested in.
4. Share Dashboard: Share the current dashboard by creating a link or create a static Snapshot of it. Make sure the Dashboard is saved before sharing. 4. Share Dashboard: Share the current dashboard by creating a link or create a static Snapshot of it. Make sure the Dashboard is saved before sharing.
5. Save dashboard: The current Dashboard will be saved with the current Dashboard name. 5. Save dashboard: The current Dashboard will be saved with the current Dashboard name.
6. Settings: Manage Dashboard settings and features such as Templating and Annotations. 6. Settings: Manage Dashboard settings and features such as Templating and Annotations.
@ -28,7 +28,7 @@ The image above shows you the top header for a dashboard.
Dashboards are at the core of what Grafana is all about. Dashboards are composed of individual Panels arranged on a number of Rows. Dashboards are at the core of what Grafana is all about. Dashboards are composed of individual Panels arranged on a number of Rows.
By adjusting the display properties of Panels and Rows, you can customize the perfect Dashboard for your exact needs. By adjusting the display properties of Panels and Rows, you can customize the perfect Dashboard for your exact needs.
Each panel can interact with data from any configured Grafana Data Source (currently InfluxDB, Graphite, OpenTSDB, and KairosDB). Each panel can interact with data from any configured Grafana Data Source (currently InfluxDB, Graphite, OpenTSDB, and KairosDB).
This allows you to create a single dashboard that unifies the data across your organization. Panels use the time range specificed This allows you to create a single dashboard that unifies the data across your organization. Panels use the time range specified
in the main Time Picker in the upper right, but they can also have relative time overrides. in the main Time Picker in the upper right, but they can also have relative time overrides.
<img src="/img/v2/dashboard_annotated.png" class="no-shadow"> <img src="/img/v2/dashboard_annotated.png" class="no-shadow">
@ -40,6 +40,14 @@ in the main Time Picker in the upper right, but they can also have relative time
5. Dashboard panel. You edit panels by clicking the panel title. 5. Dashboard panel. You edit panels by clicking the panel title.
6. Graph legend. You can change series colors, y-axis and series visibility directly from the legend. 6. Graph legend. You can change series colors, y-axis and series visibility directly from the legend.
## Adding & Editing Graphs and Panels
![](/img/v2/graph_metrics_tab_graphite.png)
1. You add panels via row menu. The row menu is the green icon to the left of each row.
2. To edit the graph you click on the graph title to open the panel menu, then `Edit`.
3. This should take you to the `Metrics` tab. In this tab you should see the editor for your default data source.
## Drag-and-Drop panels ## Drag-and-Drop panels
You can Drag-and-Drop Panels within and between Rows. Click and hold the Panel title, and drag it to its new location. You can Drag-and-Drop Panels within and between Rows. Click and hold the Panel title, and drag it to its new location.

View File

@ -15,10 +15,9 @@ no_toc: true
<div class="columns medium-6"> <div class="columns medium-6">
<h3>Episode 2 - Templated Graphite Queries</h3> <h3>Episode 2 - Templated Graphite Queries</h3>
<div class="video-container"> <div class="video-container">
<iframe height="315" src="//www.youtube.com/embed/FhNUrueWwOk?list=PLDGkOdUX1Ujo3wHw9-z5Vo12YLqXRjzg2" frameborder="0" allowfullscreen></iframe> <iframe height="215" src="//www.youtube.com/embed/FhNUrueWwOk?list=PLDGkOdUX1Ujo3wHw9-z5Vo12YLqXRjzg2" frameborder="0" allowfullscreen></iframe>
</div> </div>
</div> </div>
</div>
</div> </div>
<div class="row"> <div class="row">
@ -34,7 +33,6 @@ no_toc: true
<iframe height="215" src="https://www.youtube.com/embed/JY22EBOR9hQ?list=PLDGkOdUX1Ujo3wHw9-z5Vo12YLqXRjzg2" frameborder="0" allowfullscreen></iframe> <iframe height="215" src="https://www.youtube.com/embed/JY22EBOR9hQ?list=PLDGkOdUX1Ujo3wHw9-z5Vo12YLqXRjzg2" frameborder="0" allowfullscreen></iframe>
</div> </div>
</div> </div>
</div>
</div> </div>
<div class="row"> <div class="row">
@ -50,7 +48,6 @@ no_toc: true
<iframe height="215" src="https://www.youtube.com/embed/9ZCMVNxUf6s?list=PLDGkOdUX1Ujo3wHw9-z5Vo12YLqXRjzg2" frameborder="0" allowfullscreen></iframe> <iframe height="215" src="https://www.youtube.com/embed/9ZCMVNxUf6s?list=PLDGkOdUX1Ujo3wHw9-z5Vo12YLqXRjzg2" frameborder="0" allowfullscreen></iframe>
</div> </div>
</div> </div>
</div>
</div> </div>
<div class="row"> <div class="row">

View File

@ -4,13 +4,13 @@ page_keywords: grafana, introduction, documentation, about
# About Grafana # About Grafana
Grafana is a leading open source applications for visualizing large-scale measurement data. Grafana is a leading open source application for visualizing large-scale measurement data.
It provides a powerful and elegant way to create, share, and explore data and dashboards from your disparate metric databases, either with your team or the world. It provides a powerful and elegant way to create, share, and explore data and dashboards from your disparate metric databases, either with your team or the world.
Grafana is most commonly used for Internet infrastructure and application analytics, but many use it in other domains including industrial sensors, home automation, weather, and process control. Grafana is most commonly used for Internet infrastructure and application analytics, but many use it in other domains including industrial sensors, home automation, weather, and process control.
Grafana features pluggable panels and data sources allowing easy extensibility. There is currently rich support for [Graphite](http://graphite.readthedocs.org/en/latest/), [InfluxDB](http://influxdb.org) and [OpenTSDB](http://opentsdb.net). There is also experimental support for KairosDB, and SQL is on the roadmap. Grafana has a variety of panels, including a fully featured graph panel with rich visualization options. Grafana features pluggable panels and data sources allowing easy extensibility. There is currently rich support for [Graphite](http://graphite.readthedocs.org/en/latest/), [InfluxDB](http://influxdb.org) and [OpenTSDB](http://opentsdb.net). There is also experimental support for [KairosDB](https://github.com/kairosdb/kairosdb), and SQL is on the roadmap. Grafana has a variety of panels, including a fully featured graph panel with rich visualization options.
Version 2.0 was released in April 2015: Grafana now ships with its own backend server that brings [many changes and features](../guides/whats-new-in-v2/). Version 2.0 was released in April 2015: Grafana now ships with its own backend server that brings [many changes and features](../guides/whats-new-in-v2/).

View File

@ -6,173 +6,243 @@ page_keywords: grafana, configuration, documentation
# Configuration # Configuration
The Grafana backend has a number of configuration options that can be specified in a `.ini` config file The Grafana back-end has a number of configuration options that can be
or specified using `ENV` variables. specified in a `.ini` configuration file or specified using environment variables.
## Config file locations ## Config file locations
- Default configuration from `$WORKING_DIR/conf/defaults.ini` - Default configuration from `$WORKING_DIR/conf/defaults.ini`
- Custom configuration from `$WORKING_DIR/conf/custom.ini` - Custom configuration from `$WORKING_DIR/conf/custom.ini`
- The custom config file path can be overriden using the `--config` parameter - The custom configuration file path can be overridden using the `--config` parameter
> **Note.** If you have installed grafana using the `deb` or `rpm` packages, then your configuration file is located > **Note.** If you have installed Grafana using the `deb` or `rpm`
> at `/etc/grafana/grafana.ini`. This path is specified in the grafana init.d script using `--config` file > packages, then your configuration file is located at
> parameter. > `/etc/grafana/grafana.ini`. This path is specified in the Grafana
> init.d script using `-config` file parameter.
## Using ENV variables ## Using environment variables
All options in the config file (listed below) can be overriden using ENV variables using the syntax:
All options in the configuration file (listed below) can be overridden
using environment variables using the syntax:
GF_<SectionName>_<KeyName> GF_<SectionName>_<KeyName>
Where the section name is the text within the brackets. Everything should be upper case. Where the section name is the text within the brackets. Everything
should be upper case, `.` should be replaced by `_`. For example, given these configuration settings:
Example, given this config setting:
[security] [security]
admin_user = admin admin_user = admin
[auth.google]
client_secret = 0ldS3cretKey
Then you can override that using: Then you can override that using:
export GF_SECURITY_ADMIN_USER=true export GF_SECURITY_ADMIN_USER=true
export GF_AUTH_GOOGLE_CLIENT_SECRET=newS3cretKey
<hr> <hr>
## [paths] ## [paths]
### data ### data
Path to where grafana can store the sqlite3 database (if used), file based sessions (if used), and other data.
This path is usually specified via command line in the init.d script or the systemd service file. Path to where Grafana stores the sqlite3 database (if used), file based
sessions (if used), and other data. This path is usually specified via
command line in the init.d script or the systemd service file.
### logs ### logs
Path to where grafana can store logs. This path is usually specified via command line in the init.d script or the systemd service file.
It can be overriden in the config file or in the default environment variable file. Path to where Grafana will store logs. This path is usually specified via
command line in the init.d script or the systemd service file. It can
be overridden in the configuration file or in the default environment variable
file.
## [server] ## [server]
### http_addr ### http_addr
The ip address to bind to, if empty will bind to all interfaces
The IP address to bind to, if empty will bind to all interfaces
### http_port ### http_port
The port to bind to, defaults to `3000`. To use port 80 you need to either give the grafana binary permission for example:
``` The port to bind to, defaults to `3000`. To use port 80 you need to
$ sudo setcap 'cap_net_bind_service=+ep' /opt/grafana/current/grafana either give the Grafana binary permission for example:
```
Or redirect port 80 to the grafana port using: $ sudo setcap 'cap_net_bind_service=+ep' /opt/grafana/current/grafana
```
$ sudo iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-port 3000
```
Another way is put nginx or apache infront of Grafana and have them proxy requests to Grafana. Or redirect port 80 to the Grafana port using:
$ sudo iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-port 3000
Another way is put a webserver like Nginx or Apache in front of Grafana and have them proxy requests to Grafana.
### protocol ### protocol
`http` or `https` `http` or `https`
### domain ### domain
This setting is only used in as a part of the root_url setting (see below). Important if you
use github or google oauth. This setting is only used in as a part of the `root_url` setting (see below). Important if you
use GitHub or Google OAuth.
### enforce_domain
Redirect to correct domain if host header does not match domain.
Prevents DNS rebinding attacks. Default is false.
### root_url ### root_url
This is the full url used to access grafana from a web browser. This is important if you use
google or github oauth authentication (for the callback url to be correct).
> **Note** This setting is also important if you have a reverse proxy infront of Grafana This is the full URL used to access Grafana from a web browser. This is
> that exposes grafana through a subpath. In that case add the subpath to the end of this url setting. important if you use Google or GitHub OAuth authentication (for the
callback URL to be correct).
> **Note** This setting is also important if you have a reverse proxy
> in front of Grafana that exposes it through a subpath. In that
> case add the subpath to the end of this URL setting.
### static_root_path ### static_root_path
The path to the directory where the frontend files (html & js & css). Default to `public` which is
why the Grafana binary needs to be executed with working directory set to the installation path. The path to the directory where the front end files (HTML, JS, and CSS
files). Default to `public` which is why the Grafana binary needs to be
executed with working directory set to the installation path.
### cert_file ### cert_file
Path to cert file (if protocol is https)
Path to the certificate file (if `protocol` is set to `https`).
### cert_key ### cert_key
Path to cert key file (if protocol is https)
Path to the certificate key file (if `protocol` is set to `https`).
<hr> <hr>
<hr> <hr>
## [database] ## [database]
Grafana needs a database to store users and dashboards (and other things). By default it is configured to Grafana needs a database to store users and dashboards (and other
use `sqlite3` which is an embedded database (included in the main Grafana binary). things). By default it is configured to use `sqlite3` which is an
embedded database (included in the main Grafana binary).
### type ### type
Either `mysql`, `postgres` or `sqlite3`, it's your choice. Either `mysql`, `postgres` or `sqlite3`, it's your choice.
### path ### path
Only applicable for `sqlite3` database. The file path where the database will be stored.
Only applicable for `sqlite3` database. The file path where the database
will be stored.
### host ### host
Only applicable to mysql or postgres. Include ip/hostname & port.
Example for mysql same host as Grafana: `host = 127.0.0.1:3306` Only applicable to MySQL or Postgres. Includes IP or hostname and port.
For example, for MySQL running on the same host as Grafana: `host =
127.0.0.1:3306`
### name ### name
The name of the grafana database. Leave it set to `grafana` or some other name.
The name of the Grafana database. Leave it set to `grafana` or some
other name.
### user ### user
The database user (not applicable for `sqlite3`). The database user (not applicable for `sqlite3`).
### password ### password
The database user's password (not applicable for `sqlite3`). The database user's password (not applicable for `sqlite3`).
### ssl_mode ### ssl_mode
For `postgres` only, either "disable", "require" or "verify-full".
For `postgres` only, either `disable`, `require` or `verify-full`.
<hr> <hr>
## [security] ## [security]
### admin_user ### admin_user
The name of the default grafana admin user (who has full permissions). Defaults to `admin`.
The name of the default Grafana admin user (who has full permissions).
Defaults to `admin`.
### admin_password ### admin_password
The password of the default grafana admin. Defaults to `admin`.
The password of the default Grafana admin. Defaults to `admin`.
### login_remember_days ### login_remember_days
The number of days the keep me logged in / remember me cookie lasts. The number of days the keep me logged in / remember me cookie lasts.
### secret_key ### secret_key
Used for signing keep me logged in / remember me cookies. Used for signing keep me logged in / remember me cookies.
### disable_gravatar
Set to `true` to disable the use of Gravatar for user profile images.
Default is `false`.
<hr> <hr>
## [user]
## [users]
### allow_sign_up ### allow_sign_up
Set to `false` to prohibit users from being able to sign up / create user accounts. Defaults to `true`.
The admin can still create users from the [Grafana Admin Pages](../reference/admin.md) Set to `false` to prohibit users from being able to sign up / create
user accounts. Defaults to `true`. The admin user can still create
users from the [Grafana Admin Pages](../reference/admin.md)
### allow_org_create ### allow_org_create
Set to `false` to prohibit users from creating new organizations. Defaults to `true`.
Set to `false` to prohibit users from creating new organizations.
Defaults to `true`.
### auto_assign_org ### auto_assign_org
Set to `true` to automatically add new users to the main organization (id 1). When set to `false`,
new users will automatically cause a new organization to be created for that new user. Set to `true` to automatically add new users to the main organization
(id 1). When set to `false`, new users will automatically cause a new
organization to be created for that new user.
### auto_assign_org_role ### auto_assign_org_role
The role new users will be assigned for the main organization (if the above setting is set to true).
Defaults to `Viewer`, other valid options are `Admin` and `Editor`. The role new users will be assigned for the main organization (if the
above setting is set to true). Defaults to `Viewer`, other valid
options are `Admin` and `Editor`.
<hr> <hr>
## [auth.anonymous] ## [auth.anonymous]
### enabled ### enabled
Set to `true` to enable anonymous access. Defaults to `false`
### org_name
Set the organization name that should be used for anonymous users. If you change your organization name
in the Grafana UI this setting needs to be updated to match the new name.
### org_role
Specify role for anonymous users. Defaults to `Viewer`, other valid options are `Editor` and `Admin`.
Set to `true` to enable anonymous access. Defaults to `false`
### org_name
Set the organization name that should be used for anonymous users. If
you change your organization name in the Grafana UI this setting needs
to be updated to match the new name.
### org_role
Specify role for anonymous users. Defaults to `Viewer`, other valid
options are `Editor` and `Admin`.
## [auth.github] ## [auth.github]
You need to create a github application (you find this under the github profile page). When
you create the application you will need to specify a callback URL. Specify this as callback: You need to create a GitHub application (you find this under the GitHub
profile page). When you create the application you will need to specify
a callback URL. Specify this as callback:
http://<my_grafana_server_name_or_ip>:<grafana_server_port>/login/github http://<my_grafana_server_name_or_ip>:<grafana_server_port>/login/github
This callback url must match the full http address that you use in your browser to access grafana, but This callback URL must match the full HTTP address that you use in your
with the prefix path of `/login/github`. When the github application is created you will get a browser to access Grafana, but with the prefix path of `/login/github`.
Client ID and a Client Secret. Specify these in the grafana config file. Example: When the GitHub application is created you will get a Client ID and a
Client Secret. Specify these in the Grafana configuration file. For
example:
[auth.github] [auth.github]
enabled = true enabled = true
@ -182,22 +252,47 @@ Client ID and a Client Secret. Specify these in the grafana config file. Example
auth_url = https://github.com/login/oauth/authorize auth_url = https://github.com/login/oauth/authorize
token_url = https://github.com/login/oauth/access_token token_url = https://github.com/login/oauth/access_token
allow_sign_up = false allow_sign_up = false
team_ids =
Restart the grafana backend. You should now see a github login button on the login page. You can Restart the Grafana back-end. You should now see a GitHub login button
now login or signup with your github accounts. on the login page. You can now login or sign up with your GitHub
accounts.
You may allow users to sign-up via github auth by setting allow_sign_up to true. When this option is You may allow users to sign-up via GitHub authentication by setting the
set to true, any user successfully authenticating via github auth will be automatically signed up. `allow_sign_up` option to `true`. When this option is set to `true`, any
user successfully authenticating via GitHub authentication will be
automatically signed up.
### team_ids
Require an active team membership for at least one of the given teams on
GitHub. If the authenticated user isn't a member of at least one the
teams they will not be able to register or authenticate with your
Grafana instance. For example:
[auth.github]
enabled = true
client_id = YOUR_GITHUB_APP_CLIENT_ID
client_secret = YOUR_GITHUB_APP_CLIENT_SECRET
scopes = user:email
team_ids = 150,300
auth_url = https://github.com/login/oauth/authorize
token_url = https://github.com/login/oauth/access_token
allow_sign_up = false
## [auth.google] ## [auth.google]
You need to create a google project. You can do this in the [Google Developer Console](https://console.developers.google.com/project).
When you create the project you will need to specify a callback URL. Specify this as callback: You need to create a Google project. You can do this in the [Google
Developer Console](https://console.developers.google.com/project). When
you create the project you will need to specify a callback URL. Specify
this as callback:
http://<my_grafana_server_name_or_ip>:<grafana_server_port>/login/google http://<my_grafana_server_name_or_ip>:<grafana_server_port>/login/google
This callback url must match the full http address that you use in your browser to access grafana, but This callback URL must match the full HTTP address that you use in your
with the prefix path of `/login/google`. When the google project is created you will get a browser to access Grafana, but with the prefix path of `/login/google`.
Client ID and a Client Secret. Specify these in the grafana config file. Example: When the Google project is created you will get a Client ID and a Client
Secret. Specify these in the Grafana configuration file. For example:
[auth.google] [auth.google]
enabled = true enabled = true
@ -206,28 +301,38 @@ Client ID and a Client Secret. Specify these in the grafana config file. Example
scopes = https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email scopes = https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email
auth_url = https://accounts.google.com/o/oauth2/auth auth_url = https://accounts.google.com/o/oauth2/auth
token_url = https://accounts.google.com/o/oauth2/token token_url = https://accounts.google.com/o/oauth2/token
allowed_domains = mycompany.com allowed_domains = mycompany.com mycompany.org
allow_sign_up = false allow_sign_up = false
Restart the grafana backend. You should now see a google login button on the login page. You can Restart the Grafana back-end. You should now see a Google login button
now login or signup with your google accounts. `allowed_domains` option is optional. on the login page. You can now login or sign up with your Google
accounts. The `allowed_domains` option is optional, and domains were separated by space.
You may allow users to sign-up via google auth by setting allow_sign_up to true. When this option is You may allow users to sign-up via Google authentication by setting the
set to true, any user successfully authenticating via google auth will be automatically signed up. `allow_sign_up` option to `true`. When this option is set to `true`, any
user successfully authenticating via Google authentication will be
automatically signed up.
<hr> <hr>
## [session] ## [session]
### provider ### provider
Valid values are "memory", "file", "mysql", 'postgres'. Default is "memory".
Valid values are `memory`, `file`, `mysql`, `postgres`. Default is `file`.
### provider_config ### provider_config
This option should be configured differently depending on what type of session provider you have configured.
This option should be configured differently depending on what type of
session provider you have configured.
- **file:** session file path, e.g. `data/sessions` - **file:** session file path, e.g. `data/sessions`
- **mysql:** go-sql-driver/mysql dsn config string, e.g. `user:password@tcp(127.0.0.1)/database_name` - **mysql:** go-sql-driver/mysql dsn config string, e.g. `user:password@tcp(127.0.0.1:3306)/database_name`
- **postgres:** ex: user=a password=b host=localhost port=5432 dbname=c sslmode=disable
If you use MySQL or Postgres as the session store you need to create the
session table manually.
if you use mysql or postgres as session store you need to create the session table manually.
Mysql Example: Mysql Example:
CREATE TABLE `session` ( CREATE TABLE `session` (
@ -238,24 +343,38 @@ Mysql Example:
) ENGINE=MyISAM DEFAULT CHARSET=utf8; ) ENGINE=MyISAM DEFAULT CHARSET=utf8;
### cookie_name ### cookie_name
The name of the grafana session cookie
The name of the Grafana session cookie.
### cookie_secure ### cookie_secure
Set to true if you host Grafana behind HTTPs only. Defaults to `false`. Set to true if you host Grafana behind HTTPs only. Defaults to `false`.
### session_life_time ### session_life_time
How long sessions lasts in seconds. Defaults to `86400` (24 hours). How long sessions lasts in seconds. Defaults to `86400` (24 hours).
## [analytics] ## [analytics]
### reporting_enabled ### reporting_enabled
When enabled Grafana will send anonymous usage statistics to stats.grafana.org.
No ip addresses are being tracked, only simple counters to track running instances, When enabled Grafana will send anonymous usage statistics to `stats.grafana.org`.
No IP addresses are being tracked, only simple counters to track running instances,
versions, dashboard & error counts. It is very helpful to us, please leave this versions, dashboard & error counts. It is very helpful to us, please leave this
enabled. Counters are sent every 24 hours. Default value is `true`. enabled. Counters are sent every 24 hours. Default value is `true`.
### google_analytics_ua_id ### google_analytics_ua_id
If you want to track Grafana usage via Google analytics specify *your* Univeral Analytics ID
here. By defualt this feature is disabled.
If you want to track Grafana usage via Google analytics specify *your* Universal Analytics ID
here. By default this feature is disabled.
## [dashboards.json]
If you have a system that automatically builds dashboards as json files you can enable this feature to have the
Grafana backend index those json dashboards which will make them appear in regular dashboard search.
### enabled
`true` or `false`. Is disabled by default.
### path
The full path to a directory containing your json dashboards.

View File

@ -16,80 +16,93 @@ Description | Download
$ wget https://grafanarel.s3.amazonaws.com/builds/grafana_2.0.2_amd64.deb $ wget https://grafanarel.s3.amazonaws.com/builds/grafana_2.0.2_amd64.deb
$ sudo apt-get install -y adduser libfontconfig $ sudo apt-get install -y adduser libfontconfig
$ sudo dpkg -i grafana_2.0.1_amd64.deb $ sudo dpkg -i grafana_2.0.2_amd64.deb
## APT Repository ## APT Repository
Add the following line to your `/etc/apt/sources.list` Add the following line to your `/etc/apt/sources.list` file.
deb https://packagecloud.io/grafana/stable/debian/ wheezy main deb https://packagecloud.io/grafana/stable/debian/ wheezy main
Use the above line even if you are on Ubuntu or another debian version. There is also testing Use the above line even if you are on Ubuntu or another Debian version.
repository if you want beta or release candidates. There is also a testing repository if you want beta or release
candidates.
deb https://packagecloud.io/grafana/testing/debian/ wheezy main deb https://packagecloud.io/grafana/testing/debian/ wheezy main
Then add the [Package Cloud](https://packagecloud.io/grafana) key (signs repo metadata). Then add the [Package Cloud](https://packagecloud.io/grafana) key. This
allows you to install signed packages.
$ curl https://packagecloud.io/gpg.key | sudo apt-key add - $ curl https://packagecloud.io/gpg.key | sudo apt-key add -
Update apt and install Grafana Update your Apt repositories and install Grafana
$ sudo apt-get update $ sudo apt-get update
$ sudo apt-get install grafana $ sudo apt-get install grafana
On some older versions of Ubuntu and Debian you may need to install `apt-transport-https`, On some older versions of Ubuntu and Debian you may need to install the
needed to fetch packages over HTTPS. `apt-transport-https` package which is needed to fetch packages over
HTTPS.
$ sudo apt-get install -y apt-transport-https $ sudo apt-get install -y apt-transport-https
## Package details ## Package details
- Installs binary to `/usr/sbin/grafana-server` - Installs binary to `/usr/sbin/grafana-server`
- Init.d script to `/etc/init.d/grafana-server` - Installs Init.d script to `/etc/init.d/grafana-server`
- Default file (environment vars) to `/etc/default/grafana-server` - Creates default file (environment vars) to `/etc/default/grafana-server`
- Configuration file to `/etc/grafana/grafana.ini` - Installs configuration file to `/etc/grafana/grafana.ini`
- Systemd service (if systemd is available) name `grafana-server.service` - Installs systemd service (if systemd is available) name `grafana-server.service`
- The default configuration specifies log file at `/var/log/grafana/grafana.log` - The default configuration sets the log file at `/var/log/grafana/grafana.log`
- The default configuration specifies sqlite3 db at `/var/lib/grafana/grafana.db` - The default configuration specifies an sqlite3 db at `/var/lib/grafana/grafana.db`
## Start the server (init.d service) ## Start the server (init.d service)
- Start grafana by `sudo service grafana-server start` You can start Grafana by running:
- This will start the grafana-server process as the `grafana` user (created during package install)
- Default http port is `3000`, and default user is admin/admin
To configure Grafana server to start at boot time: $ sudo service grafana-server start
This will start the `grafana-server` process as the `grafana` user,
which was created during the package installation. The default HTTP port
is `3000` and default user and group is `admin`.
To configure the Grafana server to start at boot time:
$ sudo update-rc.d grafana-server defaults 95 10 $ sudo update-rc.d grafana-server defaults 95 10
## Start the server (via systemd) ## Start the server (via systemd)
To start the service using systemd.
$ systemctl daemon-reload $ systemctl daemon-reload
$ systemctl start grafana-server $ systemctl start grafana-server
$ systemctl status grafana-server $ systemctl status grafana-server
Enable the systemd service (so grafana starts at boot) Enable the systemd service so that Grafana starts at boot.
sudo systemctl enable grafana-server.service sudo systemctl enable grafana-server.service
## Environment file ## Environment file
The systemd service file and init.d script both use the file located at `/etc/default/grafana-server` for The systemd service file and init.d script both use the file located at
environment variables used when starting the backend. Here you can override log directory, data directory and other `/etc/default/grafana-server` for environment variables used when
variables. starting the back-end. Here you can override log directory, data
directory and other variables.
### Logging ### Logging
By default grafana will log to `/var/log/grafana` By default Grafana will log to `/var/log/grafana`
### Database ### Database
The default configuration specifies a sqlite3 database located at `/var/lib/grafana/grafana.db`. Please backup The default configuration specifies a sqlite3 database located at
this database before upgrades. You can also use mysql or postgres as the Grafana database. `/var/lib/grafana/grafana.db`. Please backup this database before
upgrades. You can also use MySQL or Postgres as the Grafana database.
## Configuration ## Configuration
The configuration file is located at `/etc/grafana/grafana.ini`. Go the [Configuration](configuration) page for details The configuration file is located at `/etc/grafana/grafana.ini`. Go the
on all those options. [Configuration](/installation/configuration) page for details on all
those options.
### Adding data sources ### Adding data sources
@ -99,12 +112,18 @@ on all those options.
## Installing from binary tar file ## Installing from binary tar file
Start by [downloading](http://grafana.org/download/builds) the latest `.tar.gz` file and extract it. Start by [downloading](http://grafana.org/download/builds) the latest
This will extract into a folder named after the version you downloaded. This folder contains all files required to run grafana. `.tar.gz` file and extract it. This will extract into a folder named
There are no init scripts or install scripts in this package. after the version you downloaded. This folder contains all files
required to run Grafana. There are no init scripts or install scripts
in this package.
To configure grafana add a config file named `custom.ini` to the `conf` folder and override any of the settings defined in To configure Grafana add a configuration file named `custom.ini` to the
`conf/defaults.ini`. Start grafana by excecuting `./grafana web`. The grafana binary needs the working directory `conf` folder and override any of the settings defined in
to be the root install dir (where the binary is and the public folder is located). `conf/defaults.ini`.
Start Grafana by executing `./grafana web`. The `grafana` binary needs
the working directory to be the root install directory (where the binary
and the `public` folder is located).

View File

@ -8,27 +8,31 @@ page_keywords: grafana, installation, docker, container, guide
## Install from offical docker image ## Install from offical docker image
Grafana has an offical docker container. Grafana has an official Docker container.
$ docker run -i -p 3000:3000 grafana/grafana $ docker run -i -p 3000:3000 grafana/grafana
All grafana configuration settings can be defined using ENVIRONMENT variables, this is especially useful when using the All Grafana configuration settings can be defined using environment
above container. variables, this is especially useful when using the above container.
## Docker volumes & ENV config ## Docker volumes & ENV config
The docker container exposes two volumes, the sqlite3 database in the folder `/var/lib/grafana` and The Docker container exposes two volumes, the sqlite3 database in the
configuration files is in `/etc/grafana/` folder. You can map these volumes to host folders when you start the container: folder `/var/lib/grafana` and configuration files is in `/etc/grafana/`
folder. You can map these volumes to host folders when you start the
container:
$ docker run -d -p 3000:3000 \ $ docker run -d -p 3000:3000 \
-v /var/lib/grafana:/var/lib/grafana \ -v /var/lib/grafana:/var/lib/grafana \
-e "GF_SECURITY_ADMIN_PASSWORD=secret \ -e "GF_SECURITY_ADMIN_PASSWORD=secret" \
grafana/grafana:develop grafana/grafana:develop
In the above example I map the data folder and set a config option via an `ENV` variable. In the above example I map the data folder and sets a configuration option via
an `ENV` instruction.
## Configuration ## Configuration
The backend web server has a number of configuration options. Go the [Configuration](configuration) page for details The back-end web server has a number of configuration options. Go the
on all those options. [Configuration](../installation/configuration.md) page for details on all
those options.

View File

@ -6,9 +6,12 @@ page_keywords: grafana, installation, documentation
# Installation # Installation
Grafana is easily installed via a Debian/Ubuntu package (.deb), via Redhat/Centos package (.rpm) or manually via Grafana is easily installed via a Debian/Ubuntu package (.deb), via
a tar that contains all required files and binaries. If you can't find a package or binary for your platform you might be able Redhat/Centos package (.rpm) or manually via a tarball that contains all
to build one your self, read [build from source](../project/building_from_source) instructions for more information. required files and binaries. If you can't find a package or binary for
your platform you might be able to build one your self, read the [build
from source](../project/building_from_source) instructions for more
information.
- [Installing on Debian / Ubuntu](debian.md) - [Installing on Debian / Ubuntu](debian.md)
- [Installing on RPM-based Linux (CentOS, Fedora, OpenSuse, RedHat)](rpm.md) - [Installing on RPM-based Linux (CentOS, Fedora, OpenSuse, RedHat)](rpm.md)
@ -20,8 +23,9 @@ to build one your self, read [build from source](../project/building_from_source
## Configuration ## Configuration
The backend web server has a number of configuration options. Go the [Configuration](configuration) page for details The back-end web server has a number of configuration options. Go the
on all those options. [Configuration](/installation/configuration) page for details on all
those options.
## Adding data sources ## Adding data sources

View File

@ -6,7 +6,8 @@ page_keywords: grafana, installation, mac, osx, guide
# Installing on Mac # Installing on Mac
There are currently no binary build for Mac. But read the [build from source](../project/building_from_source) There is currently no binary build for Mac. But read the [build from
page for instructions on how to build it yourself. source](/project/building_from_source) page for instructions on how to
build it yourself.

View File

@ -6,53 +6,81 @@ page_keywords: grafana, installation, migration, documentation
# Migrating from v1.x to v2.x # Migrating from v1.x to v2.x
Grafana 2.0 represents a major update to Grafana. It brings new capabilities, many of which are enabled by its new backend server and integrated database. Grafana 2.0 represents a major update to Grafana. It brings new
capabilities, many of which are enabled by its new back-end server and
integrated database.
The new backend lays a solid foundation that we hope to build on over the coming months. For the 2.0 release, it enables authentication as well as server-side sharing and rendering. The new back-end lays a solid foundation that we hope to build on over
the coming months. For the 2.0 release, it enables authentication as
well as server-side sharing and rendering.
We've attempted to provide a smooth migration path for V1.9 users to migrate to Grafana 2.0. We've attempted to provide a smooth migration path for v1.9 users to
migrate to Grafana 2.0.
## Adding Data sources ## Adding Data sources
The config.js file has been deprecated. Data sources are now managed via the UI or [HTTP API](../reference/http_api.md). Manage your organizations data sources by clicking on the `Data Sources` menu on the side menu (which can be toggled via the Grafana icon in the upper left of your browser). The `config.js` file has been deprecated. Data sources are now managed via
the UI or [HTTP API](../reference/http_api.md). Manage your
organizations data sources by clicking on the `Data Sources` menu on the
side menu (which can be toggled via the Grafana icon in the upper left
of your browser).
From here, you can add any Graphite, InfluxDB, elasticsearch, and OpenTSDB datasources that you were using with Grafana 1.x. Grafana 2.0 can be configured to communicate with your datasource using a backend mode which can eliminate many CORS-related issues, as well as provide more secure authentication to your datasources. From here, you can add any Graphite, InfluxDB, elasticsearch, and
OpenTSDB data sources that you were using with Grafana 1.x. Grafana 2.0
can be configured to communicate with your data source using a back-end
mode which can eliminate many CORS-related issues, as well as provide
more secure authentication to your data sources.
> *Note* When you add your data sources please name them exacly as you named them in config.js in Grafana 1.x. That name is referenced by panels > *Note* When you add your data sources please name them exactly as you
> , annotation and template queries. That way when you import your old dashboard they will work without any changes. > named them in `config.js` in Grafana 1.x. That name is referenced by
> panels, annotation and template queries. That way when you import
> your old dashboard they will work without any changes.
## Importing your existing dashboards ## Importing your existing dashboards
Grafana 2.0 now has integrated dashboard storage engine that can be configured to use an internal sqlite database, MySQL, or Postgres. This eliminates the need to use Elasticsearch for dashboard storage for Graphite users. Grafana 2.0 does not support storing dashboards in InfluxDB. Grafana 2.0 now has integrated dashboard storage engine that can be
configured to use an internal sqlite3 database, MySQL, or Postgres. This
eliminates the need to use Elasticsearch for dashboard storage for
Graphite users. Grafana 2.0 does not support storing dashboards in
InfluxDB.
You can seamlessly import your existing dashboards. You can seamlessly import your existing dashboards.
### dashboards from Elasticsearch ### Importing dashboards from Elasticsearch
Start by going to the `Data Sources` view (via the side menu), and make sure your elasticsearch datasource is added. Specify the elasticsearch index name where your existing Grafana v1.x dashboards are stored (default is `grafana-dash`). Start by going to the `Data Sources` view (via the side menu), and make
sure your Elasticsearch data source is added. Specify the Elasticsearch
index name where your existing Grafana v1.x dashboards are stored
(the default is `grafana-dash`).
![](/img/v2/datasource_edit_elastic.jpg) ![](/img/v2/datasource_edit_elastic.jpg)
### dashboards from InfluxDB ### Importing dashboards from InfluxDB
Start by going to the `Data Sources` view (via the side menu), and make sure your InfluxDB datasource is added. Specify the database name where your Grafana v1.x dashboards are stored, default is `grafana`. Start by going to the `Data Sources` view (via the side menu), and make
sure your InfluxDB data source is added. Specify the database name where
your Grafana v1.x dashboards are stored, the default is `grafana`.
### Go to Import dashboards view ### Go to Import dashboards view
Go to the `Dashboards` view and click on the dashboards search dropdown. Click the `Import` button at the bottom of the search dropdown. Go to the `Dashboards` view and click on the dashboards search drop
down. Click the `Import` button at the bottom of the search drop down.
![](/img/v2/dashboard_import.jpg) ![](/img/v2/dashboard_import.jpg)
### Import view ### Import view
In the Import view you find the section `Migrate dashboards`. Pick the datasource you added (from elasticsearch or InfluxDB), In the Import view you find the section `Migrate dashboards`. Pick the
and click the `Import` button. data source you added (from Elasticsearch or InfluxDB), and click the
`Import` button.
![](/img/v2/migrate_dashboards.jpg) ![](/img/v2/migrate_dashboards.jpg)
Your dashboards should be automatically imported into the Grafana 2.0 backend. Your dashboards should be automatically imported into the Grafana 2.0
back-end.
Dashboards will no longer be stored in your previous elasticsearch or InfluxDB databases. Dashboards will no longer be stored in your previous Elasticsearch or
InfluxDB databases.
### Invite your team ### Invite your team

View File

@ -8,9 +8,15 @@ page_keywords: grafana, performance, documentation
## Graphite ## Graphite
Graphite 0.9.13 adds a much needed feature to the json rendering API that is very important for Grafana. If you are experiance slow Graphite 0.9.13 adds a much needed feature to the JSON rendering API
load & rendering times for large time ranges then it is most likely caused by running Graphite 0.9.12 or lower. The latest version that is very important for Grafana. If you are experiencing slow load &
of Graphite adds a maxDataPoints parameter to the json render API, without this feature Graphite can return hundreds of thousands of data points per graph, which rendering times for large time ranges then it is most likely caused by
can hang your browser. Be sure to upgrade to [0.9.13](http://graphite.readthedocs.org/en/latest/releases/0_9_13.html). running Graphite 0.9.12 or lower.
The latest version of Graphite adds a `maxDataPoints` parameter to the
JSON render API, without this feature Graphite can return hundreds of
thousands of data points per graph, which can hang your browser. Be sure
to upgrade to
[0.9.13](http://graphite.readthedocs.org/en/latest/releases/0_9_13.html).

View File

@ -6,8 +6,9 @@ page_keywords: grafana, provisioning, documentation
# Provisioning # Provisioning
Here are links for how to install Grafana (and some include graphite or influxdb as well) via a provisioning Here are links for how to install Grafana (and some include Graphite or
system. These are not maintained by any core Grafana team member and might be out of date. InfluxDB as well) via a provisioning system. These are not maintained by
any core Grafana team member and might be out of date.
## Puppet ## Puppet
@ -17,6 +18,7 @@ system. These are not maintained by any core Grafana team member and might be ou
* [github.com/bobrik/ansible-grafana](https://github.com/bobrik/ansible-grafana) * [github.com/bobrik/ansible-grafana](https://github.com/bobrik/ansible-grafana)
* [github.com/bitmazk/ansible-digitalocean-influxdb-grafana](https://github.com/bitmazk/ansible-digitalocean-influxdb-grafana) * [github.com/bitmazk/ansible-digitalocean-influxdb-grafana](https://github.com/bitmazk/ansible-digitalocean-influxdb-grafana)
* [github.com/picotrading/ansible-grafana](https://github.com/picotrading/ansible-grafana)
## Docker ## Docker

View File

@ -12,17 +12,18 @@ Description | Download
------------ | ------------- ------------ | -------------
.RPM for Fedora / RHEL / CentOS Linux | [grafana-2.0.2-1.x86_64.rpm](https://grafanarel.s3.amazonaws.com/builds/grafana-2.0.2-1.x86_64.rpm) .RPM for Fedora / RHEL / CentOS Linux | [grafana-2.0.2-1.x86_64.rpm](https://grafanarel.s3.amazonaws.com/builds/grafana-2.0.2-1.x86_64.rpm)
## Install ## Install from package file
You can install using yum
You can install Grafana using Yum directly.
$ sudo yum install https://grafanarel.s3.amazonaws.com/builds/grafana-2.0.2-1.x86_64.rpm $ sudo yum install https://grafanarel.s3.amazonaws.com/builds/grafana-2.0.2-1.x86_64.rpm
Or manually using `rpm` Or install manually using `rpm`.
$ sudo yum install initscripts fontconfig $ sudo yum install initscripts fontconfig
$ sudo rpm -Uvh grafana-2.0.1-1.x86_64.rpm $ sudo rpm -Uvh grafana-2.0.1-1.x86_64.rpm
## YUM Repository ## Install via YUM Repository
Add the following to a new file at `/etc/yum.repos.d/grafana.repo` Add the following to a new file at `/etc/yum.repos.d/grafana.repo`
@ -36,33 +37,43 @@ Add the following to a new file at `/etc/yum.repos.d/grafana.repo`
sslverify=1 sslverify=1
sslcacert=/etc/pki/tls/certs/ca-bundle.crt sslcacert=/etc/pki/tls/certs/ca-bundle.crt
There is also testing repository if you want beta or release candidates. There is also a testing repository if you want beta or release
candidates.
baseurl=https://packagecloud.io/grafana/testing/el/6/$basearch baseurl=https://packagecloud.io/grafana/testing/el/6/$basearch
Install Grafana Then install Grafana via the `yum` command.
$ sudo yum install grafana $ sudo yum install grafana
### RPM GPG Key ### RPM GPG Key
The rpms are signed, you can verify the signature with this [public GPG key](https://grafanarel.s3.amazonaws.com/RPM-GPG-KEY-grafana).
The RPMs are signed, you can verify the signature with this [public GPG
key](https://grafanarel.s3.amazonaws.com/RPM-GPG-KEY-grafana).
## Package details ## Package details
- Installs binary to `/usr/sbin/grafana-server` - Installs binary to `/usr/sbin/grafana-server`
- Init.d script to `/etc/init.d/grafana-server` - Copies init.d script to `/etc/init.d/grafana-server`
- Default file (environment vars) to `/etc/sysconfig/grafana-server` - Installs default file (environment vars) to `/etc/sysconfig/grafana-server`
- Configuration file to `/etc/grafana/grafana.ini` - Copies configuration file to `/etc/grafana/grafana.ini`
- Systemd service (if systemd is available) name `grafana-server.service` - Installs systemd service (if systemd is available) name `grafana-server.service`
- The default configuration specifies log file at `/var/log/grafana/grafana.log` - The default configuration uses a log file at `/var/log/grafana/grafana.log`
- The default configuration specifies sqlite3 db at `/var/lib/grafana/grafana.db` - The default configuration specifies an sqlite3 database at `/var/lib/grafana/grafana.db`
## Start the server (init.d service) ## Start the server (init.d service)
- Start grafana by `sudo service grafana-server start` You can start Grafana by running:
- This will start the grafana-server process as the `grafana` user (created during package install)
- Default http port is `3000`, and default user is admin/admin $ sudo service grafana-server start
- To configure grafana server to start at boot time: `sudo /sbin/chkconfig --add grafana-server`
This will start the `grafana-server` process as the `grafana` user,
which is created during package installation. The default HTTP port is
`3000`, and default user and group is `admin`.
To configure the Grafana server to start at boot time:
$ sudo /sbin/chkconfig --add grafana-server
## Start the server (via systemd) ## Start the server (via systemd)
@ -70,29 +81,32 @@ The rpms are signed, you can verify the signature with this [public GPG key](htt
$ systemctl start grafana-server $ systemctl start grafana-server
$ systemctl status grafana-server $ systemctl status grafana-server
### Enable the systemd service (so grafana starts at boot) ### Enable the systemd service to start at boot
sudo systemctl enable grafana-server.service sudo systemctl enable grafana-server.service
## Environment file ## Environment file
The systemd service file and init.d script both use the file located at `/etc/sysconfig/grafana-server` for The systemd service file and init.d script both use the file located at
environment variables used when starting the backend. Here you can override log directory, data directory and other `/etc/sysconfig/grafana-server` for environment variables used when
variables. starting the back-end. Here you can override log directory, data
directory and other variables.
### Logging ### Logging
By default grafana will log to `/var/log/grafana` By default Grafana will log to `/var/log/grafana`
### Database ### Database
The default configuration specifies a sqlite3 database located at `/var/lib/grafana/grafana.db`. Please backup The default configuration specifies a sqlite3 database located at
this database before upgrades. You can also use mysql or postgres as the Grafana database. `/var/lib/grafana/grafana.db`. Please backup this database before
upgrades. You can also use MySQL or Postgres as the Grafana database.
## Configuration ## Configuration
The configuration file is located at `/etc/grafana/grafana.ini`. Go the [Configuration](configuration) page for details The configuration file is located at `/etc/grafana/grafana.ini`. Go the
on all those options. [Configuration](/installation/configuration) page for details on all
those options.
### Adding data sources ### Adding data sources

View File

@ -4,45 +4,70 @@ page_keywords: grafana, support, documentation
# Troubleshooting # Troubleshooting
This page is dedicated to helping you solve any problem you have getting Grafana to work. Please review it before This page is dedicated to helping you solve any problem you have getting
opening a new github issue or asking a question in #grafana on freenode. Grafana to work. Please review it before opening a new [GitHub
issue](https://github.com/grafana/grafana/issues/new) or asking a
question in the `#grafana` IRC channel on freenode.
## General connection issues ## General connection issues
When setting up Grafana for the first time you might experiance issues with Grafana being unable to query Graphite, OpenTSDB or InfluxDB.
You might not be able to get metric name completion or the graph might show an error like this: When setting up Grafana for the first time you might experience issues
with Grafana being unable to query Graphite, OpenTSDB or InfluxDB. You
might not be able to get metric name completion or the graph might show
an error like this:
![](/img/v1/graph_timestore_error.png) ![](/img/v1/graph_timestore_error.png)
For some type of errors the ``View details`` link will show you error details. But for many types of HTTP connection errors there is For some type of errors the `View details` link will show you error
very little information. The best way to troubleshoot these issues is use details. But for many types of HTTP connection errors there is very
[Chrome developer tools](https://developer.chrome.com/devtools/index). By pressing F12 you can bring up the chrome dev tools. little information. The best way to troubleshoot these issues is use
the [Chrome developer tools](https://developer.chrome.com/devtools/index).
By pressing `F12` you can bring up the chrome dev tools.
![](/img/v1/toubleshooting_chrome_dev_tools.png) ![](/img/v1/toubleshooting_chrome_dev_tools.png)
There are two important tabs in the chrome dev tools, ``Network`` and ``Console``. Console will show you javascript errors and HTTP There are two important tabs in the Chrome developer tools: `Network`
request errors. In the Network tab you will be able to identifiy the request that failed and review request and response parameters. and `Console`. The `Console` tab will show you Javascript errors and
This information will be of great help in finding the cause of the error. If you are unable to solve the issue, even after reading HTTP request errors. In the Network tab you will be able to identify the
the remainder of this troubleshooting guide, you may open a [github support issue](https://github.com/grafana/grafana/issues). request that failed and review request and response parameters. This
Before you do that please search the existing closed or open issues. Also if you need to create a support issue, information will be of great help in finding the cause of the error.
screenshots and or text information about the chrome console error, request and response information from the network tab in chrome
developer tools are of great help. If you are unable to solve the issue, even after reading the remainder
of this troubleshooting guide, you should open a [GitHub support
issue](https://github.com/grafana/grafana/issues). Before you do that
please search the existing closed or open issues. Also if you need to
create a support issue, screen shots and or text information about the
chrome console error, request and response information from the
`Network` tab in Chrome developer tools are of great help.
### Inspecting Grafana metric requests ### Inspecting Grafana metric requests
![](/img/v1/toubleshooting_chrome_dev_tools_network.png) ![](/img/v1/toubleshooting_chrome_dev_tools_network.png)
After open chrome developer tools for the first time the Network tab is empty you need to refresh the page to get requests to show. After opening the Chrome developer tools for the first time the
For some type of errors (CORS related) there might not be a response at all. `Network` tab is empty. You will need to refresh the page to get
requests to show. For some type of errors, especially CORS-related,
there might not be a response at all.
## Graphite connection issues ## Graphite connection issues
If your Graphite web server is on another domain or IP than your Grafana web server you will need to [setup
CORS](../install/#graphite-server-config) (Cross Origin Resource Sharing).
You know if you are having CORS related issues if you get an error like this in chrome developer tools: If your Graphite web server is on another domain or IP address from your
Grafana web server you will need to [setup
CORS](../install/#graphite-server-config) (Cross Origin Resource
Sharing).
You know if you are having CORS-related issues if you get an error like
this in the Chrome developer tools:
![](/img/v1/toubleshooting_graphite_cors_error.png) ![](/img/v1/toubleshooting_graphite_cors_error.png)
If the request failed on method ``OPTIONS`` then you need to review your graphite web server configuration. If the request failed on method `OPTIONS` then you need to review your
Graphite web server configuration.
## Only blank white page ## Only blank white page
When you load Grafana and all you get is a blank white page then you probably have a javascript syntax error in ``config.js``.
In chrome developer tools console you will quickly identify the line of the syntax error. When you load Grafana and all you get is a blank white page then you
probably have a Javascript syntax error in `config.js`. In the Chrome
developer tools console you will quickly identify the line of the syntax
error.

View File

@ -13,21 +13,30 @@ Description | Download
Zip package for Windows | [grafana.2.0.2.windows-x64.zip](https://grafanarel.s3.amazonaws.com/winbuilds/dist/grafana-2.0.2.windows-x64.zip) Zip package for Windows | [grafana.2.0.2.windows-x64.zip](https://grafanarel.s3.amazonaws.com/winbuilds/dist/grafana-2.0.2.windows-x64.zip)
## Configure ## Configure
The zip file contains a folder with the current grafana version. Extract this folder to anywhere you want Grafana to run from.
Go into the `conf` directory and copy `sample.ini` to `custom.ini`. You should edit `custom.ini`, never `defaults.ini`.
The default grafana port is `3000`, this port requires extra permissions on windows. Edit `custom.ini` and uncomment the `http_port` The zip file contains a folder with the current Grafana version. Extract
config and change it to something like `8080` or similar. That port should not require extra windows privileges. this folder to anywhere you want Grafana to run from. Go into the
`conf` directory and copy `sample.ini` to `custom.ini`. You should edit
`custom.ini`, never `defaults.ini`.
Start grafana by executing `grafana-server.exe`, preferbly from the command line. If you want to run Grafana as The default Grafana port is `3000`, this port requires extra permissions
windows service, download [NSSM](https://nssm.cc/). It is very easy add grafana as windows service using that tool. on windows. Edit `custom.ini` and uncomment the `http_port`
configuration option and change it to something like `8080` or similar.
That port should not require extra Windows privileges.
Read more about the [configuration options](configuration.md). Start Grafana by executing `grafana-server.exe`, preferably from the
command line. If you want to run Grafana as windows service, download
[NSSM](https://nssm.cc/). It is very easy add Grafana as a Windows
service using that tool.
Read more about the [configuration options](/installation/configuration).
## Building on Windows ## Building on Windows
The Grafana backend includes Sqlite3 which requires GCC to compile. So in order to compile Grafana on windows you need The Grafana backend includes Sqlite3 which requires GCC to compile. So
to install GCC. We recommend [TDM-GCC](http://tdm-gcc.tdragon.net/download). in order to compile Grafana on Windows you need to install GCC. We
recommend [TDM-GCC](http://tdm-gcc.tdragon.net/download).
Copy conf/sample.ini to a file named conf/custom.ini and change the web server port to something like 8080. The default Copy `conf/sample.ini` to a file named `conf/custom.ini` and change the
Grafana port(3000) requires special privileges on Windows. web server port to something like 8080. The default Grafana port, 3000,
requires special privileges on Windows.

View File

@ -17,6 +17,7 @@ dev environment.
## Get Code ## Get Code
``` ```
export GOPATH=`pwd`
go get github.com/grafana/grafana go get github.com/grafana/grafana
``` ```
@ -24,11 +25,11 @@ go get github.com/grafana/grafana
``` ```
cd $GOPATH/src/github.com/grafana/grafana cd $GOPATH/src/github.com/grafana/grafana
go run build.go setup (only needed once to install godep) go run build.go setup (only needed once to install godep)
godep restore (will pull down all golang lib dependecies in your current GOPATH) $GOPATH/bin/godep restore (will pull down all golang lib dependecies in your current GOPATH)
go build . go build .
``` ```
# Building on Windows ## Building on Windows
The Grafana backend includes Sqlite3 which requires GCC to compile. So in order to compile Grafana on windows you need The Grafana backend includes Sqlite3 which requires GCC to compile. So in order to compile Grafana on windows you need
to install GCC. We recommend [TDM-GCC](http://tdm-gcc.tdragon.net/download). to install GCC. We recommend [TDM-GCC](http://tdm-gcc.tdragon.net/download).
@ -58,6 +59,8 @@ bra run
Open grafana in your browser (default http://localhost:3000) and login with admin user (default user/pass = admin/admin). Open grafana in your browser (default http://localhost:3000) and login with admin user (default user/pass = admin/admin).
## Creating optimized release packages ## Creating optimized release packages
This step builds linux packages and requires that fpm is installed. Install fpm via `gem install fpm`.
``` ```
go run build.go build package go run build.go build package
``` ```
@ -72,4 +75,4 @@ You only need to add the options you want to override. Config files are applied
## Create a pull requests ## Create a pull requests
Before or after your create a pull requests, sign the [contributor license aggrement](/docs/contributing/cla.html). Before or after your create a pull requests, sign the [contributor license agreement](/project/cla.html).

View File

@ -13,14 +13,14 @@ you can get title, tags, and text information for the event.
To add an annotation query click dashboard settings icon in top menu and select `Annotations` from the To add an annotation query click dashboard settings icon in top menu and select `Annotations` from the
dropdown. This will open the `Annotations` edit view. Click the `Add` tab to add a new annotation query. dropdown. This will open the `Annotations` edit view. Click the `Add` tab to add a new annotation query.
### Graphite annotations ## Graphite annotations
Graphite supports two ways to query annotations. Graphite supports two ways to query annotations.
- A regular metric query, use the `Graphite target expression` text input for this - A regular metric query, use the `Graphite target expression` text input for this
- Graphite events query, use the `Graphite event tags` text input, especify an tag or wildcard (leave empty should also work) - Graphite events query, use the `Graphite event tags` text input, specify an tag or wildcard (leave empty should also work)
## Elasticsearch annoations ## Elasticsearch annotations
![](/img/v2/annotations_es.png) ![](/img/v2/annotations_es.png)
Grafana can query any Elasticsearch index for annotation events. The index name can be the name of an alias or an index wildcard pattern. Grafana can query any Elasticsearch index for annotation events. The index name can be the name of an alias or an index wildcard pattern.
@ -36,5 +36,4 @@ as the name for the fields that should be used for the annotation title, tags an
For InfluxDB you need to enter a query like in the above screenshot. You need to have the ```where $timeFilter``` part. For InfluxDB you need to enter a query like in the above screenshot. You need to have the ```where $timeFilter``` part.
If you only select one column you will not need to enter anything in the column mapping fields. If you only select one column you will not need to enter anything in the column mapping fields.
If you have multiple columns you need to specify which column should be treated as title, tags and text column.

View File

@ -6,4 +6,22 @@ page_keywords: grafana, dashlist, panel, documentation
# Dashlist Panel # Dashlist Panel
## Overview
![](/img/v2/dashboard_list_panel.png)
The dashboard list panel allows you to show a list of links to other dashboards. The list
can be based on a search query or dashboard tag query. You can also configure it to show your starred
dashboards.
## Options
![](/img/v2/dashboard_list_panel_options.png)
Name | Description
------------ | -------------
Mode | Set search or starred mode
Query | If in search mode specify the search query
Tags | if in search mode specify dashboard tags to search for
Limit number to | Specify the maximum number of dashboards

View File

@ -62,7 +62,7 @@ The ``Left Y`` and ``Right Y`` can be customized using:
- ``Unit`` - The display unit for the Y value - ``Unit`` - The display unit for the Y value
- ``Grid Max`` - The maximum Y value. (default auto) - ``Grid Max`` - The maximum Y value. (default auto)
- ``Grid Min`` - The minium Y value. (default auto) - ``Grid Min`` - The minimum Y value. (default auto)
- ``Label`` - The Y axis label (default "") - ``Label`` - The Y axis label (default "")
Axes can also be hidden by unchecking the appropriate box from `Show Axis`. Axes can also be hidden by unchecking the appropriate box from `Show Axis`.

View File

@ -53,7 +53,7 @@ Creates a new dashboard or updates an existing dashboard.
"rows": [ "rows": [
{ {
} }
] ],
"schemaVersion": 6, "schemaVersion": 6,
"version": 0 "version": 0
}, },
@ -84,8 +84,8 @@ Status Codes:
- **401** Unauthorized - **401** Unauthorized
- **412** Precondition failed - **412** Precondition failed
The **412** status code is used when a newer dashboard already exists (newer, its version is greater than the verison that was sent). The The **412** status code is used when a newer dashboard already exists (newer, its version is greater than the version that was sent). The
same status code is also used if another dashboar exists with the same title. The response body will look like this: same status code is also used if another dashboard exists with the same title. The response body will look like this:
HTTP/1.1 412 Precondition Failed HTTP/1.1 412 Precondition Failed
Content-Type: application/json; charset=UTF-8 Content-Type: application/json; charset=UTF-8
@ -121,7 +121,7 @@ Will return the dashboard given the dashboard slug. Slug is the url friendly ver
"isStarred": false, "isStarred": false,
"slug": "production-overview" "slug": "production-overview"
}, },
"dashboard": { "model": {
"id": null, "id": null,
"title": "Production Overview", "title": "Production Overview",
"tags": [ "templated" ], "tags": [ "templated" ],
@ -141,12 +141,236 @@ Will return the dashboard given the dashboard slug. Slug is the url friendly ver
The above will delete the dashboard with the specified slug. The slug is the url friendly (unique) version of the dashboard title. The above will delete the dashboard with the specified slug. The slug is the url friendly (unique) version of the dashboard title.
### Gets the home dashboard
`GET /api/dashboards/home`
### Tags for Dashboard
`GET /api/dashboards/tags`
### Dashboard from JSON file
`GET /file/:file`
### Search Dashboards
`GET /api/search/`
Status Codes:
- **query** Search Query
- **tags** Tags to use
- **starred** Flag indicating if only starred Dashboards should be returned
- **tagcloud** - Flag indicating if a tagcloud should be returned
**Example Request**:
GET /api/search?query=MyDashboard&starred=true&tag=prod HTTP/1.1
Accept: application/json
Content-Type: application/json
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
## Data sources ## Data sources
### Get all datasources
`GET /api/datasources`
### Get a single data sources by Id
`GET /api/datasources/:datasourceId`
### Create data source ### Create data source
## Organizations `POST /api/datasources`
**Example Response**:
HTTP/1.1 200
Content-Type: application/json
{"message":"Datasource added"}
### Update an existing data source
`PUT /api/datasources/:datasourceId`
### Delete an existing data source
`DELETE /api/datasources/:datasourceId`
**Example Response**:
HTTP/1.1 200
Content-Type: application/json
{"message":"Data source deleted"}
### Available data source types
`GET /api/datasources/plugins`
## Data source proxy calls
`GET /api/datasources/proxy/:datasourceId/*`
Proxies all calls to the actual datasource.
## Organisation
### Get current Organisation
`GET /api/org`
### Get all users within the actual organisation
`GET /api/org/users`
### Add a new user to the actual organisation
`POST /api/org/users`
Adds a global user to the actual organisation.
### Updates the given user
`PATCH /api/org/users/:userId`
### Delete user in actual organisation
`DELETE /api/org/users/:userId`
### Get all Users
`GET /api/org/users`
## Organisations
### Search all Organisations
`GET /api/orgs`
### Update Organisation
`PUT /api/orgs/:orgId`
### Get Users in Organisation
`GET /api/orgs/:orgId/users`
### Add User in Organisation
`POST /api/orgs/:orgId/users`
### Update Users in Organisation
`PATCH /api/orgs/:orgId/users/:userId`
### Delete User in Organisation
`DELETE /api/orgs/:orgId/users/:userId`
## Users ## Users
### Search Users
`GET /api/users`
### Get single user by Id
`GET /api/users/:id`
### User Update
`PUT /api/users/:id`
### Get Organisations for user
`GET /api/users/:id/orgs`
## User
### Change Password
`PUT /api/user/password`
Changes the password for the user
### Actual User
`GET /api/user`
The above will return the current user.
### Switch user context
`POST /api/user/using/:organisationId`
Switch user context to the given organisation.
### Organisations of the actual User
`GET /api/user/orgs`
The above will return a list of all organisations of the current user.
### Star a dashboard
`POST /api/user/stars/dashboard/:dashboardId`
Stars the given Dashboard for the actual user.
### Unstar a dashboard
`DELETE /api/user/stars/dashboard/:dashboardId`
Deletes the staring of the given Dashboard for the actual user.
## Snapshots
### Create new snapshot
`POST /api/snapshots`
### Get Snapshot by Id
`GET /api/snapshots/:key`
### Delete Snapshot by Id
`DELETE /api/snapshots-delete/:key`
## Frontend Settings
### Get Settings
`GET /api/frontend/settings`
## Login
### Renew session based on remember cookie
`GET /api/login/ping`
## Admin
### Settings
`GET /api/admin/settings`
### Global Users
`POST /api/admin/users`
### Password for User
`PUT /api/admin/users/:id/password`
### Permissions
`PUT /api/admin/users/:id/permissions`
### Delete global User
`DELETE /api/admin/users/:id`

View File

@ -12,7 +12,7 @@ With scripted dashboards you can dynamically create your dashboards using javasc
under `public/dashboards/` there is a file named `scripted.js`. This file contains an example of a scripted dashboard. You can access it by using the url: under `public/dashboards/` there is a file named `scripted.js`. This file contains an example of a scripted dashboard. You can access it by using the url:
`http://grafana_url/dashboard/script/scripted.js?rows=3&name=myName` `http://grafana_url/dashboard/script/scripted.js?rows=3&name=myName`
If you open scripted.js you can see how it reads url paramters from ARGS variable and then adds rows and panels. If you open scripted.js you can see how it reads url parameters from ARGS variable and then adds rows and panels.
## Example ## Example

View File

@ -11,5 +11,48 @@ page_keywords: grafana, singlestat, panel, documentation
The singlestat panel allows you to show the one main summery stat of a single series (like max, min, avg, sum). It also The singlestat panel allows you to show the one main summery stat of a single series (like max, min, avg, sum). It also
provides thresholds to color that singlestat metric or the panel background. provides thresholds to color that singlestat metric or the panel background.
## Options ### Big Value Configuration
- TODO
The big value configuration allows you to both customize the look of your singlestat metric, as well as add additional labels to contexualize the metric.
<img class="no-shadow" src="/img/v1/Singlestat-BaseSettings.png">
1. `Big Value`: Big Value refers to the collection of values displayed in the singlestat panel.
2. `Prefixes`: The Prefix fields let you define a custom label and font-size (as a %) to appear *before* the singlestat metric.
3. `Values`: The Value fields let you set the (min, max, average, current, total) and font-size (as a %) of the singlestat metric.
4. `Potsfixes`: The Postfix fields let you define a custom label and font-size (as a %) to appear *after* the singlestat metric.
5. `Units`: Units are appended to the the singlestat metric within the panel, and will respect the color and threshold settings for the Value.
6. `Decimals`: The Decimal field allows you to override automatic decimal precision, inceasing the digits displayed for your singlestat metric.
### Coloring
The coloring options of the singlestat config allow you to dynamically change the colors based on the displayed data.
<img class="no-shadow" src="/img/v1/Singlestat-Coloring.png">
1. `Background`: The Background checkbox applies the configured thresholds and colors to the entirity of the singlestat panel background.
2. `Value`: The Value checkbox applies the configured thresholds and colors to the value within the singlestat panel.
3. `Thresholds`: Thresholds allow you to change the background and value colors dyanmically within the panel. The threshold field accepts **3 comma-separated** values, corresponding to the three colors directly to the right.
4. `Colors`: The color picker allows you to select a color and opacity
5. `Invert order`: This link toggles the threshold color order.</br>For example: Green, Orange, Red (<img class="no-shadow" src="/img/v1/gyr.png">) will become Red, Orange, Green (<img class="no-shadow" src="/img/v1/ryg.png">).
### Spark Lines
Spark lines are a great way of seeing the historical data associated with a single stat value, providing valuable context at a glance. Spark lines act differently than traditional graph panels and do not include x or y axis, coordinates, a legend, or ability to interact with the graph.
<img class="no-shadow" src="/img/v1/Singlestat-Sparklines.png">
1. `Show`: The show checkbox will toggle whether the spark line is shown in the panel. When unselected, only the value will appear.
2. `Background`: Check if you want the sparklines to take up the full panel width or uncheck if they should only be at the bottom.
3. `Line Color`: This color selection applies to the color of the sparkline itself.
4. `Fill Color`: This color selection applies to the area below the sparkline.
> ***Pro-tip:*** Reduce the opacity on fill colors for nice looking panels.
### Value to text mapping
Value to text mapping allows you to translate values into explcit text. The text will respect all styling, thresholds and customization defined for the value.
<img class="no-shadow" src="/img/v1/Singlestat-ValueMapping.png">

View File

@ -5,14 +5,43 @@ page_keywords: grafana, templating, variables, guide, documentation
--- ---
# Templated Dashboards # Templated Dashboards
![](/img/v2/templating_var_list.png)
Templating feature can be enabled under dashboard settings, in the Features tab. The templating feature allows ## Overview
you to create variables that can be used in your metric queries, series names and panel titles. Use this feature to Templating allows you to create dashboard variables that can be used in your metric queries, series
create generic dashboards that can quickly be changed to show graphs for different servers or metrics. names and panel titles. Use this feature to create generic dashboards that can quickly be
changed to show graphs for different servers or metrics.
You find this feature in the dashboard cog dropdown menu.
## Variable types
There are three different types of template variables. They can all be used in the
same way but they differ in how the list variables values is created.
### Query
This is the most common type of variable. It allows you to create a variable
with values fetched directly from a data source via a metric exploration query.
For example a query like `prod.servers.*` will fill the variable with all possible
values that exists in the wildcard position (Graphite example).
You can also create nested variables that use other variables in their definition. For example
`apps.$app.servers.*` uses the variable `$app` in its query definition.
> For examples of template queries appropriate for your data source checkout the documentation
> page for your data source.
### Interval
This variable type is useful for time ranges like `1m`,`1h`, `1d`. There is also an auto
option that will change depending on the current time range, you can specify how many times
the current time range should be divided to calculate the current `auto` range.
![](/img/v2/templated_variable_parameter.png)
### Custom
This variable type allow you to manually specify all the different values as a comma seperated
string.
## Screencast - Templated Graphite Queries ## Screencast - Templated Graphite Queries
<iframe width="561" height="315" src="//www.youtube.com/embed/FhNUrueWwOk?list=PLDGkOdUX1Ujo3wHw9-z5Vo12YLqXRjzg2" frameborder="0" allowfullscreen></iframe> <iframe width="561" height="315" src="//www.youtube.com/embed/FhNUrueWwOk?list=PLDGkOdUX1Ujo3wHw9-z5Vo12YLqXRjzg2" frameborder="0" allowfullscreen></iframe>
<br>
## Screencast - Templated InfluxDB Queries
Coming soon

View File

@ -24,7 +24,7 @@ All of this applies to all Panels in the Dashboard (except those with Panel Time
It's possible to customize the options displayed for relative time and the auto-refresh options. It's possible to customize the options displayed for relative time and the auto-refresh options.
From Dashboard setttings, click the Timepicker tab. From here you can specify the relative and auto refresh intervals. The Timepicker tab settings are saved on a per Dashboard basis. From Dashboard settings, click the Timepicker tab. From here you can specify the relative and auto refresh intervals. The Timepicker tab settings are saved on a per Dashboard basis. Entries are comma separated and accept a number followed by one of the following units: s (seconds), m (minutes), h (hours), d (days), w (weeks), M (months), y (years).
![](/img/v1/timepicker_editor.png) ![](/img/v1/timepicker_editor.png)

View File

@ -1,2 +1,3 @@
<li><a class='version' href='/v2.1'>Version v2.1</a></li>
<li><a class='version' href='/v2.0'>Version v2.0</a></li> <li><a class='version' href='/v2.0'>Version v2.0</a></li>
<li><a class='version' href='/v1.9'>Version v1.9</a></li> <li><a class='version' href='/v1.9'>Version v1.9</a></li>

View File

@ -1,3 +1,3 @@
{ {
"version": "2.0.1", "version": "2.0.2"
} }

View File

@ -15,6 +15,8 @@ import (
"github.com/grafana/grafana/pkg/metrics" "github.com/grafana/grafana/pkg/metrics"
"github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/services/eventpublisher" "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/services/sqlstore" "github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/social" "github.com/grafana/grafana/pkg/social"
@ -48,13 +50,18 @@ func main() {
flag.Parse() flag.Parse()
initRuntime()
writePIDFile() writePIDFile()
initRuntime()
search.Init()
social.NewOAuthService() social.NewOAuthService()
eventpublisher.Init() eventpublisher.Init()
plugins.Init() plugins.Init()
if err := notifications.Init(); err != nil {
log.Fatal(3, "Notification service failed to initialize", err)
}
if setting.ReportingEnabled { if setting.ReportingEnabled {
go metrics.StartUsageReportLoop() go metrics.StartUsageReportLoop()
} }

View File

@ -4,7 +4,7 @@
"company": "Coding Instinct AB" "company": "Coding Instinct AB"
}, },
"name": "grafana", "name": "grafana",
"version": "2.0.2", "version": "2.1.0-pre1",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "http://github.com/torkelo/grafana.git" "url": "http://github.com/torkelo/grafana.git"
@ -34,22 +34,19 @@
"grunt-string-replace": "~0.2.4", "grunt-string-replace": "~0.2.4",
"grunt-usemin": "3.0.0", "grunt-usemin": "3.0.0",
"jshint-stylish": "~0.1.5", "jshint-stylish": "~0.1.5",
"karma": "~0.12.21", "karma": "~0.12.31",
"karma-chrome-launcher": "~0.1.4", "karma-chrome-launcher": "~0.1.4",
"karma-coffee-preprocessor": "~0.1.2", "karma-coffee-preprocessor": "~0.1.2",
"karma-coverage": "^0.2.5", "karma-coverage": "0.3.1",
"karma-coveralls": "^0.1.4", "karma-coveralls": "0.1.5",
"karma-expect": "~1.1.0", "karma-expect": "~1.1.0",
"karma-firefox-launcher": "~0.1.3", "karma-mocha": "~0.1.10",
"karma-html2js-preprocessor": "~0.1.0", "karma-phantomjs-launcher": "0.1.4",
"karma-jasmine": "~0.2.2", "karma-requirejs": "0.2.2",
"karma-mocha": "~0.1.4", "karma-script-launcher": "0.1.0",
"karma-phantomjs-launcher": "~0.1.4", "load-grunt-tasks": "0.2.0",
"karma-requirejs": "~0.2.1", "mocha": "2.2.4",
"karma-script-launcher": "~0.1.0", "requirejs": "2.1.17",
"load-grunt-tasks": "~0.2.0",
"mocha": "~1.16.1",
"requirejs": "~2.1.14",
"rjs-build-analysis": "0.0.3" "rjs-build-analysis": "0.0.3"
}, },
"engines": { "engines": {
@ -65,6 +62,6 @@
"grunt-jscs": "~1.5.x", "grunt-jscs": "~1.5.x",
"karma-sinon": "^1.0.3", "karma-sinon": "^1.0.3",
"lodash": "^2.4.1", "lodash": "^2.4.1",
"sinon": "^1.10.3" "sinon": "1.10.3"
} }
} }

View File

@ -43,9 +43,9 @@ case "$1" in
chmod 755 /var/log/grafana /var/lib/grafana chmod 755 /var/log/grafana /var/lib/grafana
# configuration files should not be modifiable by grafana user, as this can be a security issue # configuration files should not be modifiable by grafana user, as this can be a security issue
chown -Rh root:root /etc/grafana/* chown -Rh root:$GRAFANA_GROUP /etc/grafana/*
chmod 755 /etc/grafana chmod 755 /etc/grafana
find /etc/grafana -type f -exec chmod 644 {} ';' find /etc/grafana -type f -exec chmod 640 {} ';'
find /etc/grafana -type d -exec chmod 755 {} ';' find /etc/grafana -type d -exec chmod 755 {} ';'
# if $2 is set, this is an upgrade # if $2 is set, this is an upgrade

View File

@ -38,7 +38,12 @@ DAEMON=/usr/sbin/$NAME
if [ `id -u` -ne 0 ]; then if [ `id -u` -ne 0 ]; then
echo "You need root privileges to run this script" echo "You need root privileges to run this script"
exit 1 exit 4
fi
if [ ! -x $DAEMON ]; then
echo "Program not installed or not executable"
exit 5
fi fi
. /lib/lsb/init-functions . /lib/lsb/init-functions
@ -54,9 +59,6 @@ fi
DAEMON_OPTS="--pidfile=${PID_FILE} --config=${CONF_FILE} cfg:default.paths.data=${DATA_DIR} cfg:default.paths.logs=${LOG_DIR}" DAEMON_OPTS="--pidfile=${PID_FILE} --config=${CONF_FILE} cfg:default.paths.data=${DATA_DIR} cfg:default.paths.logs=${LOG_DIR}"
# Check DAEMON exists
test -x $DAEMON || exit 0
case "$1" in case "$1" in
start) start)
@ -137,8 +139,6 @@ case "$1" in
;; ;;
*) *)
log_success_msg "Usage: $0 {start|stop|restart|force-reload|status}" log_success_msg "Usage: $0 {start|stop|restart|force-reload|status}"
exit 1 exit 3
;; ;;
esac esac
exit 0

View File

@ -43,9 +43,9 @@ if [ $1 -eq 1 ] ; then
chmod 755 /var/log/grafana /var/lib/grafana chmod 755 /var/log/grafana /var/lib/grafana
# configuration files should not be modifiable by grafana user, as this can be a security issue # configuration files should not be modifiable by grafana user, as this can be a security issue
chown -Rh root:root /etc/grafana/* chown -Rh root:$GRAFANA_GROUP /etc/grafana/*
chmod 755 /etc/grafana chmod 755 /etc/grafana
find /etc/grafana -type f -exec chmod 644 {} ';' find /etc/grafana -type f -exec chmod 640 {} ';'
find /etc/grafana -type d -exec chmod 755 {} ';' find /etc/grafana -type d -exec chmod 755 {} ';'
if [ -x /bin/systemctl ] ; then if [ -x /bin/systemctl ] ; then

View File

@ -35,6 +35,16 @@ MAX_OPEN_FILES=10000
PID_FILE=/var/run/$NAME.pid PID_FILE=/var/run/$NAME.pid
DAEMON=/usr/sbin/$NAME DAEMON=/usr/sbin/$NAME
if [ `id -u` -ne 0 ]; then
echo "You need root privileges to run this script"
exit 4
fi
if [ ! -x $DAEMON ]; then
echo "Program not installed or not executable"
exit 5
fi
# #
# init.d / servicectl compatibility (openSUSE) # init.d / servicectl compatibility (openSUSE)
# #
@ -55,9 +65,6 @@ fi
DAEMON_OPTS="--pidfile=${PID_FILE} --config=${CONF_FILE} cfg:default.paths.data=${DATA_DIR} cfg:default.paths.logs=${LOG_DIR}" DAEMON_OPTS="--pidfile=${PID_FILE} --config=${CONF_FILE} cfg:default.paths.data=${DATA_DIR} cfg:default.paths.logs=${LOG_DIR}"
# Check DAEMON exists
test -x $DAEMON || exit 0
function isRunning() { function isRunning() {
status -p $PID_FILE $NAME > /dev/null 2>&1 status -p $PID_FILE $NAME > /dev/null 2>&1
} }
@ -69,7 +76,7 @@ case "$1" in
isRunning isRunning
if [ $? -eq 0 ]; then if [ $? -eq 0 ]; then
echo "Already running." echo "Already running."
exit 2 exit 0
fi fi
# Prepare environment # Prepare environment
@ -82,7 +89,7 @@ case "$1" in
# Start Daemon # Start Daemon
cd $GRAFANA_HOME cd $GRAFANA_HOME
su -s /bin/sh -c "nohup ${DAEMON} ${DAEMON_OPTS} >> /dev/null 3>&1 &" $GRAFANA_USER su -s /bin/sh -c "nohup ${DAEMON} ${DAEMON_OPTS} >> /dev/null 3>&1 &" $GRAFANA_USER 2> /dev/null
return=$? return=$?
if [ $return -eq 0 ] if [ $return -eq 0 ]
then then
@ -90,7 +97,7 @@ case "$1" in
# check if pid file has been written two # check if pid file has been written two
if ! [[ -s $PID_FILE ]]; then if ! [[ -s $PID_FILE ]]; then
echo "FAILED" echo "FAILED"
exit 3 exit 1
fi fi
i=0 i=0
timeout=10 timeout=10
@ -101,7 +108,7 @@ case "$1" in
i=$(($i + 1)) i=$(($i + 1))
if [ $i -gt $timeout ]; then if [ $i -gt $timeout ]; then
echo "FAILED" echo "FAILED"
exit 4 exit 1
fi fi
done done
fi fi
@ -131,6 +138,7 @@ case "$1" in
;; ;;
status) status)
status -p $PID_FILE $NAME status -p $PID_FILE $NAME
exit $?
;; ;;
restart|force-reload) restart|force-reload)
if [ -f "$PID_FILE" ]; then if [ -f "$PID_FILE" ]; then
@ -141,8 +149,6 @@ case "$1" in
;; ;;
*) *)
echo -n "Usage: $0 {start|stop|restart|force-reload|status}" echo -n "Usage: $0 {start|stop|restart|force-reload|status}"
exit 1 exit 3
;; ;;
esac esac
exit 0

View File

@ -17,7 +17,7 @@ func AdminGetSettings(c *middleware.Context) {
for _, key := range section.Keys() { for _, key := range section.Keys() {
keyName := key.Name() keyName := key.Name()
value := key.Value() value := key.Value()
if strings.Contains(keyName, "secret") || strings.Contains(keyName, "password") { if strings.Contains(keyName, "secret") || strings.Contains(keyName, "password") || (strings.Contains(keyName, "provider_config") && strings.Contains(value, "@")) {
value = "************" value = "************"
} }

View File

@ -9,36 +9,6 @@ import (
"github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/util"
) )
func AdminSearchUsers(c *middleware.Context) {
query := m.SearchUsersQuery{Query: "", Page: 0, Limit: 1000}
if err := bus.Dispatch(&query); err != nil {
c.JsonApiErr(500, "Failed to fetch users", err)
return
}
c.JSON(200, query.Result)
}
func AdminGetUser(c *middleware.Context) {
userId := c.ParamsInt64(":id")
query := m.GetUserByIdQuery{Id: userId}
if err := bus.Dispatch(&query); err != nil {
c.JsonApiErr(500, "Failed to fetch user", err)
return
}
result := dtos.AdminUserListItem{
Name: query.Result.Name,
Email: query.Result.Email,
Login: query.Result.Login,
IsGrafanaAdmin: query.Result.IsAdmin,
}
c.JSON(200, result)
}
func AdminCreateUser(c *middleware.Context, form dtos.AdminCreateUserForm) { func AdminCreateUser(c *middleware.Context, form dtos.AdminCreateUserForm) {
cmd := m.CreateUserCommand{ cmd := m.CreateUserCommand{
Login: form.Login, Login: form.Login,
@ -70,32 +40,6 @@ func AdminCreateUser(c *middleware.Context, form dtos.AdminCreateUserForm) {
c.JsonOK("User created") c.JsonOK("User created")
} }
func AdminUpdateUser(c *middleware.Context, form dtos.AdminUpdateUserForm) {
userId := c.ParamsInt64(":id")
cmd := m.UpdateUserCommand{
UserId: userId,
Login: form.Login,
Email: form.Email,
Name: form.Name,
}
if len(cmd.Login) == 0 {
cmd.Login = cmd.Email
if len(cmd.Login) == 0 {
c.JsonApiErr(400, "Validation error, need specify either username or email", nil)
return
}
}
if err := bus.Dispatch(&cmd); err != nil {
c.JsonApiErr(500, "failed to update user", err)
return
}
c.JsonOK("User updated")
}
func AdminUpdateUserPassword(c *middleware.Context, form dtos.AdminUpdateUserPasswordForm) { func AdminUpdateUserPassword(c *middleware.Context, form dtos.AdminUpdateUserPasswordForm) {
userId := c.ParamsInt64(":id") userId := c.ParamsInt64(":id")

View File

@ -13,7 +13,7 @@ func Register(r *macaron.Macaron) {
reqSignedIn := middleware.Auth(&middleware.AuthOptions{ReqSignedIn: true}) reqSignedIn := middleware.Auth(&middleware.AuthOptions{ReqSignedIn: true})
reqGrafanaAdmin := middleware.Auth(&middleware.AuthOptions{ReqSignedIn: true, ReqGrafanaAdmin: true}) reqGrafanaAdmin := middleware.Auth(&middleware.AuthOptions{ReqSignedIn: true, ReqGrafanaAdmin: true})
reqEditorRole := middleware.RoleAuth(m.ROLE_EDITOR, m.ROLE_ADMIN) reqEditorRole := middleware.RoleAuth(m.ROLE_EDITOR, m.ROLE_ADMIN)
reqAccountAdmin := middleware.RoleAuth(m.ROLE_ADMIN) regOrgAdmin := middleware.RoleAuth(m.ROLE_ADMIN)
bind := binding.Bind bind := binding.Bind
// not logged in views // not logged in views
@ -26,6 +26,7 @@ func Register(r *macaron.Macaron) {
// authed views // authed views
r.Get("/profile/", reqSignedIn, Index) r.Get("/profile/", reqSignedIn, Index)
r.Get("/org/", reqSignedIn, Index) r.Get("/org/", reqSignedIn, Index)
r.Get("/org/new", reqSignedIn, Index)
r.Get("/datasources/", reqSignedIn, Index) r.Get("/datasources/", reqSignedIn, Index)
r.Get("/datasources/edit/*", reqSignedIn, Index) r.Get("/datasources/edit/*", reqSignedIn, Index)
r.Get("/org/users/", reqSignedIn, Index) r.Get("/org/users/", reqSignedIn, Index)
@ -39,7 +40,14 @@ func Register(r *macaron.Macaron) {
// sign up // sign up
r.Get("/signup", Index) r.Get("/signup", Index)
r.Post("/api/user/signup", bind(m.CreateUserCommand{}), SignUp) r.Post("/api/user/signup", bind(m.CreateUserCommand{}), wrap(SignUp))
// reset password
r.Get("/user/password/send-reset-email", Index)
r.Get("/user/password/reset", Index)
r.Post("/api/user/password/send-reset-email", bind(dtos.SendResetPasswordEmailForm{}), wrap(SendResetPasswordEmail))
r.Post("/api/user/password/reset", bind(dtos.ResetUserPasswordForm{}), wrap(ResetPassword))
// dashboard snapshots // dashboard snapshots
r.Post("/api/snapshots/", bind(m.CreateDashboardSnapshotCommand{}), CreateDashboardSnapshot) r.Post("/api/snapshots/", bind(m.CreateDashboardSnapshotCommand{}), CreateDashboardSnapshot)
@ -53,53 +61,79 @@ func Register(r *macaron.Macaron) {
// authed api // authed api
r.Group("/api", func() { r.Group("/api", func() {
// user
// user (signed in)
r.Group("/user", func() { r.Group("/user", func() {
r.Get("/", GetUser) r.Get("/", wrap(GetSignedInUser))
r.Put("/", bind(m.UpdateUserCommand{}), UpdateUser) r.Put("/", bind(m.UpdateUserCommand{}), wrap(UpdateSignedInUser))
r.Post("/using/:id", UserSetUsingOrg) r.Post("/using/:id", wrap(UserSetUsingOrg))
r.Get("/orgs", GetUserOrgList) r.Get("/orgs", wrap(GetSignedInUserOrgList))
r.Post("/stars/dashboard/:id", StarDashboard) r.Post("/stars/dashboard/:id", wrap(StarDashboard))
r.Delete("/stars/dashboard/:id", UnstarDashboard) r.Delete("/stars/dashboard/:id", wrap(UnstarDashboard))
r.Put("/password", bind(m.ChangeUserPasswordCommand{}), ChangeUserPassword) r.Put("/password", bind(m.ChangeUserPasswordCommand{}), wrap(ChangeUserPassword))
}) })
// account // users (admin permission required)
r.Group("/users", func() {
r.Get("/", wrap(SearchUsers))
r.Get("/:id", wrap(GetUserById))
r.Get("/:id/orgs", wrap(GetUserOrgList))
r.Put("/:id", bind(m.UpdateUserCommand{}), wrap(UpdateUser))
}, reqGrafanaAdmin)
// current org
r.Group("/org", func() { r.Group("/org", func() {
r.Get("/", GetOrg) r.Get("/", wrap(GetOrgCurrent))
r.Post("/", bind(m.CreateOrgCommand{}), CreateOrg) r.Put("/", bind(m.UpdateOrgCommand{}), wrap(UpdateOrgCurrent))
r.Put("/", bind(m.UpdateOrgCommand{}), UpdateOrg) r.Post("/users", bind(m.AddOrgUserCommand{}), wrap(AddOrgUserToCurrentOrg))
r.Post("/users", bind(m.AddOrgUserCommand{}), AddOrgUser) r.Get("/users", wrap(GetOrgUsersForCurrentOrg))
r.Get("/users", GetOrgUsers) r.Patch("/users/:userId", bind(m.UpdateOrgUserCommand{}), wrap(UpdateOrgUserForCurrentOrg))
r.Delete("/users/:id", RemoveOrgUser) r.Delete("/users/:userId", wrap(RemoveOrgUserForCurrentOrg))
}, reqAccountAdmin) }, regOrgAdmin)
// create new org
r.Post("/orgs", bind(m.CreateOrgCommand{}), wrap(CreateOrg))
// search all orgs
r.Get("/orgs", reqGrafanaAdmin, wrap(SearchOrgs))
// orgs (admin routes)
r.Group("/orgs/:orgId", func() {
r.Put("/", bind(m.UpdateOrgCommand{}), wrap(UpdateOrg))
r.Get("/users", wrap(GetOrgUsers))
r.Post("/users", bind(m.AddOrgUserCommand{}), wrap(AddOrgUser))
r.Patch("/users/:userId", bind(m.UpdateOrgUserCommand{}), wrap(UpdateOrgUser))
r.Delete("/users/:userId", wrap(RemoveOrgUser))
}, reqGrafanaAdmin)
// auth api keys // auth api keys
r.Group("/auth/keys", func() { r.Group("/auth/keys", func() {
r.Get("/", GetApiKeys) r.Get("/", wrap(GetApiKeys))
r.Post("/", bind(m.AddApiKeyCommand{}), AddApiKey) r.Post("/", bind(m.AddApiKeyCommand{}), wrap(AddApiKey))
r.Delete("/:id", DeleteApiKey) r.Delete("/:id", wrap(DeleteApiKey))
}, reqAccountAdmin) }, regOrgAdmin)
// Data sources // Data sources
r.Group("/datasources", func() { r.Group("/datasources", func() {
r.Combo("/"). r.Get("/", GetDataSources)
Get(GetDataSources). r.Post("/", bind(m.AddDataSourceCommand{}), AddDataSource)
Put(bind(m.AddDataSourceCommand{}), AddDataSource). r.Put("/:id", bind(m.UpdateDataSourceCommand{}), UpdateDataSource)
Post(bind(m.UpdateDataSourceCommand{}), UpdateDataSource)
r.Delete("/:id", DeleteDataSource) r.Delete("/:id", DeleteDataSource)
r.Get("/:id", GetDataSourceById) r.Get("/:id", GetDataSourceById)
r.Get("/plugins", GetDataSourcePlugins) r.Get("/plugins", GetDataSourcePlugins)
}, reqAccountAdmin) }, regOrgAdmin)
r.Get("/frontend/settings/", GetFrontendSettings) r.Get("/frontend/settings/", GetFrontendSettings)
r.Any("/datasources/proxy/:id/*", reqSignedIn, ProxyDataSourceRequest) r.Any("/datasources/proxy/:id/*", reqSignedIn, ProxyDataSourceRequest)
r.Any("/datasources/proxy/:id", reqSignedIn, ProxyDataSourceRequest)
// Dashboard // Dashboard
r.Group("/dashboards", func() { r.Group("/dashboards", func() {
r.Combo("/db/:slug").Get(GetDashboard).Delete(DeleteDashboard) r.Combo("/db/:slug").Get(GetDashboard).Delete(DeleteDashboard)
r.Post("/db", reqEditorRole, bind(m.SaveDashboardCommand{}), PostDashboard) r.Post("/db", reqEditorRole, bind(m.SaveDashboardCommand{}), PostDashboard)
r.Get("/file/:file", GetDashboardFromJsonFile)
r.Get("/home", GetHomeDashboard) r.Get("/home", GetHomeDashboard)
r.Get("/tags", GetDashboardTags)
}) })
// Search // Search
@ -112,10 +146,7 @@ func Register(r *macaron.Macaron) {
// admin api // admin api
r.Group("/api/admin", func() { r.Group("/api/admin", func() {
r.Get("/settings", AdminGetSettings) r.Get("/settings", AdminGetSettings)
r.Get("/users", AdminSearchUsers)
r.Get("/users/:id", AdminGetUser)
r.Post("/users", bind(dtos.AdminCreateUserForm{}), AdminCreateUser) r.Post("/users", bind(dtos.AdminCreateUserForm{}), AdminCreateUser)
r.Put("/users/:id/details", bind(dtos.AdminUpdateUserForm{}), AdminUpdateUser)
r.Put("/users/:id/password", bind(dtos.AdminUpdateUserPasswordForm{}), AdminUpdateUserPassword) r.Put("/users/:id/password", bind(dtos.AdminUpdateUserPasswordForm{}), AdminUpdateUserPassword)
r.Put("/users/:id/permissions", bind(dtos.AdminUpdateUserPermissionsForm{}), AdminUpdateUserPermissions) r.Put("/users/:id/permissions", bind(dtos.AdminUpdateUserPermissionsForm{}), AdminUpdateUserPermissions)
r.Delete("/users/:id", AdminDeleteUser) r.Delete("/users/:id", AdminDeleteUser)
@ -124,5 +155,5 @@ func Register(r *macaron.Macaron) {
// rendering // rendering
r.Get("/render/*", reqSignedIn, RenderToPng) r.Get("/render/*", reqSignedIn, RenderToPng)
r.NotFound(NotFound) r.NotFound(NotFoundHandler)
} }

35
pkg/api/api_test.go Normal file
View File

@ -0,0 +1,35 @@
package api
import (
"testing"
)
func TestHttpApi(t *testing.T) {
// Convey("Given the grafana api", t, func() {
// ConveyApiScenario("Can sign up", func(c apiTestContext) {
// c.PostJson()
// So(c.Resp, ShouldEqualJsonApiResponse, "User created and logged in")
// })
//
// m := macaron.New()
// m.Use(middleware.GetContextHandler())
// m.Use(middleware.Sessioner(&session.Options{}))
// Register(m)
//
// var context *middleware.Context
// m.Get("/", func(c *middleware.Context) {
// context = c
// })
//
// resp := httptest.NewRecorder()
// req, err := http.NewRequest("GET", "/", nil)
// So(err, ShouldBeNil)
//
// m.ServeHTTP(resp, req)
//
// Convey("should red 200", func() {
// So(resp.Code, ShouldEqual, 200)
// })
// })
}

View File

@ -8,12 +8,11 @@ import (
m "github.com/grafana/grafana/pkg/models" m "github.com/grafana/grafana/pkg/models"
) )
func GetApiKeys(c *middleware.Context) { func GetApiKeys(c *middleware.Context) Response {
query := m.GetApiKeysQuery{OrgId: c.OrgId} query := m.GetApiKeysQuery{OrgId: c.OrgId}
if err := bus.Dispatch(&query); err != nil { if err := bus.Dispatch(&query); err != nil {
c.JsonApiErr(500, "Failed to list api keys", err) return ApiError(500, "Failed to list api keys", err)
return
} }
result := make([]*m.ApiKeyDTO, len(query.Result)) result := make([]*m.ApiKeyDTO, len(query.Result))
@ -24,27 +23,26 @@ func GetApiKeys(c *middleware.Context) {
Role: t.Role, Role: t.Role,
} }
} }
c.JSON(200, result)
return Json(200, result)
} }
func DeleteApiKey(c *middleware.Context) { func DeleteApiKey(c *middleware.Context) Response {
id := c.ParamsInt64(":id") id := c.ParamsInt64(":id")
cmd := &m.DeleteApiKeyCommand{Id: id, OrgId: c.OrgId} cmd := &m.DeleteApiKeyCommand{Id: id, OrgId: c.OrgId}
err := bus.Dispatch(cmd) err := bus.Dispatch(cmd)
if err != nil { if err != nil {
c.JsonApiErr(500, "Failed to delete API key", err) return ApiError(500, "Failed to delete API key", err)
return
} }
c.JsonOK("API key deleted") return ApiSuccess("API key deleted")
} }
func AddApiKey(c *middleware.Context, cmd m.AddApiKeyCommand) { func AddApiKey(c *middleware.Context, cmd m.AddApiKeyCommand) Response {
if !cmd.Role.IsValid() { if !cmd.Role.IsValid() {
c.JsonApiErr(400, "Invalid role specified", nil) return ApiError(400, "Invalid role specified", nil)
return
} }
cmd.OrgId = c.OrgId cmd.OrgId = c.OrgId
@ -53,14 +51,12 @@ func AddApiKey(c *middleware.Context, cmd m.AddApiKeyCommand) {
cmd.Key = newKeyInfo.HashedKey cmd.Key = newKeyInfo.HashedKey
if err := bus.Dispatch(&cmd); err != nil { if err := bus.Dispatch(&cmd); err != nil {
c.JsonApiErr(500, "Failed to add API key", err) return ApiError(500, "Failed to add API key", err)
return
} }
result := &dtos.NewApiKeyResult{ result := &dtos.NewApiKeyResult{
Name: cmd.Result.Name, Name: cmd.Result.Name,
Key: newKeyInfo.ClientSecret, Key: newKeyInfo.ClientSecret}
}
c.JSON(200, result) return Json(200, result)
} }

122
pkg/api/common.go Normal file
View File

@ -0,0 +1,122 @@
package api
import (
"encoding/json"
"net/http"
"github.com/Unknwon/macaron"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/metrics"
"github.com/grafana/grafana/pkg/middleware"
"github.com/grafana/grafana/pkg/setting"
)
var (
NotFound = ApiError(404, "Not found", nil)
ServerError = ApiError(500, "Server error", nil)
)
type Response interface {
WriteTo(out http.ResponseWriter)
}
type NormalResponse struct {
status int
body []byte
header http.Header
}
func wrap(action interface{}) macaron.Handler {
return func(c *middleware.Context) {
var res Response
val, err := c.Invoke(action)
if err == nil && val != nil && len(val) > 0 {
res = val[0].Interface().(Response)
} else {
res = ServerError
}
res.WriteTo(c.Resp)
}
}
func (r *NormalResponse) WriteTo(out http.ResponseWriter) {
header := out.Header()
for k, v := range r.header {
header[k] = v
}
out.WriteHeader(r.status)
out.Write(r.body)
}
func (r *NormalResponse) Cache(ttl string) *NormalResponse {
return r.Header("Cache-Control", "public,max-age="+ttl)
}
func (r *NormalResponse) Header(key, value string) *NormalResponse {
r.header.Set(key, value)
return r
}
// functions to create responses
func Empty(status int) *NormalResponse {
return Respond(status, nil)
}
func Json(status int, body interface{}) *NormalResponse {
return Respond(status, body).Header("Content-Type", "application/json")
}
func ApiSuccess(message string) *NormalResponse {
resp := make(map[string]interface{})
resp["message"] = message
return Respond(200, resp)
}
func ApiError(status int, message string, err error) *NormalResponse {
resp := make(map[string]interface{})
if err != nil {
log.Error(4, "%s: %v", message, err)
if setting.Env != setting.PROD {
resp["error"] = err.Error()
}
}
switch status {
case 404:
metrics.M_Api_Status_404.Inc(1)
resp["message"] = "Not Found"
case 500:
metrics.M_Api_Status_500.Inc(1)
resp["message"] = "Internal Server Error"
}
if message != "" {
resp["message"] = message
}
return Json(status, resp)
}
func Respond(status int, body interface{}) *NormalResponse {
var b []byte
var err error
switch t := body.(type) {
case []byte:
b = t
case string:
b = []byte(t)
default:
if b, err = json.Marshal(body); err != nil {
return ApiError(500, "body json marshal", err)
}
}
return &NormalResponse{
body: b,
status: status,
header: make(http.Header),
}
}

View File

@ -4,12 +4,14 @@ import (
"encoding/json" "encoding/json"
"os" "os"
"path" "path"
"strings"
"github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/metrics" "github.com/grafana/grafana/pkg/metrics"
"github.com/grafana/grafana/pkg/middleware" "github.com/grafana/grafana/pkg/middleware"
m "github.com/grafana/grafana/pkg/models" m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/search"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/util"
) )
@ -30,7 +32,7 @@ func isDasboardStarredByUser(c *middleware.Context, dashId int64) (bool, error)
func GetDashboard(c *middleware.Context) { func GetDashboard(c *middleware.Context) {
metrics.M_Api_Dashboard_Get.Inc(1) metrics.M_Api_Dashboard_Get.Inc(1)
slug := c.Params(":slug") slug := strings.ToLower(c.Params(":slug"))
query := m.GetDashboardQuery{Slug: slug, OrgId: c.OrgId} query := m.GetDashboardQuery{Slug: slug, OrgId: c.OrgId}
err := bus.Dispatch(&query) err := bus.Dispatch(&query)
@ -46,9 +48,16 @@ func GetDashboard(c *middleware.Context) {
} }
dash := query.Result dash := query.Result
dto := dtos.Dashboard{ dto := dtos.DashboardFullWithMeta{
Model: dash.Data, Dashboard: dash.Data,
Meta: dtos.DashboardMeta{IsStarred: isStarred, Slug: slug}, Meta: dtos.DashboardMeta{
IsStarred: isStarred,
Slug: slug,
Type: m.DashTypeDB,
CanStar: c.IsSignedIn,
CanSave: c.OrgRole == m.ROLE_ADMIN || c.OrgRole == m.ROLE_EDITOR,
CanEdit: c.OrgRole == m.ROLE_ADMIN || c.OrgRole == m.ROLE_EDITOR || c.OrgRole == m.ROLE_READ_ONLY_EDITOR,
},
} }
c.JSON(200, dto) c.JSON(200, dto)
@ -87,6 +96,10 @@ func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) {
c.JSON(412, util.DynMap{"status": "version-mismatch", "message": err.Error()}) c.JSON(412, util.DynMap{"status": "version-mismatch", "message": err.Error()})
return return
} }
if err == m.ErrDashboardNotFound {
c.JSON(404, util.DynMap{"status": "not-found", "message": err.Error()})
return
}
c.JsonApiErr(500, "Failed to save dashboard", err) c.JsonApiErr(500, "Failed to save dashboard", err)
return return
} }
@ -104,13 +117,39 @@ func GetHomeDashboard(c *middleware.Context) {
return return
} }
dash := dtos.Dashboard{} dash := dtos.DashboardFullWithMeta{}
dash.Meta.IsHome = true dash.Meta.IsHome = true
jsonParser := json.NewDecoder(file) jsonParser := json.NewDecoder(file)
if err := jsonParser.Decode(&dash.Model); err != nil { if err := jsonParser.Decode(&dash.Dashboard); err != nil {
c.JsonApiErr(500, "Failed to load home dashboard", err) c.JsonApiErr(500, "Failed to load home dashboard", err)
return return
} }
c.JSON(200, &dash) c.JSON(200, &dash)
} }
func GetDashboardFromJsonFile(c *middleware.Context) {
file := c.Params(":file")
dashboard := search.GetDashboardFromJsonIndex(file)
if dashboard == nil {
c.JsonApiErr(404, "Dashboard not found", nil)
return
}
dash := dtos.DashboardFullWithMeta{Dashboard: dashboard.Data}
dash.Meta.Type = m.DashTypeJson
c.JSON(200, &dash)
}
func GetDashboardTags(c *middleware.Context) {
query := m.GetDashboardTagsQuery{OrgId: c.OrgId}
err := bus.Dispatch(&query)
if err != nil {
c.JsonApiErr(500, "Failed to get tags from database", err)
return
}
c.JSON(200, query.Result)
}

View File

@ -45,8 +45,8 @@ func CreateDashboardSnapshot(c *middleware.Context, cmd m.CreateDashboardSnapsho
} }
func GetDashboardSnapshot(c *middleware.Context) { func GetDashboardSnapshot(c *middleware.Context) {
key := c.Params(":key")
key := c.Params(":key")
query := &m.GetDashboardSnapshotQuery{Key: key} query := &m.GetDashboardSnapshotQuery{Key: key}
err := bus.Dispatch(query) err := bus.Dispatch(query)
@ -59,13 +59,14 @@ func GetDashboardSnapshot(c *middleware.Context) {
// expired snapshots should also be removed from db // expired snapshots should also be removed from db
if snapshot.Expires.Before(time.Now()) { if snapshot.Expires.Before(time.Now()) {
c.JsonApiErr(404, "Snapshot not found", err) c.JsonApiErr(404, "Dashboard snapshot not found", err)
return return
} }
dto := dtos.Dashboard{ dto := dtos.DashboardFullWithMeta{
Model: snapshot.Dashboard, Dashboard: snapshot.Dashboard,
Meta: dtos.DashboardMeta{ Meta: dtos.DashboardMeta{
Type: m.DashTypeSnapshot,
IsSnapshot: true, IsSnapshot: true,
Created: snapshot.Created, Created: snapshot.Created,
Expires: snapshot.Expires, Expires: snapshot.Expires,

View File

@ -1,9 +1,12 @@
package api package api
import ( import (
"crypto/tls"
"net"
"net/http" "net/http"
"net/http/httputil" "net/http/httputil"
"net/url" "net/url"
"time"
"github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/middleware" "github.com/grafana/grafana/pkg/middleware"
@ -11,6 +14,16 @@ import (
"github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/util"
) )
var dataProxyTransport = &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
Proxy: http.ProxyFromEnvironment,
Dial: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).Dial,
TLSHandshakeTimeout: 10 * time.Second,
}
func NewReverseProxy(ds *m.DataSource, proxyPath string) *httputil.ReverseProxy { func NewReverseProxy(ds *m.DataSource, proxyPath string) *httputil.ReverseProxy {
target, _ := url.Parse(ds.Url) target, _ := url.Parse(ds.Url)
@ -56,5 +69,6 @@ func ProxyDataSourceRequest(c *middleware.Context) {
proxyPath := c.Params("*") proxyPath := c.Params("*")
proxy := NewReverseProxy(&query.Result, proxyPath) proxy := NewReverseProxy(&query.Result, proxyPath)
proxy.Transport = dataProxyTransport
proxy.ServeHTTP(c.RW(), c.Req.Request) proxy.ServeHTTP(c.RW(), c.Req.Request)
} }

View File

@ -6,6 +6,7 @@ import (
"github.com/grafana/grafana/pkg/middleware" "github.com/grafana/grafana/pkg/middleware"
m "github.com/grafana/grafana/pkg/models" m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/util"
) )
func GetDataSources(c *middleware.Context) { func GetDataSources(c *middleware.Context) {
@ -94,11 +95,12 @@ func AddDataSource(c *middleware.Context, cmd m.AddDataSourceCommand) {
return return
} }
c.JsonOK("Datasource added") c.JSON(200, util.DynMap{"message": "Datasource added", "id": cmd.Result.Id})
} }
func UpdateDataSource(c *middleware.Context, cmd m.UpdateDataSourceCommand) { func UpdateDataSource(c *middleware.Context, cmd m.UpdateDataSourceCommand) {
cmd.OrgId = c.OrgId cmd.OrgId = c.OrgId
cmd.Id = c.ParamsInt64(":id")
err := bus.Dispatch(&cmd) err := bus.Dispatch(&cmd)
if err != nil { if err != nil {

View File

@ -21,24 +21,29 @@ type CurrentUser struct {
Email string `json:"email"` Email string `json:"email"`
Name string `json:"name"` Name string `json:"name"`
LightTheme bool `json:"lightTheme"` LightTheme bool `json:"lightTheme"`
OrgRole m.RoleType `json:"orgRole"` OrgId int64 `json:"orgId"`
OrgName string `json:"orgName"` OrgName string `json:"orgName"`
OrgRole m.RoleType `json:"orgRole"`
IsGrafanaAdmin bool `json:"isGrafanaAdmin"` IsGrafanaAdmin bool `json:"isGrafanaAdmin"`
GravatarUrl string `json:"gravatarUrl"` GravatarUrl string `json:"gravatarUrl"`
} }
type DashboardMeta struct { type DashboardMeta struct {
IsStarred bool `json:"isStarred"` IsStarred bool `json:"isStarred,omitempty"`
IsHome bool `json:"isHome"` IsHome bool `json:"isHome,omitempty"`
IsSnapshot bool `json:"isSnapshot"` IsSnapshot bool `json:"isSnapshot,omitempty"`
Type string `json:"type,omitempty"`
CanSave bool `json:"canSave"`
CanEdit bool `json:"canEdit"`
CanStar bool `json:"canStar"`
Slug string `json:"slug"` Slug string `json:"slug"`
Expires time.Time `json:"expires"` Expires time.Time `json:"expires"`
Created time.Time `json:"created"` Created time.Time `json:"created"`
} }
type Dashboard struct { type DashboardFullWithMeta struct {
Meta DashboardMeta `json:"meta"` Meta DashboardMeta `json:"meta"`
Model map[string]interface{} `json:"model"` Dashboard map[string]interface{} `json:"dashboard"`
} }
type DataSource struct { type DataSource struct {

View File

@ -18,7 +18,7 @@ type AdminUpdateUserPasswordForm struct {
} }
type AdminUpdateUserPermissionsForm struct { type AdminUpdateUserPermissionsForm struct {
IsGrafanaAdmin bool `json:"IsGrafanaAdmin" binding:"Required"` IsGrafanaAdmin bool `json:"IsGrafanaAdmin"`
} }
type AdminUserListItem struct { type AdminUserListItem struct {
@ -27,3 +27,13 @@ type AdminUserListItem struct {
Login string `json:"login"` Login string `json:"login"`
IsGrafanaAdmin bool `json:"isGrafanaAdmin"` IsGrafanaAdmin bool `json:"isGrafanaAdmin"`
} }
type SendResetPasswordEmailForm struct {
UserOrEmail string `json:"userOrEmail" binding:"Required"`
}
type ResetUserPasswordForm struct {
Code string `json:"code"`
NewPassword string `json:"newPassword"`
ConfirmPassword string `json:"confirmPassword"`
}

View File

@ -54,6 +54,10 @@ func getFrontendSettingsMap(c *middleware.Context) (map[string]interface{}, erro
defaultDatasource = ds.Name defaultDatasource = ds.Name
} }
if len(ds.JsonData) > 0 {
dsMap["jsonData"] = ds.JsonData
}
if ds.Access == m.DS_ACCESS_DIRECT { if ds.Access == m.DS_ACCESS_DIRECT {
if ds.BasicAuth { if ds.BasicAuth {
dsMap["basicAuth"] = util.GetBasicAuthHeader(ds.BasicAuthUser, ds.BasicAuthPassword) dsMap["basicAuth"] = util.GetBasicAuthHeader(ds.BasicAuthUser, ds.BasicAuthPassword)
@ -95,6 +99,7 @@ func getFrontendSettingsMap(c *middleware.Context) (map[string]interface{}, erro
"defaultDatasource": defaultDatasource, "defaultDatasource": defaultDatasource,
"datasources": datasources, "datasources": datasources,
"appSubUrl": setting.AppSubUrl, "appSubUrl": setting.AppSubUrl,
"allowOrgCreate": (setting.AllowUserOrgCreate && c.IsSignedIn) || c.IsGrafanaAdmin,
"buildInfo": map[string]interface{}{ "buildInfo": map[string]interface{}{
"version": setting.BuildVersion, "version": setting.BuildVersion,
"commit": setting.BuildCommit, "commit": setting.BuildCommit,

View File

@ -18,12 +18,17 @@ func setIndexViewData(c *middleware.Context) error {
Email: c.Email, Email: c.Email,
Name: c.Name, Name: c.Name,
LightTheme: c.Theme == "light", LightTheme: c.Theme == "light",
OrgId: c.OrgId,
OrgName: c.OrgName, OrgName: c.OrgName,
OrgRole: c.OrgRole, OrgRole: c.OrgRole,
GravatarUrl: dtos.GetGravatarUrl(c.Email), GravatarUrl: dtos.GetGravatarUrl(c.Email),
IsGrafanaAdmin: c.IsGrafanaAdmin, IsGrafanaAdmin: c.IsGrafanaAdmin,
} }
if setting.DisableGravatar {
currentUser.GravatarUrl = setting.AppSubUrl + "/img/user_profile.png"
}
if len(currentUser.Name) == 0 { if len(currentUser.Name) == 0 {
currentUser.Name = currentUser.Login currentUser.Name = currentUser.Login
} }
@ -54,7 +59,7 @@ func Index(c *middleware.Context) {
c.HTML(200, "index") c.HTML(200, "index")
} }
func NotFound(c *middleware.Context) { func NotFoundHandler(c *middleware.Context) {
if c.IsApiRequest() { if c.IsApiRequest() {
c.JsonApiErr(404, "Not found", nil) c.JsonApiErr(404, "Not found", nil)
return return

View File

@ -3,6 +3,7 @@ package api
import ( import (
"errors" "errors"
"fmt" "fmt"
"net/url"
"golang.org/x/oauth2" "golang.org/x/oauth2"
@ -45,7 +46,13 @@ func OAuthLogin(ctx *middleware.Context) {
userInfo, err := connect.UserInfo(token) userInfo, err := connect.UserInfo(token)
if err != nil { if err != nil {
ctx.Handle(500, fmt.Sprintf("login.OAuthLogin(get info from %s)", name), err) if err == social.ErrMissingTeamMembership {
ctx.Redirect(setting.AppSubUrl + "/login?failedMsg=" + url.QueryEscape("Required Github team membership not fulfilled"))
} else if err == social.ErrMissingOrganizationMembership {
ctx.Redirect(setting.AppSubUrl + "/login?failedMsg=" + url.QueryEscape("Required Github organization membership not fulfilled"))
} else {
ctx.Handle(500, fmt.Sprintf("login.OAuthLogin(get info from %s)", name), err)
}
return return
} }
@ -54,7 +61,7 @@ func OAuthLogin(ctx *middleware.Context) {
// validate that the email is allowed to login to grafana // validate that the email is allowed to login to grafana
if !connect.IsEmailAllowed(userInfo.Email) { if !connect.IsEmailAllowed(userInfo.Email) {
log.Info("OAuth login attempt with unallowed email, %s", userInfo.Email) log.Info("OAuth login attempt with unallowed email, %s", userInfo.Email)
ctx.Redirect(setting.AppSubUrl + "/login?email_not_allowed=1") ctx.Redirect(setting.AppSubUrl + "/login?failedMsg=" + url.QueryEscape("Required email domain not fulfilled"))
return return
} }

View File

@ -6,19 +6,28 @@ import (
"github.com/grafana/grafana/pkg/middleware" "github.com/grafana/grafana/pkg/middleware"
m "github.com/grafana/grafana/pkg/models" m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
) )
func GetOrg(c *middleware.Context) { // GET /api/org
query := m.GetOrgByIdQuery{Id: c.OrgId} func GetOrgCurrent(c *middleware.Context) Response {
return getOrgHelper(c.OrgId)
}
// GET /api/orgs/:orgId
func GetOrgById(c *middleware.Context) Response {
return getOrgHelper(c.ParamsInt64(":orgId"))
}
func getOrgHelper(orgId int64) Response {
query := m.GetOrgByIdQuery{Id: orgId}
if err := bus.Dispatch(&query); err != nil { if err := bus.Dispatch(&query); err != nil {
if err == m.ErrOrgNotFound { if err == m.ErrOrgNotFound {
c.JsonApiErr(404, "Organization not found", err) return ApiError(404, "Organization not found", err)
return
} }
c.JsonApiErr(500, "Failed to get organization", err) return ApiError(500, "Failed to get organization", err)
return
} }
org := m.OrgDTO{ org := m.OrgDTO{
@ -26,33 +35,59 @@ func GetOrg(c *middleware.Context) {
Name: query.Result.Name, Name: query.Result.Name,
} }
c.JSON(200, &org) return Json(200, &org)
} }
func CreateOrg(c *middleware.Context, cmd m.CreateOrgCommand) { // POST /api/orgs
if !setting.AllowUserOrgCreate && !c.IsGrafanaAdmin { func CreateOrg(c *middleware.Context, cmd m.CreateOrgCommand) Response {
c.JsonApiErr(401, "Access denied", nil) if !c.IsSignedIn || (!setting.AllowUserOrgCreate && !c.IsGrafanaAdmin) {
return return ApiError(401, "Access denied", nil)
} }
cmd.UserId = c.UserId cmd.UserId = c.UserId
if err := bus.Dispatch(&cmd); err != nil { if err := bus.Dispatch(&cmd); err != nil {
c.JsonApiErr(500, "Failed to create organization", err) return ApiError(500, "Failed to create organization", err)
return
} }
metrics.M_Api_Org_Create.Inc(1) metrics.M_Api_Org_Create.Inc(1)
c.JsonOK("Organization created") return Json(200, &util.DynMap{
"orgId": cmd.Result.Id,
"message": "Organization created",
})
} }
func UpdateOrg(c *middleware.Context, cmd m.UpdateOrgCommand) { // PUT /api/org
func UpdateOrgCurrent(c *middleware.Context, cmd m.UpdateOrgCommand) Response {
cmd.OrgId = c.OrgId cmd.OrgId = c.OrgId
return updateOrgHelper(cmd)
}
// PUT /api/orgs/:orgId
func UpdateOrg(c *middleware.Context, cmd m.UpdateOrgCommand) Response {
cmd.OrgId = c.ParamsInt64(":orgId")
return updateOrgHelper(cmd)
}
func updateOrgHelper(cmd m.UpdateOrgCommand) Response {
if err := bus.Dispatch(&cmd); err != nil { if err := bus.Dispatch(&cmd); err != nil {
c.JsonApiErr(500, "Failed to update organization", err) return ApiError(500, "Failed to update organization", err)
return
} }
c.JsonOK("Organization updated") return ApiSuccess("Organization updated")
}
func SearchOrgs(c *middleware.Context) Response {
query := m.SearchOrgsQuery{
Query: c.Query("query"),
Name: c.Query("name"),
Page: 0,
Limit: 1000,
}
if err := bus.Dispatch(&query); err != nil {
return ApiError(500, "Failed to search orgs", err)
}
return Json(200, query.Result)
} }

View File

@ -6,60 +6,115 @@ import (
m "github.com/grafana/grafana/pkg/models" m "github.com/grafana/grafana/pkg/models"
) )
func AddOrgUser(c *middleware.Context, cmd m.AddOrgUserCommand) { // POST /api/org/users
func AddOrgUserToCurrentOrg(c *middleware.Context, cmd m.AddOrgUserCommand) Response {
cmd.OrgId = c.OrgId
return addOrgUserHelper(cmd)
}
// POST /api/orgs/:orgId/users
func AddOrgUser(c *middleware.Context, cmd m.AddOrgUserCommand) Response {
cmd.OrgId = c.ParamsInt64(":orgId")
return addOrgUserHelper(cmd)
}
func addOrgUserHelper(cmd m.AddOrgUserCommand) Response {
if !cmd.Role.IsValid() { if !cmd.Role.IsValid() {
c.JsonApiErr(400, "Invalid role specified", nil) return ApiError(400, "Invalid role specified", nil)
return
} }
userQuery := m.GetUserByLoginQuery{LoginOrEmail: cmd.LoginOrEmail} userQuery := m.GetUserByLoginQuery{LoginOrEmail: cmd.LoginOrEmail}
err := bus.Dispatch(&userQuery) err := bus.Dispatch(&userQuery)
if err != nil { if err != nil {
c.JsonApiErr(404, "User not found", nil) return ApiError(404, "User not found", nil)
return
} }
userToAdd := userQuery.Result userToAdd := userQuery.Result
if userToAdd.Id == c.UserId { // if userToAdd.Id == c.UserId {
c.JsonApiErr(400, "Cannot add yourself as user", nil) // return ApiError(400, "Cannot add yourself as user", nil)
return // }
}
cmd.OrgId = c.OrgId
cmd.UserId = userToAdd.Id cmd.UserId = userToAdd.Id
if err := bus.Dispatch(&cmd); err != nil { if err := bus.Dispatch(&cmd); err != nil {
c.JsonApiErr(500, "Could not add user to organization", err) return ApiError(500, "Could not add user to organization", err)
return
} }
c.JsonOK("User added to organization") return ApiSuccess("User added to organization")
} }
func GetOrgUsers(c *middleware.Context) { // GET /api/org/users
query := m.GetOrgUsersQuery{OrgId: c.OrgId} func GetOrgUsersForCurrentOrg(c *middleware.Context) Response {
return getOrgUsersHelper(c.OrgId)
}
// GET /api/orgs/:orgId/users
func GetOrgUsers(c *middleware.Context) Response {
return getOrgUsersHelper(c.ParamsInt64(":orgId"))
}
func getOrgUsersHelper(orgId int64) Response {
query := m.GetOrgUsersQuery{OrgId: orgId}
if err := bus.Dispatch(&query); err != nil { if err := bus.Dispatch(&query); err != nil {
c.JsonApiErr(500, "Failed to get account user", err) return ApiError(500, "Failed to get account user", err)
return
} }
c.JSON(200, query.Result) return Json(200, query.Result)
} }
func RemoveOrgUser(c *middleware.Context) { // PATCH /api/org/users/:userId
userId := c.ParamsInt64(":id") func UpdateOrgUserForCurrentOrg(c *middleware.Context, cmd m.UpdateOrgUserCommand) Response {
cmd.OrgId = c.OrgId
cmd.UserId = c.ParamsInt64(":userId")
return updateOrgUserHelper(cmd)
}
cmd := m.RemoveOrgUserCommand{OrgId: c.OrgId, UserId: userId} // PATCH /api/orgs/:orgId/users/:userId
func UpdateOrgUser(c *middleware.Context, cmd m.UpdateOrgUserCommand) Response {
cmd.OrgId = c.ParamsInt64(":orgId")
cmd.UserId = c.ParamsInt64(":userId")
return updateOrgUserHelper(cmd)
}
func updateOrgUserHelper(cmd m.UpdateOrgUserCommand) Response {
if !cmd.Role.IsValid() {
return ApiError(400, "Invalid role specified", nil)
}
if err := bus.Dispatch(&cmd); err != nil { if err := bus.Dispatch(&cmd); err != nil {
if err == m.ErrLastOrgAdmin { if err == m.ErrLastOrgAdmin {
c.JsonApiErr(400, "Cannot remove last organization admin", nil) return ApiError(400, "Cannot change role so that there is no organization admin left", nil)
return
} }
c.JsonApiErr(500, "Failed to remove user from organization", err) return ApiError(500, "Failed update org user", err)
} }
c.JsonOK("User removed from organization") return ApiSuccess("Organization user updated")
}
// DELETE /api/org/users/:userId
func RemoveOrgUserForCurrentOrg(c *middleware.Context) Response {
userId := c.ParamsInt64(":userId")
return removeOrgUserHelper(c.OrgId, userId)
}
// DELETE /api/orgs/:orgId/users/:userId
func RemoveOrgUser(c *middleware.Context) Response {
userId := c.ParamsInt64(":userId")
orgId := c.ParamsInt64(":orgId")
return removeOrgUserHelper(orgId, userId)
}
func removeOrgUserHelper(orgId int64, userId int64) Response {
cmd := m.RemoveOrgUserCommand{OrgId: orgId, UserId: userId}
if err := bus.Dispatch(&cmd); err != nil {
if err == m.ErrLastOrgAdmin {
return ApiError(400, "Cannot remove last organization admin", nil)
}
return ApiError(500, "Failed to remove user from organization", err)
}
return ApiSuccess("User removed from organization")
} }

49
pkg/api/password.go Normal file
View File

@ -0,0 +1,49 @@
package api
import (
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/middleware"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/util"
)
func SendResetPasswordEmail(c *middleware.Context, form dtos.SendResetPasswordEmailForm) Response {
userQuery := m.GetUserByLoginQuery{LoginOrEmail: form.UserOrEmail}
if err := bus.Dispatch(&userQuery); err != nil {
return ApiError(404, "User does not exist", err)
}
emailCmd := m.SendResetPasswordEmailCommand{User: userQuery.Result}
if err := bus.Dispatch(&emailCmd); err != nil {
return ApiError(500, "Failed to send email", err)
}
return ApiSuccess("Email sent")
}
func ResetPassword(c *middleware.Context, form dtos.ResetUserPasswordForm) Response {
query := m.ValidateResetPasswordCodeQuery{Code: form.Code}
if err := bus.Dispatch(&query); err != nil {
if err == m.ErrInvalidEmailCode {
return ApiError(400, "Invalid or expired reset password code", nil)
}
return ApiError(500, "Unknown error validating email code", err)
}
if form.NewPassword != form.ConfirmPassword {
return ApiError(400, "Passwords do not match", nil)
}
cmd := m.ChangeUserPasswordCommand{}
cmd.UserId = query.Result.Id
cmd.NewPassword = util.EncodePassword(form.NewPassword, query.Result.Salt)
if err := bus.Dispatch(&cmd); err != nil {
return ApiError(500, "Failed to change user password", err)
}
return ApiSuccess("User password changed")
}

View File

@ -3,79 +3,33 @@ package api
import ( import (
"github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/middleware" "github.com/grafana/grafana/pkg/middleware"
m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/search"
) )
// TODO: this needs to be cached or improved somehow
func setIsStarredFlagOnSearchResults(c *middleware.Context, hits []*m.DashboardSearchHit) error {
if !c.IsSignedIn {
return nil
}
query := m.GetUserStarsQuery{UserId: c.UserId}
if err := bus.Dispatch(&query); err != nil {
return err
}
for _, dash := range hits {
if _, exists := query.Result[dash.Id]; exists {
dash.IsStarred = true
}
}
return nil
}
func Search(c *middleware.Context) { func Search(c *middleware.Context) {
query := c.Query("query") query := c.Query("query")
tag := c.Query("tag") tags := c.QueryStrings("tag")
tagcloud := c.Query("tagcloud")
starred := c.Query("starred") starred := c.Query("starred")
limit := c.QueryInt("limit") limit := c.QueryInt("limit")
if limit == 0 { if limit == 0 {
limit = 200 limit = 1000
} }
result := m.SearchResult{ searchQuery := search.Query{
Dashboards: []*m.DashboardSearchHit{}, Title: query,
Tags: []*m.DashboardTagCloudItem{}, Tags: tags,
UserId: c.UserId,
Limit: limit,
IsStarred: starred == "true",
OrgId: c.OrgId,
} }
if tagcloud == "true" { err := bus.Dispatch(&searchQuery)
if err != nil {
query := m.GetDashboardTagsQuery{OrgId: c.OrgId} c.JsonApiErr(500, "Search failed", err)
err := bus.Dispatch(&query) return
if err != nil {
c.JsonApiErr(500, "Failed to get tags from database", err)
return
}
result.Tags = query.Result
result.TagsOnly = true
} else {
query := m.SearchDashboardsQuery{
Title: query,
Tag: tag,
UserId: c.UserId,
Limit: limit,
IsStarred: starred == "true",
OrgId: c.OrgId,
}
err := bus.Dispatch(&query)
if err != nil {
c.JsonApiErr(500, "Search failed", err)
return
}
if err := setIsStarredFlagOnSearchResults(c, query.Result); err != nil {
c.JsonApiErr(500, "Failed to get user stars", err)
return
}
result.Dashboards = query.Result
} }
c.JSON(200, result) c.JSON(200, searchQuery.Result)
} }

View File

@ -2,6 +2,7 @@ package api
import ( import (
"github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/events"
"github.com/grafana/grafana/pkg/metrics" "github.com/grafana/grafana/pkg/metrics"
"github.com/grafana/grafana/pkg/middleware" "github.com/grafana/grafana/pkg/middleware"
m "github.com/grafana/grafana/pkg/models" m "github.com/grafana/grafana/pkg/models"
@ -9,24 +10,29 @@ import (
) )
// POST /api/user/signup // POST /api/user/signup
func SignUp(c *middleware.Context, cmd m.CreateUserCommand) { func SignUp(c *middleware.Context, cmd m.CreateUserCommand) Response {
if !setting.AllowUserSignUp { if !setting.AllowUserSignUp {
c.JsonApiErr(401, "User signup is disabled", nil) return ApiError(401, "User signup is disabled", nil)
return
} }
cmd.Login = cmd.Email cmd.Login = cmd.Email
if err := bus.Dispatch(&cmd); err != nil { if err := bus.Dispatch(&cmd); err != nil {
c.JsonApiErr(500, "failed to create user", err) return ApiError(500, "failed to create user", err)
return
} }
user := cmd.Result user := cmd.Result
bus.Publish(&events.UserSignedUp{
Id: user.Id,
Name: user.Name,
Email: user.Email,
Login: user.Login,
})
loginUserWithUser(&user, c) loginUserWithUser(&user, c)
c.JsonOK("User created and logged in")
metrics.M_Api_User_SignUp.Inc(1) metrics.M_Api_User_SignUp.Inc(1)
return ApiSuccess("User created and logged in")
} }

View File

@ -6,40 +6,35 @@ import (
m "github.com/grafana/grafana/pkg/models" m "github.com/grafana/grafana/pkg/models"
) )
func StarDashboard(c *middleware.Context) { func StarDashboard(c *middleware.Context) Response {
var cmd = m.StarDashboardCommand{ if !c.IsSignedIn {
UserId: c.UserId, return ApiError(412, "You need to sign in to star dashboards", nil)
DashboardId: c.ParamsInt64(":id"),
} }
cmd := m.StarDashboardCommand{UserId: c.UserId, DashboardId: c.ParamsInt64(":id")}
if cmd.DashboardId <= 0 { if cmd.DashboardId <= 0 {
c.JsonApiErr(400, "Missing dashboard id", nil) return ApiError(400, "Missing dashboard id", nil)
return
} }
if err := bus.Dispatch(&cmd); err != nil { if err := bus.Dispatch(&cmd); err != nil {
c.JsonApiErr(500, "Failed to star dashboard", err) return ApiError(500, "Failed to star dashboard", err)
return
} }
c.JsonOK("Dashboard starred!") return ApiSuccess("Dashboard starred!")
} }
func UnstarDashboard(c *middleware.Context) { func UnstarDashboard(c *middleware.Context) Response {
var cmd = m.UnstarDashboardCommand{
UserId: c.UserId, cmd := m.UnstarDashboardCommand{UserId: c.UserId, DashboardId: c.ParamsInt64(":id")}
DashboardId: c.ParamsInt64(":id"),
}
if cmd.DashboardId <= 0 { if cmd.DashboardId <= 0 {
c.JsonApiErr(400, "Missing dashboard id", nil) return ApiError(400, "Missing dashboard id", nil)
return
} }
if err := bus.Dispatch(&cmd); err != nil { if err := bus.Dispatch(&cmd); err != nil {
c.JsonApiErr(500, "Failed to unstar dashboard", err) return ApiError(500, "Failed to unstar dashboard", err)
return
} }
c.JsonOK("Dashboard unstarred") return ApiSuccess("Dashboard unstarred")
} }

View File

@ -7,44 +7,71 @@ import (
"github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/util"
) )
func GetUser(c *middleware.Context) { // GET /api/user (current authenticated user)
query := m.GetUserProfileQuery{UserId: c.UserId} func GetSignedInUser(c *middleware.Context) Response {
return getUserUserProfile(c.UserId)
if err := bus.Dispatch(&query); err != nil {
c.JsonApiErr(500, "Failed to get user", err)
return
}
c.JSON(200, query.Result)
} }
func UpdateUser(c *middleware.Context, cmd m.UpdateUserCommand) { // GET /api/user/:id
func GetUserById(c *middleware.Context) Response {
return getUserUserProfile(c.ParamsInt64(":id"))
}
func getUserUserProfile(userId int64) Response {
query := m.GetUserProfileQuery{UserId: userId}
if err := bus.Dispatch(&query); err != nil {
return ApiError(500, "Failed to get user", err)
}
return Json(200, query.Result)
}
// POST /api/user
func UpdateSignedInUser(c *middleware.Context, cmd m.UpdateUserCommand) Response {
cmd.UserId = c.UserId cmd.UserId = c.UserId
return handleUpdateUser(cmd)
if err := bus.Dispatch(&cmd); err != nil {
c.JsonApiErr(400, "Failed to update user", err)
return
}
c.JsonOK("User updated")
} }
func GetUserOrgList(c *middleware.Context) { // POST /api/users/:id
query := m.GetUserOrgListQuery{UserId: c.UserId} func UpdateUser(c *middleware.Context, cmd m.UpdateUserCommand) Response {
cmd.UserId = c.ParamsInt64(":id")
return handleUpdateUser(cmd)
}
if err := bus.Dispatch(&query); err != nil { func handleUpdateUser(cmd m.UpdateUserCommand) Response {
c.JsonApiErr(500, "Failed to get user organizations", err) if len(cmd.Login) == 0 {
return cmd.Login = cmd.Email
} if len(cmd.Login) == 0 {
return ApiError(400, "Validation error, need specify either username or email", nil)
for _, ac := range query.Result {
if ac.OrgId == c.OrgId {
ac.IsUsing = true
break
} }
} }
c.JSON(200, query.Result) if err := bus.Dispatch(&cmd); err != nil {
return ApiError(500, "failed to update user", err)
}
return ApiSuccess("User updated")
}
// GET /api/user/orgs
func GetSignedInUserOrgList(c *middleware.Context) Response {
return getUserOrgList(c.UserId)
}
// GET /api/user/:id/orgs
func GetUserOrgList(c *middleware.Context) Response {
return getUserOrgList(c.ParamsInt64(":id"))
}
func getUserOrgList(userId int64) Response {
query := m.GetUserOrgListQuery{UserId: userId}
if err := bus.Dispatch(&query); err != nil {
return ApiError(500, "Faile to get user organziations", err)
}
return Json(200, query.Result)
} }
func validateUsingOrg(userId int64, orgId int64) bool { func validateUsingOrg(userId int64, orgId int64) bool {
@ -65,53 +92,55 @@ func validateUsingOrg(userId int64, orgId int64) bool {
return valid return valid
} }
func UserSetUsingOrg(c *middleware.Context) { // POST /api/user/using/:id
func UserSetUsingOrg(c *middleware.Context) Response {
orgId := c.ParamsInt64(":id") orgId := c.ParamsInt64(":id")
if !validateUsingOrg(c.UserId, orgId) { if !validateUsingOrg(c.UserId, orgId) {
c.JsonApiErr(401, "Not a valid organization", nil) return ApiError(401, "Not a valid organization", nil)
return
} }
cmd := m.SetUsingOrgCommand{ cmd := m.SetUsingOrgCommand{UserId: c.UserId, OrgId: orgId}
UserId: c.UserId,
OrgId: orgId,
}
if err := bus.Dispatch(&cmd); err != nil { if err := bus.Dispatch(&cmd); err != nil {
c.JsonApiErr(500, "Failed change active organization", err) return ApiError(500, "Failed change active organization", err)
return
} }
c.JsonOK("Active organization changed") return ApiSuccess("Active organization changed")
} }
func ChangeUserPassword(c *middleware.Context, cmd m.ChangeUserPasswordCommand) { func ChangeUserPassword(c *middleware.Context, cmd m.ChangeUserPasswordCommand) Response {
userQuery := m.GetUserByIdQuery{Id: c.UserId} userQuery := m.GetUserByIdQuery{Id: c.UserId}
if err := bus.Dispatch(&userQuery); err != nil { if err := bus.Dispatch(&userQuery); err != nil {
c.JsonApiErr(500, "Could not read user from database", err) return ApiError(500, "Could not read user from database", err)
return
} }
passwordHashed := util.EncodePassword(cmd.OldPassword, userQuery.Result.Salt) passwordHashed := util.EncodePassword(cmd.OldPassword, userQuery.Result.Salt)
if passwordHashed != userQuery.Result.Password { if passwordHashed != userQuery.Result.Password {
c.JsonApiErr(401, "Invalid old password", nil) return ApiError(401, "Invalid old password", nil)
return
} }
if len(cmd.NewPassword) < 4 { if len(cmd.NewPassword) < 4 {
c.JsonApiErr(400, "New password too short", nil) return ApiError(400, "New password too short", nil)
return
} }
cmd.UserId = c.UserId cmd.UserId = c.UserId
cmd.NewPassword = util.EncodePassword(cmd.NewPassword, userQuery.Result.Salt) cmd.NewPassword = util.EncodePassword(cmd.NewPassword, userQuery.Result.Salt)
if err := bus.Dispatch(&cmd); err != nil { if err := bus.Dispatch(&cmd); err != nil {
c.JsonApiErr(500, "Failed to change user password", err) return ApiError(500, "Failed to change user password", err)
return
} }
c.JsonOK("User password changed") return ApiSuccess("User password changed")
}
// GET /api/users
func SearchUsers(c *middleware.Context) Response {
query := m.SearchUsersQuery{Query: "", Page: 0, Limit: 1000}
if err := bus.Dispatch(&query); err != nil {
return ApiError(500, "Failed to fetch users", err)
}
return Json(200, query.Result)
} }

View File

@ -1,7 +1,7 @@
package bus package bus
import ( import (
"errors" "fmt"
"reflect" "reflect"
) )
@ -39,7 +39,7 @@ func (b *InProcBus) Dispatch(msg Msg) error {
var handler = b.handlers[msgName] var handler = b.handlers[msgName]
if handler == nil { if handler == nil {
return errors.New("handler not found") return fmt.Errorf("handler not found for %s", msgName)
} }
var params = make([]reflect.Value, 1) var params = make([]reflect.Value, 1)
@ -121,3 +121,7 @@ func Dispatch(msg Msg) error {
func Publish(msg Msg) error { func Publish(msg Msg) error {
return globalBus.Publish(msg) return globalBus.Publish(msg)
} }
func ClearBusHandlers() {
globalBus = New()
}

View File

@ -33,6 +33,7 @@ func newMacaron() *macaron.Macaron {
mapStatic(m, "css", "css") mapStatic(m, "css", "css")
mapStatic(m, "img", "img") mapStatic(m, "img", "img")
mapStatic(m, "fonts", "fonts") mapStatic(m, "fonts", "fonts")
mapStatic(m, "robots.txt", "robots.txxt")
m.Use(macaron.Renderer(macaron.RenderOptions{ m.Use(macaron.Renderer(macaron.RenderOptions{
Directory: path.Join(setting.StaticRootPath, "views"), Directory: path.Join(setting.StaticRootPath, "views"),
@ -40,8 +41,12 @@ func newMacaron() *macaron.Macaron {
Delims: macaron.Delims{Left: "[[", Right: "]]"}, Delims: macaron.Delims{Left: "[[", Right: "]]"},
})) }))
if setting.EnforceDomain {
m.Use(middleware.ValidateHostHeader(setting.Domain))
}
m.Use(middleware.GetContextHandler()) m.Use(middleware.GetContextHandler())
m.Use(middleware.Sessioner(setting.SessionOptions)) m.Use(middleware.Sessioner(&setting.SessionOptions))
return m return m
} }

View File

@ -26,7 +26,7 @@ func RenderToPng(params *RenderOpts) (string, error) {
pngPath, _ := filepath.Abs(filepath.Join(setting.ImagesDir, util.GetRandomString(20))) pngPath, _ := filepath.Abs(filepath.Join(setting.ImagesDir, util.GetRandomString(20)))
pngPath = pngPath + ".png" pngPath = pngPath + ".png"
cmd := exec.Command(binPath, "--ignore-ssl-errors=true", scriptPath, "url="+params.Url, "width="+params.Width, cmd := exec.Command(binPath, "--ignore-ssl-errors=true", "--ssl-protocol=any", scriptPath, "url="+params.Url, "width="+params.Width,
"height="+params.Height, "png="+pngPath, "cookiename="+setting.SessionOptions.CookieName, "height="+params.Height, "png="+pngPath, "cookiename="+setting.SessionOptions.CookieName,
"domain="+setting.Domain, "sessionid="+params.SessionId) "domain="+setting.Domain, "sessionid="+params.SessionId)
stdout, err := cmd.StdoutPipe() stdout, err := cmd.StdoutPipe()
@ -54,7 +54,7 @@ func RenderToPng(params *RenderOpts) (string, error) {
}() }()
select { select {
case <-time.After(10 * time.Second): case <-time.After(15 * time.Second):
if err := cmd.Process.Kill(); err != nil { if err := cmd.Process.Kill(); err != nil {
log.Error(4, "failed to kill: %v", err) log.Error(4, "failed to kill: %v", err)
} }

View File

@ -5,7 +5,7 @@ import (
"time" "time"
) )
// Events can be passed to external systems via for example AMPQ // Events can be passed to external systems via for example AMQP
// Treat these events as basically DTOs so changes has to be backward compatible // Treat these events as basically DTOs so changes has to be backward compatible
type Priority string type Priority string
@ -70,6 +70,14 @@ type UserCreated struct {
Email string `json:"email"` Email string `json:"email"`
} }
type UserSignedUp struct {
Timestamp time.Time `json:"timestamp"`
Id int64 `json:"id"`
Name string `json:"name"`
Login string `json:"login"`
Email string `json:"email"`
}
type UserUpdated struct { type UserUpdated struct {
Timestamp time.Time `json:"timestamp"` Timestamp time.Time `json:"timestamp"`
Id int64 `json:"id"` Id int64 `json:"id"`

View File

@ -0,0 +1,71 @@
package middleware
import (
"github.com/grafana/grafana/pkg/bus"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
)
func initContextWithAuthProxy(ctx *Context) bool {
if !setting.AuthProxyEnabled {
return false
}
proxyHeaderValue := ctx.Req.Header.Get(setting.AuthProxyHeaderName)
if len(proxyHeaderValue) == 0 {
return false
}
query := getSignedInUserQueryForProxyAuth(proxyHeaderValue)
if err := bus.Dispatch(query); err != nil {
if err != m.ErrUserNotFound {
ctx.Handle(500, "Failed find user specifed in auth proxy header", err)
return true
}
if setting.AuthProxyAutoSignUp {
cmd := getCreateUserCommandForProxyAuth(proxyHeaderValue)
if err := bus.Dispatch(cmd); err != nil {
ctx.Handle(500, "Failed to create user specified in auth proxy header", err)
return true
}
query = &m.GetSignedInUserQuery{UserId: cmd.Result.Id}
if err := bus.Dispatch(query); err != nil {
ctx.Handle(500, "Failed find user after creation", err)
return true
}
} else {
return false
}
}
ctx.SignedInUser = query.Result
ctx.IsSignedIn = true
return true
}
func getSignedInUserQueryForProxyAuth(headerVal string) *m.GetSignedInUserQuery {
query := m.GetSignedInUserQuery{}
if setting.AuthProxyHeaderProperty == "username" {
query.Login = headerVal
} else if setting.AuthProxyHeaderProperty == "email" {
query.Email = headerVal
} else {
panic("Auth proxy header property invalid")
}
return &query
}
func getCreateUserCommandForProxyAuth(headerVal string) *m.CreateUserCommand {
cmd := m.CreateUserCommand{}
if setting.AuthProxyHeaderProperty == "username" {
cmd.Login = headerVal
cmd.Email = headerVal
} else if setting.AuthProxyHeaderProperty == "email" {
cmd.Email = headerVal
cmd.Login = headerVal
} else {
panic("Auth proxy header property invalid")
}
return &cmd
}

View File

@ -0,0 +1,35 @@
package middleware
import (
"testing"
. "github.com/smartystreets/goconvey/convey"
)
func TestMiddlewareAuth(t *testing.T) {
Convey("Given the grafana middleware", t, func() {
reqSignIn := Auth(&AuthOptions{ReqSignedIn: true})
middlewareScenario("ReqSignIn true and unauthenticated request", func(sc *scenarioContext) {
sc.m.Get("/secure", reqSignIn, sc.defaultHandler)
sc.fakeReq("GET", "/secure").exec()
Convey("Should redirect to login", func() {
So(sc.resp.Code, ShouldEqual, 302)
})
})
middlewareScenario("ReqSignIn true and unauthenticated API request", func(sc *scenarioContext) {
sc.m.Get("/api/secure", reqSignIn, sc.defaultHandler)
sc.fakeReq("GET", "/api/secure").exec()
Convey("Should return 401", func() {
So(sc.resp.Code, ShouldEqual, 401)
})
})
})
}

View File

@ -12,6 +12,7 @@ import (
"github.com/grafana/grafana/pkg/metrics" "github.com/grafana/grafana/pkg/metrics"
m "github.com/grafana/grafana/pkg/models" m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
) )
type Context struct { type Context struct {
@ -40,6 +41,8 @@ func GetContextHandler() macaron.Handler {
// then look for api key in session (special case for render calls via api) // then look for api key in session (special case for render calls via api)
// then test if anonymous access is enabled // then test if anonymous access is enabled
if initContextWithApiKey(ctx) || if initContextWithApiKey(ctx) ||
initContextWithBasicAuth(ctx) ||
initContextWithAuthProxy(ctx) ||
initContextWithUserSessionCookie(ctx) || initContextWithUserSessionCookie(ctx) ||
initContextWithApiKeyFromSession(ctx) || initContextWithApiKeyFromSession(ctx) ||
initContextWithAnonymousUser(ctx) { initContextWithAnonymousUser(ctx) {
@ -83,6 +86,7 @@ func initContextWithUserSessionCookie(ctx *Context) bool {
query := m.GetSignedInUserQuery{UserId: userId} query := m.GetSignedInUserQuery{UserId: userId}
if err := bus.Dispatch(&query); err != nil { if err := bus.Dispatch(&query); err != nil {
log.Error(3, "Failed to get user with id %v", userId)
return false return false
} else { } else {
ctx.SignedInUser = query.Result ctx.SignedInUser = query.Result
@ -126,6 +130,47 @@ func initContextWithApiKey(ctx *Context) bool {
} }
} }
func initContextWithBasicAuth(ctx *Context) bool {
if !setting.BasicAuthEnabled {
return false
}
header := ctx.Req.Header.Get("Authorization")
if header == "" {
return false
}
username, password, err := util.DecodeBasicAuthHeader(header)
if err != nil {
ctx.JsonApiErr(401, "Invalid Basic Auth Header", err)
return true
}
loginQuery := m.GetUserByLoginQuery{LoginOrEmail: username}
if err := bus.Dispatch(&loginQuery); err != nil {
ctx.JsonApiErr(401, "Basic auth failed", err)
return true
}
user := loginQuery.Result
// validate password
if util.EncodePassword(password, user.Salt) != user.Password {
ctx.JsonApiErr(401, "Invalid username or password", nil)
return true
}
query := m.GetSignedInUserQuery{UserId: user.Id}
if err := bus.Dispatch(&query); err != nil {
ctx.JsonApiErr(401, "Authentication error", err)
return true
} else {
ctx.SignedInUser = query.Result
ctx.IsSignedIn = true
return true
}
}
// special case for panel render calls with api key // special case for panel render calls with api key
func initContextWithApiKeyFromSession(ctx *Context) bool { func initContextWithApiKeyFromSession(ctx *Context) bool {
keyId := ctx.Session.Get(SESS_KEY_APIKEY) keyId := ctx.Session.Get(SESS_KEY_APIKEY)
@ -195,10 +240,10 @@ func (ctx *Context) JsonApiErr(status int, message string, err error) {
switch status { switch status {
case 404: case 404:
resp["message"] = "Not Found"
metrics.M_Api_Status_500.Inc(1)
case 500:
metrics.M_Api_Status_404.Inc(1) metrics.M_Api_Status_404.Inc(1)
resp["message"] = "Not Found"
case 500:
metrics.M_Api_Status_500.Inc(1)
resp["message"] = "Internal Server Error" resp["message"] = "Internal Server Error"
} }

Some files were not shown because too many files have changed in this diff Show More