Merge remote-tracking branch 'grafana/master'

* grafana/master: (51 commits)
  changing callback fn into arrow functions for correct usage of this (#12673)
  Fix requested changes
  Update CHANGELOG.md
  Add support for interval in query variable
  Change to arrow functions
  Add graph_ctrl jest
  changelog: add notes about closing #12691
  Update kbn.ts
  Add jest test file
  Id validation of CloudWatch GetMetricData
  changelog: adds note for #11487
  Datasource for Grafana logging platform
  fix: postgres/mysql engine cache was not being used, fixes #12636 (#12642)
  added: replaces added to grafana
  fix: datasource search was not working properly
  docs: minor docs fix
  Fix label suggestions in Explore query field
  pluginloader: expose flot gauge plugin
  alert: add missing test after refactor
  Handle query string in storage public_url (#9351) (#12555)
  ...
This commit is contained in:
ryan
2018-07-23 14:31:33 -07:00
136 changed files with 8307 additions and 3213 deletions
+4
View File
@@ -2,6 +2,8 @@
* **Dataproxy**: Pass configured/auth headers to a Datasource [#10971](https://github.com/grafana/grafana/issues/10971), thx [@mrsiano](https://github.com/mrsiano)
* **Cleanup**: Make temp file time to live configurable [#11607](https://github.com/grafana/grafana/issues/11607), thx [@xapon](https://github.com/xapon)
* **LDAP**: Define Grafana Admin permission in ldap group mappings [#2469](https://github.com/grafana/grafana/issues/2496), PR [#12622](https://github.com/grafana/grafana/issues/12622)
* **Cloudwatch**: CloudWatch GetMetricData support [#11487](https://github.com/grafana/grafana/issues/11487), thx [@mtanda](https://github.com/mtanda)
### Minor
@@ -11,11 +13,13 @@
* **Table**: Make table sorting stable when null values exist [#12362](https://github.com/grafana/grafana/pull/12362), thx [@bz2](https://github.com/bz2)
* **Prometheus**: Fix graph panel bar width issue in aligned prometheus queries [#12379](https://github.com/grafana/grafana/issues/12379)
* **Prometheus**: Heatmap - fix unhandled error when some points are missing [#12484](https://github.com/grafana/grafana/issues/12484)
* **Prometheus**: Add $interval, $interval_ms, $range, and $range_ms support for dashboard and template queries [#12597](https://github.com/grafana/grafana/issues/12597)
* **Variables**: Skip unneeded extra query request when de-selecting variable values used for repeated panels [#8186](https://github.com/grafana/grafana/issues/8186), thx [@mtanda](https://github.com/mtanda)
* **Postgres/MySQL/MSSQL**: Use floor rounding in $__timeGroup macro function [#12460](https://github.com/grafana/grafana/issues/12460), thx [@svenklemm](https://github.com/svenklemm)
* **MySQL/MSSQL**: Use datetime format instead of epoch for $__timeFilter, $__timeFrom and $__timeTo macros [#11618](https://github.com/grafana/grafana/issues/11618) [#11619](https://github.com/grafana/grafana/issues/11619), thx [@AustinWinstanley](https://github.com/AustinWinstanley)
* **Github OAuth**: Allow changes of user info at Github to be synched to Grafana when signing in [#11818](https://github.com/grafana/grafana/issues/11818), thx [@rwaweber](https://github.com/rwaweber)
* **Alerting**: Fix diff and percent_diff reducers [#11563](https://github.com/grafana/grafana/issues/11563), thx [@jessetane](https://github.com/jessetane)
* **Units**: Polish złoty currency [#12691](https://github.com/grafana/grafana/pull/12691), thx [@mwegrzynek](https://github.com/mwegrzynek)
# 5.2.2 (unreleased)
Generated
+12 -3
View File
@@ -32,6 +32,7 @@
"aws/credentials/ec2rolecreds",
"aws/credentials/endpointcreds",
"aws/credentials/stscreds",
"aws/csm",
"aws/defaults",
"aws/ec2metadata",
"aws/endpoints",
@@ -43,6 +44,8 @@
"internal/shareddefaults",
"private/protocol",
"private/protocol/ec2query",
"private/protocol/eventstream",
"private/protocol/eventstream/eventstreamapi",
"private/protocol/query",
"private/protocol/query/queryutil",
"private/protocol/rest",
@@ -54,8 +57,8 @@
"service/s3",
"service/sts"
]
revision = "c7cd1ebe87257cde9b65112fc876b0339ea0ac30"
version = "v1.13.49"
revision = "fde4ded7becdeae4d26bf1212916aabba79349b4"
version = "v1.14.12"
[[projects]]
branch = "master"
@@ -424,6 +427,12 @@
revision = "1744e2970ca51c86172c8190fadad617561ed6e7"
version = "v1.0.0"
[[projects]]
branch = "master"
name = "github.com/shurcooL/sanitized_anchor_name"
packages = ["."]
revision = "86672fcb3f950f35f2e675df2240550f2a50762f"
[[projects]]
name = "github.com/smartystreets/assertions"
packages = [
@@ -670,6 +679,6 @@
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
inputs-digest = "85cc057e0cc074ab5b43bd620772d63d51e07b04e8782fcfe55e6929d2fc40f7"
inputs-digest = "cb8e7fd81f23ec987fc4d5dd9d31ae0f1164bc2f30cbea2fe86e0d97dd945beb"
solver-name = "gps-cdcl"
solver-version = 1
+1 -1
View File
@@ -36,7 +36,7 @@ ignored = [
[[constraint]]
name = "github.com/aws/aws-sdk-go"
version = "1.12.65"
version = "1.13.56"
[[constraint]]
branch = "master"
+1
View File
@@ -330,6 +330,7 @@ func createPackage(options linuxPackageOptions) {
name := "grafana"
if enterprise {
name += "-enterprise"
args = append(args, "--replaces", "grafana")
}
args = append(args, "--name", name)
+2
View File
@@ -72,6 +72,8 @@ email = "email"
[[servers.group_mappings]]
group_dn = "cn=admins,dc=grafana,dc=org"
org_role = "Admin"
# To make user an instance admin (Grafana Admin) uncomment line below
# grafana_admin = true
# The Grafana organization database id, optional, if left out the default org (id 1) will be used
# org_id = 1
+10 -5
View File
@@ -1,11 +1,16 @@
This folder contains useful scripts and configuration for...
* Configuring datasources in Grafana
* Provision example dashboards in Grafana
* Run preconfiured datasources as docker containers
want to know more? run setup!
* Configuring dev datasources in Grafana
* Configuring dev & test scenarios dashboards.
```bash
./setup.sh
```
After restarting grafana server there should now be a number of datasources named `gdev-<type>` provisioned as well as a dashboard folder named `gdev dashboards`. This folder contains dashboard & panel features tests dashboards.
# Dev dashboards
Please update these dashboards or make new ones as new panels & dashboards features are developed or new bugs are found. The dashboards are located in the `devenv/dev-dashboards` folder.
+6 -1
View File
@@ -14,6 +14,9 @@ datasources:
isDefault: true
url: http://localhost:9090
- name: gdev-testdata
type: testdata
- name: gdev-influxdb
type: influxdb
access: proxy
@@ -60,7 +63,8 @@ datasources:
url: localhost:5432
database: grafana
user: grafana
password: password
secureJsonData:
password: password
jsonData:
sslmode: "disable"
@@ -71,3 +75,4 @@ datasources:
authType: credentials
defaultRegion: eu-west-2
@@ -1,592 +0,0 @@
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": "-- Grafana --",
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"type": "dashboard"
}
]
},
"editable": true,
"gnetId": null,
"graphTooltip": 0,
"id": 59,
"links": [],
"panels": [
{
"collapsed": false,
"gridPos": {
"h": 1,
"w": 24,
"x": 0,
"y": 0
},
"id": 9,
"panels": [],
"title": "Row title",
"type": "row"
},
{
"aliasColors": {},
"bars": false,
"dashLength": 10,
"dashes": false,
"datasource": "Prometheus",
"fill": 1,
"gridPos": {
"h": 4,
"w": 12,
"x": 0,
"y": 1
},
"id": 12,
"legend": {
"avg": false,
"current": false,
"max": false,
"min": false,
"show": true,
"total": false,
"values": false
},
"lines": true,
"linewidth": 1,
"nullPointMode": "null",
"percentage": false,
"pointradius": 5,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"spaceLength": 10,
"stack": false,
"steppedLine": false,
"targets": [
{
"expr": "go_goroutines",
"format": "time_series",
"intervalFactor": 1,
"refId": "A"
}
],
"thresholds": [],
"timeFrom": null,
"timeShift": null,
"title": "Panel Title",
"tooltip": {
"shared": true,
"sort": 0,
"value_type": "individual"
},
"type": "graph",
"xaxis": {
"buckets": null,
"mode": "time",
"name": null,
"show": true,
"values": []
},
"yaxes": [
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
},
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
}
],
"yaxis": {
"align": false,
"alignLevel": null
}
},
{
"aliasColors": {},
"bars": false,
"dashLength": 10,
"dashes": false,
"datasource": "Prometheus",
"fill": 1,
"gridPos": {
"h": 4,
"w": 12,
"x": 12,
"y": 1
},
"id": 5,
"legend": {
"avg": false,
"current": false,
"max": false,
"min": false,
"show": true,
"total": false,
"values": false
},
"lines": true,
"linewidth": 1,
"nullPointMode": "null",
"percentage": false,
"pointradius": 5,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"spaceLength": 10,
"stack": false,
"steppedLine": false,
"targets": [
{
"expr": "go_goroutines",
"format": "time_series",
"intervalFactor": 1,
"refId": "A"
}
],
"thresholds": [],
"timeFrom": null,
"timeShift": null,
"title": "Panel Title",
"tooltip": {
"shared": true,
"sort": 0,
"value_type": "individual"
},
"type": "graph",
"xaxis": {
"buckets": null,
"mode": "time",
"name": null,
"show": true,
"values": []
},
"yaxes": [
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
},
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
}
],
"yaxis": {
"align": false,
"alignLevel": null
}
},
{
"collapsed": false,
"gridPos": {
"h": 1,
"w": 24,
"x": 0,
"y": 5
},
"id": 7,
"panels": [],
"title": "Row",
"type": "row"
},
{
"aliasColors": {},
"bars": false,
"dashLength": 10,
"dashes": false,
"datasource": "Prometheus",
"fill": 1,
"gridPos": {
"h": 4,
"w": 12,
"x": 0,
"y": 6
},
"id": 2,
"legend": {
"avg": false,
"current": false,
"max": false,
"min": false,
"show": true,
"total": false,
"values": false
},
"lines": true,
"linewidth": 1,
"nullPointMode": "null",
"percentage": false,
"pointradius": 5,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"spaceLength": 10,
"stack": false,
"steppedLine": false,
"targets": [
{
"expr": "go_goroutines",
"format": "time_series",
"intervalFactor": 1,
"refId": "A"
}
],
"thresholds": [],
"timeFrom": null,
"timeShift": null,
"title": "Panel Title",
"tooltip": {
"shared": true,
"sort": 0,
"value_type": "individual"
},
"type": "graph",
"xaxis": {
"buckets": null,
"mode": "time",
"name": null,
"show": true,
"values": []
},
"yaxes": [
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
},
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
}
],
"yaxis": {
"align": false,
"alignLevel": null
}
},
{
"aliasColors": {},
"bars": false,
"dashLength": 10,
"dashes": false,
"datasource": "Prometheus",
"fill": 1,
"gridPos": {
"h": 4,
"w": 12,
"x": 12,
"y": 6
},
"id": 13,
"legend": {
"avg": false,
"current": false,
"max": false,
"min": false,
"show": true,
"total": false,
"values": false
},
"lines": true,
"linewidth": 1,
"nullPointMode": "null",
"percentage": false,
"pointradius": 5,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"spaceLength": 10,
"stack": false,
"steppedLine": false,
"targets": [
{
"expr": "go_goroutines",
"format": "time_series",
"intervalFactor": 1,
"refId": "A"
}
],
"thresholds": [],
"timeFrom": null,
"timeShift": null,
"title": "Panel Title",
"tooltip": {
"shared": true,
"sort": 0,
"value_type": "individual"
},
"type": "graph",
"xaxis": {
"buckets": null,
"mode": "time",
"name": null,
"show": true,
"values": []
},
"yaxes": [
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
},
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
}
],
"yaxis": {
"align": false,
"alignLevel": null
}
},
{
"collapsed": false,
"gridPos": {
"h": 1,
"w": 24,
"x": 0,
"y": 10
},
"id": 11,
"panels": [],
"title": "Row title",
"type": "row"
},
{
"aliasColors": {},
"bars": false,
"dashLength": 10,
"dashes": false,
"datasource": "Prometheus",
"fill": 1,
"gridPos": {
"h": 4,
"w": 12,
"x": 0,
"y": 11
},
"id": 4,
"legend": {
"avg": false,
"current": false,
"max": false,
"min": false,
"show": true,
"total": false,
"values": false
},
"lines": true,
"linewidth": 1,
"nullPointMode": "null",
"percentage": false,
"pointradius": 5,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"spaceLength": 10,
"stack": false,
"steppedLine": false,
"targets": [
{
"expr": "go_goroutines",
"format": "time_series",
"intervalFactor": 1,
"refId": "A"
}
],
"thresholds": [],
"timeFrom": null,
"timeShift": null,
"title": "Panel Title",
"tooltip": {
"shared": true,
"sort": 0,
"value_type": "individual"
},
"type": "graph",
"xaxis": {
"buckets": null,
"mode": "time",
"name": null,
"show": true,
"values": []
},
"yaxes": [
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
},
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
}
],
"yaxis": {
"align": false,
"alignLevel": null
}
},
{
"aliasColors": {},
"bars": false,
"dashLength": 10,
"dashes": false,
"datasource": "Prometheus",
"fill": 1,
"gridPos": {
"h": 4,
"w": 12,
"x": 12,
"y": 11
},
"id": 3,
"legend": {
"avg": false,
"current": false,
"max": false,
"min": false,
"show": true,
"total": false,
"values": false
},
"lines": true,
"linewidth": 1,
"nullPointMode": "null",
"percentage": false,
"pointradius": 5,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"spaceLength": 10,
"stack": false,
"steppedLine": false,
"targets": [
{
"expr": "go_goroutines",
"format": "time_series",
"intervalFactor": 1,
"refId": "A"
}
],
"thresholds": [],
"timeFrom": null,
"timeShift": null,
"title": "Panel Title",
"tooltip": {
"shared": true,
"sort": 0,
"value_type": "individual"
},
"type": "graph",
"xaxis": {
"buckets": null,
"mode": "time",
"name": null,
"show": true,
"values": []
},
"yaxes": [
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
},
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
}
],
"yaxis": {
"align": false,
"alignLevel": null
}
}
],
"schemaVersion": 16,
"style": "dark",
"tags": [],
"templating": {
"list": []
},
"time": {
"from": "now-30m",
"to": "now"
},
"timepicker": {
"refresh_intervals": [
"5s",
"10s",
"30s",
"1m",
"5m",
"15m",
"30m",
"1h",
"2h",
"1d"
],
"time_options": [
"5m",
"15m",
"1h",
"6h",
"12h",
"24h",
"2d",
"7d",
"30d"
]
},
"timezone": "",
"title": "Dashboard with rows",
"uid": "1DdOzBNmk",
"version": 5
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,574 @@
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": "-- Grafana --",
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"type": "dashboard"
}
]
},
"editable": true,
"gnetId": null,
"graphTooltip": 0,
"links": [],
"panels": [
{
"cacheTimeout": null,
"colorBackground": false,
"colorValue": true,
"colors": [
"#299c46",
"rgba(237, 129, 40, 0.89)",
"#d44a3a"
],
"datasource": "gdev-testdata",
"decimals": null,
"description": "",
"format": "ms",
"gauge": {
"maxValue": 100,
"minValue": 0,
"show": false,
"thresholdLabels": false,
"thresholdMarkers": true
},
"gridPos": {
"h": 7,
"w": 8,
"x": 0,
"y": 0
},
"id": 2,
"interval": null,
"links": [],
"mappingType": 1,
"mappingTypes": [
{
"name": "value to text",
"value": 1
},
{
"name": "range to text",
"value": 2
}
],
"maxDataPoints": 100,
"nullPointMode": "connected",
"nullText": null,
"postfix": "postfix",
"postfixFontSize": "50%",
"prefix": "prefix",
"prefixFontSize": "50%",
"rangeMaps": [
{
"from": "null",
"text": "N/A",
"to": "null"
}
],
"sparkline": {
"fillColor": "rgba(31, 118, 189, 0.18)",
"full": false,
"lineColor": "rgb(31, 120, 193)",
"show": true
},
"tableColumn": "",
"targets": [
{
"expr": "",
"format": "time_series",
"intervalFactor": 1,
"refId": "A",
"scenarioId": "csv_metric_values",
"stringInput": "1,2,3,4,5"
}
],
"thresholds": "5,10",
"title": "prefix 3 ms (green) postfixt + sparkline",
"type": "singlestat",
"valueFontSize": "80%",
"valueMaps": [
{
"op": "=",
"text": "N/A",
"value": "null"
}
],
"valueName": "avg"
},
{
"cacheTimeout": null,
"colorBackground": false,
"colorPrefix": false,
"colorValue": true,
"colors": [
"#d44a3a",
"rgba(237, 129, 40, 0.89)",
"#299c46"
],
"datasource": "gdev-testdata",
"decimals": null,
"description": "",
"format": "ms",
"gauge": {
"maxValue": 100,
"minValue": 0,
"show": false,
"thresholdLabels": false,
"thresholdMarkers": true
},
"gridPos": {
"h": 7,
"w": 8,
"x": 8,
"y": 0
},
"id": 3,
"interval": null,
"links": [],
"mappingType": 1,
"mappingTypes": [
{
"name": "value to text",
"value": 1
},
{
"name": "range to text",
"value": 2
}
],
"maxDataPoints": 100,
"nullPointMode": "connected",
"nullText": null,
"postfix": "",
"postfixFontSize": "50%",
"prefix": "",
"prefixFontSize": "50%",
"rangeMaps": [
{
"from": "null",
"text": "N/A",
"to": "null"
}
],
"sparkline": {
"fillColor": "rgba(31, 118, 189, 0.18)",
"full": true,
"lineColor": "rgb(31, 120, 193)",
"show": true
},
"tableColumn": "",
"targets": [
{
"expr": "",
"format": "time_series",
"intervalFactor": 1,
"refId": "A",
"scenarioId": "csv_metric_values",
"stringInput": "1,2,3,4,5"
}
],
"thresholds": "5,10",
"title": "3 ms (red) + full height sparkline",
"type": "singlestat",
"valueFontSize": "200%",
"valueMaps": [
{
"op": "=",
"text": "N/A",
"value": "null"
}
],
"valueName": "avg"
},
{
"cacheTimeout": null,
"colorBackground": true,
"colorPrefix": false,
"colorValue": false,
"colors": [
"#d44a3a",
"rgba(237, 129, 40, 0.89)",
"#299c46"
],
"datasource": "gdev-testdata",
"decimals": null,
"description": "",
"format": "ms",
"gauge": {
"maxValue": 100,
"minValue": 0,
"show": false,
"thresholdLabels": false,
"thresholdMarkers": true
},
"gridPos": {
"h": 7,
"w": 8,
"x": 16,
"y": 0
},
"id": 4,
"interval": null,
"links": [],
"mappingType": 1,
"mappingTypes": [
{
"name": "value to text",
"value": 1
},
{
"name": "range to text",
"value": 2
}
],
"maxDataPoints": 100,
"nullPointMode": "connected",
"nullText": null,
"postfix": "",
"postfixFontSize": "50%",
"prefix": "",
"prefixFontSize": "50%",
"rangeMaps": [
{
"from": "null",
"text": "N/A",
"to": "null"
}
],
"sparkline": {
"fillColor": "rgba(31, 118, 189, 0.18)",
"full": true,
"lineColor": "rgb(31, 120, 193)",
"show": false
},
"tableColumn": "",
"targets": [
{
"expr": "",
"format": "time_series",
"intervalFactor": 1,
"refId": "A",
"scenarioId": "csv_metric_values",
"stringInput": "1,2,3,4,5"
}
],
"thresholds": "5,10",
"title": "3 ms + red background",
"type": "singlestat",
"valueFontSize": "200%",
"valueMaps": [
{
"op": "=",
"text": "N/A",
"value": "null"
}
],
"valueName": "avg"
},
{
"cacheTimeout": null,
"colorBackground": false,
"colorPrefix": false,
"colorValue": true,
"colors": [
"#299c46",
"rgba(237, 129, 40, 0.89)",
"#d44a3a"
],
"datasource": "gdev-testdata",
"decimals": null,
"description": "",
"format": "ms",
"gauge": {
"maxValue": 150,
"minValue": 0,
"show": true,
"thresholdLabels": true,
"thresholdMarkers": true
},
"gridPos": {
"h": 7,
"w": 8,
"x": 0,
"y": 7
},
"id": 5,
"interval": null,
"links": [],
"mappingType": 1,
"mappingTypes": [
{
"name": "value to text",
"value": 1
},
{
"name": "range to text",
"value": 2
}
],
"maxDataPoints": 100,
"nullPointMode": "connected",
"nullText": null,
"postfix": "",
"postfixFontSize": "50%",
"prefix": "",
"prefixFontSize": "50%",
"rangeMaps": [
{
"from": "null",
"text": "N/A",
"to": "null"
}
],
"sparkline": {
"fillColor": "rgba(31, 118, 189, 0.18)",
"full": true,
"lineColor": "rgb(31, 120, 193)",
"show": false
},
"tableColumn": "",
"targets": [
{
"expr": "",
"format": "time_series",
"intervalFactor": 1,
"refId": "A",
"scenarioId": "csv_metric_values",
"stringInput": "10,20,80"
}
],
"thresholds": "81,90",
"title": "80 ms green gauge, thresholds 81, 90",
"type": "singlestat",
"valueFontSize": "80%",
"valueMaps": [
{
"op": "=",
"text": "N/A",
"value": "null"
}
],
"valueName": "current"
},
{
"cacheTimeout": null,
"colorBackground": false,
"colorPrefix": false,
"colorValue": true,
"colors": [
"#299c46",
"rgba(237, 129, 40, 0.89)",
"#d44a3a"
],
"datasource": "gdev-testdata",
"decimals": null,
"description": "",
"format": "ms",
"gauge": {
"maxValue": 150,
"minValue": 0,
"show": true,
"thresholdLabels": false,
"thresholdMarkers": true
},
"gridPos": {
"h": 7,
"w": 8,
"x": 8,
"y": 7
},
"id": 6,
"interval": null,
"links": [],
"mappingType": 1,
"mappingTypes": [
{
"name": "value to text",
"value": 1
},
{
"name": "range to text",
"value": 2
}
],
"maxDataPoints": 100,
"nullPointMode": "connected",
"nullText": null,
"postfix": "",
"postfixFontSize": "50%",
"prefix": "",
"prefixFontSize": "50%",
"rangeMaps": [
{
"from": "null",
"text": "N/A",
"to": "null"
}
],
"sparkline": {
"fillColor": "rgba(31, 118, 189, 0.18)",
"full": true,
"lineColor": "rgb(31, 120, 193)",
"show": false
},
"tableColumn": "",
"targets": [
{
"expr": "",
"format": "time_series",
"intervalFactor": 1,
"refId": "A",
"scenarioId": "csv_metric_values",
"stringInput": "10,20,80"
}
],
"thresholds": "81,90",
"title": "80 ms green gauge, thresholds 81, 90, no labels",
"type": "singlestat",
"valueFontSize": "80%",
"valueMaps": [
{
"op": "=",
"text": "N/A",
"value": "null"
}
],
"valueName": "current"
},
{
"cacheTimeout": null,
"colorBackground": false,
"colorPrefix": false,
"colorValue": true,
"colors": [
"#299c46",
"rgba(237, 129, 40, 0.89)",
"#d44a3a"
],
"datasource": "gdev-testdata",
"decimals": null,
"description": "",
"format": "ms",
"gauge": {
"maxValue": 150,
"minValue": 0,
"show": true,
"thresholdLabels": false,
"thresholdMarkers": false
},
"gridPos": {
"h": 7,
"w": 8,
"x": 16,
"y": 7
},
"id": 7,
"interval": null,
"links": [],
"mappingType": 1,
"mappingTypes": [
{
"name": "value to text",
"value": 1
},
{
"name": "range to text",
"value": 2
}
],
"maxDataPoints": 100,
"nullPointMode": "connected",
"nullText": null,
"postfix": "",
"postfixFontSize": "50%",
"prefix": "",
"prefixFontSize": "50%",
"rangeMaps": [
{
"from": "null",
"text": "N/A",
"to": "null"
}
],
"sparkline": {
"fillColor": "rgba(31, 118, 189, 0.18)",
"full": true,
"lineColor": "rgb(31, 120, 193)",
"show": false
},
"tableColumn": "",
"targets": [
{
"expr": "",
"format": "time_series",
"intervalFactor": 1,
"refId": "A",
"scenarioId": "csv_metric_values",
"stringInput": "10,20,80"
}
],
"thresholds": "81,90",
"title": "80 ms green gauge, thresholds 81, 90, no markers or labels",
"type": "singlestat",
"valueFontSize": "80%",
"valueMaps": [
{
"op": "=",
"text": "N/A",
"value": "null"
}
],
"valueName": "current"
}
],
"refresh": false,
"revision": 8,
"schemaVersion": 16,
"style": "dark",
"tags": [
"gdev",
"panel-tests"
],
"templating": {
"list": []
},
"time": {
"from": "now-1h",
"to": "now"
},
"timepicker": {
"refresh_intervals": [
"5s",
"10s",
"30s",
"1m",
"5m",
"15m",
"30m",
"1h",
"2h",
"1d"
],
"time_options": [
"5m",
"15m",
"1h",
"6h",
"12h",
"24h",
"2d",
"7d",
"30d"
]
},
"timezone": "browser",
"title": "Panel Tests - Singlestat",
"uid": "singlestat",
"version": 14
}
@@ -0,0 +1,453 @@
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": "-- Grafana --",
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"type": "dashboard"
}
]
},
"editable": true,
"gnetId": null,
"graphTooltip": 0,
"links": [],
"panels": [
{
"columns": [],
"datasource": "gdev-testdata",
"fontSize": "100%",
"gridPos": {
"h": 11,
"w": 12,
"x": 0,
"y": 0
},
"id": 3,
"links": [],
"pageSize": 10,
"scroll": true,
"showHeader": true,
"sort": {
"col": 0,
"desc": true
},
"styles": [
{
"alias": "Time",
"dateFormat": "YYYY-MM-DD HH:mm:ss",
"pattern": "Time",
"type": "date"
},
{
"alias": "",
"colorMode": "cell",
"colors": [
"rgba(245, 54, 54, 0.9)",
"rgba(237, 129, 40, 0.89)",
"rgba(50, 172, 45, 0.97)"
],
"dateFormat": "YYYY-MM-DD HH:mm:ss",
"decimals": 2,
"mappingType": 1,
"pattern": "ColorCell",
"thresholds": [
"5",
"10"
],
"type": "number",
"unit": "currencyUSD"
},
{
"alias": "",
"colorMode": "value",
"colors": [
"rgba(245, 54, 54, 0.9)",
"rgba(237, 129, 40, 0.89)",
"rgba(50, 172, 45, 0.97)"
],
"dateFormat": "YYYY-MM-DD HH:mm:ss",
"decimals": 2,
"mappingType": 1,
"pattern": "ColorValue",
"thresholds": [
"5",
"10"
],
"type": "number",
"unit": "Bps"
},
{
"alias": "",
"colorMode": null,
"colors": [
"rgba(245, 54, 54, 0.9)",
"rgba(237, 129, 40, 0.89)",
"rgba(50, 172, 45, 0.97)"
],
"decimals": 2,
"pattern": "/.*/",
"thresholds": [],
"type": "number",
"unit": "short"
}
],
"targets": [
{
"alias": "server1",
"expr": "",
"format": "table",
"intervalFactor": 1,
"refId": "A",
"scenarioId": "csv_metric_values",
"stringInput": "1,20,90,30,5,0,20,10"
},
{
"alias": "server2",
"refId": "B",
"scenarioId": "csv_metric_values",
"stringInput": "1,20,90,30,5,0"
}
],
"title": "Time series to rows (2 pages)",
"transform": "timeseries_to_rows",
"type": "table"
},
{
"columns": [
{
"text": "Avg",
"value": "avg"
},
{
"text": "Max",
"value": "max"
},
{
"text": "Current",
"value": "current"
}
],
"datasource": "gdev-testdata",
"fontSize": "100%",
"gridPos": {
"h": 11,
"w": 12,
"x": 12,
"y": 0
},
"id": 4,
"links": [],
"pageSize": 10,
"scroll": true,
"showHeader": true,
"sort": {
"col": 0,
"desc": true
},
"styles": [
{
"alias": "Time",
"dateFormat": "YYYY-MM-DD HH:mm:ss",
"pattern": "Time",
"type": "date"
},
{
"alias": "",
"colorMode": "cell",
"colors": [
"rgba(245, 54, 54, 0.9)",
"rgba(237, 129, 40, 0.89)",
"rgba(50, 172, 45, 0.97)"
],
"dateFormat": "YYYY-MM-DD HH:mm:ss",
"decimals": 2,
"mappingType": 1,
"pattern": "ColorCell",
"thresholds": [
"5",
"10"
],
"type": "number",
"unit": "currencyUSD"
},
{
"alias": "",
"colorMode": "value",
"colors": [
"rgba(245, 54, 54, 0.9)",
"rgba(237, 129, 40, 0.89)",
"rgba(50, 172, 45, 0.97)"
],
"dateFormat": "YYYY-MM-DD HH:mm:ss",
"decimals": 2,
"mappingType": 1,
"pattern": "ColorValue",
"thresholds": [
"5",
"10"
],
"type": "number",
"unit": "Bps"
},
{
"alias": "",
"colorMode": null,
"colors": [
"rgba(245, 54, 54, 0.9)",
"rgba(237, 129, 40, 0.89)",
"rgba(50, 172, 45, 0.97)"
],
"decimals": 2,
"pattern": "/.*/",
"thresholds": [],
"type": "number",
"unit": "short"
}
],
"targets": [
{
"alias": "server1",
"expr": "",
"format": "table",
"intervalFactor": 1,
"refId": "A",
"scenarioId": "csv_metric_values",
"stringInput": "1,20,90,30,5,0,20,10"
},
{
"alias": "server2",
"refId": "B",
"scenarioId": "csv_metric_values",
"stringInput": "1,20,90,30,5,0"
}
],
"title": "Time series aggregations",
"transform": "timeseries_aggregations",
"type": "table"
},
{
"columns": [],
"datasource": "gdev-testdata",
"fontSize": "100%",
"gridPos": {
"h": 7,
"w": 24,
"x": 0,
"y": 11
},
"id": 5,
"links": [],
"pageSize": null,
"scroll": true,
"showHeader": true,
"sort": {
"col": 0,
"desc": true
},
"styles": [
{
"alias": "Time",
"dateFormat": "YYYY-MM-DD HH:mm:ss",
"pattern": "Time",
"type": "date"
},
{
"alias": "",
"colorMode": "row",
"colors": [
"rgba(245, 54, 54, 0.9)",
"rgba(237, 129, 40, 0.89)",
"rgba(50, 172, 45, 0.97)"
],
"dateFormat": "YYYY-MM-DD HH:mm:ss",
"decimals": 2,
"mappingType": 1,
"pattern": "/Color/",
"thresholds": [
"5",
"10"
],
"type": "number",
"unit": "currencyUSD"
},
{
"alias": "",
"colorMode": null,
"colors": [
"rgba(245, 54, 54, 0.9)",
"rgba(237, 129, 40, 0.89)",
"rgba(50, 172, 45, 0.97)"
],
"decimals": 2,
"pattern": "/.*/",
"thresholds": [],
"type": "number",
"unit": "short"
}
],
"targets": [
{
"alias": "ColorValue",
"expr": "",
"format": "table",
"intervalFactor": 1,
"refId": "A",
"scenarioId": "csv_metric_values",
"stringInput": "1,20,90,30,5,0,20,10"
}
],
"title": "color row by threshold",
"transform": "timeseries_to_columns",
"type": "table"
},
{
"columns": [],
"datasource": "gdev-testdata",
"fontSize": "100%",
"gridPos": {
"h": 8,
"w": 24,
"x": 0,
"y": 18
},
"id": 2,
"links": [],
"pageSize": null,
"scroll": true,
"showHeader": true,
"sort": {
"col": 0,
"desc": true
},
"styles": [
{
"alias": "Time",
"dateFormat": "YYYY-MM-DD HH:mm:ss",
"pattern": "Time",
"type": "date"
},
{
"alias": "",
"colorMode": "cell",
"colors": [
"rgba(245, 54, 54, 0.9)",
"rgba(237, 129, 40, 0.89)",
"rgba(50, 172, 45, 0.97)"
],
"dateFormat": "YYYY-MM-DD HH:mm:ss",
"decimals": 2,
"mappingType": 1,
"pattern": "ColorCell",
"thresholds": [
"5",
"10"
],
"type": "number",
"unit": "currencyUSD"
},
{
"alias": "",
"colorMode": "value",
"colors": [
"rgba(245, 54, 54, 0.9)",
"rgba(237, 129, 40, 0.89)",
"rgba(50, 172, 45, 0.97)"
],
"dateFormat": "YYYY-MM-DD HH:mm:ss",
"decimals": 2,
"mappingType": 1,
"pattern": "ColorValue",
"thresholds": [
"5",
"10"
],
"type": "number",
"unit": "Bps"
},
{
"alias": "",
"colorMode": null,
"colors": [
"rgba(245, 54, 54, 0.9)",
"rgba(237, 129, 40, 0.89)",
"rgba(50, 172, 45, 0.97)"
],
"decimals": 2,
"pattern": "/.*/",
"thresholds": [],
"type": "number",
"unit": "short"
}
],
"targets": [
{
"alias": "ColorValue",
"expr": "",
"format": "table",
"intervalFactor": 1,
"refId": "A",
"scenarioId": "csv_metric_values",
"stringInput": "1,20,90,30,5,0,20,10"
},
{
"alias": "ColorCell",
"refId": "B",
"scenarioId": "csv_metric_values",
"stringInput": "5,1,2,3,4,5,10,20"
}
],
"title": "Column style thresholds & units",
"transform": "timeseries_to_columns",
"type": "table"
}
],
"refresh": false,
"revision": 8,
"schemaVersion": 16,
"style": "dark",
"tags": [
"gdev",
"panel-tests"
],
"templating": {
"list": []
},
"time": {
"from": "now-1h",
"to": "now"
},
"timepicker": {
"refresh_intervals": [
"5s",
"10s",
"30s",
"1m",
"5m",
"15m",
"30m",
"1h",
"2h",
"1d"
],
"time_options": [
"5m",
"15m",
"1h",
"6h",
"12h",
"24h",
"2d",
"7d",
"30d"
]
},
"timezone": "browser",
"title": "Panel Tests - Table",
"uid": "pttable",
"version": 1
}
@@ -1,6 +1,6 @@
{
"revision": 2,
"title": "TestData - Alerts",
"title": "Alerting with TestData",
"tags": [
"grafana-test"
],
@@ -48,7 +48,7 @@
},
"aliasColors": {},
"bars": false,
"datasource": "Grafana TestData",
"datasource": "gdev-testdata",
"editable": true,
"error": false,
"fill": 1,
@@ -161,7 +161,7 @@
},
"aliasColors": {},
"bars": false,
"datasource": "Grafana TestData",
"datasource": "gdev-testdata",
"editable": true,
"error": false,
"fill": 1,
+13 -7
View File
@@ -1,4 +1,4 @@
#/bin/bash
#!/bin/bash
bulkDashboard() {
@@ -22,31 +22,37 @@ requiresJsonnet() {
fi
}
defaultDashboards() {
devDashboards() {
echo -e "\xE2\x9C\x94 Setting up all dev dashboards using provisioning"
ln -s -f ../../../devenv/dashboards.yaml ../conf/provisioning/dashboards/dev.yaml
}
defaultDatasources() {
echo "setting up all default datasources using provisioning"
devDatasources() {
echo -e "\xE2\x9C\x94 Setting up all dev datasources using provisioning"
ln -s -f ../../../devenv/datasources.yaml ../conf/provisioning/datasources/dev.yaml
}
usage() {
echo -e "install.sh\n\tThis script setups dev provision for datasources and dashboards"
echo -e "\n"
echo "Usage:"
echo " bulk-dashboards - create and provisioning 400 dashboards"
echo " no args - provisiong core datasources and dev dashboards"
}
main() {
echo -e "------------------------------------------------------------------"
echo -e "This script setups provisioning for dev datasources and dashboards"
echo -e "------------------------------------------------------------------"
echo -e "\n"
local cmd=$1
if [[ $cmd == "bulk-dashboards" ]]; then
bulkDashboard
else
defaultDashboards
defaultDatasources
devDashboards
devDatasources
fi
if [[ -z "$cmd" ]]; then
+2 -1
View File
@@ -1,3 +1,4 @@
FROM nginx:alpine
COPY nginx.conf /etc/nginx/nginx.conf
COPY nginx.conf /etc/nginx/nginx.conf
COPY htpasswd /etc/nginx/htpasswd
+3
View File
@@ -0,0 +1,3 @@
user1:$apr1$1odeeQb.$kwV8D/VAAGUDU7pnHuKoV0
user2:$apr1$A2kf25r.$6S0kp3C7vIuixS5CL0XA9.
admin:$apr1$IWn4DoRR$E2ol7fS/dkI18eU4bXnBO1
+20 -1
View File
@@ -13,7 +13,26 @@ http {
listen 10080;
location /grafana/ {
################################################################
# Enable these settings to test with basic auth and an auth proxy header
# the htpasswd file contains an admin user with password admin and
# user1: grafana and user2: grafana
################################################################
# auth_basic "Restricted Content";
# auth_basic_user_file /etc/nginx/htpasswd;
################################################################
# To use the auth proxy header, set the following in custom.ini:
# [auth.proxy]
# enabled = true
# header_name = X-WEBAUTH-USER
# header_property = username
################################################################
# proxy_set_header X-WEBAUTH-USER $remote_user;
proxy_pass http://localhost:3000/;
}
}
}
}
+85
View File
@@ -0,0 +1,85 @@
# To troubleshoot and get more log info enable ldap debug logging in grafana.ini
# [log]
# filters = ldap:debug
[[servers]]
# Ldap server host (specify multiple hosts space separated)
host = "127.0.0.1"
# Default port is 389 or 636 if use_ssl = true
port = 389
# Set to true if ldap server supports TLS
use_ssl = false
# Set to true if connect ldap server with STARTTLS pattern (create connection in insecure, then upgrade to secure connection with TLS)
start_tls = false
# set to true if you want to skip ssl cert validation
ssl_skip_verify = false
# set to the path to your root CA certificate or leave unset to use system defaults
# root_ca_cert = "/path/to/certificate.crt"
# Search user bind dn
bind_dn = "cn=admin,dc=grafana,dc=org"
# Search user bind password
# If the password contains # or ; you have to wrap it with triple quotes. Ex """#password;"""
bind_password = 'grafana'
# User search filter, for example "(cn=%s)" or "(sAMAccountName=%s)" or "(uid=%s)"
search_filter = "(cn=%s)"
# An array of base dns to search through
search_base_dns = ["dc=grafana,dc=org"]
# In POSIX LDAP schemas, without memberOf attribute a secondary query must be made for groups.
# This is done by enabling group_search_filter below. You must also set member_of= "cn"
# in [servers.attributes] below.
# Users with nested/recursive group membership and an LDAP server that supports LDAP_MATCHING_RULE_IN_CHAIN
# can set group_search_filter, group_search_filter_user_attribute, group_search_base_dns and member_of
# below in such a way that the user's recursive group membership is considered.
#
# Nested Groups + Active Directory (AD) Example:
#
# AD groups store the Distinguished Names (DNs) of members, so your filter must
# recursively search your groups for the authenticating user's DN. For example:
#
# group_search_filter = "(member:1.2.840.113556.1.4.1941:=%s)"
# group_search_filter_user_attribute = "distinguishedName"
# group_search_base_dns = ["ou=groups,dc=grafana,dc=org"]
#
# [servers.attributes]
# ...
# member_of = "distinguishedName"
## Group search filter, to retrieve the groups of which the user is a member (only set if memberOf attribute is not available)
# group_search_filter = "(&(objectClass=posixGroup)(memberUid=%s))"
## Group search filter user attribute defines what user attribute gets substituted for %s in group_search_filter.
## Defaults to the value of username in [server.attributes]
## Valid options are any of your values in [servers.attributes]
## If you are using nested groups you probably want to set this and member_of in
## [servers.attributes] to "distinguishedName"
# group_search_filter_user_attribute = "distinguishedName"
## An array of the base DNs to search through for groups. Typically uses ou=groups
# group_search_base_dns = ["ou=groups,dc=grafana,dc=org"]
# Specify names of the ldap attributes your ldap uses
[servers.attributes]
name = "givenName"
surname = "sn"
username = "cn"
member_of = "memberOf"
email = "email"
# Map ldap groups to grafana org roles
[[servers.group_mappings]]
group_dn = "cn=admins,ou=groups,dc=grafana,dc=org"
org_role = "Admin"
# The Grafana organization database id, optional, if left out the default org (id 1) will be used
# org_id = 1
[[servers.group_mappings]]
group_dn = "cn=editors,ou=groups,dc=grafana,dc=org"
org_role = "Editor"
[[servers.group_mappings]]
# If you want to match all (or no ldap groups) then you can use wildcard
group_dn = "*"
org_role = "Viewer"
+2 -5
View File
@@ -14,12 +14,12 @@ After adding ldif files to `prepopulate`:
## Enabling LDAP in Grafana
The default `ldap.toml` file in `conf` has host set to `127.0.0.1` and port to set to 389 so all you need to do is enable it in the .ini file to get Grafana to use this block:
Copy the ldap_dev.toml file in this folder into your `conf` folder (it is gitignored already). To enable it in the .ini file to get Grafana to use this block:
```ini
[auth.ldap]
enabled = true
config_file = conf/ldap.toml
config_file = conf/ldap_dev.toml
; allow_sign_up = true
```
@@ -43,6 +43,3 @@ editors
no groups
ldap-viewer
+286
View File
@@ -0,0 +1,286 @@
+++
title = "Playlist HTTP API "
description = "Playlist Admin HTTP API"
keywords = ["grafana", "http", "documentation", "api", "playlist"]
aliases = ["/http_api/playlist/"]
type = "docs"
[menu.docs]
name = "Playlist"
parent = "http_api"
+++
# Playlist API
## Search Playlist
`GET /api/playlists`
Get all existing playlist for the current organization using pagination
**Example Request**:
```bash
GET /api/playlists HTTP/1.1
Accept: application/json
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
```
Querystring Parameters:
These parameters are used as querystring parameters.
- **query** - Limit response to playlist having a name like this value.
- **limit** - Limit response to *X* number of playlist.
**Example Response**:
```json
HTTP/1.1 200
Content-Type: application/json
[
{
"id": 1,
"name": "my playlist",
"interval": "5m"
}
]
```
## Get one playlist
`GET /api/playlists/:id`
**Example Request**:
```bash
GET /api/playlists/1 HTTP/1.1
Accept: application/json
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
```
**Example Response**:
```json
HTTP/1.1 200
Content-Type: application/json
{
"id" : 1,
"name": "my playlist",
"interval": "5m",
"orgId": "my org",
"items": [
{
"id": 1,
"playlistId": 1,
"type": "dashboard_by_id",
"value": "3",
"order": 1,
"title":"my third dasboard"
},
{
"id": 2,
"playlistId": 1,
"type": "dashboard_by_tag",
"value": "myTag",
"order": 2,
"title":"my other dasboard"
}
]
}
```
## Get Playlist items
`GET /api/playlists/:id/items`
**Example Request**:
```bash
GET /api/playlists/1/items HTTP/1.1
Accept: application/json
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
```
**Example Response**:
```json
HTTP/1.1 200
Content-Type: application/json
[
{
"id": 1,
"playlistId": 1,
"type": "dashboard_by_id",
"value": "3",
"order": 1,
"title":"my third dasboard"
},
{
"id": 2,
"playlistId": 1,
"type": "dashboard_by_tag",
"value": "myTag",
"order": 2,
"title":"my other dasboard"
}
]
```
## Get Playlist dashboards
`GET /api/playlists/:id/dashboards`
**Example Request**:
```bash
GET /api/playlists/1/dashboards HTTP/1.1
Accept: application/json
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
```
**Example Response**:
```json
HTTP/1.1 200
Content-Type: application/json
[
{
"id": 3,
"title": "my third dasboard",
"order": 1,
},
{
"id": 5,
"title":"my other dasboard"
"order": 2,
}
]
```
## Create a playlist
`POST /api/playlists/`
**Example Request**:
```bash
PUT /api/playlists/1 HTTP/1.1
Accept: application/json
Content-Type: application/json
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
{
"name": "my playlist",
"interval": "5m",
"items": [
{
"type": "dashboard_by_id",
"value": "3",
"order": 1,
"title":"my third dasboard"
},
{
"type": "dashboard_by_tag",
"value": "myTag",
"order": 2,
"title":"my other dasboard"
}
]
}
```
**Example Response**:
```json
HTTP/1.1 200
Content-Type: application/json
{
"id": 1,
"name": "my playlist",
"interval": "5m"
}
```
## Update a playlist
`PUT /api/playlists/:id`
**Example Request**:
```bash
PUT /api/playlists/1 HTTP/1.1
Accept: application/json
Content-Type: application/json
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
{
"name": "my playlist",
"interval": "5m",
"items": [
{
"playlistId": 1,
"type": "dashboard_by_id",
"value": "3",
"order": 1,
"title":"my third dasboard"
},
{
"playlistId": 1,
"type": "dashboard_by_tag",
"value": "myTag",
"order": 2,
"title":"my other dasboard"
}
]
}
```
**Example Response**:
```json
HTTP/1.1 200
Content-Type: application/json
{
"id" : 1,
"name": "my playlist",
"interval": "5m",
"orgId": "my org",
"items": [
{
"id": 1,
"playlistId": 1,
"type": "dashboard_by_id",
"value": "3",
"order": 1,
"title":"my third dasboard"
},
{
"id": 2,
"playlistId": 1,
"type": "dashboard_by_tag",
"value": "myTag",
"order": 2,
"title":"my other dasboard"
}
]
}
```
## Delete a playlist
`DELETE /api/playlists/:id`
**Example Request**:
```bash
DELETE /api/playlists/1 HTTP/1.1
Accept: application/json
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
```
**Example Response**:
```json
HTTP/1.1 200
Content-Type: application/json
{}
```
+7 -1
View File
@@ -296,6 +296,12 @@ 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_id
Set this value to automatically add new users to the provided org.
This requires `auto_assign_org` to be set to `true`. Please make sure
that this organization does already exists.
### auto_assign_org_role
The role new users will be assigned for the main organization (if the
@@ -857,7 +863,7 @@ Secret key. e.g. AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Url to where Grafana will send PUT request with images
### public_url
Optional parameter. Url to send to users in notifications, directly appended with the resulting uploaded file name.
Optional parameter. Url to send to users in notifications. If the string contains the sequence ${file}, it will be replaced with the uploaded filename. Otherwise, the file name will be appended to the path part of the url, leaving any query string unchanged.
### username
basic auth username
+9 -2
View File
@@ -23,8 +23,9 @@ specific configuration file (default: `/etc/grafana/ldap.toml`).
### Example config
```toml
# Set to true to log user information returned from LDAP
verbose_logging = false
# To troubleshoot and get more log info enable ldap debug logging in grafana.ini
# [log]
# filters = ldap:debug
[[servers]]
# Ldap server host (specify multiple hosts space separated)
@@ -73,6 +74,8 @@ email = "email"
[[servers.group_mappings]]
group_dn = "cn=admins,dc=grafana,dc=org"
org_role = "Admin"
# To make user an instance admin (Grafana Admin) uncomment line below
# grafana_admin = true
# The Grafana organization database id, optional, if left out the default org (id 1) will be used. Setting this allows for multiple group_dn's to be assigned to the same org_role provided the org_id differs
# org_id = 1
@@ -132,6 +135,10 @@ Users page, this change will be reset the next time the user logs in. If you
change the LDAP groups of a user, the change will take effect the next
time the user logs in.
### Grafana Admin
with a servers.group_mappings section you can set grafana_admin = true or false to sync Grafana Admin permission. A Grafana server admin has admin access over all orgs &
users.
### Priority
The first group mapping that an LDAP user is matched to will be used for the sync. If you have LDAP users that fit multiple mappings, the topmost mapping in the TOML config will be used.
+1 -1
View File
@@ -11,7 +11,7 @@ weight = 1
# Variables
Variables allows for more interactive and dynamic dashboards. Instead of hard-coding things like server, application
and sensor name in you metric queries you can use variables in their place. Variables are shown as dropdown select boxes at the top of
and sensor name in your metric queries you can use variables in their place. Variables are shown as dropdown select boxes at the top of
the dashboard. These dropdowns make it easy to change the data being displayed in your dashboard.
{{< docs-imagebox img="/img/docs/v50/variables_dashboard.png" >}}
+1 -2
View File
@@ -73,8 +73,7 @@ func (hs *HTTPServer) registerRoutes() {
r.Get("/dashboards/", reqSignedIn, Index)
r.Get("/dashboards/*", reqSignedIn, Index)
r.Get("/explore/", reqEditorRole, Index)
r.Get("/explore/*", reqEditorRole, Index)
r.Get("/explore", reqEditorRole, Index)
r.Get("/playlists/", reqSignedIn, Index)
r.Get("/playlists/*", reqSignedIn, Index)
+1
View File
@@ -160,6 +160,7 @@ func CreatePlaylist(c *m.ReqContext, cmd m.CreatePlaylistCommand) Response {
func UpdatePlaylist(c *m.ReqContext, cmd m.UpdatePlaylistCommand) Response {
cmd.OrgId = c.OrgId
cmd.Id = c.ParamsInt64(":id")
if err := bus.Dispatch(&cmd); err != nil {
return Error(500, "Failed to save playlist", err)
+12 -3
View File
@@ -9,6 +9,7 @@ import (
"net/http"
"net/url"
"path"
"strings"
"time"
"github.com/grafana/grafana/pkg/util"
@@ -35,6 +36,16 @@ var netClient = &http.Client{
Transport: netTransport,
}
func (u *WebdavUploader) PublicURL(filename string) string {
if strings.Contains(u.public_url, "${file}") {
return strings.Replace(u.public_url, "${file}", filename, -1)
} else {
publicURL, _ := url.Parse(u.public_url)
publicURL.Path = path.Join(publicURL.Path, filename)
return publicURL.String()
}
}
func (u *WebdavUploader) Upload(ctx context.Context, pa string) (string, error) {
url, _ := url.Parse(u.url)
filename := util.GetRandomString(20) + ".png"
@@ -65,9 +76,7 @@ func (u *WebdavUploader) Upload(ctx context.Context, pa string) (string, error)
}
if u.public_url != "" {
publicURL, _ := url.Parse(u.public_url)
publicURL.Path = path.Join(publicURL.Path, filename)
return publicURL.String(), nil
return u.PublicURL(filename), nil
}
return url.String(), nil
@@ -2,6 +2,7 @@ package imguploader
import (
"context"
"net/url"
"testing"
. "github.com/smartystreets/goconvey/convey"
@@ -26,3 +27,15 @@ func TestUploadToWebdav(t *testing.T) {
So(path, ShouldStartWith, "http://publicurl:8888/webdav/")
})
}
func TestPublicURL(t *testing.T) {
Convey("Given a public URL with parameters, and no template", t, func() {
webdavUploader, _ := NewWebdavImageUploader("http://localhost:8888/webdav/", "test", "test", "http://cloudycloud.me/s/DOIFDOMV/download?files=")
parsed, _ := url.Parse(webdavUploader.PublicURL("fileyfile.png"))
So(parsed.Path, ShouldEndWith, "fileyfile.png")
})
Convey("Given a public URL with parameters, and a template", t, func() {
webdavUploader, _ := NewWebdavImageUploader("http://localhost:8888/webdav/", "test", "test", "http://cloudycloud.me/s/DOIFDOMV/download?files=${file}")
So(webdavUploader.PublicURL("fileyfile.png"), ShouldEndWith, "fileyfile.png")
})
}
+7
View File
@@ -72,6 +72,13 @@ func UpsertUser(cmd *m.UpsertUserCommand) error {
return err
}
// Sync isGrafanaAdmin permission
if extUser.IsGrafanaAdmin != nil && *extUser.IsGrafanaAdmin != cmd.Result.IsAdmin {
if err := bus.Dispatch(&m.UpdateUserPermissionsCommand{UserId: cmd.Result.Id, IsGrafanaAdmin: *extUser.IsGrafanaAdmin}); err != nil {
return err
}
}
err = bus.Dispatch(&m.SyncTeamsCommand{
User: cmd.Result,
ExternalUser: extUser,
+4 -3
View File
@@ -175,6 +175,7 @@ func (a *ldapAuther) GetGrafanaUserFor(ctx *m.ReqContext, ldapUser *LdapUserInfo
if ldapUser.isMemberOf(group.GroupDN) {
extUser.OrgRoles[group.OrgId] = group.OrgRole
extUser.IsGrafanaAdmin = group.IsGrafanaAdmin
}
}
@@ -190,18 +191,18 @@ func (a *ldapAuther) GetGrafanaUserFor(ctx *m.ReqContext, ldapUser *LdapUserInfo
}
// add/update user in grafana
userQuery := &m.UpsertUserCommand{
upsertUserCmd := &m.UpsertUserCommand{
ReqContext: ctx,
ExternalUser: extUser,
SignupAllowed: setting.LdapAllowSignup,
}
err := bus.Dispatch(userQuery)
err := bus.Dispatch(upsertUserCmd)
if err != nil {
return nil, err
}
return userQuery.Result, nil
return upsertUserCmd.Result, nil
}
func (a *ldapAuther) serverBind() error {
+4 -3
View File
@@ -44,9 +44,10 @@ type LdapAttributeMap struct {
}
type LdapGroupToOrgRole struct {
GroupDN string `toml:"group_dn"`
OrgId int64 `toml:"org_id"`
OrgRole m.RoleType `toml:"org_role"`
GroupDN string `toml:"group_dn"`
OrgId int64 `toml:"org_id"`
IsGrafanaAdmin *bool `toml:"grafana_admin"` // This is a pointer to know if it was set or not (for backwards compatability)
OrgRole m.RoleType `toml:"org_role"`
}
var LdapCfg LdapConfig
+42 -8
View File
@@ -98,6 +98,10 @@ func TestLdapAuther(t *testing.T) {
So(result.Login, ShouldEqual, "torkelo")
})
Convey("Should set isGrafanaAdmin to false by default", func() {
So(result.IsAdmin, ShouldBeFalse)
})
})
})
@@ -223,8 +227,32 @@ func TestLdapAuther(t *testing.T) {
So(sc.addOrgUserCmd.Role, ShouldEqual, m.ROLE_ADMIN)
So(sc.setUsingOrgCmd.OrgId, ShouldEqual, 1)
})
Convey("Should not update permissions unless specified", func() {
So(err, ShouldBeNil)
So(sc.updateUserPermissionsCmd, ShouldBeNil)
})
})
ldapAutherScenario("given ldap groups with grafana_admin=true", func(sc *scenarioContext) {
trueVal := true
ldapAuther := NewLdapAuthenticator(&LdapServerConf{
LdapGroups: []*LdapGroupToOrgRole{
{GroupDN: "cn=admins", OrgId: 1, OrgRole: "Admin", IsGrafanaAdmin: &trueVal},
},
})
sc.userOrgsQueryReturns([]*m.UserOrgDTO{})
_, err := ldapAuther.GetGrafanaUserFor(nil, &LdapUserInfo{
MemberOf: []string{"cn=admins"},
})
Convey("Should create user with admin set to true", func() {
So(err, ShouldBeNil)
So(sc.updateUserPermissionsCmd.IsGrafanaAdmin, ShouldBeTrue)
})
})
})
Convey("When calling SyncUser", t, func() {
@@ -332,6 +360,11 @@ func ldapAutherScenario(desc string, fn scenarioFunc) {
return nil
})
bus.AddHandlerCtx("test", func(ctx context.Context, cmd *m.UpdateUserPermissionsCommand) error {
sc.updateUserPermissionsCmd = cmd
return nil
})
bus.AddHandler("test", func(cmd *m.GetUserByAuthInfoQuery) error {
sc.getUserByAuthInfoQuery = cmd
sc.getUserByAuthInfoQuery.Result = &m.User{Login: cmd.Login}
@@ -379,14 +412,15 @@ func ldapAutherScenario(desc string, fn scenarioFunc) {
}
type scenarioContext struct {
getUserByAuthInfoQuery *m.GetUserByAuthInfoQuery
getUserOrgListQuery *m.GetUserOrgListQuery
createUserCmd *m.CreateUserCommand
addOrgUserCmd *m.AddOrgUserCommand
updateOrgUserCmd *m.UpdateOrgUserCommand
removeOrgUserCmd *m.RemoveOrgUserCommand
updateUserCmd *m.UpdateUserCommand
setUsingOrgCmd *m.SetUsingOrgCommand
getUserByAuthInfoQuery *m.GetUserByAuthInfoQuery
getUserOrgListQuery *m.GetUserOrgListQuery
createUserCmd *m.CreateUserCommand
addOrgUserCmd *m.AddOrgUserCommand
updateOrgUserCmd *m.UpdateOrgUserCommand
removeOrgUserCmd *m.RemoveOrgUserCommand
updateUserCmd *m.UpdateUserCommand
setUsingOrgCmd *m.SetUsingOrgCommand
updateUserPermissionsCmd *m.UpdateUserPermissionsCommand
}
func (sc *scenarioContext) userQueryReturns(user *m.User) {
+8
View File
@@ -44,6 +44,7 @@ var (
M_Alerting_Notification_Sent *prometheus.CounterVec
M_Aws_CloudWatch_GetMetricStatistics prometheus.Counter
M_Aws_CloudWatch_ListMetrics prometheus.Counter
M_Aws_CloudWatch_GetMetricData prometheus.Counter
M_DB_DataSource_QueryById prometheus.Counter
// Timers
@@ -218,6 +219,12 @@ func init() {
Namespace: exporterName,
})
M_Aws_CloudWatch_GetMetricData = prometheus.NewCounter(prometheus.CounterOpts{
Name: "aws_cloudwatch_get_metric_data_total",
Help: "counter for getting metric data time series from aws",
Namespace: exporterName,
})
M_DB_DataSource_QueryById = prometheus.NewCounter(prometheus.CounterOpts{
Name: "db_datasource_query_by_id_total",
Help: "counter for getting datasource by id",
@@ -307,6 +314,7 @@ func initMetricVars() {
M_Alerting_Notification_Sent,
M_Aws_CloudWatch_GetMetricStatistics,
M_Aws_CloudWatch_ListMetrics,
M_Aws_CloudWatch_GetMetricData,
M_DB_DataSource_QueryById,
M_Alerting_Active_Alerts,
M_StatTotal_Dashboards,
+1 -1
View File
@@ -63,7 +63,7 @@ type PlaylistDashboards []*PlaylistDashboard
type UpdatePlaylistCommand struct {
OrgId int64 `json:"-"`
Id int64 `json:"id" binding:"Required"`
Id int64 `json:"id"`
Name string `json:"name" binding:"Required"`
Interval string `json:"interval"`
Items []PlaylistItemDTO `json:"items"`
+9 -8
View File
@@ -13,14 +13,15 @@ type UserAuth struct {
}
type ExternalUserInfo struct {
AuthModule string
AuthId string
UserId int64
Email string
Login string
Name string
Groups []string
OrgRoles map[int64]RoleType
AuthModule string
AuthId string
UserId int64
Email string
Login string
Name string
Groups []string
OrgRoles map[int64]RoleType
IsGrafanaAdmin *bool // This is a pointer to know if we should sync this or not (nil = ignore sync)
}
// ---------------------
+3
View File
@@ -17,11 +17,14 @@ import (
plugin "github.com/hashicorp/go-plugin"
)
// DataSourcePlugin contains all metadata about a datasource plugin
type DataSourcePlugin struct {
FrontendPluginBase
Annotations bool `json:"annotations"`
Metrics bool `json:"metrics"`
Alerting bool `json:"alerting"`
Explore bool `json:"explore"`
Logs bool `json:"logs"`
QueryOptions map[string]bool `json:"queryOptions,omitempty"`
BuiltIn bool `json:"builtIn,omitempty"`
Mixed bool `json:"mixed,omitempty"`
+1
View File
@@ -73,6 +73,7 @@ func HandleAlertsQuery(query *m.GetAlertsQuery) error {
alert.name,
alert.state,
alert.new_state_date,
alert.eval_data,
alert.eval_date,
alert.execution_error,
dashboard.uid as dashboard_uid,
+13 -2
View File
@@ -13,7 +13,7 @@ func mockTimeNow() {
var timeSeed int64
timeNow = func() time.Time {
fakeNow := time.Unix(timeSeed, 0)
timeSeed += 1
timeSeed++
return fakeNow
}
}
@@ -30,7 +30,7 @@ func TestAlertingDataAccess(t *testing.T) {
InitTestDB(t)
testDash := insertTestDashboard("dashboard with alerts", 1, 0, false, "alert")
evalData, _ := simplejson.NewJson([]byte(`{"test": "test"}`))
items := []*m.Alert{
{
PanelId: 1,
@@ -40,6 +40,7 @@ func TestAlertingDataAccess(t *testing.T) {
Message: "Alerting message",
Settings: simplejson.New(),
Frequency: 1,
EvalData: evalData,
},
}
@@ -104,8 +105,18 @@ func TestAlertingDataAccess(t *testing.T) {
alert := alertQuery.Result[0]
So(err2, ShouldBeNil)
So(alert.Id, ShouldBeGreaterThan, 0)
So(alert.DashboardId, ShouldEqual, testDash.Id)
So(alert.PanelId, ShouldEqual, 1)
So(alert.Name, ShouldEqual, "Alerting title")
So(alert.State, ShouldEqual, "pending")
So(alert.NewStateDate, ShouldNotBeNil)
So(alert.EvalData, ShouldNotBeNil)
So(alert.EvalData.Get("test").MustString(), ShouldEqual, "test")
So(alert.EvalDate, ShouldNotBeNil)
So(alert.ExecutionError, ShouldEqual, "")
So(alert.DashboardUid, ShouldNotBeNil)
So(alert.DashboardSlug, ShouldEqual, "dashboard-with-alerts")
})
Convey("Viewer cannot read alerts", func() {
+1
View File
@@ -387,6 +387,7 @@ func insertTestDashboardForPlugin(title string, orgId int64, folderId int64, isF
func createUser(name string, role string, isAdmin bool) m.User {
setting.AutoAssignOrg = true
setting.AutoAssignOrgId = 1
setting.AutoAssignOrgRole = role
currentUserCmd := m.CreateUserCommand{Login: name, Email: name + "@test.com", Name: "a " + name, IsAdmin: isAdmin}
+1
View File
@@ -17,6 +17,7 @@ func TestAccountDataAccess(t *testing.T) {
Convey("Given single org mode", func() {
setting.AutoAssignOrg = true
setting.AutoAssignOrgId = 1
setting.AutoAssignOrgRole = "Viewer"
Convey("Users should be added to default organization", func() {
+11 -4
View File
@@ -42,16 +42,23 @@ func getOrgIdForNewUser(cmd *m.CreateUserCommand, sess *DBSession) (int64, error
var org m.Org
if setting.AutoAssignOrg {
// right now auto assign to org with id 1
has, err := sess.Where("id=?", 1).Get(&org)
has, err := sess.Where("id=?", setting.AutoAssignOrgId).Get(&org)
if err != nil {
return 0, err
}
if has {
return org.Id, nil
} else {
if setting.AutoAssignOrgId == 1 {
org.Name = "Main Org."
org.Id = int64(setting.AutoAssignOrgId)
} else {
sqlog.Info("Could not create user: organization id %v does not exist",
setting.AutoAssignOrgId)
return 0, fmt.Errorf("Could not create user: organization id %v does not exist",
setting.AutoAssignOrgId)
}
}
org.Name = "Main Org."
org.Id = 1
} else {
org.Name = cmd.OrgName
if len(org.Name) == 0 {
+2
View File
@@ -100,6 +100,7 @@ var (
AllowUserSignUp bool
AllowUserOrgCreate bool
AutoAssignOrg bool
AutoAssignOrgId int
AutoAssignOrgRole string
VerifyEmailEnabled bool
LoginHint string
@@ -592,6 +593,7 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error {
AllowUserSignUp = users.Key("allow_sign_up").MustBool(true)
AllowUserOrgCreate = users.Key("allow_org_create").MustBool(true)
AutoAssignOrg = users.Key("auto_assign_org").MustBool(true)
AutoAssignOrgId = users.Key("auto_assign_org_id").MustInt(1)
AutoAssignOrgRole = users.Key("auto_assign_org_role").In("Editor", []string{"Editor", "Admin", "Viewer"})
VerifyEmailEnabled = users.Key("verify_email_enabled").MustBool(false)
LoginHint = users.Key("login_hint").String()
+192 -27
View File
@@ -14,6 +14,7 @@ import (
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/tsdb"
"golang.org/x/sync/errgroup"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/request"
@@ -88,48 +89,67 @@ func (e *CloudWatchExecutor) executeTimeSeriesQuery(ctx context.Context, queryCo
Results: make(map[string]*tsdb.QueryResult),
}
errCh := make(chan error, 1)
resCh := make(chan *tsdb.QueryResult, 1)
eg, ectx := errgroup.WithContext(ctx)
currentlyExecuting := 0
getMetricDataQueries := make(map[string]map[string]*CloudWatchQuery)
for i, model := range queryContext.Queries {
queryType := model.Model.Get("type").MustString()
if queryType != "timeSeriesQuery" && queryType != "" {
continue
}
currentlyExecuting++
go func(refId string, index int) {
queryRes, err := e.executeQuery(ctx, queryContext.Queries[index].Model, queryContext)
currentlyExecuting--
if err != nil {
errCh <- err
} else {
queryRes.RefId = refId
resCh <- queryRes
query, err := parseQuery(queryContext.Queries[i].Model)
if err != nil {
return nil, err
}
query.RefId = queryContext.Queries[i].RefId
if query.Id != "" {
if _, ok := getMetricDataQueries[query.Region]; !ok {
getMetricDataQueries[query.Region] = make(map[string]*CloudWatchQuery)
}
}(model.RefId, i)
getMetricDataQueries[query.Region][query.Id] = query
continue
}
if query.Id == "" && query.Expression != "" {
return nil, fmt.Errorf("Invalid query: id should be set if using expression")
}
eg.Go(func() error {
queryRes, err := e.executeQuery(ectx, query, queryContext)
if err != nil {
return err
}
result.Results[queryRes.RefId] = queryRes
return nil
})
}
for currentlyExecuting != 0 {
select {
case res := <-resCh:
result.Results[res.RefId] = res
case err := <-errCh:
return result, err
case <-ctx.Done():
return result, ctx.Err()
if len(getMetricDataQueries) > 0 {
for region, getMetricDataQuery := range getMetricDataQueries {
q := getMetricDataQuery
eg.Go(func() error {
queryResponses, err := e.executeGetMetricDataQuery(ectx, region, q, queryContext)
if err != nil {
return err
}
for _, queryRes := range queryResponses {
result.Results[queryRes.RefId] = queryRes
}
return nil
})
}
}
if err := eg.Wait(); err != nil {
return nil, err
}
return result, nil
}
func (e *CloudWatchExecutor) executeQuery(ctx context.Context, parameters *simplejson.Json, queryContext *tsdb.TsdbQuery) (*tsdb.QueryResult, error) {
query, err := parseQuery(parameters)
if err != nil {
return nil, err
}
func (e *CloudWatchExecutor) executeQuery(ctx context.Context, query *CloudWatchQuery, queryContext *tsdb.TsdbQuery) (*tsdb.QueryResult, error) {
client, err := e.getClient(query.Region)
if err != nil {
return nil, err
@@ -201,6 +221,139 @@ func (e *CloudWatchExecutor) executeQuery(ctx context.Context, parameters *simpl
return queryRes, nil
}
func (e *CloudWatchExecutor) executeGetMetricDataQuery(ctx context.Context, region string, queries map[string]*CloudWatchQuery, queryContext *tsdb.TsdbQuery) ([]*tsdb.QueryResult, error) {
queryResponses := make([]*tsdb.QueryResult, 0)
// validate query
for _, query := range queries {
if !(len(query.Statistics) == 1 && len(query.ExtendedStatistics) == 0) &&
!(len(query.Statistics) == 0 && len(query.ExtendedStatistics) == 1) {
return queryResponses, errors.New("Statistics count should be 1")
}
}
client, err := e.getClient(region)
if err != nil {
return queryResponses, err
}
startTime, err := queryContext.TimeRange.ParseFrom()
if err != nil {
return queryResponses, err
}
endTime, err := queryContext.TimeRange.ParseTo()
if err != nil {
return queryResponses, err
}
params := &cloudwatch.GetMetricDataInput{
StartTime: aws.Time(startTime),
EndTime: aws.Time(endTime),
ScanBy: aws.String("TimestampAscending"),
}
for _, query := range queries {
// 1 minutes resolutin metrics is stored for 15 days, 15 * 24 * 60 = 21600
if query.HighResolution && (((endTime.Unix() - startTime.Unix()) / int64(query.Period)) > 21600) {
return nil, errors.New("too long query period")
}
mdq := &cloudwatch.MetricDataQuery{
Id: aws.String(query.Id),
ReturnData: aws.Bool(query.ReturnData),
}
if query.Expression != "" {
mdq.Expression = aws.String(query.Expression)
} else {
mdq.MetricStat = &cloudwatch.MetricStat{
Metric: &cloudwatch.Metric{
Namespace: aws.String(query.Namespace),
MetricName: aws.String(query.MetricName),
},
Period: aws.Int64(int64(query.Period)),
}
for _, d := range query.Dimensions {
mdq.MetricStat.Metric.Dimensions = append(mdq.MetricStat.Metric.Dimensions,
&cloudwatch.Dimension{
Name: d.Name,
Value: d.Value,
})
}
if len(query.Statistics) == 1 {
mdq.MetricStat.Stat = query.Statistics[0]
} else {
mdq.MetricStat.Stat = query.ExtendedStatistics[0]
}
}
params.MetricDataQueries = append(params.MetricDataQueries, mdq)
}
nextToken := ""
mdr := make(map[string]*cloudwatch.MetricDataResult)
for {
if nextToken != "" {
params.NextToken = aws.String(nextToken)
}
resp, err := client.GetMetricDataWithContext(ctx, params)
if err != nil {
return queryResponses, err
}
metrics.M_Aws_CloudWatch_GetMetricData.Add(float64(len(params.MetricDataQueries)))
for _, r := range resp.MetricDataResults {
if _, ok := mdr[*r.Id]; !ok {
mdr[*r.Id] = r
} else {
mdr[*r.Id].Timestamps = append(mdr[*r.Id].Timestamps, r.Timestamps...)
mdr[*r.Id].Values = append(mdr[*r.Id].Values, r.Values...)
}
}
if resp.NextToken == nil || *resp.NextToken == "" {
break
}
nextToken = *resp.NextToken
}
for i, r := range mdr {
if *r.StatusCode != "Complete" {
return queryResponses, fmt.Errorf("Part of query is failed: %s", *r.StatusCode)
}
queryRes := tsdb.NewQueryResult()
queryRes.RefId = queries[i].RefId
query := queries[*r.Id]
series := tsdb.TimeSeries{
Tags: map[string]string{},
Points: make([]tsdb.TimePoint, 0),
}
for _, d := range query.Dimensions {
series.Tags[*d.Name] = *d.Value
}
s := ""
if len(query.Statistics) == 1 {
s = *query.Statistics[0]
} else {
s = *query.ExtendedStatistics[0]
}
series.Name = formatAlias(query, s, series.Tags)
for j, t := range r.Timestamps {
expectedTimestamp := r.Timestamps[j].Add(time.Duration(query.Period) * time.Second)
if j > 0 && expectedTimestamp.Before(*t) {
series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFromPtr(nil), float64(expectedTimestamp.Unix()*1000)))
}
series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFrom(*r.Values[j]), float64((*t).Unix())*1000))
}
queryRes.Series = append(queryRes.Series, &series)
queryResponses = append(queryResponses, queryRes)
}
return queryResponses, nil
}
func parseDimensions(model *simplejson.Json) ([]*cloudwatch.Dimension, error) {
var result []*cloudwatch.Dimension
@@ -257,6 +410,9 @@ func parseQuery(model *simplejson.Json) (*CloudWatchQuery, error) {
return nil, err
}
id := model.Get("id").MustString("")
expression := model.Get("expression").MustString("")
dimensions, err := parseDimensions(model)
if err != nil {
return nil, err
@@ -295,6 +451,7 @@ func parseQuery(model *simplejson.Json) (*CloudWatchQuery, error) {
alias = "{{metric}}_{{stat}}"
}
returnData := model.Get("returnData").MustBool(false)
highResolution := model.Get("highResolution").MustBool(false)
return &CloudWatchQuery{
@@ -306,11 +463,18 @@ func parseQuery(model *simplejson.Json) (*CloudWatchQuery, error) {
ExtendedStatistics: aws.StringSlice(extendedStatistics),
Period: period,
Alias: alias,
Id: id,
Expression: expression,
ReturnData: returnData,
HighResolution: highResolution,
}, nil
}
func formatAlias(query *CloudWatchQuery, stat string, dimensions map[string]string) string {
if len(query.Id) > 0 && len(query.Expression) > 0 {
return query.Id
}
data := map[string]string{}
data["region"] = query.Region
data["namespace"] = query.Namespace
@@ -338,6 +502,7 @@ func formatAlias(query *CloudWatchQuery, stat string, dimensions map[string]stri
func parseResponse(resp *cloudwatch.GetMetricStatisticsOutput, query *CloudWatchQuery) (*tsdb.QueryResult, error) {
queryRes := tsdb.NewQueryResult()
queryRes.RefId = query.RefId
var value float64
for _, s := range append(query.Statistics, query.ExtendedStatistics...) {
series := tsdb.TimeSeries{
+4
View File
@@ -5,6 +5,7 @@ import (
)
type CloudWatchQuery struct {
RefId string
Region string
Namespace string
MetricName string
@@ -13,5 +14,8 @@ type CloudWatchQuery struct {
ExtendedStatistics []*string
Period int
Alias string
Id string
Expression string
ReturnData bool
HighResolution bool
}
+1
View File
@@ -68,6 +68,7 @@ func (e *DefaultSqlEngine) InitEngine(driverName string, dsInfo *models.DataSour
engine.SetMaxOpenConns(10)
engine.SetMaxIdleConns(10)
engineCache.versions[dsInfo.Id] = dsInfo.Version
engineCache.cache[dsInfo.Id] = engine
e.XormEngine = engine
+1 -1
View File
@@ -21,7 +21,7 @@ func NewTestDataExecutor(dsInfo *models.DataSource) (tsdb.TsdbQueryEndpoint, err
}
func init() {
tsdb.RegisterTsdbQueryEndpoint("grafana-testdata-datasource", NewTestDataExecutor)
tsdb.RegisterTsdbQueryEndpoint("testdata", NewTestDataExecutor)
}
func (e *TestDataExecutor) Query(ctx context.Context, dsInfo *models.DataSource, tsdbQuery *tsdb.TsdbQuery) (*tsdb.Response, error) {
+210 -54
View File
@@ -1,16 +1,20 @@
import React from 'react';
import { hot } from 'react-hot-loader';
import Select from 'react-select';
import kbn from 'app/core/utils/kbn';
import colors from 'app/core/utils/colors';
import TimeSeries from 'app/core/time_series2';
import { decodePathComponent } from 'app/core/utils/location_util';
import { parse as parseDate } from 'app/core/utils/datemath';
import ElapsedTime from './ElapsedTime';
import QueryRows from './QueryRows';
import Graph from './Graph';
import Logs from './Logs';
import Table from './Table';
import TimePicker, { DEFAULT_RANGE } from './TimePicker';
import { DatasourceSrv } from 'app/features/plugins/datasource_srv';
import { buildQueryOptions, ensureQueries, generateQueryKey, hasQuery } from './utils/query';
import { decodePathComponent } from 'app/core/utils/location_util';
import { ensureQueries, generateQueryKey, hasQuery } from './utils/query';
function makeTimeSeriesList(dataList, options) {
return dataList.map((seriesData, index) => {
@@ -30,74 +34,136 @@ function makeTimeSeriesList(dataList, options) {
});
}
function parseInitialState(initial) {
try {
const parsed = JSON.parse(decodePathComponent(initial));
return {
queries: parsed.queries.map(q => q.query),
range: parsed.range,
};
} catch (e) {
console.error(e);
return { queries: [], range: DEFAULT_RANGE };
function parseInitialState(initial: string | undefined) {
if (initial) {
try {
const parsed = JSON.parse(decodePathComponent(initial));
return {
datasource: parsed.datasource,
queries: parsed.queries.map(q => q.query),
range: parsed.range,
};
} catch (e) {
console.error(e);
}
}
return { datasource: null, queries: [], range: DEFAULT_RANGE };
}
interface IExploreState {
datasource: any;
datasourceError: any;
datasourceLoading: any;
datasourceLoading: boolean | null;
datasourceMissing: boolean;
graphResult: any;
initialDatasource?: string;
latency: number;
loading: any;
logsResult: any;
queries: any;
queryError: any;
range: any;
requestOptions: any;
showingGraph: boolean;
showingLogs: boolean;
showingTable: boolean;
supportsGraph: boolean | null;
supportsLogs: boolean | null;
supportsTable: boolean | null;
tableResult: any;
}
// @observer
export class Explore extends React.Component<any, IExploreState> {
datasourceSrv: DatasourceSrv;
el: any;
constructor(props) {
super(props);
const { range, queries } = parseInitialState(props.routeParams.initial);
const { datasource, queries, range } = parseInitialState(props.routeParams.state);
this.state = {
datasource: null,
datasourceError: null,
datasourceLoading: true,
datasourceLoading: null,
datasourceMissing: false,
graphResult: null,
initialDatasource: datasource,
latency: 0,
loading: false,
logsResult: null,
queries: ensureQueries(queries),
queryError: null,
range: range || { ...DEFAULT_RANGE },
requestOptions: null,
showingGraph: true,
showingLogs: true,
showingTable: true,
supportsGraph: null,
supportsLogs: null,
supportsTable: null,
tableResult: null,
...props.initialState,
};
}
async componentDidMount() {
const datasource = await this.props.datasourceSrv.get();
const testResult = await datasource.testDatasource();
if (testResult.status === 'success') {
this.setState({ datasource, datasourceError: null, datasourceLoading: false }, () => this.handleSubmit());
const { datasourceSrv } = this.props;
const { initialDatasource } = this.state;
if (!datasourceSrv) {
throw new Error('No datasource service passed as props.');
}
const datasources = datasourceSrv.getExploreSources();
if (datasources.length > 0) {
this.setState({ datasourceLoading: true });
// Priority: datasource in url, default datasource, first explore datasource
let datasource;
if (initialDatasource) {
datasource = await datasourceSrv.get(initialDatasource);
} else {
datasource = await datasourceSrv.get();
}
if (!datasource.meta.explore) {
datasource = await datasourceSrv.get(datasources[0].name);
}
this.setDatasource(datasource);
} else {
this.setState({ datasource: null, datasourceError: testResult.message, datasourceLoading: false });
this.setState({ datasourceMissing: true });
}
}
componentDidCatch(error) {
this.setState({ datasourceError: error });
console.error(error);
}
async setDatasource(datasource) {
const supportsGraph = datasource.meta.metrics;
const supportsLogs = datasource.meta.logs;
const supportsTable = datasource.meta.metrics;
let datasourceError = null;
try {
const testResult = await datasource.testDatasource();
datasourceError = testResult.status === 'success' ? null : testResult.message;
} catch (error) {
datasourceError = (error && error.statusText) || error;
}
this.setState(
{
datasource,
datasourceError,
supportsGraph,
supportsLogs,
supportsTable,
datasourceLoading: false,
},
() => datasourceError === null && this.handleSubmit()
);
}
getRef = el => {
this.el = el;
};
handleAddQueryRow = index => {
const { queries } = this.state;
const nextQueries = [
@@ -108,6 +174,19 @@ export class Explore extends React.Component<any, IExploreState> {
this.setState({ queries: nextQueries });
};
handleChangeDatasource = async option => {
this.setState({
datasource: null,
datasourceError: null,
datasourceLoading: true,
graphResult: null,
logsResult: null,
tableResult: null,
});
const datasource = await this.props.datasourceSrv.get(option.value);
this.setDatasource(datasource);
};
handleChangeQuery = (query, index) => {
const { queries } = this.state;
const nextQuery = {
@@ -138,6 +217,10 @@ export class Explore extends React.Component<any, IExploreState> {
this.setState(state => ({ showingGraph: !state.showingGraph }));
};
handleClickLogsButton = () => {
this.setState(state => ({ showingLogs: !state.showingLogs }));
};
handleClickSplit = () => {
const { onChangeSplit } = this.props;
if (onChangeSplit) {
@@ -159,29 +242,45 @@ export class Explore extends React.Component<any, IExploreState> {
};
handleSubmit = () => {
const { showingGraph, showingTable } = this.state;
if (showingTable) {
const { showingLogs, showingGraph, showingTable, supportsGraph, supportsLogs, supportsTable } = this.state;
if (showingTable && supportsTable) {
this.runTableQuery();
}
if (showingGraph) {
if (showingGraph && supportsGraph) {
this.runGraphQuery();
}
if (showingLogs && supportsLogs) {
this.runLogsQuery();
}
};
async runGraphQuery() {
buildQueryOptions(targetOptions: { format: string; instant?: boolean }) {
const { datasource, queries, range } = this.state;
const resolution = this.el.offsetWidth;
const absoluteRange = {
from: parseDate(range.from, false),
to: parseDate(range.to, true),
};
const { interval } = kbn.calculateInterval(absoluteRange, resolution, datasource.interval);
const targets = queries.map(q => ({
...targetOptions,
expr: q.query,
}));
return {
interval,
range,
targets,
};
}
async runGraphQuery() {
const { datasource, queries } = this.state;
if (!hasQuery(queries)) {
return;
}
this.setState({ latency: 0, loading: true, graphResult: null, queryError: null });
const now = Date.now();
const options = buildQueryOptions({
format: 'time_series',
interval: datasource.interval,
instant: false,
range,
queries: queries.map(q => q.query),
});
const options = this.buildQueryOptions({ format: 'time_series', instant: false });
try {
const res = await datasource.query(options);
const result = makeTimeSeriesList(res.data, options);
@@ -195,18 +294,15 @@ export class Explore extends React.Component<any, IExploreState> {
}
async runTableQuery() {
const { datasource, queries, range } = this.state;
const { datasource, queries } = this.state;
if (!hasQuery(queries)) {
return;
}
this.setState({ latency: 0, loading: true, queryError: null, tableResult: null });
const now = Date.now();
const options = buildQueryOptions({
const options = this.buildQueryOptions({
format: 'table',
interval: datasource.interval,
instant: true,
range,
queries: queries.map(q => q.query),
});
try {
const res = await datasource.query(options);
@@ -220,35 +316,71 @@ export class Explore extends React.Component<any, IExploreState> {
}
}
async runLogsQuery() {
const { datasource, queries } = this.state;
if (!hasQuery(queries)) {
return;
}
this.setState({ latency: 0, loading: true, queryError: null, logsResult: null });
const now = Date.now();
const options = this.buildQueryOptions({
format: 'logs',
});
try {
const res = await datasource.query(options);
const logsData = res.data;
const latency = Date.now() - now;
this.setState({ latency, loading: false, logsResult: logsData, requestOptions: options });
} catch (response) {
console.error(response);
const queryError = response.data ? response.data.error : response;
this.setState({ loading: false, queryError });
}
}
request = url => {
const { datasource } = this.state;
return datasource.metadataRequest(url);
};
render() {
const { position, split } = this.props;
const { datasourceSrv, position, split } = this.props;
const {
datasource,
datasourceError,
datasourceLoading,
datasourceMissing,
graphResult,
latency,
loading,
logsResult,
queries,
queryError,
range,
requestOptions,
showingGraph,
showingLogs,
showingTable,
supportsGraph,
supportsLogs,
supportsTable,
tableResult,
} = this.state;
const showingBoth = showingGraph && showingTable;
const graphHeight = showingBoth ? '200px' : '400px';
const graphButtonActive = showingBoth || showingGraph ? 'active' : '';
const logsButtonActive = showingLogs ? 'active' : '';
const tableButtonActive = showingBoth || showingTable ? 'active' : '';
const exploreClass = split ? 'explore explore-split' : 'explore';
const datasources = datasourceSrv.getExploreSources().map(ds => ({
value: ds.name,
label: ds.name,
}));
const selectedDatasource = datasource ? datasource.name : undefined;
return (
<div className={exploreClass}>
<div className={exploreClass} ref={this.getRef}>
<div className="navbar">
{position === 'left' ? (
<div>
@@ -264,6 +396,18 @@ export class Explore extends React.Component<any, IExploreState> {
</button>
</div>
)}
{!datasourceMissing ? (
<div className="navbar-buttons">
<Select
className="datasource-picker"
clearable={false}
onChange={this.handleChangeDatasource}
options={datasources}
placeholder="Loading datasources..."
value={selectedDatasource}
/>
</div>
) : null}
<div className="navbar__spacer" />
{position === 'left' && !split ? (
<div className="navbar-buttons">
@@ -273,12 +417,21 @@ export class Explore extends React.Component<any, IExploreState> {
</div>
) : null}
<div className="navbar-buttons">
<button className={`btn navbar-button ${graphButtonActive}`} onClick={this.handleClickGraphButton}>
Graph
</button>
<button className={`btn navbar-button ${tableButtonActive}`} onClick={this.handleClickTableButton}>
Table
</button>
{supportsGraph ? (
<button className={`btn navbar-button ${graphButtonActive}`} onClick={this.handleClickGraphButton}>
Graph
</button>
) : null}
{supportsTable ? (
<button className={`btn navbar-button ${tableButtonActive}`} onClick={this.handleClickTableButton}>
Table
</button>
) : null}
{supportsLogs ? (
<button className={`btn navbar-button ${logsButtonActive}`} onClick={this.handleClickLogsButton}>
Logs
</button>
) : null}
</div>
<TimePicker range={range} onChangeTime={this.handleChangeTime} />
<div className="navbar-buttons relative">
@@ -291,13 +444,15 @@ export class Explore extends React.Component<any, IExploreState> {
{datasourceLoading ? <div className="explore-container">Loading datasource...</div> : null}
{datasourceError ? (
<div className="explore-container" title={datasourceError}>
Error connecting to datasource.
</div>
{datasourceMissing ? (
<div className="explore-container">Please add a datasource that supports Explore (e.g., Prometheus).</div>
) : null}
{datasource ? (
{datasourceError ? (
<div className="explore-container">Error connecting to datasource. [{datasourceError}]</div>
) : null}
{datasource && !datasourceError ? (
<div className="explore-container">
<QueryRows
queries={queries}
@@ -309,7 +464,7 @@ export class Explore extends React.Component<any, IExploreState> {
/>
{queryError ? <div className="text-warning m-a-2">{queryError}</div> : null}
<main className="m-t-2">
{showingGraph ? (
{supportsGraph && showingGraph ? (
<Graph
data={graphResult}
id={`explore-graph-${position}`}
@@ -318,7 +473,8 @@ export class Explore extends React.Component<any, IExploreState> {
split={split}
/>
) : null}
{showingTable ? <Table data={tableResult} className="m-t-3" /> : null}
{supportsTable && showingTable ? <Table data={tableResult} className="m-t-3" /> : null}
{supportsLogs && showingLogs ? <Logs data={logsResult} /> : null}
</main>
</div>
) : null}
@@ -0,0 +1,9 @@
import React from 'react';
export default function({ value }) {
return (
<div>
<pre>{JSON.stringify(value, undefined, 2)}</pre>
</div>
);
}
+66
View File
@@ -0,0 +1,66 @@
import React, { Fragment, PureComponent } from 'react';
import { LogsModel, LogRow } from 'app/core/logs_model';
interface LogsProps {
className?: string;
data: LogsModel;
}
const EXAMPLE_QUERY = '{job="default/prometheus"}';
const Entry: React.SFC<LogRow> = props => {
const { entry, searchMatches } = props;
if (searchMatches && searchMatches.length > 0) {
let lastMatchEnd = 0;
const spans = searchMatches.reduce((acc, match, i) => {
// Insert non-match
if (match.start !== lastMatchEnd) {
acc.push(<>{entry.slice(lastMatchEnd, match.start)}</>);
}
// Match
acc.push(
<span className="logs-row-match-highlight" title={`Matching expression: ${match.text}`}>
{entry.substr(match.start, match.length)}
</span>
);
lastMatchEnd = match.start + match.length;
// Non-matching end
if (i === searchMatches.length - 1) {
acc.push(<>{entry.slice(lastMatchEnd)}</>);
}
return acc;
}, []);
return <>{spans}</>;
}
return <>{props.entry}</>;
};
export default class Logs extends PureComponent<LogsProps, any> {
render() {
const { className = '', data } = this.props;
const hasData = data && data.rows && data.rows.length > 0;
return (
<div className={`${className} logs`}>
{hasData ? (
<div className="logs-entries panel-container">
{data.rows.map(row => (
<Fragment key={row.key}>
<div className={row.logLevel ? `logs-row-level logs-row-level-${row.logLevel}` : ''} />
<div title={`${row.timestamp} (${row.timeFromNow})`}>{row.timeLocal}</div>
<div>
<Entry {...row} />
</div>
</Fragment>
))}
</div>
) : null}
{!hasData ? (
<div className="panel-container">
Enter a query like <code>{EXAMPLE_QUERY}</code>
</div>
) : null}
</div>
);
}
}
+4 -2
View File
@@ -17,6 +17,7 @@ import { processLabels, RATE_RANGES, cleanText } from './utils/prometheus';
import Typeahead from './Typeahead';
const EMPTY_METRIC = '';
const METRIC_MARK = 'metric';
export const TYPEAHEAD_DEBOUNCE = 300;
function flattenSuggestions(s) {
@@ -135,7 +136,7 @@ class QueryField extends React.Component<any, any> {
if (!this.state.metrics) {
return;
}
setPrismTokens(this.props.prismLanguage, 'metrics', this.state.metrics);
setPrismTokens(this.props.prismLanguage, METRIC_MARK, this.state.metrics);
// Trigger re-render
window.requestAnimationFrame(() => {
@@ -184,7 +185,7 @@ class QueryField extends React.Component<any, any> {
let typeaheadContext = null;
// Take first metric as lucky guess
const metricNode = editorNode.querySelector('.metric');
const metricNode = editorNode.querySelector(`.${METRIC_MARK}`);
if (wrapperClasses.contains('context-range')) {
// Rate ranges
@@ -416,6 +417,7 @@ class QueryField extends React.Component<any, any> {
const url = `/api/v1/label/${key}/values`;
try {
const res = await this.request(url);
console.log(res);
const body = await (res.data || res.json());
const pairs = this.state.labelValues[EMPTY_METRIC];
const values = {
@@ -1,15 +1,3 @@
export function buildQueryOptions({ format, interval, instant, range, queries }) {
return {
interval,
range,
targets: queries.map(expr => ({
expr,
format,
instant,
})),
};
}
export function generateQueryKey(index = 0) {
return `Q-${Date.now()}-${Math.random()}-${index}`;
}
@@ -29,11 +29,13 @@ export function pageScrollbar() {
scope.$on('$routeChangeSuccess', () => {
lastPos = 0;
elem[0].scrollTop = 0;
elem[0].focus();
// Focus page to enable scrolling by keyboard
elem[0].focus({ preventScroll: true });
});
elem[0].tabIndex = -1;
elem[0].focus();
// Focus page to enable scrolling by keyboard
elem[0].focus({ preventScroll: true });
},
};
}
+29
View File
@@ -0,0 +1,29 @@
export enum LogLevel {
crit = 'crit',
warn = 'warn',
err = 'error',
error = 'error',
info = 'info',
debug = 'debug',
trace = 'trace',
}
export interface LogSearchMatch {
start: number;
length: number;
text?: string;
}
export interface LogRow {
key: string;
entry: string;
logLevel: LogLevel;
timestamp: string;
timeFromNow: string;
timeLocal: string;
searchMatches?: LogSearchMatch[];
}
export interface LogsModel {
rows: LogRow[];
}
+1 -1
View File
@@ -191,7 +191,7 @@ export class KeybindingSrv {
range,
};
const exploreState = encodePathComponent(JSON.stringify(state));
this.$location.url(`/explore/${exploreState}`);
this.$location.url(`/explore?state=${exploreState}`);
}
}
});
+2
View File
@@ -449,6 +449,7 @@ kbn.valueFormats.currencyNOK = kbn.formatBuilders.currency('kr');
kbn.valueFormats.currencySEK = kbn.formatBuilders.currency('kr');
kbn.valueFormats.currencyCZK = kbn.formatBuilders.currency('czk');
kbn.valueFormats.currencyCHF = kbn.formatBuilders.currency('CHF');
kbn.valueFormats.currencyPLN = kbn.formatBuilders.currency('zł');
// Data (Binary)
kbn.valueFormats.bits = kbn.formatBuilders.binarySIPrefix('b');
@@ -880,6 +881,7 @@ kbn.getUnitFormats = function() {
{ text: 'Swedish Krona (kr)', value: 'currencySEK' },
{ text: 'Czech koruna (czk)', value: 'currencyCZK' },
{ text: 'Swiss franc (CHF)', value: 'currencyCHF' },
{ text: 'Polish Złoty (PLN)', value: 'currencyPLN' },
],
},
{
@@ -222,7 +222,7 @@ class MetricsPanelCtrl extends PanelCtrl {
// and add built in variables interval and interval_ms
var scopedVars = Object.assign({}, this.panel.scopedVars, {
__interval: { text: this.interval, value: this.interval },
__interval_ms: { text: String(this.intervalMs), value: String(this.intervalMs) },
__interval_ms: { text: this.intervalMs, value: this.intervalMs },
});
var metricsQuery = {
@@ -332,7 +332,7 @@ class MetricsPanelCtrl extends PanelCtrl {
range,
};
const exploreState = encodePathComponent(JSON.stringify(state));
this.$location.url(`/explore/${exploreState}`);
this.$location.url(`/explore?state=${exploreState}`);
}
addQuery(target) {
@@ -4,11 +4,13 @@ import * as elasticsearchPlugin from 'app/plugins/datasource/elasticsearch/modul
import * as opentsdbPlugin from 'app/plugins/datasource/opentsdb/module';
import * as grafanaPlugin from 'app/plugins/datasource/grafana/module';
import * as influxdbPlugin from 'app/plugins/datasource/influxdb/module';
import * as loggingPlugin from 'app/plugins/datasource/logging/module';
import * as mixedPlugin from 'app/plugins/datasource/mixed/module';
import * as mysqlPlugin from 'app/plugins/datasource/mysql/module';
import * as postgresPlugin from 'app/plugins/datasource/postgres/module';
import * as prometheusPlugin from 'app/plugins/datasource/prometheus/module';
import * as mssqlPlugin from 'app/plugins/datasource/mssql/module';
import * as testDataDSPlugin from 'app/plugins/datasource/testdata/module';
import * as textPanel from 'app/plugins/panel/text/module';
import * as graphPanel from 'app/plugins/panel/graph/module';
@@ -20,9 +22,6 @@ import * as tablePanel from 'app/plugins/panel/table/module';
import * as singlestatPanel from 'app/plugins/panel/singlestat/module';
import * as gettingStartedPanel from 'app/plugins/panel/gettingstarted/module';
import * as testDataAppPlugin from 'app/plugins/app/testdata/module';
import * as testDataDSPlugin from 'app/plugins/app/testdata/datasource/module';
const builtInPlugins = {
'app/plugins/datasource/graphite/module': graphitePlugin,
'app/plugins/datasource/cloudwatch/module': cloudwatchPlugin,
@@ -30,13 +29,13 @@ const builtInPlugins = {
'app/plugins/datasource/opentsdb/module': opentsdbPlugin,
'app/plugins/datasource/grafana/module': grafanaPlugin,
'app/plugins/datasource/influxdb/module': influxdbPlugin,
'app/plugins/datasource/logging/module': loggingPlugin,
'app/plugins/datasource/mixed/module': mixedPlugin,
'app/plugins/datasource/mysql/module': mysqlPlugin,
'app/plugins/datasource/postgres/module': postgresPlugin,
'app/plugins/datasource/mssql/module': mssqlPlugin,
'app/plugins/datasource/prometheus/module': prometheusPlugin,
'app/plugins/app/testdata/module': testDataAppPlugin,
'app/plugins/app/testdata/datasource/module': testDataDSPlugin,
'app/plugins/datasource/testdata/module': testDataDSPlugin,
'app/plugins/panel/text/module': textPanel,
'app/plugins/panel/graph/module': graphPanel,
+14 -5
View File
@@ -34,13 +34,13 @@ export class DatasourceSrv {
}
loadDatasource(name) {
var dsConfig = config.datasources[name];
const dsConfig = config.datasources[name];
if (!dsConfig) {
return this.$q.reject({ message: 'Datasource named ' + name + ' was not found' });
}
var deferred = this.$q.defer();
var pluginDef = dsConfig.meta;
const deferred = this.$q.defer();
const pluginDef = dsConfig.meta;
importPluginModule(pluginDef.module)
.then(plugin => {
@@ -55,7 +55,7 @@ export class DatasourceSrv {
throw new Error('Plugin module is missing Datasource constructor');
}
var instance = this.$injector.instantiate(plugin.Datasource, { instanceSettings: dsConfig });
const instance = this.$injector.instantiate(plugin.Datasource, { instanceSettings: dsConfig });
instance.meta = pluginDef;
instance.name = name;
this.datasources[name] = instance;
@@ -73,7 +73,7 @@ export class DatasourceSrv {
}
getAnnotationSources() {
var sources = [];
const sources = [];
this.addDataSourceVariables(sources);
@@ -86,6 +86,14 @@ export class DatasourceSrv {
return sources;
}
getExploreSources() {
const { datasources } = config;
const es = Object.keys(datasources)
.map(name => datasources[name])
.filter(ds => ds.meta && ds.meta.explore);
return _.sortBy(es, ['name']);
}
getMetricSources(options) {
var metricSources = [];
@@ -155,3 +163,4 @@ export class DatasourceSrv {
}
coreModule.service('datasourceSrv', DatasourceSrv);
export default DatasourceSrv;
@@ -19,6 +19,7 @@ export class DataSourcesCtrl {
onQueryUpdated() {
let regex = new RegExp(this.searchQuery, 'ig');
this.datasources = _.filter(this.unfiltered, item => {
regex.lastIndex = 0;
return regex.test(item.name) || regex.test(item.type);
});
}
+3 -1
View File
@@ -56,7 +56,7 @@ System.config({
css: 'vendor/plugin-css/css.js',
},
meta: {
'*': {
'/*': {
esModule: true,
authorization: true,
loader: 'plugin-loader',
@@ -126,6 +126,7 @@ import 'vendor/flot/jquery.flot.stackpercent';
import 'vendor/flot/jquery.flot.fillbelow';
import 'vendor/flot/jquery.flot.crosshair';
import 'vendor/flot/jquery.flot.dashes';
import 'vendor/flot/jquery.flot.gauge';
const flotDeps = [
'jquery.flot',
@@ -137,6 +138,7 @@ const flotDeps = [
'jquery.flot.selection',
'jquery.flot.stackpercent',
'jquery.flot.events',
'jquery.flot.gauge',
];
for (let flotDep of flotDeps) {
exposeToPlugin(flotDep, { fakeDep: 1 });
@@ -17,9 +17,35 @@ const templateSrv = {
describe('datasource_srv', function() {
let _datasourceSrv = new DatasourceSrv({}, {}, {}, templateSrv);
let metricSources;
describe('when loading explore sources', () => {
beforeEach(() => {
config.datasources = {
explore1: {
name: 'explore1',
meta: { explore: true, metrics: true },
},
explore2: {
name: 'explore2',
meta: { explore: true, metrics: false },
},
nonExplore: {
name: 'nonExplore',
meta: { explore: false, metrics: true },
},
};
});
it('should return list of explore sources', () => {
const exploreSources = _datasourceSrv.getExploreSources();
expect(exploreSources.length).toBe(2);
expect(exploreSources[0].name).toBe('explore1');
expect(exploreSources[1].name).toBe('explore2');
});
});
describe('when loading metric sources', () => {
let metricSources;
let unsortedDatasources = {
mmm: {
type: 'test-db',
File diff suppressed because it is too large Load Diff
-34
View File
@@ -1,34 +0,0 @@
export class ConfigCtrl {
static template = '';
appEditCtrl: any;
/** @ngInject **/
constructor(private backendSrv) {
this.appEditCtrl.setPreUpdateHook(this.initDatasource.bind(this));
}
initDatasource() {
return this.backendSrv.get('/api/datasources').then(res => {
var found = false;
for (let ds of res) {
if (ds.type === 'grafana-testdata-datasource') {
found = true;
}
}
if (!found) {
var dsInstance = {
name: 'Grafana TestData',
type: 'grafana-testdata-datasource',
access: 'direct',
jsonData: {},
};
return this.backendSrv.post('/api/datasources', dsInstance);
}
return Promise.resolve();
});
}
}
-32
View File
@@ -1,32 +0,0 @@
{
"type": "app",
"name": "Grafana TestData",
"id": "testdata",
"info": {
"description": "Grafana test data app",
"author": {
"name": "Grafana Project",
"url": "https://grafana.com"
},
"version": "1.0.17",
"updated": "2016-09-26"
},
"includes": [
{
"type": "dashboard",
"name": "TestData - Graph Last 1h",
"path": "dashboards/graph_last_1h.json"
},
{
"type": "dashboard",
"name": "TestData - Alerts",
"path": "dashboards/alerts.json"
}
],
"dependencies": {
"grafanaVersion": "4.x.x"
}
}
@@ -30,7 +30,9 @@ export default class CloudWatchDatasource {
var queries = _.filter(options.targets, item => {
return (
item.hide !== true && !!item.region && !!item.namespace && !!item.metricName && !_.isEmpty(item.statistics)
(item.id !== '' || item.hide !== true) &&
((!!item.region && !!item.namespace && !!item.metricName && !_.isEmpty(item.statistics)) ||
item.expression.length > 0)
);
}).map(item => {
item.region = this.templateSrv.replace(this.getActualRegion(item.region), options.scopedVars);
@@ -38,6 +40,9 @@ export default class CloudWatchDatasource {
item.metricName = this.templateSrv.replace(item.metricName, options.scopedVars);
item.dimensions = this.convertDimensionFormat(item.dimensions, options.scopedVars);
item.period = String(this.getPeriod(item, options)); // use string format for period in graph query, and alerting
item.id = this.templateSrv.replace(item.id, options.scopedVars);
item.expression = this.templateSrv.replace(item.expression, options.scopedVars);
item.returnData = typeof item.hide === 'undefined' ? true : !item.hide;
// valid ExtendedStatistics is like p90.00, check the pattern
let hasInvalidStatistics = item.statistics.some(s => {
@@ -407,6 +412,11 @@ export default class CloudWatchDatasource {
scopedVar[variable.name] = v;
t.refId = target.refId + '_' + v.value;
t.dimensions[dimensionKey] = templateSrv.replace(t.dimensions[dimensionKey], scopedVar);
if (variable.multi && target.id) {
t.id = target.id + window.btoa(v.value).replace(/=/g, '0'); // generate unique id
} else {
t.id = target.id;
}
return t;
});
}
@@ -1,4 +1,4 @@
<div class="gf-form-inline">
<div class="gf-form-inline" ng-if="target.expression.length === 0">
<div class="gf-form">
<label class="gf-form-label query-keyword width-8">Metric</label>
@@ -20,7 +20,7 @@
</div>
</div>
<div class="gf-form-inline">
<div class="gf-form-inline" ng-if="target.expression.length === 0">
<div class="gf-form">
<label class="gf-form-label query-keyword width-8">Dimensions</label>
<metric-segment ng-repeat="segment in dimSegments" segment="segment" get-options="getDimSegments(segment, $index)" on-change="dimSegmentChanged(segment, $index)"></metric-segment>
@@ -31,18 +31,35 @@
</div>
</div>
<div class="gf-form-inline">
<div class="gf-form-inline" ng-if="target.statistics.length === 1">
<div class="gf-form">
<label class="gf-form-label query-keyword width-8">
Min period
<info-popover mode="right-normal">Minimum interval between points in seconds</info-popover>
<label class=" gf-form-label query-keyword width-8 ">
Id
<info-popover mode="right-normal ">Id can include numbers, letters, and underscore, and must start with a lowercase letter.</info-popover>
</label>
<input type="text" class="gf-form-input" ng-model="target.period" spellcheck='false' placeholder="auto" ng-model-onblur ng-change="onChange()" />
<input type="text " class="gf-form-input " ng-model="target.id " spellcheck='false' ng-pattern='/^[a-z][A-Z0-9_]*/' ng-model-onblur
ng-change="onChange() ">
</div>
<div class="gf-form max-width-30">
<label class="gf-form-label query-keyword width-7">Alias</label>
<input type="text" class="gf-form-input" ng-model="target.alias" spellcheck='false' ng-model-onblur ng-change="onChange()">
<info-popover mode="right-absolute">
<div class="gf-form max-width-30 ">
<label class="gf-form-label query-keyword width-7 ">Expression</label>
<input type="text " class="gf-form-input " ng-model="target.expression
" spellcheck='false' ng-model-onblur ng-change="onChange() ">
</div>
</div>
<div class="gf-form-inline ">
<div class="gf-form ">
<label class="gf-form-label query-keyword width-8 ">
Min period
<info-popover mode="right-normal ">Minimum interval between points in seconds</info-popover>
</label>
<input type="text " class="gf-form-input " ng-model="target.period " spellcheck='false' placeholder="auto
" ng-model-onblur ng-change="onChange() " />
</div>
<div class="gf-form max-width-30 ">
<label class="gf-form-label query-keyword width-7 ">Alias</label>
<input type="text " class="gf-form-input " ng-model="target.alias " spellcheck='false' ng-model-onblur ng-change="onChange() ">
<info-popover mode="right-absolute ">
Alias replacement variables:
<ul ng-non-bindable>
<li>{{metric}}</li>
@@ -54,12 +71,12 @@
</ul>
</info-popover>
</div>
<div class="gf-form">
<gf-form-switch class="gf-form" label="HighRes" label-class="width-5" checked="target.highResolution" on-change="onChange()">
<div class="gf-form ">
<gf-form-switch class="gf-form " label="HighRes " label-class="width-5 " checked="target.highResolution " on-change="onChange() ">
</gf-form-switch>
</div>
<div class="gf-form gf-form--grow">
<div class="gf-form-label gf-form-label--grow"></div>
<div class="gf-form gf-form--grow ">
<div class="gf-form-label gf-form-label--grow "></div>
</div>
</div>
@@ -27,6 +27,9 @@ export class CloudWatchQueryParameterCtrl {
target.dimensions = target.dimensions || {};
target.period = target.period || '';
target.region = target.region || 'default';
target.id = target.id || '';
target.expression = target.expression || '';
target.returnData = target.returnData || false;
target.highResolution = target.highResolution || false;
$scope.regionSegment = uiSegmentSrv.getSegmentForValue($scope.target.region, 'select region');
@@ -0,0 +1,3 @@
# Grafana Logging Datasource - Native Plugin
This is a **built in** datasource that allows you to connect to Grafana's logging service.
@@ -0,0 +1,38 @@
import { parseQuery } from './datasource';
describe('parseQuery', () => {
it('returns empty for empty string', () => {
expect(parseQuery('')).toEqual({
query: '',
regexp: '',
});
});
it('returns regexp for strings without query', () => {
expect(parseQuery('test')).toEqual({
query: '',
regexp: 'test',
});
});
it('returns query for strings without regexp', () => {
expect(parseQuery('{foo="bar"}')).toEqual({
query: '{foo="bar"}',
regexp: '',
});
});
it('returns query for strings with query and search string', () => {
expect(parseQuery('x {foo="bar"}')).toEqual({
query: '{foo="bar"}',
regexp: 'x',
});
});
it('returns query for strings with query and regexp', () => {
expect(parseQuery('{foo="bar"} x|y')).toEqual({
query: '{foo="bar"}',
regexp: 'x|y',
});
});
});
@@ -0,0 +1,134 @@
import _ from 'lodash';
import * as dateMath from 'app/core/utils/datemath';
import { processStreams } from './result_transformer';
const DEFAULT_LIMIT = 100;
const DEFAULT_QUERY_PARAMS = {
direction: 'BACKWARD',
limit: DEFAULT_LIMIT,
regexp: '',
query: '',
};
const QUERY_REGEXP = /({\w+="[^"]+"})?\s*(\w[^{]+)?\s*({\w+="[^"]+"})?/;
export function parseQuery(input: string) {
const match = input.match(QUERY_REGEXP);
let query = '';
let regexp = '';
if (match) {
if (match[1]) {
query = match[1];
}
if (match[2]) {
regexp = match[2].trim();
}
if (match[3]) {
if (match[1]) {
query = `${match[1].slice(0, -1)},${match[3].slice(1)}`;
} else {
query = match[3];
}
}
}
return { query, regexp };
}
function serializeParams(data: any) {
return Object.keys(data)
.map(k => {
const v = data[k];
return encodeURIComponent(k) + '=' + encodeURIComponent(v);
})
.join('&');
}
export default class LoggingDatasource {
/** @ngInject */
constructor(private instanceSettings, private backendSrv, private templateSrv) {}
_request(apiUrl: string, data?, options?: any) {
const baseUrl = this.instanceSettings.url;
const params = data ? serializeParams(data) : '';
const url = `${baseUrl}${apiUrl}?${params}`;
const req = {
...options,
url,
};
return this.backendSrv.datasourceRequest(req);
}
prepareQueryTarget(target, options) {
const interpolated = this.templateSrv.replace(target.expr);
const start = this.getTime(options.range.from, false);
const end = this.getTime(options.range.to, true);
return {
...DEFAULT_QUERY_PARAMS,
...parseQuery(interpolated),
start,
end,
};
}
query(options) {
const queryTargets = options.targets
.filter(target => target.expr)
.map(target => this.prepareQueryTarget(target, options));
if (queryTargets.length === 0) {
return Promise.resolve({ data: [] });
}
const queries = queryTargets.map(target => this._request('/api/prom/query', target));
return Promise.all(queries).then((results: any[]) => {
// Flatten streams from multiple queries
const allStreams = results.reduce((acc, response, i) => {
const streams = response.data.streams || [];
// Inject search for match highlighting
const search = queryTargets[i].regexp;
streams.forEach(s => {
s.search = search;
});
return [...acc, ...streams];
}, []);
const model = processStreams(allStreams, DEFAULT_LIMIT);
return { data: model };
});
}
metadataRequest(url) {
// HACK to get label values for {job=|}, will be replaced when implementing LoggingQueryField
const apiUrl = url.replace('v1', 'prom');
return this._request(apiUrl, { silent: true }).then(res => {
const data = { data: { data: res.data.values || [] } };
return data;
});
}
getTime(date, roundUp) {
if (_.isString(date)) {
date = dateMath.parse(date, roundUp);
}
return Math.ceil(date.valueOf() * 1e6);
}
testDatasource() {
return this._request('/api/prom/label')
.then(res => {
if (res && res.data && res.data.values && res.data.values.length > 0) {
return { status: 'success', message: 'Data source connected and labels found.' };
}
return {
status: 'error',
message: 'Data source connected, but no labels received. Verify that logging is configured properly.',
};
})
.catch(err => {
return { status: 'error', message: err.message };
});
}
}
@@ -0,0 +1,57 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 20.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="351px" height="365px" viewBox="0 0 351 365" style="enable-background:new 0 0 351 365;" xml:space="preserve">
<style type="text/css">
.st0{fill:url(#SVGID_1_);}
</style>
<g id="Layer_1_1_">
</g>
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="175.5" y1="445.4948" x2="175.5" y2="114.0346">
<stop offset="0" style="stop-color:#FFF100"/>
<stop offset="1" style="stop-color:#F05A28"/>
</linearGradient>
<path class="st0" d="M342,161.2c-0.6-6.1-1.6-13.1-3.6-20.9c-2-7.7-5-16.2-9.4-25c-4.4-8.8-10.1-17.9-17.5-26.8
c-2.9-3.5-6.1-6.9-9.5-10.2c5.1-20.3-6.2-37.9-6.2-37.9c-19.5-1.2-31.9,6.1-36.5,9.4c-0.8-0.3-1.5-0.7-2.3-1
c-3.3-1.3-6.7-2.6-10.3-3.7c-3.5-1.1-7.1-2.1-10.8-3c-3.7-0.9-7.4-1.6-11.2-2.2c-0.7-0.1-1.3-0.2-2-0.3
c-8.5-27.2-32.9-38.6-32.9-38.6c-27.3,17.3-32.4,41.5-32.4,41.5s-0.1,0.5-0.3,1.4c-1.5,0.4-3,0.9-4.5,1.3c-2.1,0.6-4.2,1.4-6.2,2.2
c-2.1,0.8-4.1,1.6-6.2,2.5c-4.1,1.8-8.2,3.8-12.2,6c-3.9,2.2-7.7,4.6-11.4,7.1c-0.5-0.2-1-0.4-1-0.4c-37.8-14.4-71.3,2.9-71.3,2.9
c-3.1,40.2,15.1,65.5,18.7,70.1c-0.9,2.5-1.7,5-2.5,7.5c-2.8,9.1-4.9,18.4-6.2,28.1c-0.2,1.4-0.4,2.8-0.5,4.2
C18.8,192.7,8.5,228,8.5,228c29.1,33.5,63.1,35.6,63.1,35.6c0,0,0.1-0.1,0.1-0.1c4.3,7.7,9.3,15,14.9,21.9c2.4,2.9,4.8,5.6,7.4,8.3
c-10.6,30.4,1.5,55.6,1.5,55.6c32.4,1.2,53.7-14.2,58.2-17.7c3.2,1.1,6.5,2.1,9.8,2.9c10,2.6,20.2,4.1,30.4,4.5
c2.5,0.1,5.1,0.2,7.6,0.1l1.2,0l0.8,0l1.6,0l1.6-0.1l0,0.1c15.3,21.8,42.1,24.9,42.1,24.9c19.1-20.1,20.2-40.1,20.2-44.4l0,0
c0,0,0-0.1,0-0.3c0-0.4,0-0.6,0-0.6l0,0c0-0.3,0-0.6,0-0.9c4-2.8,7.8-5.8,11.4-9.1c7.6-6.9,14.3-14.8,19.9-23.3
c0.5-0.8,1-1.6,1.5-2.4c21.6,1.2,36.9-13.4,36.9-13.4c-3.6-22.5-16.4-33.5-19.1-35.6l0,0c0,0-0.1-0.1-0.3-0.2
c-0.2-0.1-0.2-0.2-0.2-0.2c0,0,0,0,0,0c-0.1-0.1-0.3-0.2-0.5-0.3c0.1-1.4,0.2-2.7,0.3-4.1c0.2-2.4,0.2-4.9,0.2-7.3l0-1.8l0-0.9
l0-0.5c0-0.6,0-0.4,0-0.6l-0.1-1.5l-0.1-2c0-0.7-0.1-1.3-0.2-1.9c-0.1-0.6-0.1-1.3-0.2-1.9l-0.2-1.9l-0.3-1.9
c-0.4-2.5-0.8-4.9-1.4-7.4c-2.3-9.7-6.1-18.9-11-27.2c-5-8.3-11.2-15.6-18.3-21.8c-7-6.2-14.9-11.2-23.1-14.9
c-8.3-3.7-16.9-6.1-25.5-7.2c-4.3-0.6-8.6-0.8-12.9-0.7l-1.6,0l-0.4,0c-0.1,0-0.6,0-0.5,0l-0.7,0l-1.6,0.1c-0.6,0-1.2,0.1-1.7,0.1
c-2.2,0.2-4.4,0.5-6.5,0.9c-8.6,1.6-16.7,4.7-23.8,9c-7.1,4.3-13.3,9.6-18.3,15.6c-5,6-8.9,12.7-11.6,19.6c-2.7,6.9-4.2,14.1-4.6,21
c-0.1,1.7-0.1,3.5-0.1,5.2c0,0.4,0,0.9,0,1.3l0.1,1.4c0.1,0.8,0.1,1.7,0.2,2.5c0.3,3.5,1,6.9,1.9,10.1c1.9,6.5,4.9,12.4,8.6,17.4
c3.7,5,8.2,9.1,12.9,12.4c4.7,3.2,9.8,5.5,14.8,7c5,1.5,10,2.1,14.7,2.1c0.6,0,1.2,0,1.7,0c0.3,0,0.6,0,0.9,0c0.3,0,0.6,0,0.9-0.1
c0.5,0,1-0.1,1.5-0.1c0.1,0,0.3,0,0.4-0.1l0.5-0.1c0.3,0,0.6-0.1,0.9-0.1c0.6-0.1,1.1-0.2,1.7-0.3c0.6-0.1,1.1-0.2,1.6-0.4
c1.1-0.2,2.1-0.6,3.1-0.9c2-0.7,4-1.5,5.7-2.4c1.8-0.9,3.4-2,5-3c0.4-0.3,0.9-0.6,1.3-1c1.6-1.3,1.9-3.7,0.6-5.3
c-1.1-1.4-3.1-1.8-4.7-0.9c-0.4,0.2-0.8,0.4-1.2,0.6c-1.4,0.7-2.8,1.3-4.3,1.8c-1.5,0.5-3.1,0.9-4.7,1.2c-0.8,0.1-1.6,0.2-2.5,0.3
c-0.4,0-0.8,0.1-1.3,0.1c-0.4,0-0.9,0-1.2,0c-0.4,0-0.8,0-1.2,0c-0.5,0-1,0-1.5-0.1c0,0-0.3,0-0.1,0l-0.2,0l-0.3,0
c-0.2,0-0.5,0-0.7-0.1c-0.5-0.1-0.9-0.1-1.4-0.2c-3.7-0.5-7.4-1.6-10.9-3.2c-3.6-1.6-7-3.8-10.1-6.6c-3.1-2.8-5.8-6.1-7.9-9.9
c-2.1-3.8-3.6-8-4.3-12.4c-0.3-2.2-0.5-4.5-0.4-6.7c0-0.6,0.1-1.2,0.1-1.8c0,0.2,0-0.1,0-0.1l0-0.2l0-0.5c0-0.3,0.1-0.6,0.1-0.9
c0.1-1.2,0.3-2.4,0.5-3.6c1.7-9.6,6.5-19,13.9-26.1c1.9-1.8,3.9-3.4,6-4.9c2.1-1.5,4.4-2.8,6.8-3.9c2.4-1.1,4.8-2,7.4-2.7
c2.5-0.7,5.1-1.1,7.8-1.4c1.3-0.1,2.6-0.2,4-0.2c0.4,0,0.6,0,0.9,0l1.1,0l0.7,0c0.3,0,0,0,0.1,0l0.3,0l1.1,0.1
c2.9,0.2,5.7,0.6,8.5,1.3c5.6,1.2,11.1,3.3,16.2,6.1c10.2,5.7,18.9,14.5,24.2,25.1c2.7,5.3,4.6,11,5.5,16.9c0.2,1.5,0.4,3,0.5,4.5
l0.1,1.1l0.1,1.1c0,0.4,0,0.8,0,1.1c0,0.4,0,0.8,0,1.1l0,1l0,1.1c0,0.7-0.1,1.9-0.1,2.6c-0.1,1.6-0.3,3.3-0.5,4.9
c-0.2,1.6-0.5,3.2-0.8,4.8c-0.3,1.6-0.7,3.2-1.1,4.7c-0.8,3.1-1.8,6.2-3,9.3c-2.4,6-5.6,11.8-9.4,17.1
c-7.7,10.6-18.2,19.2-30.2,24.7c-6,2.7-12.3,4.7-18.8,5.7c-3.2,0.6-6.5,0.9-9.8,1l-0.6,0l-0.5,0l-1.1,0l-1.6,0l-0.8,0
c0.4,0-0.1,0-0.1,0l-0.3,0c-1.8,0-3.5-0.1-5.3-0.3c-7-0.5-13.9-1.8-20.7-3.7c-6.7-1.9-13.2-4.6-19.4-7.8
c-12.3-6.6-23.4-15.6-32-26.5c-4.3-5.4-8.1-11.3-11.2-17.4c-3.1-6.1-5.6-12.6-7.4-19.1c-1.8-6.6-2.9-13.3-3.4-20.1l-0.1-1.3l0-0.3
l0-0.3l0-0.6l0-1.1l0-0.3l0-0.4l0-0.8l0-1.6l0-0.3c0,0,0,0.1,0-0.1l0-0.6c0-0.8,0-1.7,0-2.5c0.1-3.3,0.4-6.8,0.8-10.2
c0.4-3.4,1-6.9,1.7-10.3c0.7-3.4,1.5-6.8,2.5-10.2c1.9-6.7,4.3-13.2,7.1-19.3c5.7-12.2,13.1-23.1,22-31.8c2.2-2.2,4.5-4.2,6.9-6.2
c2.4-1.9,4.9-3.7,7.5-5.4c2.5-1.7,5.2-3.2,7.9-4.6c1.3-0.7,2.7-1.4,4.1-2c0.7-0.3,1.4-0.6,2.1-0.9c0.7-0.3,1.4-0.6,2.1-0.9
c2.8-1.2,5.7-2.2,8.7-3.1c0.7-0.2,1.5-0.4,2.2-0.7c0.7-0.2,1.5-0.4,2.2-0.6c1.5-0.4,3-0.8,4.5-1.1c0.7-0.2,1.5-0.3,2.3-0.5
c0.8-0.2,1.5-0.3,2.3-0.5c0.8-0.1,1.5-0.3,2.3-0.4l1.1-0.2l1.2-0.2c0.8-0.1,1.5-0.2,2.3-0.3c0.9-0.1,1.7-0.2,2.6-0.3
c0.7-0.1,1.9-0.2,2.6-0.3c0.5-0.1,1.1-0.1,1.6-0.2l1.1-0.1l0.5-0.1l0.6,0c0.9-0.1,1.7-0.1,2.6-0.2l1.3-0.1c0,0,0.5,0,0.1,0l0.3,0
l0.6,0c0.7,0,1.5-0.1,2.2-0.1c2.9-0.1,5.9-0.1,8.8,0c5.8,0.2,11.5,0.9,17,1.9c11.1,2.1,21.5,5.6,31,10.3
c9.5,4.6,17.9,10.3,25.3,16.5c0.5,0.4,0.9,0.8,1.4,1.2c0.4,0.4,0.9,0.8,1.3,1.2c0.9,0.8,1.7,1.6,2.6,2.4c0.9,0.8,1.7,1.6,2.5,2.4
c0.8,0.8,1.6,1.6,2.4,2.5c3.1,3.3,6,6.6,8.6,10c5.2,6.7,9.4,13.5,12.7,19.9c0.2,0.4,0.4,0.8,0.6,1.2c0.2,0.4,0.4,0.8,0.6,1.2
c0.4,0.8,0.8,1.6,1.1,2.4c0.4,0.8,0.7,1.5,1.1,2.3c0.3,0.8,0.7,1.5,1,2.3c1.2,3,2.4,5.9,3.3,8.6c1.5,4.4,2.6,8.3,3.5,11.7
c0.3,1.4,1.6,2.3,3,2.1c1.5-0.1,2.6-1.3,2.6-2.8C342.6,170.4,342.5,166.1,342,161.2z"/>
</svg>

After

Width:  |  Height:  |  Size: 5.6 KiB

@@ -0,0 +1,7 @@
import Datasource from './datasource';
export class LoggingConfigCtrl {
static templateUrl = 'partials/config.html';
}
export { Datasource, LoggingConfigCtrl as ConfigCtrl };
@@ -0,0 +1,2 @@
<datasource-http-settings current="ctrl.current" no-direct-access="true">
</datasource-http-settings>
@@ -0,0 +1,28 @@
{
"type": "datasource",
"name": "Grafana Logging",
"id": "logging",
"metrics": false,
"alerting": false,
"annotations": false,
"logs": true,
"explore": true,
"info": {
"description": "Grafana Logging Data Source for Grafana",
"author": {
"name": "Grafana Project",
"url": "https://grafana.com"
},
"logos": {
"small": "img/grafana_icon.svg",
"large": "img/grafana_icon.svg"
},
"links": [
{
"name": "Grafana Logging",
"url": "https://grafana.com/"
}
],
"version": "5.3.0"
}
}
@@ -0,0 +1,45 @@
import { LogLevel } from 'app/core/logs_model';
import { getLogLevel, getSearchMatches } from './result_transformer';
describe('getSearchMatches()', () => {
it('gets no matches for when search and or line are empty', () => {
expect(getSearchMatches('', '')).toEqual([]);
expect(getSearchMatches('foo', '')).toEqual([]);
expect(getSearchMatches('', 'foo')).toEqual([]);
});
it('gets no matches for unmatched search string', () => {
expect(getSearchMatches('foo', 'bar')).toEqual([]);
});
it('gets matches for matched search string', () => {
expect(getSearchMatches('foo', 'foo')).toEqual([{ length: 3, start: 0, text: 'foo' }]);
expect(getSearchMatches(' foo ', 'foo')).toEqual([{ length: 3, start: 1, text: 'foo' }]);
});
expect(getSearchMatches(' foo foo bar ', 'foo|bar')).toEqual([
{ length: 3, start: 1, text: 'foo' },
{ length: 3, start: 5, text: 'foo' },
{ length: 3, start: 9, text: 'bar' },
]);
});
describe('getLoglevel()', () => {
it('returns no log level on empty line', () => {
expect(getLogLevel('')).toBe(undefined);
});
it('returns no log level on when level is part of a word', () => {
expect(getLogLevel('this is a warning')).toBe(undefined);
});
it('returns log level on line contains a log level', () => {
expect(getLogLevel('warn: it is looking bad')).toBe(LogLevel.warn);
expect(getLogLevel('2007-12-12 12:12:12 [WARN]: it is looking bad')).toBe(LogLevel.warn);
});
it('returns first log level found', () => {
expect(getLogLevel('WARN this could be a debug message')).toBe(LogLevel.warn);
});
});
@@ -0,0 +1,71 @@
import _ from 'lodash';
import moment from 'moment';
import { LogLevel, LogsModel, LogRow } from 'app/core/logs_model';
export function getLogLevel(line: string): LogLevel {
if (!line) {
return undefined;
}
let level: LogLevel;
Object.keys(LogLevel).forEach(key => {
if (!level) {
const regexp = new RegExp(`\\b${key}\\b`, 'i');
if (regexp.test(line)) {
level = LogLevel[key];
}
}
});
return level;
}
export function getSearchMatches(line: string, search: string) {
// Empty search can send re.exec() into infinite loop, exit early
if (!line || !search) {
return [];
}
const regexp = new RegExp(`(?:${search})`, 'g');
const matches = [];
let match;
while ((match = regexp.exec(line))) {
matches.push({
text: match[0],
start: match.index,
length: match[0].length,
});
}
return matches;
}
export function processEntry(entry: { line: string; timestamp: string }, stream): LogRow {
const { line, timestamp } = entry;
const { labels } = stream;
const key = `EK${timestamp}${labels}`;
const time = moment(timestamp);
const timeFromNow = time.fromNow();
const timeLocal = time.format('YYYY-MM-DD HH:mm:ss');
const searchMatches = getSearchMatches(line, stream.search);
const logLevel = getLogLevel(line);
return {
key,
logLevel,
searchMatches,
timeFromNow,
timeLocal,
entry: line,
timestamp: timestamp,
};
}
export function processStreams(streams, limit?: number): LogsModel {
const combinedEntries = streams.reduce((acc, stream) => {
return [...acc, ...stream.entries.map(entry => processEntry(entry, stream))];
}, []);
const sortedEntries = _.chain(combinedEntries)
.sortBy('timestamp')
.reverse()
.slice(0, limit || combinedEntries.length)
.value();
return { rows: sortedEntries };
}
@@ -480,17 +480,17 @@ export default class OpenTsDatasource {
mapMetricsToTargets(metrics, options, tsdbVersion) {
var interpolatedTagValue, arrTagV;
return _.map(metrics, function(metricData) {
return _.map(metrics, metricData => {
if (tsdbVersion === 3) {
return metricData.query.index;
} else {
return _.findIndex(options.targets, function(target) {
return _.findIndex(options.targets, target => {
if (target.filters && target.filters.length > 0) {
return target.metric === metricData.metric;
} else {
return (
target.metric === metricData.metric &&
_.every(target.tags, function(tagV, tagK) {
_.every(target.tags, (tagV, tagK) => {
interpolatedTagValue = this.templateSrv.replace(tagV, options.scopedVars, 'pipe');
arrTagV = interpolatedTagValue.split('|');
return _.includes(arrTagV, metricData.tags[tagK]) || interpolatedTagValue === '*';
@@ -17,11 +17,17 @@ export function alignRange(start, end, step) {
}
export function prometheusRegularEscape(value) {
return value.replace(/'/g, "\\\\'");
if (typeof value === 'string') {
return value.replace(/'/g, "\\\\'");
}
return value;
}
export function prometheusSpecialRegexEscape(value) {
return prometheusRegularEscape(value.replace(/\\/g, '\\\\\\\\').replace(/[$^*{}\[\]+?.()]/g, '\\\\$&'));
if (typeof value === 'string') {
return prometheusRegularEscape(value.replace(/\\/g, '\\\\\\\\').replace(/[$^*{}\[\]+?.()]/g, '\\\\$&'));
}
return value;
}
export class PrometheusDatasource {
@@ -190,13 +196,14 @@ export class PrometheusDatasource {
var intervalFactor = target.intervalFactor || 1;
// Adjust the interval to take into account any specified minimum and interval factor plus Prometheus limits
var adjustedInterval = this.adjustInterval(interval, minInterval, range, intervalFactor);
var scopedVars = options.scopedVars;
var scopedVars = { ...options.scopedVars, ...this.getRangeScopedVars() };
// If the interval was adjusted, make a shallow copy of scopedVars with updated interval vars
if (interval !== adjustedInterval) {
interval = adjustedInterval;
scopedVars = Object.assign({}, options.scopedVars, {
__interval: { text: interval + 's', value: interval + 's' },
__interval_ms: { text: String(interval * 1000), value: String(interval * 1000) },
__interval_ms: { text: interval * 1000, value: interval * 1000 },
...this.getRangeScopedVars(),
});
}
query.step = interval;
@@ -279,11 +286,26 @@ export class PrometheusDatasource {
return this.$q.when([]);
}
let interpolated = this.templateSrv.replace(query, {}, this.interpolateQueryExpr);
let scopedVars = {
__interval: { text: this.interval, value: this.interval },
__interval_ms: { text: kbn.interval_to_ms(this.interval), value: kbn.interval_to_ms(this.interval) },
...this.getRangeScopedVars(),
};
let interpolated = this.templateSrv.replace(query, scopedVars, this.interpolateQueryExpr);
var metricFindQuery = new PrometheusMetricFindQuery(this, interpolated, this.timeSrv);
return metricFindQuery.process();
}
getRangeScopedVars() {
let range = this.timeSrv.timeRange();
let msRange = range.to.diff(range.from);
let regularRange = kbn.secondsToHms(msRange / 1000);
return {
__range_ms: { text: msRange, value: msRange },
__range: { text: regularRange, value: regularRange },
};
}
annotationQuery(options) {
var annotation = options.annotation;
var expr = annotation.expr || '';
@@ -357,6 +379,7 @@ export class PrometheusDatasource {
state = {
...state,
queries,
datasource: this.name,
};
}
return state;
@@ -2,21 +2,30 @@
"type": "datasource",
"name": "Prometheus",
"id": "prometheus",
"includes": [
{"type": "dashboard", "name": "Prometheus Stats", "path": "dashboards/prometheus_stats.json"},
{"type": "dashboard", "name": "Prometheus 2.0 Stats", "path": "dashboards/prometheus_2_stats.json"},
{"type": "dashboard", "name": "Grafana Stats", "path": "dashboards/grafana_stats.json"}
{
"type": "dashboard",
"name": "Prometheus Stats",
"path": "dashboards/prometheus_stats.json"
},
{
"type": "dashboard",
"name": "Prometheus 2.0 Stats",
"path": "dashboards/prometheus_2_stats.json"
},
{
"type": "dashboard",
"name": "Grafana Stats",
"path": "dashboards/grafana_stats.json"
}
],
"metrics": true,
"alerting": true,
"annotations": true,
"explore": true,
"queryOptions": {
"minInterval": true
},
"info": {
"description": "Prometheus Data Source for Grafana",
"author": {
@@ -28,8 +37,11 @@
"large": "img/prometheus_logo.svg"
},
"links": [
{"name": "Prometheus", "url": "https://prometheus.io/"}
{
"name": "Prometheus",
"url": "https://prometheus.io/"
}
],
"version": "5.0.0"
}
}
}
@@ -2,6 +2,7 @@ import _ from 'lodash';
import moment from 'moment';
import q from 'q';
import { alignRange, PrometheusDatasource, prometheusSpecialRegexEscape, prometheusRegularEscape } from '../datasource';
jest.mock('../metric_find_query');
describe('PrometheusDatasource', () => {
let ctx: any = {};
@@ -18,7 +19,14 @@ describe('PrometheusDatasource', () => {
ctx.templateSrvMock = {
replace: a => a,
};
ctx.timeSrvMock = {};
ctx.timeSrvMock = {
timeRange: () => {
return {
from: moment(1531468681),
to: moment(1531489712),
};
},
};
beforeEach(() => {
ctx.ds = new PrometheusDatasource(instanceSettings, q, ctx.backendSrvMock, ctx.templateSrvMock, ctx.timeSrvMock);
@@ -166,6 +174,9 @@ describe('PrometheusDatasource', () => {
});
describe('Prometheus regular escaping', function() {
it('should not escape non-string', function() {
expect(prometheusRegularEscape(12)).toEqual(12);
});
it('should not escape simple string', function() {
expect(prometheusRegularEscape('cryptodepression')).toEqual('cryptodepression');
});
@@ -201,4 +212,37 @@ describe('PrometheusDatasource', () => {
expect(prometheusSpecialRegexEscape('+looking$glass?')).toEqual('\\\\+looking\\\\$glass\\\\?');
});
});
describe('metricFindQuery', () => {
beforeEach(() => {
let query = 'query_result(topk(5,rate(http_request_duration_microseconds_count[$__interval])))';
ctx.templateSrvMock.replace = jest.fn();
ctx.timeSrvMock.timeRange = () => {
return {
from: moment(1531468681),
to: moment(1531489712),
};
};
ctx.ds = new PrometheusDatasource(instanceSettings, q, ctx.backendSrvMock, ctx.templateSrvMock, ctx.timeSrvMock);
ctx.ds.metricFindQuery(query);
});
it('should call templateSrv.replace with scopedVars', () => {
expect(ctx.templateSrvMock.replace.mock.calls[0][1]).toBeDefined();
});
it('should have the correct range and range_ms', () => {
let range = ctx.templateSrvMock.replace.mock.calls[0][1].__range;
let rangeMs = ctx.templateSrvMock.replace.mock.calls[0][1].__range_ms;
expect(range).toEqual({ text: '21s', value: '21s' });
expect(rangeMs).toEqual({ text: 21031, value: 21031 });
});
it('should pass the default interval value', () => {
let interval = ctx.templateSrvMock.replace.mock.calls[0][1].__interval;
let intervalMs = ctx.templateSrvMock.replace.mock.calls[0][1].__interval_ms;
expect(interval).toEqual({ text: '15s', value: '15s' });
expect(intervalMs).toEqual({ text: 15000, value: 15000 });
});
});
});
@@ -452,7 +452,7 @@ describe('PrometheusDatasource', function() {
interval: '10s',
scopedVars: {
__interval: { text: '10s', value: '10s' },
__interval_ms: { text: String(10 * 1000), value: String(10 * 1000) },
__interval_ms: { text: 10 * 1000, value: 10 * 1000 },
},
};
var urlExpected =
@@ -463,8 +463,8 @@ describe('PrometheusDatasource', function() {
expect(query.scopedVars.__interval.text).to.be('10s');
expect(query.scopedVars.__interval.value).to.be('10s');
expect(query.scopedVars.__interval_ms.text).to.be(String(10 * 1000));
expect(query.scopedVars.__interval_ms.value).to.be(String(10 * 1000));
expect(query.scopedVars.__interval_ms.text).to.be(10 * 1000);
expect(query.scopedVars.__interval_ms.value).to.be(10 * 1000);
});
it('should be min interval when it is greater than auto interval', function() {
var query = {
@@ -479,7 +479,7 @@ describe('PrometheusDatasource', function() {
interval: '5s',
scopedVars: {
__interval: { text: '5s', value: '5s' },
__interval_ms: { text: String(5 * 1000), value: String(5 * 1000) },
__interval_ms: { text: 5 * 1000, value: 5 * 1000 },
},
};
var urlExpected =
@@ -490,8 +490,8 @@ describe('PrometheusDatasource', function() {
expect(query.scopedVars.__interval.text).to.be('5s');
expect(query.scopedVars.__interval.value).to.be('5s');
expect(query.scopedVars.__interval_ms.text).to.be(String(5 * 1000));
expect(query.scopedVars.__interval_ms.value).to.be(String(5 * 1000));
expect(query.scopedVars.__interval_ms.text).to.be(5 * 1000);
expect(query.scopedVars.__interval_ms.value).to.be(5 * 1000);
});
it('should account for intervalFactor', function() {
var query = {
@@ -507,7 +507,7 @@ describe('PrometheusDatasource', function() {
interval: '10s',
scopedVars: {
__interval: { text: '10s', value: '10s' },
__interval_ms: { text: String(10 * 1000), value: String(10 * 1000) },
__interval_ms: { text: 10 * 1000, value: 10 * 1000 },
},
};
var urlExpected =
@@ -518,8 +518,8 @@ describe('PrometheusDatasource', function() {
expect(query.scopedVars.__interval.text).to.be('10s');
expect(query.scopedVars.__interval.value).to.be('10s');
expect(query.scopedVars.__interval_ms.text).to.be(String(10 * 1000));
expect(query.scopedVars.__interval_ms.value).to.be(String(10 * 1000));
expect(query.scopedVars.__interval_ms.text).to.be(10 * 1000);
expect(query.scopedVars.__interval_ms.value).to.be(10 * 1000);
});
it('should be interval * intervalFactor when greater than min interval', function() {
var query = {
@@ -535,7 +535,7 @@ describe('PrometheusDatasource', function() {
interval: '5s',
scopedVars: {
__interval: { text: '5s', value: '5s' },
__interval_ms: { text: String(5 * 1000), value: String(5 * 1000) },
__interval_ms: { text: 5 * 1000, value: 5 * 1000 },
},
};
var urlExpected =
@@ -546,8 +546,8 @@ describe('PrometheusDatasource', function() {
expect(query.scopedVars.__interval.text).to.be('5s');
expect(query.scopedVars.__interval.value).to.be('5s');
expect(query.scopedVars.__interval_ms.text).to.be(String(5 * 1000));
expect(query.scopedVars.__interval_ms.value).to.be(String(5 * 1000));
expect(query.scopedVars.__interval_ms.text).to.be(5 * 1000);
expect(query.scopedVars.__interval_ms.value).to.be(5 * 1000);
});
it('should be min interval when greater than interval * intervalFactor', function() {
var query = {
@@ -563,7 +563,7 @@ describe('PrometheusDatasource', function() {
interval: '5s',
scopedVars: {
__interval: { text: '5s', value: '5s' },
__interval_ms: { text: String(5 * 1000), value: String(5 * 1000) },
__interval_ms: { text: 5 * 1000, value: 5 * 1000 },
},
};
var urlExpected =
@@ -574,8 +574,8 @@ describe('PrometheusDatasource', function() {
expect(query.scopedVars.__interval.text).to.be('5s');
expect(query.scopedVars.__interval.value).to.be('5s');
expect(query.scopedVars.__interval_ms.text).to.be(String(5 * 1000));
expect(query.scopedVars.__interval_ms.value).to.be(String(5 * 1000));
expect(query.scopedVars.__interval_ms.text).to.be(5 * 1000);
expect(query.scopedVars.__interval_ms.value).to.be(5 * 1000);
});
it('should be determined by the 11000 data points limit, accounting for intervalFactor', function() {
var query = {
@@ -590,7 +590,7 @@ describe('PrometheusDatasource', function() {
interval: '5s',
scopedVars: {
__interval: { text: '5s', value: '5s' },
__interval_ms: { text: String(5 * 1000), value: String(5 * 1000) },
__interval_ms: { text: 5 * 1000, value: 5 * 1000 },
},
};
var end = 7 * 24 * 60 * 60;
@@ -609,8 +609,8 @@ describe('PrometheusDatasource', function() {
expect(query.scopedVars.__interval.text).to.be('5s');
expect(query.scopedVars.__interval.value).to.be('5s');
expect(query.scopedVars.__interval_ms.text).to.be(String(5 * 1000));
expect(query.scopedVars.__interval_ms.value).to.be(String(5 * 1000));
expect(query.scopedVars.__interval_ms.text).to.be(5 * 1000);
expect(query.scopedVars.__interval_ms.value).to.be(5 * 1000);
});
});
});
@@ -37,4 +37,3 @@
</div>
</div>
</query-editor-row>
@@ -1,7 +1,7 @@
{
"type": "datasource",
"name": "Grafana TestDataDB",
"id": "grafana-testdata-datasource",
"name": "TestData DB",
"id": "testdata",
"metrics": true,
"alerting": true,
@@ -13,8 +13,8 @@
"url": "https://grafana.com"
},
"logos": {
"small": "",
"large": ""
"small": "../../../../img/grafana_icon.svg",
"large": "../../../../img/grafana_icon.svg"
}
}
}
@@ -1,604 +0,0 @@
define([
'jquery',
'lodash',
'angular',
'tether-drop',
],
function ($, _, angular, Drop) {
'use strict';
function createAnnotationToolip(element, event, plot) {
var injector = angular.element(document).injector();
var content = document.createElement('div');
content.innerHTML = '<annotation-tooltip event="event" on-edit="onEdit()"></annotation-tooltip>';
injector.invoke(["$compile", "$rootScope", function($compile, $rootScope) {
var eventManager = plot.getOptions().events.manager;
var tmpScope = $rootScope.$new(true);
tmpScope.event = event;
tmpScope.onEdit = function() {
eventManager.editEvent(event);
};
$compile(content)(tmpScope);
tmpScope.$digest();
tmpScope.$destroy();
var drop = new Drop({
target: element[0],
content: content,
position: "bottom center",
classes: 'drop-popover drop-popover--annotation',
openOn: 'hover',
hoverCloseDelay: 200,
tetherOptions: {
constraints: [{to: 'window', pin: true, attachment: "both"}]
}
});
drop.open();
drop.on('close', function() {
setTimeout(function() {
drop.destroy();
});
});
}]);
}
var markerElementToAttachTo = null;
function createEditPopover(element, event, plot) {
var eventManager = plot.getOptions().events.manager;
if (eventManager.editorOpen) {
// update marker element to attach to (needed in case of legend on the right
// when there is a double render pass and the initial marker element is removed)
markerElementToAttachTo = element;
return;
}
// mark as openend
eventManager.editorOpened();
// set marker element to attache to
markerElementToAttachTo = element;
// wait for element to be attached and positioned
setTimeout(function() {
var injector = angular.element(document).injector();
var content = document.createElement('div');
content.innerHTML = '<event-editor panel-ctrl="panelCtrl" event="event" close="close()"></event-editor>';
injector.invoke(["$compile", "$rootScope", function($compile, $rootScope) {
var scope = $rootScope.$new(true);
var drop;
scope.event = event;
scope.panelCtrl = eventManager.panelCtrl;
scope.close = function() {
drop.close();
};
$compile(content)(scope);
scope.$digest();
drop = new Drop({
target: markerElementToAttachTo[0],
content: content,
position: "bottom center",
classes: 'drop-popover drop-popover--form',
openOn: 'click',
tetherOptions: {
constraints: [{to: 'window', pin: true, attachment: "both"}]
}
});
drop.open();
eventManager.editorOpened();
drop.on('close', function() {
// need timeout here in order call drop.destroy
setTimeout(function() {
eventManager.editorClosed();
scope.$destroy();
drop.destroy();
});
});
}]);
}, 100);
}
/*
* jquery.flot.events
*
* description: Flot plugin for adding events/markers to the plot
* version: 0.2.5
* authors:
* Alexander Wunschik <alex@wunschik.net>
* Joel Oughton <joeloughton@gmail.com>
* Nicolas Joseph <www.nicolasjoseph.com>
*
* website: https://github.com/mojoaxel/flot-events
*
* released under MIT License and GPLv2+
*/
/**
* A class that allows for the drawing an remove of some object
*/
var DrawableEvent = function(object, drawFunc, clearFunc, moveFunc, left, top, width, height) {
var _object = object;
var _drawFunc = drawFunc;
var _clearFunc = clearFunc;
var _moveFunc = moveFunc;
var _position = { left: left, top: top };
var _width = width;
var _height = height;
this.width = function() { return _width; };
this.height = function() { return _height; };
this.position = function() { return _position; };
this.draw = function() { _drawFunc(_object); };
this.clear = function() { _clearFunc(_object); };
this.getObject = function() { return _object; };
this.moveTo = function(position) {
_position = position;
_moveFunc(_object, _position);
};
};
/**
* Event class that stores options (eventType, min, max, title, description) and the object to draw.
*/
var VisualEvent = function(options, drawableEvent) {
var _parent;
var _options = options;
var _drawableEvent = drawableEvent;
var _hidden = false;
this.visual = function() { return _drawableEvent; };
this.getOptions = function() { return _options; };
this.getParent = function() { return _parent; };
this.isHidden = function() { return _hidden; };
this.hide = function() { _hidden = true; };
this.unhide = function() { _hidden = false; };
};
/**
* A Class that handles the event-markers inside the given plot
*/
var EventMarkers = function(plot) {
var _events = [];
this._types = [];
this._plot = plot;
this.eventsEnabled = false;
this.getEvents = function() {
return _events;
};
this.setTypes = function(types) {
return this._types = types;
};
/**
* create internal objects for the given events
*/
this.setupEvents = function(events) {
var that = this;
var parts = _.partition(events, 'isRegion');
var regions = parts[0];
events = parts[1];
$.each(events, function(index, event) {
var ve = new VisualEvent(event, that._buildDiv(event));
_events.push(ve);
});
$.each(regions, function (index, event) {
var vre = new VisualEvent(event, that._buildRegDiv(event));
_events.push(vre);
});
_events.sort(function(a, b) {
var ao = a.getOptions(), bo = b.getOptions();
if (ao.min > bo.min) { return 1; }
if (ao.min < bo.min) { return -1; }
return 0;
});
};
/**
* draw the events to the plot
*/
this.drawEvents = function() {
var that = this;
// var o = this._plot.getPlotOffset();
$.each(_events, function(index, event) {
// check event is inside the graph range
if (that._insidePlot(event.getOptions().min) && !event.isHidden()) {
event.visual().draw();
} else {
event.visual().getObject().hide();
}
});
};
/**
* update the position of the event-markers (e.g. after scrolling or zooming)
*/
this.updateEvents = function() {
var that = this;
var o = this._plot.getPlotOffset(), left, top;
var xaxis = this._plot.getXAxes()[this._plot.getOptions().events.xaxis - 1];
$.each(_events, function(index, event) {
top = o.top + that._plot.height() - event.visual().height();
left = xaxis.p2c(event.getOptions().min) + o.left - event.visual().width() / 2;
event.visual().moveTo({ top: top, left: left });
});
};
/**
* remove all events from the plot
*/
this._clearEvents = function() {
$.each(_events, function(index, val) {
val.visual().clear();
});
_events = [];
};
/**
* create a DOM element for the given event
*/
this._buildDiv = function(event) {
var that = this;
var container = this._plot.getPlaceholder();
var o = this._plot.getPlotOffset();
var axes = this._plot.getAxes();
var xaxis = this._plot.getXAxes()[this._plot.getOptions().events.xaxis - 1];
var yaxis, top, left, color, markerSize, markerShow, lineStyle, lineWidth;
var markerTooltip;
// determine the y axis used
if (axes.yaxis && axes.yaxis.used) { yaxis = axes.yaxis; }
if (axes.yaxis2 && axes.yaxis2.used) { yaxis = axes.yaxis2; }
// map the eventType to a types object
var eventTypeId = event.eventType;
if (this._types === null || !this._types[eventTypeId] || !this._types[eventTypeId].color) {
color = '#666';
} else {
color = this._types[eventTypeId].color;
}
if (this._types === null || !this._types[eventTypeId] || !this._types[eventTypeId].markerSize) {
markerSize = 8; //default marker size
} else {
markerSize = this._types[eventTypeId].markerSize;
}
if (this._types === null || !this._types[eventTypeId] || this._types[eventTypeId].markerShow === undefined) {
markerShow = true;
} else {
markerShow = this._types[eventTypeId].markerShow;
}
if (this._types === null || !this._types[eventTypeId] || this._types[eventTypeId].markerTooltip === undefined) {
markerTooltip = true;
} else {
markerTooltip = this._types[eventTypeId].markerTooltip;
}
if (this._types == null || !this._types[eventTypeId] || !this._types[eventTypeId].lineStyle) {
lineStyle = 'dashed'; //default line style
} else {
lineStyle = this._types[eventTypeId].lineStyle.toLowerCase();
}
if (this._types == null || !this._types[eventTypeId] || this._types[eventTypeId].lineWidth === undefined) {
lineWidth = 1; //default line width
} else {
lineWidth = this._types[eventTypeId].lineWidth;
}
var topOffset = xaxis.options.eventSectionHeight || 0;
topOffset = topOffset / 3;
top = o.top + this._plot.height() + topOffset;
left = xaxis.p2c(event.min) + o.left;
var line = $('<div class="events_line flot-temp-elem"></div>').css({
"position": "absolute",
"opacity": 0.8,
"left": left + 'px',
"top": 8,
"width": lineWidth + "px",
"height": this._plot.height() + topOffset * 0.8,
"border-left-width": lineWidth + "px",
"border-left-style": lineStyle,
"border-left-color": color,
"color": color
})
.appendTo(container);
if (markerShow) {
var marker = $('<div class="events_marker"></div>').css({
"position": "absolute",
"left": (-markerSize - Math.round(lineWidth / 2)) + "px",
"font-size": 0,
"line-height": 0,
"width": 0,
"height": 0,
"border-left": markerSize+"px solid transparent",
"border-right": markerSize+"px solid transparent"
});
marker.appendTo(line);
if (this._types[eventTypeId] && this._types[eventTypeId].position && this._types[eventTypeId].position.toUpperCase() === 'BOTTOM') {
marker.css({
"top": top-markerSize-8 +"px",
"border-top": "none",
"border-bottom": markerSize+"px solid " + color
});
} else {
marker.css({
"top": "0px",
"border-top": markerSize+"px solid " + color,
"border-bottom": "none"
});
}
marker.data({
"event": event
});
var mouseenter = function() {
createAnnotationToolip(marker, $(this).data("event"), that._plot);
};
if (event.editModel) {
createEditPopover(marker, event.editModel, that._plot);
}
var mouseleave = function() {
that._plot.clearSelection();
};
if (markerTooltip) {
marker.css({ "cursor": "help" });
marker.hover(mouseenter, mouseleave);
}
}
var drawableEvent = new DrawableEvent(
line,
function drawFunc(obj) { obj.show(); },
function(obj) { obj.remove(); },
function(obj, position) {
obj.css({
top: position.top,
left: position.left
});
},
left,
top,
line.width(),
line.height()
);
return drawableEvent;
};
/**
* create a DOM element for the given region
*/
this._buildRegDiv = function (event) {
var that = this;
var container = this._plot.getPlaceholder();
var o = this._plot.getPlotOffset();
var axes = this._plot.getAxes();
var xaxis = this._plot.getXAxes()[this._plot.getOptions().events.xaxis - 1];
var yaxis, top, left, lineWidth, regionWidth, lineStyle, color, markerTooltip;
// determine the y axis used
if (axes.yaxis && axes.yaxis.used) { yaxis = axes.yaxis; }
if (axes.yaxis2 && axes.yaxis2.used) { yaxis = axes.yaxis2; }
// map the eventType to a types object
var eventTypeId = event.eventType;
if (this._types === null || !this._types[eventTypeId] || !this._types[eventTypeId].color) {
color = '#666';
} else {
color = this._types[eventTypeId].color;
}
if (this._types === null || !this._types[eventTypeId] || this._types[eventTypeId].markerTooltip === undefined) {
markerTooltip = true;
} else {
markerTooltip = this._types[eventTypeId].markerTooltip;
}
if (this._types == null || !this._types[eventTypeId] || this._types[eventTypeId].lineWidth === undefined) {
lineWidth = 1; //default line width
} else {
lineWidth = this._types[eventTypeId].lineWidth;
}
if (this._types == null || !this._types[eventTypeId] || !this._types[eventTypeId].lineStyle) {
lineStyle = 'dashed'; //default line style
} else {
lineStyle = this._types[eventTypeId].lineStyle.toLowerCase();
}
var topOffset = 2;
top = o.top + this._plot.height() + topOffset;
var timeFrom = Math.min(event.min, event.timeEnd);
var timeTo = Math.max(event.min, event.timeEnd);
left = xaxis.p2c(timeFrom) + o.left;
var right = xaxis.p2c(timeTo) + o.left;
regionWidth = right - left;
_.each([left, right], function(position) {
var line = $('<div class="events_line flot-temp-elem"></div>').css({
"position": "absolute",
"opacity": 0.8,
"left": position + 'px',
"top": 8,
"width": lineWidth + "px",
"height": that._plot.height() + topOffset,
"border-left-width": lineWidth + "px",
"border-left-style": lineStyle,
"border-left-color": color,
"color": color
});
line.appendTo(container);
});
var region = $('<div class="events_marker region_marker flot-temp-elem"></div>').css({
"position": "absolute",
"opacity": 0.5,
"left": left + 'px',
"top": top,
"width": Math.round(regionWidth + lineWidth) + "px",
"height": "0.5rem",
"border-left-color": color,
"color": color,
"background-color": color
});
region.appendTo(container);
region.data({
"event": event
});
var mouseenter = function () {
createAnnotationToolip(region, $(this).data("event"), that._plot);
};
if (event.editModel) {
createEditPopover(region, event.editModel, that._plot);
}
var mouseleave = function () {
that._plot.clearSelection();
};
if (markerTooltip) {
region.css({ "cursor": "help" });
region.hover(mouseenter, mouseleave);
}
var drawableEvent = new DrawableEvent(
region,
function drawFunc(obj) { obj.show(); },
function (obj) { obj.remove(); },
function (obj, position) {
obj.css({
top: position.top,
left: position.left
});
},
left,
top,
region.width(),
region.height()
);
return drawableEvent;
};
/**
* check if the event is inside visible range
*/
this._insidePlot = function(x) {
var xaxis = this._plot.getXAxes()[this._plot.getOptions().events.xaxis - 1];
var xc = xaxis.p2c(x);
return xc > 0 && xc < xaxis.p2c(xaxis.max);
};
};
/**
* initialize the plugin for the given plot
*/
function init(plot) {
/*jshint validthis:true */
var that = this;
var eventMarkers = new EventMarkers(plot);
plot.getEvents = function() {
return eventMarkers._events;
};
plot.hideEvents = function() {
$.each(eventMarkers._events, function(index, event) {
event.visual().getObject().hide();
});
};
plot.showEvents = function() {
plot.hideEvents();
$.each(eventMarkers._events, function(index, event) {
event.hide();
});
that.eventMarkers.drawEvents();
};
// change events on an existing plot
plot.setEvents = function(events) {
if (eventMarkers.eventsEnabled) {
eventMarkers.setupEvents(events);
}
};
plot.hooks.processOptions.push(function(plot, options) {
// enable the plugin
if (options.events.data != null) {
eventMarkers.eventsEnabled = true;
}
});
plot.hooks.draw.push(function(plot) {
var options = plot.getOptions();
if (eventMarkers.eventsEnabled) {
// check for first run
if (eventMarkers.getEvents().length < 1) {
eventMarkers.setTypes(options.events.types);
eventMarkers.setupEvents(options.events.data);
} else {
eventMarkers.updateEvents();
}
}
eventMarkers.drawEvents();
});
}
var defaultOptions = {
events: {
data: null,
types: null,
xaxis: 1,
position: 'BOTTOM'
}
};
$.plot.plugins.push({
init: init,
options: defaultOptions,
name: "events",
version: "0.2.5"
});
});
@@ -0,0 +1,671 @@
import angular from 'angular';
import $ from 'jquery';
import _ from 'lodash';
import Drop from 'tether-drop';
/** @ngInject */
export function createAnnotationToolip(element, event, plot) {
let injector = angular.element(document).injector();
let content = document.createElement('div');
content.innerHTML = '<annotation-tooltip event="event" on-edit="onEdit()"></annotation-tooltip>';
injector.invoke([
'$compile',
'$rootScope',
function($compile, $rootScope) {
let eventManager = plot.getOptions().events.manager;
let tmpScope = $rootScope.$new(true);
tmpScope.event = event;
tmpScope.onEdit = function() {
eventManager.editEvent(event);
};
$compile(content)(tmpScope);
tmpScope.$digest();
tmpScope.$destroy();
let drop = new Drop({
target: element[0],
content: content,
position: 'bottom center',
classes: 'drop-popover drop-popover--annotation',
openOn: 'hover',
hoverCloseDelay: 200,
tetherOptions: {
constraints: [{ to: 'window', pin: true, attachment: 'both' }],
},
});
drop.open();
drop.on('close', function() {
setTimeout(function() {
drop.destroy();
});
});
},
]);
}
let markerElementToAttachTo = null;
/** @ngInject */
export function createEditPopover(element, event, plot) {
let eventManager = plot.getOptions().events.manager;
if (eventManager.editorOpen) {
// update marker element to attach to (needed in case of legend on the right
// when there is a double render pass and the inital marker element is removed)
markerElementToAttachTo = element;
return;
}
// mark as openend
eventManager.editorOpened();
// set marker elment to attache to
markerElementToAttachTo = element;
// wait for element to be attached and positioned
setTimeout(function() {
let injector = angular.element(document).injector();
let content = document.createElement('div');
content.innerHTML = '<event-editor panel-ctrl="panelCtrl" event="event" close="close()"></event-editor>';
injector.invoke([
'$compile',
'$rootScope',
function($compile, $rootScope) {
let scope = $rootScope.$new(true);
let drop;
scope.event = event;
scope.panelCtrl = eventManager.panelCtrl;
scope.close = function() {
drop.close();
};
$compile(content)(scope);
scope.$digest();
drop = new Drop({
target: markerElementToAttachTo[0],
content: content,
position: 'bottom center',
classes: 'drop-popover drop-popover--form',
openOn: 'click',
tetherOptions: {
constraints: [{ to: 'window', pin: true, attachment: 'both' }],
},
});
drop.open();
eventManager.editorOpened();
drop.on('close', function() {
// need timeout here in order call drop.destroy
setTimeout(function() {
eventManager.editorClosed();
scope.$destroy();
drop.destroy();
});
});
},
]);
}, 100);
}
/*
* jquery.flot.events
*
* description: Flot plugin for adding events/markers to the plot
* version: 0.2.5
* authors:
* Alexander Wunschik <alex@wunschik.net>
* Joel Oughton <joeloughton@gmail.com>
* Nicolas Joseph <www.nicolasjoseph.com>
*
* website: https://github.com/mojoaxel/flot-events
*
* released under MIT License and GPLv2+
*/
/**
* A class that allows for the drawing an remove of some object
*/
export class DrawableEvent {
_object: any;
_drawFunc: any;
_clearFunc: any;
_moveFunc: any;
_position: any;
_width: any;
_height: any;
/** @ngInject */
constructor(object, drawFunc, clearFunc, moveFunc, left, top, width, height) {
this._object = object;
this._drawFunc = drawFunc;
this._clearFunc = clearFunc;
this._moveFunc = moveFunc;
this._position = { left: left, top: top };
this._width = width;
this._height = height;
}
width() {
return this._width;
}
height() {
return this._height;
}
position() {
return this._position;
}
draw() {
this._drawFunc(this._object);
}
clear() {
this._clearFunc(this._object);
}
getObject() {
return this._object;
}
moveTo(position) {
this._position = position;
this._moveFunc(this._object, this._position);
}
}
/**
* Event class that stores options (eventType, min, max, title, description) and the object to draw.
*/
export class VisualEvent {
_parent: any;
_options: any;
_drawableEvent: any;
_hidden: any;
/** @ngInject */
constructor(options, drawableEvent) {
this._options = options;
this._drawableEvent = drawableEvent;
this._hidden = false;
}
visual() {
return this._drawableEvent;
}
getOptions() {
return this._options;
}
getParent() {
return this._parent;
}
isHidden() {
return this._hidden;
}
hide() {
this._hidden = true;
}
unhide() {
this._hidden = false;
}
}
/**
* A Class that handles the event-markers inside the given plot
*/
export class EventMarkers {
_events: any;
_types: any;
_plot: any;
eventsEnabled: any;
/** @ngInject */
constructor(plot) {
this._events = [];
this._types = [];
this._plot = plot;
this.eventsEnabled = false;
}
getEvents() {
return this._events;
}
setTypes(types) {
return (this._types = types);
}
/**
* create internal objects for the given events
*/
setupEvents(events) {
let parts = _.partition(events, 'isRegion');
let regions = parts[0];
events = parts[1];
$.each(events, (index, event) => {
let ve = new VisualEvent(event, this._buildDiv(event));
this._events.push(ve);
});
$.each(regions, (index, event) => {
let vre = new VisualEvent(event, this._buildRegDiv(event));
this._events.push(vre);
});
this._events.sort((a, b) => {
let ao = a.getOptions(),
bo = b.getOptions();
if (ao.min > bo.min) {
return 1;
}
if (ao.min < bo.min) {
return -1;
}
return 0;
});
}
/**
* draw the events to the plot
*/
drawEvents() {
// var o = this._plot.getPlotOffset();
$.each(this._events, (index, event) => {
// check event is inside the graph range
if (this._insidePlot(event.getOptions().min) && !event.isHidden()) {
event.visual().draw();
} else {
event
.visual()
.getObject()
.hide();
}
});
}
/**
* update the position of the event-markers (e.g. after scrolling or zooming)
*/
updateEvents() {
let o = this._plot.getPlotOffset(),
left,
top;
let xaxis = this._plot.getXAxes()[this._plot.getOptions().events.xaxis - 1];
$.each(this._events, (index, event) => {
top = o.top + this._plot.height() - event.visual().height();
left = xaxis.p2c(event.getOptions().min) + o.left - event.visual().width() / 2;
event.visual().moveTo({ top: top, left: left });
});
}
/**
* remove all events from the plot
*/
_clearEvents() {
$.each(this._events, (index, val) => {
val.visual().clear();
});
this._events = [];
}
/**
* create a DOM element for the given event
*/
_buildDiv(event) {
let that = this;
let container = this._plot.getPlaceholder();
let o = this._plot.getPlotOffset();
let xaxis = this._plot.getXAxes()[this._plot.getOptions().events.xaxis - 1];
let top, left, color, markerSize, markerShow, lineStyle, lineWidth;
let markerTooltip;
// map the eventType to a types object
let eventTypeId = event.eventType;
if (this._types === null || !this._types[eventTypeId] || !this._types[eventTypeId].color) {
color = '#666';
} else {
color = this._types[eventTypeId].color;
}
if (this._types === null || !this._types[eventTypeId] || !this._types[eventTypeId].markerSize) {
markerSize = 8; //default marker size
} else {
markerSize = this._types[eventTypeId].markerSize;
}
if (this._types === null || !this._types[eventTypeId] || this._types[eventTypeId].markerShow === undefined) {
markerShow = true;
} else {
markerShow = this._types[eventTypeId].markerShow;
}
if (this._types === null || !this._types[eventTypeId] || this._types[eventTypeId].markerTooltip === undefined) {
markerTooltip = true;
} else {
markerTooltip = this._types[eventTypeId].markerTooltip;
}
if (this._types == null || !this._types[eventTypeId] || !this._types[eventTypeId].lineStyle) {
lineStyle = 'dashed'; //default line style
} else {
lineStyle = this._types[eventTypeId].lineStyle.toLowerCase();
}
if (this._types == null || !this._types[eventTypeId] || this._types[eventTypeId].lineWidth === undefined) {
lineWidth = 1; //default line width
} else {
lineWidth = this._types[eventTypeId].lineWidth;
}
let topOffset = xaxis.options.eventSectionHeight || 0;
topOffset = topOffset / 3;
top = o.top + this._plot.height() + topOffset;
left = xaxis.p2c(event.min) + o.left;
let line = $('<div class="events_line flot-temp-elem"></div>')
.css({
position: 'absolute',
opacity: 0.8,
left: left + 'px',
top: 8,
width: lineWidth + 'px',
height: this._plot.height() + topOffset * 0.8,
'border-left-width': lineWidth + 'px',
'border-left-style': lineStyle,
'border-left-color': color,
color: color,
})
.appendTo(container);
if (markerShow) {
let marker = $('<div class="events_marker"></div>').css({
position: 'absolute',
left: -markerSize - Math.round(lineWidth / 2) + 'px',
'font-size': 0,
'line-height': 0,
width: 0,
height: 0,
'border-left': markerSize + 'px solid transparent',
'border-right': markerSize + 'px solid transparent',
});
marker.appendTo(line);
if (
this._types[eventTypeId] &&
this._types[eventTypeId].position &&
this._types[eventTypeId].position.toUpperCase() === 'BOTTOM'
) {
marker.css({
top: top - markerSize - 8 + 'px',
'border-top': 'none',
'border-bottom': markerSize + 'px solid ' + color,
});
} else {
marker.css({
top: '0px',
'border-top': markerSize + 'px solid ' + color,
'border-bottom': 'none',
});
}
marker.data({
event: event,
});
let mouseenter = function() {
createAnnotationToolip(marker, $(this).data('event'), that._plot);
};
if (event.editModel) {
createEditPopover(marker, event.editModel, that._plot);
}
let mouseleave = function() {
that._plot.clearSelection();
};
if (markerTooltip) {
marker.css({ cursor: 'help' });
marker.hover(mouseenter, mouseleave);
}
}
let drawableEvent = new DrawableEvent(
line,
function drawFunc(obj) {
obj.show();
},
function(obj) {
obj.remove();
},
function(obj, position) {
obj.css({
top: position.top,
left: position.left,
});
},
left,
top,
line.width(),
line.height()
);
return drawableEvent;
}
/**
* create a DOM element for the given region
*/
_buildRegDiv(event) {
let that = this;
let container = this._plot.getPlaceholder();
let o = this._plot.getPlotOffset();
let xaxis = this._plot.getXAxes()[this._plot.getOptions().events.xaxis - 1];
let top, left, lineWidth, regionWidth, lineStyle, color, markerTooltip;
// map the eventType to a types object
let eventTypeId = event.eventType;
if (this._types === null || !this._types[eventTypeId] || !this._types[eventTypeId].color) {
color = '#666';
} else {
color = this._types[eventTypeId].color;
}
if (this._types === null || !this._types[eventTypeId] || this._types[eventTypeId].markerTooltip === undefined) {
markerTooltip = true;
} else {
markerTooltip = this._types[eventTypeId].markerTooltip;
}
if (this._types == null || !this._types[eventTypeId] || this._types[eventTypeId].lineWidth === undefined) {
lineWidth = 1; //default line width
} else {
lineWidth = this._types[eventTypeId].lineWidth;
}
if (this._types == null || !this._types[eventTypeId] || !this._types[eventTypeId].lineStyle) {
lineStyle = 'dashed'; //default line style
} else {
lineStyle = this._types[eventTypeId].lineStyle.toLowerCase();
}
let topOffset = 2;
top = o.top + this._plot.height() + topOffset;
let timeFrom = Math.min(event.min, event.timeEnd);
let timeTo = Math.max(event.min, event.timeEnd);
left = xaxis.p2c(timeFrom) + o.left;
let right = xaxis.p2c(timeTo) + o.left;
regionWidth = right - left;
_.each([left, right], position => {
let line = $('<div class="events_line flot-temp-elem"></div>').css({
position: 'absolute',
opacity: 0.8,
left: position + 'px',
top: 8,
width: lineWidth + 'px',
height: this._plot.height() + topOffset,
'border-left-width': lineWidth + 'px',
'border-left-style': lineStyle,
'border-left-color': color,
color: color,
});
line.appendTo(container);
});
let region = $('<div class="events_marker region_marker flot-temp-elem"></div>').css({
position: 'absolute',
opacity: 0.5,
left: left + 'px',
top: top,
width: Math.round(regionWidth + lineWidth) + 'px',
height: '0.5rem',
'border-left-color': color,
color: color,
'background-color': color,
});
region.appendTo(container);
region.data({
event: event,
});
let mouseenter = function() {
createAnnotationToolip(region, $(this).data('event'), that._plot);
};
if (event.editModel) {
createEditPopover(region, event.editModel, that._plot);
}
let mouseleave = function() {
that._plot.clearSelection();
};
if (markerTooltip) {
region.css({ cursor: 'help' });
region.hover(mouseenter, mouseleave);
}
let drawableEvent = new DrawableEvent(
region,
function drawFunc(obj) {
obj.show();
},
function(obj) {
obj.remove();
},
function(obj, position) {
obj.css({
top: position.top,
left: position.left,
});
},
left,
top,
region.width(),
region.height()
);
return drawableEvent;
}
/**
* check if the event is inside visible range
*/
_insidePlot(x) {
let xaxis = this._plot.getXAxes()[this._plot.getOptions().events.xaxis - 1];
let xc = xaxis.p2c(x);
return xc > 0 && xc < xaxis.p2c(xaxis.max);
}
}
/**
* initialize the plugin for the given plot
*/
/** @ngInject */
export function init(plot) {
/*jshint validthis:true */
let that = this;
let eventMarkers = new EventMarkers(plot);
plot.getEvents = function() {
return eventMarkers._events;
};
plot.hideEvents = function() {
$.each(eventMarkers._events, (index, event) => {
event
.visual()
.getObject()
.hide();
});
};
plot.showEvents = function() {
plot.hideEvents();
$.each(eventMarkers._events, (index, event) => {
event.hide();
});
that.eventMarkers.drawEvents();
};
// change events on an existing plot
plot.setEvents = function(events) {
if (eventMarkers.eventsEnabled) {
eventMarkers.setupEvents(events);
}
};
plot.hooks.processOptions.push(function(plot, options) {
// enable the plugin
if (options.events.data != null) {
eventMarkers.eventsEnabled = true;
}
});
plot.hooks.draw.push(function(plot) {
let options = plot.getOptions();
if (eventMarkers.eventsEnabled) {
// check for first run
if (eventMarkers.getEvents().length < 1) {
eventMarkers.setTypes(options.events.types);
eventMarkers.setupEvents(options.events.data);
} else {
eventMarkers.updateEvents();
}
}
eventMarkers.drawEvents();
});
}
let defaultOptions = {
events: {
data: null,
types: null,
xaxis: 1,
position: 'BOTTOM',
},
};
$.plot.plugins.push({
init: init,
options: defaultOptions,
name: 'events',
version: '0.2.5',
});
@@ -0,0 +1,94 @@
import moment from 'moment';
import { GraphCtrl } from '../module';
jest.mock('../graph', () => ({}));
describe('GraphCtrl', () => {
let injector = {
get: () => {
return {
timeRange: () => {
return {
from: '',
to: '',
};
},
};
},
};
let scope = {
$on: () => {},
};
GraphCtrl.prototype.panel = {
events: {
on: () => {},
},
gridPos: {
w: 100,
},
};
let ctx = <any>{};
beforeEach(() => {
ctx.ctrl = new GraphCtrl(scope, injector, {});
ctx.ctrl.annotationsPromise = Promise.resolve({});
ctx.ctrl.updateTimeRange();
});
describe('when time series are outside range', () => {
beforeEach(() => {
var data = [
{
target: 'test.cpu1',
datapoints: [[45, 1234567890], [60, 1234567899]],
},
];
ctx.ctrl.range = { from: moment().valueOf(), to: moment().valueOf() };
ctx.ctrl.onDataReceived(data);
});
it('should set datapointsOutside', () => {
expect(ctx.ctrl.dataWarning.title).toBe('Data points outside time range');
});
});
describe('when time series are inside range', () => {
beforeEach(() => {
var range = {
from: moment()
.subtract(1, 'days')
.valueOf(),
to: moment().valueOf(),
};
var data = [
{
target: 'test.cpu1',
datapoints: [[45, range.from + 1000], [60, range.from + 10000]],
},
];
ctx.ctrl.range = range;
ctx.ctrl.onDataReceived(data);
});
it('should set datapointsOutside', () => {
expect(ctx.ctrl.dataWarning).toBe(null);
});
});
describe('datapointsCount given 2 series', () => {
beforeEach(() => {
var data = [{ target: 'test.cpu1', datapoints: [] }, { target: 'test.cpu2', datapoints: [] }];
ctx.ctrl.onDataReceived(data);
});
it('should set datapointsCount warning', () => {
expect(ctx.ctrl.dataWarning.title).toBe('No data points');
});
});
});
@@ -1,78 +0,0 @@
import { describe, beforeEach, it, expect, angularMocks } from '../../../../../test/lib/common';
import moment from 'moment';
import { GraphCtrl } from '../module';
import helpers from '../../../../../test/specs/helpers';
describe('GraphCtrl', function() {
var ctx = new helpers.ControllerTestContext();
beforeEach(angularMocks.module('grafana.services'));
beforeEach(angularMocks.module('grafana.controllers'));
beforeEach(
angularMocks.module(function($compileProvider) {
$compileProvider.preAssignBindingsEnabled(true);
})
);
beforeEach(ctx.providePhase());
beforeEach(ctx.createPanelController(GraphCtrl));
beforeEach(() => {
ctx.ctrl.annotationsPromise = Promise.resolve({});
ctx.ctrl.updateTimeRange();
});
describe('when time series are outside range', function() {
beforeEach(function() {
var data = [
{
target: 'test.cpu1',
datapoints: [[45, 1234567890], [60, 1234567899]],
},
];
ctx.ctrl.range = { from: moment().valueOf(), to: moment().valueOf() };
ctx.ctrl.onDataReceived(data);
});
it('should set datapointsOutside', function() {
expect(ctx.ctrl.dataWarning.title).to.be('Data points outside time range');
});
});
describe('when time series are inside range', function() {
beforeEach(function() {
var range = {
from: moment()
.subtract(1, 'days')
.valueOf(),
to: moment().valueOf(),
};
var data = [
{
target: 'test.cpu1',
datapoints: [[45, range.from + 1000], [60, range.from + 10000]],
},
];
ctx.ctrl.range = range;
ctx.ctrl.onDataReceived(data);
});
it('should set datapointsOutside', function() {
expect(ctx.ctrl.dataWarning).to.be(null);
});
});
describe('datapointsCount given 2 series', function() {
beforeEach(function() {
var data = [{ target: 'test.cpu1', datapoints: [] }, { target: 'test.cpu2', datapoints: [] }];
ctx.ctrl.onDataReceived(data);
});
it('should set datapointsCount warning', function() {
expect(ctx.ctrl.dataWarning.title).to.be('No data points');
});
});
});
@@ -56,10 +56,10 @@
<h5 class="section-heading">Coloring</h5>
<div class="gf-form-inline">
<gf-form-switch class="gf-form" label-class="width-8" label="Background" checked="ctrl.panel.colorBackground" on-change="ctrl.render()"></gf-form-switch>
<gf-form-switch class="gf-form" label-class="width-4" label="Value" checked="ctrl.panel.colorValue" on-change="ctrl.render()"></gf-form-switch>
<gf-form-switch class="gf-form" label-class="width-6" label="Value" checked="ctrl.panel.colorValue" on-change="ctrl.render()"></gf-form-switch>
</div>
<div class="gf-form-inline">
<gf-form-switch class="gf-form" label-class="width-6" label="Prefix" checked="ctrl.panel.colorPrefix" on-change="ctrl.render()" ng-disabled="!ctrl.canModifyText()"></gf-form-switch>
<gf-form-switch class="gf-form" label-class="width-8" label="Prefix" checked="ctrl.panel.colorPrefix" on-change="ctrl.render()" ng-disabled="!ctrl.canModifyText()"></gf-form-switch>
<gf-form-switch class="gf-form" label-class="width-6" label="Postfix" checked="ctrl.panel.colorPostfix" on-change="ctrl.render()" ng-disabled="!ctrl.canModifyText()"></gf-form-switch>
</div>
<div class="gf-form-inline">
+1 -1
View File
@@ -112,7 +112,7 @@ export function setupAngularRoutes($routeProvider, $locationProvider) {
controller: 'FolderDashboardsCtrl',
controllerAs: 'ctrl',
})
.when('/explore/:initial?', {
.when('/explore', {
template: '<react-container />',
resolve: {
roles: () => ['Editor', 'Admin'],
-36
View File
@@ -93,24 +93,14 @@ $headings-color: darken($white, 11%);
$abbr-border-color: $gray-3 !default;
$text-muted: $text-color-weak;
$blockquote-small-color: $gray-3 !default;
$blockquote-border-color: $gray-4 !default;
$hr-border-color: rgba(0, 0, 0, 0.1) !default;
// Components
$component-active-color: #fff !default;
$component-active-bg: $brand-primary !default;
// Panel
// -------------------------
$panel-bg: #212124;
$panel-border-color: $dark-1;
$panel-border: solid 1px $panel-border-color;
$panel-drop-zone-bg: repeating-linear-gradient(-128deg, #111, #111 10px, #191919 10px, #222 20px);
$panel-header-hover-bg: $dark-4;
$panel-header-menu-hover-bg: $dark-5;
$panel-edit-shadow: 0 -30px 30px -30px $black;
// page header
$page-header-bg: linear-gradient(90deg, #292a2d, black);
@@ -205,7 +195,6 @@ $input-box-shadow-focus: rgba(102, 175, 233, 0.6);
$input-color-placeholder: $gray-1 !default;
$input-label-bg: $gray-blue;
$input-label-border-color: $dark-3;
$input-invalid-border-color: lighten($red, 5%);
// Search
$search-shadow: 0 0 30px 0 $black;
@@ -223,7 +212,6 @@ $dropdownBorder: rgba(0, 0, 0, 0.2);
$dropdownDividerTop: transparent;
$dropdownDividerBottom: #444;
$dropdownDivider: $dropdownDividerBottom;
$dropdownTitle: $link-color-disabled;
$dropdownLinkColor: $text-color;
$dropdownLinkColorHover: $white;
@@ -232,8 +220,6 @@ $dropdownLinkColorActive: $white;
$dropdownLinkBackgroundActive: $dark-4;
$dropdownLinkBackgroundHover: $dark-4;
$dropdown-link-color: $gray-3;
// COMPONENT VARIABLES
// --------------------------------------------------
@@ -246,22 +232,13 @@ $horizontalComponentOffset: 180px;
// Wells
// -------------------------
$wellBackground: #131517;
$navbarHeight: 55px;
$navbarBackgroundHighlight: $dark-3;
$navbarBackground: $panel-bg;
$navbarBorder: 1px solid $dark-3;
$navbarShadow: 0 0 20px black;
$navbarText: $gray-4;
$navbarLinkColor: $gray-4;
$navbarLinkColorHover: $white;
$navbarLinkColorActive: $navbarLinkColorHover;
$navbarLinkBackgroundHover: transparent;
$navbarLinkBackgroundActive: $navbarBackground;
$navbarBrandColor: $link-color;
$navbarDropdownShadow: inset 0px 4px 10px -4px $body-bg;
$navbarButtonBackground: $navbarBackground;
$navbarButtonBackgroundHighlight: $body-bg;
@@ -275,20 +252,15 @@ $side-menu-bg-mobile: $side-menu-bg;
$side-menu-item-hover-bg: $dark-2;
$side-menu-shadow: 0 0 20px black;
$side-menu-link-color: $link-color;
$breadcrumb-hover-hl: #111;
// Menu dropdowns
// -------------------------
$menu-dropdown-bg: $body-bg;
$menu-dropdown-hover-bg: $dark-2;
$menu-dropdown-border-color: $dark-3;
$menu-dropdown-shadow: 5px 5px 20px -5px $black;
// Breadcrumb
// -------------------------
$page-nav-bg: $black;
$page-nav-shadow: 5px 5px 20px -5px $black;
$page-nav-breadcrumb-color: $gray-3;
// Tabs
// -------------------------
@@ -296,9 +268,6 @@ $tab-border-color: $dark-4;
// Pagination
// -------------------------
$paginationBackground: $body-bg;
$paginationBorder: transparent;
$paginationActiveBackground: $blue;
// Form states and alerts
// -------------------------
@@ -343,10 +312,6 @@ $info-box-color: $gray-4;
$footer-link-color: $gray-2;
$footer-link-hover: $gray-4;
// collapse box
$collapse-box-body-border: $dark-5;
$collapse-box-body-error-border: $red;
// json-explorer
$json-explorer-default-color: $text-color;
$json-explorer-string-color: #23d662;
@@ -357,7 +322,6 @@ $json-explorer-undefined-color: rgb(239, 143, 190);
$json-explorer-function-color: #fd48cb;
$json-explorer-rotate-time: 100ms;
$json-explorer-toggler-opacity: 0.6;
$json-explorer-toggler-color: #45376f;
$json-explorer-bracket-color: #9494ff;
$json-explorer-key-color: #23a0db;
$json-explorer-url-color: #027bff;
-31
View File
@@ -90,25 +90,15 @@ $headings-color: $text-color;
$abbr-border-color: $gray-2 !default;
$text-muted: $text-color-weak;
$blockquote-small-color: $gray-2 !default;
$blockquote-border-color: $gray-3 !default;
$hr-border-color: $dark-3 !default;
// Components
$component-active-color: $white !default;
$component-active-bg: $brand-primary !default;
// Panel
// -------------------------
$panel-bg: $white;
$panel-border-color: $gray-5;
$panel-border: solid 1px $panel-border-color;
$panel-drop-zone-bg: repeating-linear-gradient(-128deg, $body-bg, $body-bg 10px, $gray-6 10px, $gray-6 20px);
$panel-header-hover-bg: $gray-6;
$panel-header-menu-hover-bg: $gray-4;
$panel-edit-shadow: 0 0 30px 20px $black;
// Page header
$page-header-bg: linear-gradient(90deg, $white, $gray-7);
@@ -201,7 +191,6 @@ $input-box-shadow-focus: $blue !default;
$input-color-placeholder: $gray-4 !default;
$input-label-bg: $gray-5;
$input-label-border-color: $gray-5;
$input-invalid-border-color: lighten($red, 5%);
// Sidemenu
// -------------------------
@@ -215,15 +204,10 @@ $side-menu-link-color: $gray-6;
// -------------------------
$menu-dropdown-bg: $gray-7;
$menu-dropdown-hover-bg: $gray-6;
$menu-dropdown-border-color: $gray-4;
$menu-dropdown-shadow: 5px 5px 10px -5px $gray-1;
// Breadcrumb
// -------------------------
$page-nav-bg: $gray-5;
$page-nav-shadow: 5px 5px 20px -5px $gray-4;
$page-nav-breadcrumb-color: $black;
$breadcrumb-hover-hl: #d9dadd;
// Tabs
// -------------------------
@@ -245,7 +229,6 @@ $dropdownBorder: $gray-4;
$dropdownDividerTop: $gray-6;
$dropdownDividerBottom: $white;
$dropdownDivider: $dropdownDividerTop;
$dropdownTitle: $gray-3;
$dropdownLinkColor: $dark-3;
$dropdownLinkColorHover: $link-color;
@@ -271,24 +254,16 @@ $horizontalComponentOffset: 180px;
// Wells
// -------------------------
$wellBackground: $gray-3;
// Navbar
// -------------------------
$navbarHeight: 52px;
$navbarBackgroundHighlight: $white;
$navbarBackground: $white;
$navbarBorder: 1px solid $gray-4;
$navbarShadow: 0 0 3px #c1c1c1;
$navbarText: #444;
$navbarLinkColor: #444;
$navbarLinkColorHover: #000;
$navbarLinkColorActive: #333;
$navbarLinkBackgroundHover: transparent;
$navbarLinkBackgroundActive: darken($navbarBackground, 6.5%);
$navbarDropdownShadow: inset 0px 4px 7px -4px darken($body-bg, 20%);
$navbarBrandColor: $navbarLinkColor;
@@ -299,9 +274,6 @@ $navbar-button-border: $gray-4;
// Pagination
// -------------------------
$paginationBackground: $gray-2;
$paginationBorder: transparent;
$paginationActiveBackground: $blue;
// Form states and alerts
// -------------------------
@@ -346,8 +318,6 @@ $footer-link-color: $gray-3;
$footer-link-hover: $dark-5;
// collapse box
$collapse-box-body-border: $gray-4;
$collapse-box-body-error-border: $red;
// json explorer
$json-explorer-default-color: black;
@@ -359,7 +329,6 @@ $json-explorer-undefined-color: rgb(202, 11, 105);
$json-explorer-function-color: #ff20ed;
$json-explorer-rotate-time: 100ms;
$json-explorer-toggler-opacity: 0.6;
$json-explorer-toggler-color: #45376f;
$json-explorer-bracket-color: blue;
$json-explorer-key-color: #00008b;
$json-explorer-url-color: blue;
+9 -36
View File
@@ -3,13 +3,7 @@
// Quickly modify global styling by enabling or disabling optional features.
$enable-flex: true !default;
$enable-rounded: true !default;
$enable-shadows: false !default;
$enable-gradients: false !default;
$enable-transitions: false !default;
$enable-hover-media-query: false !default;
$enable-grid-classes: true !default;
$enable-print-styles: true !default;
// Spacing
//
@@ -53,9 +47,9 @@ $enable-flex: true;
// Typography
// -------------------------
$font-family-sans-serif: "Roboto", Helvetica, Arial, sans-serif;
$font-family-serif: Georgia, "Times New Roman", Times, serif;
$font-family-monospace: Menlo, Monaco, Consolas, "Courier New", monospace;
$font-family-sans-serif: 'Roboto', Helvetica, Arial, sans-serif;
$font-family-serif: Georgia, 'Times New Roman', Times, serif;
$font-family-monospace: Menlo, Monaco, Consolas, 'Courier New', monospace;
$font-family-base: $font-family-sans-serif !default;
$font-size-root: 14px !default;
@@ -90,16 +84,12 @@ $lead-font-size: 1.25rem !default;
$lead-font-weight: 300 !default;
$headings-margin-bottom: ($spacer / 2) !default;
$headings-font-family: "Roboto", "Helvetica Neue", Helvetica, Arial, sans-serif;
$headings-font-family: 'Roboto', 'Helvetica Neue', Helvetica, Arial, sans-serif;
$headings-font-weight: 400 !default;
$headings-line-height: 1.1 !default;
$blockquote-font-size: ($font-size-base * 1.25) !default;
$blockquote-border-width: 0.25rem !default;
$hr-border-width: $border-width !default;
$dt-font-weight: bold !default;
$list-inline-padding: 5px !default;
// Components
//
@@ -112,9 +102,6 @@ $border-radius: 3px !default;
$border-radius-lg: 5px !default;
$border-radius-sm: 2px!default;
$caret-width: 0.3em !default;
$caret-width-lg: $caret-width !default;
// Page
$page-sidebar-width: 11rem;
@@ -130,7 +117,6 @@ $link-hover-decoration: none !default;
// Customizes the `.table` component with basic values, each used across all table variations.
$table-cell-padding: 4px 10px !default;
$table-sm-cell-padding: 0.3rem !default;
// Forms
$input-padding-x: 10px !default;
@@ -139,31 +125,18 @@ $input-line-height: 18px !default;
$input-btn-border-width: 1px;
$input-border-radius: 0 $border-radius $border-radius 0 !default;
$input-border-radius-lg: 0 $border-radius-lg $border-radius-lg 0 !default;
$input-border-radius-sm: 0 $border-radius-sm $border-radius-sm 0 !default;
$label-border-radius: $border-radius 0 0 $border-radius !default;
$label-border-radius-lg: $border-radius-lg 0 0 $border-radius-lg !default;
$label-border-radius-sm: $border-radius-sm 0 0 $border-radius-sm !default;
$input-padding-x-sm: 7px !default;
$input-padding-y-sm: 4px !default;
$input-padding-x-lg: 20px !default;
$input-padding-y-lg: 10px !default;
$input-height: (($font-size-base * $line-height-base) + ($input-padding-y * 2))
!default;
$input-height-lg: (
($font-size-lg * $line-height-lg) + ($input-padding-y-lg * 2)
)
!default;
$input-height-sm: (
($font-size-sm * $line-height-sm) + ($input-padding-y-sm * 2)
)
!default;
$input-height: (($font-size-base * $line-height-base) + ($input-padding-y * 2)) !default;
$form-group-margin-bottom: $spacer-y !default;
$gf-form-margin: 0.2rem;
$cursor-disabled: not-allowed !default;
@@ -221,9 +194,9 @@ $panel-padding: 0px 10px 5px 10px;
$tabs-padding: 10px 15px 9px;
$external-services: (
github: (bgColor: #464646, borderColor: #393939, icon: ""),
google: (bgColor: #e84d3c, borderColor: #b83e31, icon: ""),
grafanacom: (bgColor: inherit, borderColor: #393939, icon: ""),
oauth: (bgColor: inherit, borderColor: #393939, icon: "")
github: (bgColor: #464646, borderColor: #393939, icon: ''),
google: (bgColor: #e84d3c, borderColor: #b83e31, icon: ''),
grafanacom: (bgColor: inherit, borderColor: #393939, icon: ''),
oauth: (bgColor: inherit, borderColor: #393939, icon: '')
)
!default;
+7 -3
View File
@@ -24,7 +24,7 @@ small {
font-size: 85%;
}
strong {
font-weight: bold;
font-weight: $font-weight-semi-bold;
}
em {
font-style: italic;
@@ -249,7 +249,7 @@ dd {
line-height: $line-height-base;
}
dt {
font-weight: bold;
font-weight: $font-weight-semi-bold;
}
dd {
margin-left: $line-height-base / 2;
@@ -376,7 +376,7 @@ a.external-link {
padding: $spacer*0.5 $spacer;
}
th {
font-weight: normal;
font-weight: $font-weight-semi-bold;
background: $table-bg-accent;
}
}
@@ -415,3 +415,7 @@ a.external-link {
color: $yellow;
padding: 0;
}
th {
font-weight: $font-weight-semi-bold;
}
+1
View File
@@ -16,6 +16,7 @@ div.flot-text {
height: 100%;
&--solo {
margin: 0;
.panel-container {
border: none;
z-index: $zindex-sidemenu + 1;
+41
View File
@@ -60,6 +60,10 @@
flex-wrap: wrap;
}
.datasource-picker {
min-width: 10rem;
}
.timepicker {
display: flex;
@@ -93,3 +97,40 @@
.query-row-tools {
width: 4rem;
}
.explore {
.logs {
.logs-entries {
display: grid;
grid-column-gap: 1rem;
grid-row-gap: 0.1rem;
grid-template-columns: 4px minmax(100px, max-content) 1fr;
font-family: $font-family-monospace;
}
.logs-row-match-highlight {
background-color: lighten($blue, 20%);
}
.logs-row-level {
background-color: transparent;
margin: 6px 0;
border-radius: 2px;
opacity: 0.8;
}
.logs-row-level-crit,
.logs-row-level-error,
.logs-row-level-err {
background-color: $red;
}
.logs-row-level-warn {
background-color: $orange;
}
.logs-row-level-info {
background-color: $green;
}
}
}
+2 -2
View File
@@ -91,6 +91,6 @@ func (c *Client) AddDebugHandlers() {
return
}
c.Handlers.Send.PushFrontNamed(request.NamedHandler{Name: "awssdk.client.LogRequest", Fn: logRequest})
c.Handlers.Send.PushBackNamed(request.NamedHandler{Name: "awssdk.client.LogResponse", Fn: logResponse})
c.Handlers.Send.PushFrontNamed(LogHTTPRequestHandler)
c.Handlers.Send.PushBackNamed(LogHTTPResponseHandler)
}
+87 -15
View File
@@ -44,12 +44,22 @@ func (reader *teeReaderCloser) Close() error {
return reader.Source.Close()
}
// LogHTTPRequestHandler is a SDK request handler to log the HTTP request sent
// to a service. Will include the HTTP request body if the LogLevel of the
// request matches LogDebugWithHTTPBody.
var LogHTTPRequestHandler = request.NamedHandler{
Name: "awssdk.client.LogRequest",
Fn: logRequest,
}
func logRequest(r *request.Request) {
logBody := r.Config.LogLevel.Matches(aws.LogDebugWithHTTPBody)
bodySeekable := aws.IsReaderSeekable(r.Body)
dumpedBody, err := httputil.DumpRequestOut(r.HTTPRequest, logBody)
b, err := httputil.DumpRequestOut(r.HTTPRequest, logBody)
if err != nil {
r.Config.Logger.Log(fmt.Sprintf(logReqErrMsg, r.ClientInfo.ServiceName, r.Operation.Name, err))
r.Config.Logger.Log(fmt.Sprintf(logReqErrMsg,
r.ClientInfo.ServiceName, r.Operation.Name, err))
return
}
@@ -63,7 +73,28 @@ func logRequest(r *request.Request) {
r.ResetBody()
}
r.Config.Logger.Log(fmt.Sprintf(logReqMsg, r.ClientInfo.ServiceName, r.Operation.Name, string(dumpedBody)))
r.Config.Logger.Log(fmt.Sprintf(logReqMsg,
r.ClientInfo.ServiceName, r.Operation.Name, string(b)))
}
// LogHTTPRequestHeaderHandler is a SDK request handler to log the HTTP request sent
// to a service. Will only log the HTTP request's headers. The request payload
// will not be read.
var LogHTTPRequestHeaderHandler = request.NamedHandler{
Name: "awssdk.client.LogRequestHeader",
Fn: logRequestHeader,
}
func logRequestHeader(r *request.Request) {
b, err := httputil.DumpRequestOut(r.HTTPRequest, false)
if err != nil {
r.Config.Logger.Log(fmt.Sprintf(logReqErrMsg,
r.ClientInfo.ServiceName, r.Operation.Name, err))
return
}
r.Config.Logger.Log(fmt.Sprintf(logReqMsg,
r.ClientInfo.ServiceName, r.Operation.Name, string(b)))
}
const logRespMsg = `DEBUG: Response %s/%s Details:
@@ -76,27 +107,44 @@ const logRespErrMsg = `DEBUG ERROR: Response %s/%s:
%s
-----------------------------------------------------`
// LogHTTPResponseHandler is a SDK request handler to log the HTTP response
// received from a service. Will include the HTTP response body if the LogLevel
// of the request matches LogDebugWithHTTPBody.
var LogHTTPResponseHandler = request.NamedHandler{
Name: "awssdk.client.LogResponse",
Fn: logResponse,
}
func logResponse(r *request.Request) {
lw := &logWriter{r.Config.Logger, bytes.NewBuffer(nil)}
r.HTTPResponse.Body = &teeReaderCloser{
Reader: io.TeeReader(r.HTTPResponse.Body, lw),
Source: r.HTTPResponse.Body,
logBody := r.Config.LogLevel.Matches(aws.LogDebugWithHTTPBody)
if logBody {
r.HTTPResponse.Body = &teeReaderCloser{
Reader: io.TeeReader(r.HTTPResponse.Body, lw),
Source: r.HTTPResponse.Body,
}
}
handlerFn := func(req *request.Request) {
body, err := httputil.DumpResponse(req.HTTPResponse, false)
b, err := httputil.DumpResponse(req.HTTPResponse, false)
if err != nil {
lw.Logger.Log(fmt.Sprintf(logRespErrMsg, req.ClientInfo.ServiceName, req.Operation.Name, err))
lw.Logger.Log(fmt.Sprintf(logRespErrMsg,
req.ClientInfo.ServiceName, req.Operation.Name, err))
return
}
b, err := ioutil.ReadAll(lw.buf)
if err != nil {
lw.Logger.Log(fmt.Sprintf(logRespErrMsg, req.ClientInfo.ServiceName, req.Operation.Name, err))
return
}
lw.Logger.Log(fmt.Sprintf(logRespMsg, req.ClientInfo.ServiceName, req.Operation.Name, string(body)))
if req.Config.LogLevel.Matches(aws.LogDebugWithHTTPBody) {
lw.Logger.Log(fmt.Sprintf(logRespMsg,
req.ClientInfo.ServiceName, req.Operation.Name, string(b)))
if logBody {
b, err := ioutil.ReadAll(lw.buf)
if err != nil {
lw.Logger.Log(fmt.Sprintf(logRespErrMsg,
req.ClientInfo.ServiceName, req.Operation.Name, err))
return
}
lw.Logger.Log(string(b))
}
}
@@ -110,3 +158,27 @@ func logResponse(r *request.Request) {
Name: handlerName, Fn: handlerFn,
})
}
// LogHTTPResponseHeaderHandler is a SDK request handler to log the HTTP
// response received from a service. Will only log the HTTP response's headers.
// The response payload will not be read.
var LogHTTPResponseHeaderHandler = request.NamedHandler{
Name: "awssdk.client.LogResponseHeader",
Fn: logResponseHeader,
}
func logResponseHeader(r *request.Request) {
if r.Config.Logger == nil {
return
}
b, err := httputil.DumpResponse(r.HTTPResponse, false)
if err != nil {
r.Config.Logger.Log(fmt.Sprintf(logRespErrMsg,
r.ClientInfo.ServiceName, r.Operation.Name, err))
return
}
r.Config.Logger.Log(fmt.Sprintf(logRespMsg,
r.ClientInfo.ServiceName, r.Operation.Name, string(b)))
}
+1
View File
@@ -3,6 +3,7 @@ package metadata
// ClientInfo wraps immutable data from the client.Client structure.
type ClientInfo struct {
ServiceName string
ServiceID string
APIVersion string
Endpoint string
SigningName string
+15 -3
View File
@@ -178,7 +178,8 @@ func (e *Expiry) IsExpired() bool {
type Credentials struct {
creds Value
forceRefresh bool
m sync.Mutex
m sync.RWMutex
provider Provider
}
@@ -201,6 +202,17 @@ func NewCredentials(provider Provider) *Credentials {
// If Credentials.Expire() was called the credentials Value will be force
// expired, and the next call to Get() will cause them to be refreshed.
func (c *Credentials) Get() (Value, error) {
// Check the cached credentials first with just the read lock.
c.m.RLock()
if !c.isExpired() {
creds := c.creds
c.m.RUnlock()
return creds, nil
}
c.m.RUnlock()
// Credentials are expired need to retrieve the credentials taking the full
// lock.
c.m.Lock()
defer c.m.Unlock()
@@ -234,8 +246,8 @@ func (c *Credentials) Expire() {
// If the Credentials were forced to be expired with Expire() this will
// reflect that override.
func (c *Credentials) IsExpired() bool {
c.m.Lock()
defer c.m.Unlock()
c.m.RLock()
defer c.m.RUnlock()
return c.isExpired()
}

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