From b450b778cb44e1432c86430f5d1bff206b63c5b7 Mon Sep 17 00:00:00 2001 From: Dominik Henneke Date: Tue, 4 Dec 2018 19:19:16 +0100 Subject: [PATCH 01/33] Use buildTableConstraint instead of buildSchemaConstraint to find the datatype of a column if using a table from a different database schema --- public/app/plugins/datasource/postgres/meta_query.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/public/app/plugins/datasource/postgres/meta_query.ts b/public/app/plugins/datasource/postgres/meta_query.ts index fd13f3b4482..07ea3e51d87 100644 --- a/public/app/plugins/datasource/postgres/meta_query.ts +++ b/public/app/plugins/datasource/postgres/meta_query.ts @@ -151,8 +151,7 @@ table_schema IN ( buildDatatypeQuery(column: string) { let query = 'SELECT udt_name FROM information_schema.columns WHERE '; - query += this.buildSchemaConstraint(); - query += ' AND table_name = ' + this.quoteIdentAsLiteral(this.target.table); + query += this.buildTableConstraint(this.target.table); query += ' AND column_name = ' + this.quoteIdentAsLiteral(column); return query; } From 1e53c12921993887e5541a558559b7e9dee54ffa Mon Sep 17 00:00:00 2001 From: Tom Nitti Date: Tue, 4 Dec 2018 11:06:48 -0600 Subject: [PATCH 02/33] added support for influxdb cumulative_sum function in tsdb --- pkg/tsdb/influxdb/query_part.go | 1 + pkg/tsdb/influxdb/query_part_test.go | 1 + 2 files changed, 2 insertions(+) diff --git a/pkg/tsdb/influxdb/query_part.go b/pkg/tsdb/influxdb/query_part.go index 77f565a8597..29a77f15617 100644 --- a/pkg/tsdb/influxdb/query_part.go +++ b/pkg/tsdb/influxdb/query_part.go @@ -32,6 +32,7 @@ func init() { renders["median"] = QueryDefinition{Renderer: functionRenderer} renders["sum"] = QueryDefinition{Renderer: functionRenderer} renders["mode"] = QueryDefinition{Renderer: functionRenderer} + renders["cumulative_sum"] = QueryDefinition{Renderer: functionRenderer} renders["holt_winters"] = QueryDefinition{ Renderer: functionRenderer, diff --git a/pkg/tsdb/influxdb/query_part_test.go b/pkg/tsdb/influxdb/query_part_test.go index 08bcff9b727..76daf6446d8 100644 --- a/pkg/tsdb/influxdb/query_part_test.go +++ b/pkg/tsdb/influxdb/query_part_test.go @@ -23,6 +23,7 @@ func TestInfluxdbQueryPart(t *testing.T) { {mode: "alias", params: []string{"test"}, input: "mean(value)", expected: `mean(value) AS "test"`}, {mode: "count", params: []string{}, input: "distinct(value)", expected: `count(distinct(value))`}, {mode: "mode", params: []string{}, input: "value", expected: `mode(value)`}, + {mode: "cumulative_sum", params: []string{}, input: "mean(value)", expected: `cumulative_sum(mean(value))`}, } queryContext := &tsdb.TsdbQuery{TimeRange: tsdb.NewTimeRange("5m", "now")} From e31490ac68a223fdcda23676f7412beb6e846ee2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 4 Dec 2018 12:36:43 -0800 Subject: [PATCH 03/33] fix for panel embedding. Solo panel height was not correctly set. Made panel--solo into panel-solo class. in develop branch we have remove the need for the panel class --- .../features/panel/partials/soloPanel.html | 3 +-- public/sass/base/_type.scss | 1 - public/sass/pages/_dashboard.scss | 25 ++++++++++++------- 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/public/app/features/panel/partials/soloPanel.html b/public/app/features/panel/partials/soloPanel.html index 0940e07afdd..644bbe74ffb 100644 --- a/public/app/features/panel/partials/soloPanel.html +++ b/public/app/features/panel/partials/soloPanel.html @@ -1,5 +1,4 @@ -
+
-
diff --git a/public/sass/base/_type.scss b/public/sass/base/_type.scss index 2de8665f06a..1a005b0d511 100644 --- a/public/sass/base/_type.scss +++ b/public/sass/base/_type.scss @@ -199,7 +199,6 @@ small, mark, .mark { - padding: 0.2em; background: $alert-warning-bg; } diff --git a/public/sass/pages/_dashboard.scss b/public/sass/pages/_dashboard.scss index 125edac500f..0a578901bbd 100644 --- a/public/sass/pages/_dashboard.scss +++ b/public/sass/pages/_dashboard.scss @@ -19,16 +19,23 @@ div.flot-text { .panel { height: 100%; +} - &--solo { - position: fixed; - bottom: 0; - right: 0; - margin: 0; - .panel-container { - border: none; - z-index: $zindex-sidemenu + 1; - } +.panel-solo { + position: fixed; + bottom: 0; + right: 0; + margin: 0; + left: 0; + top: 0; + + .panel-container { + border: none; + } + + .panel-menu-toggle, + .panel-menu { + display: none; } } From fadd816d89f0f9123f81a344e2aca5e27a954b58 Mon Sep 17 00:00:00 2001 From: Mario Trangoni Date: Tue, 4 Dec 2018 23:29:34 +0100 Subject: [PATCH 04/33] pkg/*: Fix misspell issues See, $ find . -type f | xargs misspell -locale US | grep -vi -e vendor -e devenv -e Unknwon -e Destory | grep pkg ./pkg/services/sqlstore/org_test.go:190:15: "frome" is a misspelling of "from" ./pkg/tsdb/elasticsearch/response_parser.go:544:24: "Unkown" is a misspelling of "Unknown" ./pkg/tsdb/opentsdb/opentsdb.go:87:20: "marshalling" is a misspelling of "marshaling" ./pkg/components/dynmap/dynmap_test.go:2:7: "Licence" is a misspelling of "License" ./pkg/components/dynmap/dynmap.go:2:7: "Licence" is a misspelling of "License" --- pkg/components/dynmap/dynmap.go | 2 +- pkg/components/dynmap/dynmap_test.go | 2 +- pkg/services/sqlstore/org_test.go | 2 +- pkg/tsdb/elasticsearch/response_parser.go | 2 +- pkg/tsdb/opentsdb/opentsdb.go | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pkg/components/dynmap/dynmap.go b/pkg/components/dynmap/dynmap.go index 96effb24332..f247aca959f 100644 --- a/pkg/components/dynmap/dynmap.go +++ b/pkg/components/dynmap/dynmap.go @@ -1,5 +1,5 @@ // uses code from https://github.com/antonholmquist/jason/blob/master/jason.go -// MIT Licence +// MIT License package dynmap diff --git a/pkg/components/dynmap/dynmap_test.go b/pkg/components/dynmap/dynmap_test.go index 62d356bd67d..68d938214a3 100644 --- a/pkg/components/dynmap/dynmap_test.go +++ b/pkg/components/dynmap/dynmap_test.go @@ -1,5 +1,5 @@ // uses code from https://github.com/antonholmquist/jason/blob/master/jason.go -// MIT Licence +// MIT License package dynmap diff --git a/pkg/services/sqlstore/org_test.go b/pkg/services/sqlstore/org_test.go index c02686c24ba..7e966106b96 100644 --- a/pkg/services/sqlstore/org_test.go +++ b/pkg/services/sqlstore/org_test.go @@ -187,7 +187,7 @@ func TestAccountDataAccess(t *testing.T) { err := DeleteOrg(&m.DeleteOrgCommand{Id: ac2.OrgId}) So(err, ShouldBeNil) - // remove frome ac2 from ac1 org + // remove ac2 user from ac1 org remCmd := m.RemoveOrgUserCommand{OrgId: ac1.OrgId, UserId: ac2.Id, ShouldDeleteOrphanedUser: true} err = RemoveOrgUser(&remCmd) So(err, ShouldBeNil) diff --git a/pkg/tsdb/elasticsearch/response_parser.go b/pkg/tsdb/elasticsearch/response_parser.go index 0837c3dd9d5..b2c724a9b93 100644 --- a/pkg/tsdb/elasticsearch/response_parser.go +++ b/pkg/tsdb/elasticsearch/response_parser.go @@ -541,7 +541,7 @@ func getErrorFromElasticResponse(response *es.SearchResponse) *tsdb.QueryResult } else if reason != "" { result.ErrorString = reason } else { - result.ErrorString = "Unkown elasticsearch error response" + result.ErrorString = "Unknown elasticsearch error response" } return result diff --git a/pkg/tsdb/opentsdb/opentsdb.go b/pkg/tsdb/opentsdb/opentsdb.go index 16da764de54..a810d3c7338 100644 --- a/pkg/tsdb/opentsdb/opentsdb.go +++ b/pkg/tsdb/opentsdb/opentsdb.go @@ -84,7 +84,7 @@ func (e *OpenTsdbExecutor) createRequest(dsInfo *models.DataSource, data OpenTsd postData, err := json.Marshal(data) if err != nil { - plog.Info("Failed marshalling data", "error", err) + plog.Info("Failed marshaling data", "error", err) return nil, fmt.Errorf("Failed to create request. error: %v", err) } From 757cada4a5b719098b6e5805fc8f4259776714fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 5 Dec 2018 09:23:47 +0100 Subject: [PATCH 05/33] fix for panel-initialized event not being called --- public/app/features/dashboard/dashboard_model.ts | 2 ++ public/app/features/dashboard/panel_model.ts | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/public/app/features/dashboard/dashboard_model.ts b/public/app/features/dashboard/dashboard_model.ts index 3320783ec67..18a16d5c1d4 100644 --- a/public/app/features/dashboard/dashboard_model.ts +++ b/public/app/features/dashboard/dashboard_model.ts @@ -223,6 +223,8 @@ export class DashboardModel { } panelInitialized(panel: PanelModel) { + panel.initialized(); + if (!this.otherPanelInFullscreen(panel)) { panel.refresh(); } diff --git a/public/app/features/dashboard/panel_model.ts b/public/app/features/dashboard/panel_model.ts index dc8a509f2eb..737841be7e8 100644 --- a/public/app/features/dashboard/panel_model.ts +++ b/public/app/features/dashboard/panel_model.ts @@ -132,7 +132,7 @@ export class PanelModel { } } - panelInitialized() { + initialized() { this.events.emit('panel-initialized'); } From 061e06c226095a8279a53646451274276463121b Mon Sep 17 00:00:00 2001 From: moznion Date: Wed, 5 Dec 2018 22:19:40 +0900 Subject: [PATCH 06/33] Fix bug what updating org quota doesn't work https://github.com/grafana/grafana/blob/3c330c8e4c0b0f9fb258801ba8a7fe2586bbc819/pkg/services/sqlstore/quota.go#L106 In the real use case, `has` that is described by the above code is always `false` because it includes `Updated` in a query. So this commit fixes this issue. --- pkg/services/sqlstore/quota.go | 6 +++--- pkg/services/sqlstore/quota_test.go | 32 +++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/pkg/services/sqlstore/quota.go b/pkg/services/sqlstore/quota.go index 7005b341268..57c2c2476a0 100644 --- a/pkg/services/sqlstore/quota.go +++ b/pkg/services/sqlstore/quota.go @@ -99,14 +99,14 @@ func UpdateOrgQuota(cmd *m.UpdateOrgQuotaCmd) error { return inTransaction(func(sess *DBSession) error { //Check if quota is already defined in the DB quota := m.Quota{ - Target: cmd.Target, - OrgId: cmd.OrgId, - Updated: time.Now(), + Target: cmd.Target, + OrgId: cmd.OrgId, } has, err := sess.Get("a) if err != nil { return err } + quota.Updated = time.Now() quota.Limit = cmd.Limit if !has { quota.Created = time.Now() diff --git a/pkg/services/sqlstore/quota_test.go b/pkg/services/sqlstore/quota_test.go index 49e028e9cd3..f6ededb5b8b 100644 --- a/pkg/services/sqlstore/quota_test.go +++ b/pkg/services/sqlstore/quota_test.go @@ -2,6 +2,7 @@ package sqlstore import ( "testing" + "time" m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/setting" @@ -168,5 +169,36 @@ func TestQuotaCommandsAndQueries(t *testing.T) { So(query.Result.Limit, ShouldEqual, 5) So(query.Result.Used, ShouldEqual, 1) }) + + Convey("Should org quota updating is successful even if it called multiple time", func() { + orgCmd := m.UpdateOrgQuotaCmd{ + OrgId: orgId, + Target: "org_user", + Limit: 5, + } + err := UpdateOrgQuota(&orgCmd) + So(err, ShouldBeNil) + + query := m.GetOrgQuotaByTargetQuery{OrgId: orgId, Target: "org_user", Default: 1} + err = GetOrgQuotaByTarget(&query) + So(err, ShouldBeNil) + So(query.Result.Limit, ShouldEqual, 5) + + // XXX: resolution of `Updated` column is 1sec, so this makes delay + time.Sleep(1 * time.Second) + + orgCmd = m.UpdateOrgQuotaCmd{ + OrgId: orgId, + Target: "org_user", + Limit: 10, + } + err = UpdateOrgQuota(&orgCmd) + So(err, ShouldBeNil) + + query = m.GetOrgQuotaByTargetQuery{OrgId: orgId, Target: "org_user", Default: 1} + err = GetOrgQuotaByTarget(&query) + So(err, ShouldBeNil) + So(query.Result.Limit, ShouldEqual, 10) + }) }) } From d1e1cde00e5b918c514118f81d04fa7bd984b052 Mon Sep 17 00:00:00 2001 From: moznion Date: Wed, 5 Dec 2018 22:29:07 +0900 Subject: [PATCH 07/33] Fix bug what updating user quota doesn't work Reason is same as 061e06c226095a8279a53646451274276463121b --- pkg/services/sqlstore/quota.go | 6 +++--- pkg/services/sqlstore/quota_test.go | 31 +++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/pkg/services/sqlstore/quota.go b/pkg/services/sqlstore/quota.go index 57c2c2476a0..e90b7fec131 100644 --- a/pkg/services/sqlstore/quota.go +++ b/pkg/services/sqlstore/quota.go @@ -201,14 +201,14 @@ func UpdateUserQuota(cmd *m.UpdateUserQuotaCmd) error { return inTransaction(func(sess *DBSession) error { //Check if quota is already defined in the DB quota := m.Quota{ - Target: cmd.Target, - UserId: cmd.UserId, - Updated: time.Now(), + Target: cmd.Target, + UserId: cmd.UserId, } has, err := sess.Get("a) if err != nil { return err } + quota.Updated = time.Now() quota.Limit = cmd.Limit if !has { quota.Created = time.Now() diff --git a/pkg/services/sqlstore/quota_test.go b/pkg/services/sqlstore/quota_test.go index f6ededb5b8b..8ace14a6a75 100644 --- a/pkg/services/sqlstore/quota_test.go +++ b/pkg/services/sqlstore/quota_test.go @@ -200,5 +200,36 @@ func TestQuotaCommandsAndQueries(t *testing.T) { So(err, ShouldBeNil) So(query.Result.Limit, ShouldEqual, 10) }) + + Convey("Should user quota updating is successful even if it called multiple time", func() { + userQuotaCmd := m.UpdateUserQuotaCmd{ + UserId: userId, + Target: "org_user", + Limit: 5, + } + err := UpdateUserQuota(&userQuotaCmd) + So(err, ShouldBeNil) + + query := m.GetUserQuotaByTargetQuery{UserId: userId, Target: "org_user", Default: 1} + err = GetUserQuotaByTarget(&query) + So(err, ShouldBeNil) + So(query.Result.Limit, ShouldEqual, 5) + + // XXX: resolution of `Updated` column is 1sec, so this makes delay + time.Sleep(1 * time.Second) + + userQuotaCmd = m.UpdateUserQuotaCmd{ + UserId: userId, + Target: "org_user", + Limit: 10, + } + err = UpdateUserQuota(&userQuotaCmd) + So(err, ShouldBeNil) + + query = m.GetUserQuotaByTargetQuery{UserId: userId, Target: "org_user", Default: 1} + err = GetUserQuotaByTarget(&query) + So(err, ShouldBeNil) + So(query.Result.Limit, ShouldEqual, 10) + }) }) } From 4397ee61d09623bbd23e712196f6bca512c53aa6 Mon Sep 17 00:00:00 2001 From: moznion Date: Wed, 5 Dec 2018 22:47:41 +0900 Subject: [PATCH 08/33] Put issue number to test code --- pkg/services/sqlstore/quota_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/services/sqlstore/quota_test.go b/pkg/services/sqlstore/quota_test.go index 8ace14a6a75..976d54d10e2 100644 --- a/pkg/services/sqlstore/quota_test.go +++ b/pkg/services/sqlstore/quota_test.go @@ -170,6 +170,7 @@ func TestQuotaCommandsAndQueries(t *testing.T) { So(query.Result.Used, ShouldEqual, 1) }) + // related: https://github.com/grafana/grafana/issues/14342 Convey("Should org quota updating is successful even if it called multiple time", func() { orgCmd := m.UpdateOrgQuotaCmd{ OrgId: orgId, @@ -201,6 +202,7 @@ func TestQuotaCommandsAndQueries(t *testing.T) { So(query.Result.Limit, ShouldEqual, 10) }) + // related: https://github.com/grafana/grafana/issues/14342 Convey("Should user quota updating is successful even if it called multiple time", func() { userQuotaCmd := m.UpdateUserQuotaCmd{ UserId: userId, From 721e1d7555aaf3239631af606126267178c299bd Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Wed, 5 Dec 2018 23:13:57 +0100 Subject: [PATCH 09/33] renames Grafana Logging ds to Loki --- .../app/features/plugins/built_in_plugins.ts | 4 +- .../app/plugins/datasource/logging/README.md | 3 - .../app/plugins/datasource/logging/module.ts | 15 -- public/app/plugins/datasource/loki/README.md | 3 + .../components/LokiCheatSheet.tsx} | 2 +- .../components/LokiQueryField.tsx} | 14 +- .../components/LokiStartPage.tsx} | 6 +- .../{logging => loki}/datasource.ts | 6 +- .../{logging => loki}/img/grafana_icon.svg | 0 .../plugins/datasource/loki/img/loki_icon.svg | 216 ++++++++++++++++++ .../language_provider.test.ts | 0 .../{logging => loki}/language_provider.ts | 2 +- public/app/plugins/datasource/loki/module.ts | 15 ++ .../{logging => loki}/partials/config.html | 0 .../datasource/{logging => loki}/plugin.json | 16 +- .../{logging => loki}/query_utils.test.ts | 0 .../{logging => loki}/query_utils.ts | 0 .../result_transformer.test.ts | 0 .../{logging => loki}/result_transformer.ts | 0 .../datasource/{logging => loki}/syntax.ts | 0 20 files changed, 259 insertions(+), 43 deletions(-) delete mode 100644 public/app/plugins/datasource/logging/README.md delete mode 100644 public/app/plugins/datasource/logging/module.ts create mode 100644 public/app/plugins/datasource/loki/README.md rename public/app/plugins/datasource/{logging/components/LoggingCheatSheet.tsx => loki/components/LokiCheatSheet.tsx} (96%) rename public/app/plugins/datasource/{logging/components/LoggingQueryField.tsx => loki/components/LokiQueryField.tsx} (94%) rename public/app/plugins/datasource/{logging/components/LoggingStartPage.tsx => loki/components/LokiStartPage.tsx} (52%) rename public/app/plugins/datasource/{logging => loki}/datasource.ts (97%) rename public/app/plugins/datasource/{logging => loki}/img/grafana_icon.svg (100%) create mode 100644 public/app/plugins/datasource/loki/img/loki_icon.svg rename public/app/plugins/datasource/{logging => loki}/language_provider.test.ts (100%) rename public/app/plugins/datasource/{logging => loki}/language_provider.ts (99%) create mode 100644 public/app/plugins/datasource/loki/module.ts rename public/app/plugins/datasource/{logging => loki}/partials/config.html (100%) rename public/app/plugins/datasource/{logging => loki}/plugin.json (54%) rename public/app/plugins/datasource/{logging => loki}/query_utils.test.ts (100%) rename public/app/plugins/datasource/{logging => loki}/query_utils.ts (100%) rename public/app/plugins/datasource/{logging => loki}/result_transformer.test.ts (100%) rename public/app/plugins/datasource/{logging => loki}/result_transformer.ts (100%) rename public/app/plugins/datasource/{logging => loki}/syntax.ts (100%) diff --git a/public/app/features/plugins/built_in_plugins.ts b/public/app/features/plugins/built_in_plugins.ts index a0cedb3ebfd..c4621fda289 100644 --- a/public/app/features/plugins/built_in_plugins.ts +++ b/public/app/features/plugins/built_in_plugins.ts @@ -4,7 +4,7 @@ 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 lokiPlugin from 'app/plugins/datasource/loki/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'; @@ -33,7 +33,7 @@ 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/loki/module': lokiPlugin, 'app/plugins/datasource/mixed/module': mixedPlugin, 'app/plugins/datasource/mysql/module': mysqlPlugin, 'app/plugins/datasource/postgres/module': postgresPlugin, diff --git a/public/app/plugins/datasource/logging/README.md b/public/app/plugins/datasource/logging/README.md deleted file mode 100644 index 33372605973..00000000000 --- a/public/app/plugins/datasource/logging/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Grafana Logging Datasource - Native Plugin - -This is a **built in** datasource that allows you to connect to Grafana's logging service. \ No newline at end of file diff --git a/public/app/plugins/datasource/logging/module.ts b/public/app/plugins/datasource/logging/module.ts deleted file mode 100644 index da00edbf40f..00000000000 --- a/public/app/plugins/datasource/logging/module.ts +++ /dev/null @@ -1,15 +0,0 @@ -import Datasource from './datasource'; - -import LoggingStartPage from './components/LoggingStartPage'; -import LoggingQueryField from './components/LoggingQueryField'; - -export class LoggingConfigCtrl { - static templateUrl = 'partials/config.html'; -} - -export { - Datasource, - LoggingConfigCtrl as ConfigCtrl, - LoggingQueryField as ExploreQueryField, - LoggingStartPage as ExploreStartPage, -}; diff --git a/public/app/plugins/datasource/loki/README.md b/public/app/plugins/datasource/loki/README.md new file mode 100644 index 00000000000..222b7e432eb --- /dev/null +++ b/public/app/plugins/datasource/loki/README.md @@ -0,0 +1,3 @@ +# Loki Datasource - Native Plugin + +This is a **built in** datasource that allows you to connect to the Loki logging service. diff --git a/public/app/plugins/datasource/logging/components/LoggingCheatSheet.tsx b/public/app/plugins/datasource/loki/components/LokiCheatSheet.tsx similarity index 96% rename from public/app/plugins/datasource/logging/components/LoggingCheatSheet.tsx rename to public/app/plugins/datasource/loki/components/LokiCheatSheet.tsx index 4dbf8d1ab89..01c6519916d 100644 --- a/public/app/plugins/datasource/logging/components/LoggingCheatSheet.tsx +++ b/public/app/plugins/datasource/loki/components/LokiCheatSheet.tsx @@ -15,7 +15,7 @@ const CHEAT_SHEET_ITEMS = [ export default (props: any) => (
-

Logging Cheat Sheet

+

Loki Cheat Sheet

{CHEAT_SHEET_ITEMS.map(item => (
{item.title}
diff --git a/public/app/plugins/datasource/logging/components/LoggingQueryField.tsx b/public/app/plugins/datasource/loki/components/LokiQueryField.tsx similarity index 94% rename from public/app/plugins/datasource/logging/components/LoggingQueryField.tsx rename to public/app/plugins/datasource/loki/components/LokiQueryField.tsx index 5667bd9a20d..005706bb8d1 100644 --- a/public/app/plugins/datasource/logging/components/LoggingQueryField.tsx +++ b/public/app/plugins/datasource/loki/components/LokiQueryField.tsx @@ -49,7 +49,7 @@ interface CascaderOption { disabled?: boolean; } -interface LoggingQueryFieldProps { +interface LokiQueryFieldProps { datasource: any; error?: string | JSX.Element; hint?: any; @@ -60,16 +60,16 @@ interface LoggingQueryFieldProps { onQueryChange?: (value: DataQuery, override?: boolean) => void; } -interface LoggingQueryFieldState { +interface LokiQueryFieldState { logLabelOptions: any[]; syntaxLoaded: boolean; } -class LoggingQueryField extends React.PureComponent { +class LokiQueryField extends React.PureComponent { plugins: any[]; languageProvider: any; - constructor(props: LoggingQueryFieldProps, context) { + constructor(props: LokiQueryFieldProps, context) { super(props, context); if (props.datasource.languageProvider) { @@ -208,8 +208,8 @@ class LoggingQueryField extends React.PureComponent {error ?
{error}
: null} @@ -229,4 +229,4 @@ class LoggingQueryField extends React.PureComponent void; } -export default class LoggingStartPage extends PureComponent { +export default class LokiStartPage extends PureComponent { render() { return (
- +
); } diff --git a/public/app/plugins/datasource/logging/datasource.ts b/public/app/plugins/datasource/loki/datasource.ts similarity index 97% rename from public/app/plugins/datasource/logging/datasource.ts rename to public/app/plugins/datasource/loki/datasource.ts index fca49a2f253..9c5ab450542 100644 --- a/public/app/plugins/datasource/logging/datasource.ts +++ b/public/app/plugins/datasource/loki/datasource.ts @@ -27,7 +27,7 @@ function serializeParams(data: any) { .join('&'); } -export default class LoggingDatasource { +export default class LokiDatasource { languageProvider: LanguageProvider; /** @ngInject */ @@ -94,7 +94,7 @@ export default class LoggingDatasource { } metadataRequest(url) { - // HACK to get label values for {job=|}, will be replaced when implementing LoggingQueryField + // HACK to get label values for {job=|}, will be replaced when implementing LokiQueryField const apiUrl = url.replace('v1', 'prom'); return this._request(apiUrl, { silent: true }).then(res => { const data = { data: { data: res.data.values || [] } }; @@ -136,7 +136,7 @@ export default class LoggingDatasource { } return { status: 'error', - message: 'Data source connected, but no labels received. Verify that logging is configured properly.', + message: 'Data source connected, but no labels received. Verify that Loki is configured properly.', }; }) .catch(err => { diff --git a/public/app/plugins/datasource/logging/img/grafana_icon.svg b/public/app/plugins/datasource/loki/img/grafana_icon.svg similarity index 100% rename from public/app/plugins/datasource/logging/img/grafana_icon.svg rename to public/app/plugins/datasource/loki/img/grafana_icon.svg diff --git a/public/app/plugins/datasource/loki/img/loki_icon.svg b/public/app/plugins/datasource/loki/img/loki_icon.svg new file mode 100644 index 00000000000..50199611c06 --- /dev/null +++ b/public/app/plugins/datasource/loki/img/loki_icon.svg @@ -0,0 +1,216 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/app/plugins/datasource/logging/language_provider.test.ts b/public/app/plugins/datasource/loki/language_provider.test.ts similarity index 100% rename from public/app/plugins/datasource/logging/language_provider.test.ts rename to public/app/plugins/datasource/loki/language_provider.test.ts diff --git a/public/app/plugins/datasource/logging/language_provider.ts b/public/app/plugins/datasource/loki/language_provider.ts similarity index 99% rename from public/app/plugins/datasource/logging/language_provider.ts rename to public/app/plugins/datasource/loki/language_provider.ts index a992084159a..dd3b4885ea5 100644 --- a/public/app/plugins/datasource/logging/language_provider.ts +++ b/public/app/plugins/datasource/loki/language_provider.ts @@ -36,7 +36,7 @@ export function addHistoryMetadata(item: CompletionItem, history: HistoryItem[]) }; } -export default class LoggingLanguageProvider extends LanguageProvider { +export default class LokiLanguageProvider extends LanguageProvider { labelKeys?: { [index: string]: string[] }; // metric -> [labelKey,...] labelValues?: { [index: string]: { [index: string]: string[] } }; // metric -> labelKey -> [labelValue,...] logLabelOptions: any[]; diff --git a/public/app/plugins/datasource/loki/module.ts b/public/app/plugins/datasource/loki/module.ts new file mode 100644 index 00000000000..41847855c2f --- /dev/null +++ b/public/app/plugins/datasource/loki/module.ts @@ -0,0 +1,15 @@ +import Datasource from './datasource'; + +import LokiStartPage from './components/LokiStartPage'; +import LokiQueryField from './components/LokiQueryField'; + +export class LokiConfigCtrl { + static templateUrl = 'partials/config.html'; +} + +export { + Datasource, + LokiConfigCtrl as ConfigCtrl, + LokiQueryField as ExploreQueryField, + LokiStartPage as ExploreStartPage, +}; diff --git a/public/app/plugins/datasource/logging/partials/config.html b/public/app/plugins/datasource/loki/partials/config.html similarity index 100% rename from public/app/plugins/datasource/logging/partials/config.html rename to public/app/plugins/datasource/loki/partials/config.html diff --git a/public/app/plugins/datasource/logging/plugin.json b/public/app/plugins/datasource/loki/plugin.json similarity index 54% rename from public/app/plugins/datasource/logging/plugin.json rename to public/app/plugins/datasource/loki/plugin.json index 9aa844f21cb..86b26a6a589 100644 --- a/public/app/plugins/datasource/logging/plugin.json +++ b/public/app/plugins/datasource/loki/plugin.json @@ -1,28 +1,28 @@ { "type": "datasource", - "name": "Grafana Logging", - "id": "logging", + "name": "Loki", + "id": "loki", "metrics": false, "alerting": false, "annotations": false, "logs": true, "explore": true, "info": { - "description": "Grafana Logging Data Source for Grafana", + "description": "Loki Logging Data Source for Grafana", "author": { "name": "Grafana Project", "url": "https://grafana.com" }, "logos": { - "small": "img/grafana_icon.svg", - "large": "img/grafana_icon.svg" + "small": "img/loki_icon.svg", + "large": "img/loki_icon.svg" }, "links": [ { - "name": "Grafana Logging", - "url": "https://grafana.com/" + "name": "Loki", + "url": "https://github.com/grafana/loki" } ], "version": "5.3.0" } -} \ No newline at end of file +} diff --git a/public/app/plugins/datasource/logging/query_utils.test.ts b/public/app/plugins/datasource/loki/query_utils.test.ts similarity index 100% rename from public/app/plugins/datasource/logging/query_utils.test.ts rename to public/app/plugins/datasource/loki/query_utils.test.ts diff --git a/public/app/plugins/datasource/logging/query_utils.ts b/public/app/plugins/datasource/loki/query_utils.ts similarity index 100% rename from public/app/plugins/datasource/logging/query_utils.ts rename to public/app/plugins/datasource/loki/query_utils.ts diff --git a/public/app/plugins/datasource/logging/result_transformer.test.ts b/public/app/plugins/datasource/loki/result_transformer.test.ts similarity index 100% rename from public/app/plugins/datasource/logging/result_transformer.test.ts rename to public/app/plugins/datasource/loki/result_transformer.test.ts diff --git a/public/app/plugins/datasource/logging/result_transformer.ts b/public/app/plugins/datasource/loki/result_transformer.ts similarity index 100% rename from public/app/plugins/datasource/logging/result_transformer.ts rename to public/app/plugins/datasource/loki/result_transformer.ts diff --git a/public/app/plugins/datasource/logging/syntax.ts b/public/app/plugins/datasource/loki/syntax.ts similarity index 100% rename from public/app/plugins/datasource/logging/syntax.ts rename to public/app/plugins/datasource/loki/syntax.ts From 40898e1cd61fe1382334ed31be10e543caa96ee0 Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Thu, 6 Dec 2018 00:19:55 +0100 Subject: [PATCH 10/33] loki: adds proper error handling for config page This handles three different error cases for the config page so that the user gets an error and not just a blank red error box. --- .../datasource/loki/datasource.test.ts | 98 +++++++++++++++++++ .../app/plugins/datasource/loki/datasource.ts | 18 +++- 2 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 public/app/plugins/datasource/loki/datasource.test.ts diff --git a/public/app/plugins/datasource/loki/datasource.test.ts b/public/app/plugins/datasource/loki/datasource.test.ts new file mode 100644 index 00000000000..ddb4d6ed549 --- /dev/null +++ b/public/app/plugins/datasource/loki/datasource.test.ts @@ -0,0 +1,98 @@ +import LokiDatasource from './datasource'; + +describe('LokiDatasource', () => { + const instanceSettings = { + url: 'myloggingurl', + }; + + describe('when performing testDataSource', () => { + let ds; + let result; + + describe('and call succeeds', () => { + beforeEach(async () => { + const backendSrv = { + async datasourceRequest() { + return Promise.resolve({ + status: 200, + data: { + values: ['avalue'], + }, + }); + }, + }; + ds = new LokiDatasource(instanceSettings, backendSrv, {}); + result = await ds.testDatasource(); + }); + + it('should return successfully', () => { + expect(result.status).toBe('success'); + }); + }); + + describe('and call fails with 401 error', () => { + beforeEach(async () => { + const backendSrv = { + async datasourceRequest() { + return Promise.reject({ + statusText: 'Unauthorized', + status: 401, + data: { + message: 'Unauthorized', + }, + }); + }, + }; + ds = new LokiDatasource(instanceSettings, backendSrv, {}); + result = await ds.testDatasource(); + }); + + it('should return error status and a detailed error message', () => { + expect(result.status).toEqual('error'); + expect(result.message).toBe('Loki: Unauthorized. 401. Unauthorized'); + }); + }); + + describe('and call fails with 404 error', () => { + beforeEach(async () => { + const backendSrv = { + async datasourceRequest() { + return Promise.reject({ + statusText: 'Not found', + status: 404, + data: '404 page not found', + }); + }, + }; + ds = new LokiDatasource(instanceSettings, backendSrv, {}); + result = await ds.testDatasource(); + }); + + it('should return error status and a detailed error message', () => { + expect(result.status).toEqual('error'); + expect(result.message).toBe('Loki: Not found. 404. 404 page not found'); + }); + }); + + describe('and call fails with 502 error', () => { + beforeEach(async () => { + const backendSrv = { + async datasourceRequest() { + return Promise.reject({ + statusText: 'Bad Gateway', + status: 502, + data: '', + }); + }, + }; + ds = new LokiDatasource(instanceSettings, backendSrv, {}); + result = await ds.testDatasource(); + }); + + it('should return error status and a detailed error message', () => { + expect(result.status).toEqual('error'); + expect(result.message).toBe('Loki: Bad Gateway. 502'); + }); + }); + }); +}); diff --git a/public/app/plugins/datasource/loki/datasource.ts b/public/app/plugins/datasource/loki/datasource.ts index 9c5ab450542..ebe1e226a75 100644 --- a/public/app/plugins/datasource/loki/datasource.ts +++ b/public/app/plugins/datasource/loki/datasource.ts @@ -140,7 +140,23 @@ export default class LokiDatasource { }; }) .catch(err => { - return { status: 'error', message: err.message }; + let message = 'Loki: '; + if (err.statusText) { + message += err.statusText; + } else { + message += 'Cannot connect to Loki'; + } + + if (err.status) { + message += `. ${err.status}`; + } + + if (err.data && err.data.message) { + message += `. ${err.data.message}`; + } else if (err.data) { + message += `. ${err.data}`; + } + return { status: 'error', message: message }; }); } } From c7d701c8fa7f443fb1f6eca2d509382c8c4434c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 5 Dec 2018 15:53:20 +0100 Subject: [PATCH 11/33] explore logs styling poc, WIP --- public/app/features/explore/Logs.tsx | 48 +++++++++++++------------- public/sass/_grafana.scss | 1 + public/sass/components/_logs-rows.scss | 23 ++++++++++++ public/sass/pages/_explore.scss | 11 ++---- 4 files changed, 51 insertions(+), 32 deletions(-) create mode 100644 public/sass/components/_logs-rows.scss diff --git a/public/app/features/explore/Logs.tsx b/public/app/features/explore/Logs.tsx index 3a584cd5664..017483cccd4 100644 --- a/public/app/features/explore/Logs.tsx +++ b/public/app/features/explore/Logs.tsx @@ -64,18 +64,18 @@ function Row({ 'logs-row-match-highlight--preview': previewHighlights, }); return ( - <> +
{showDuplicates && (
{row.duplicates > 0 ? `${row.duplicates + 1}x` : null}
)}
{showUtc && ( -
+
{row.timestamp}
)} {showLocalTime && ( -
+
{row.timeLocal}
)} @@ -96,7 +96,7 @@ function Row({ row.entry )}
- +
); } @@ -263,25 +263,25 @@ export default class Logs extends PureComponent { } // Grid options - const cssColumnSizes = []; - if (showDuplicates) { - cssColumnSizes.push('max-content'); - } - // Log-level indicator line - cssColumnSizes.push('3px'); - if (showUtc) { - cssColumnSizes.push('minmax(100px, max-content)'); - } - if (showLocalTime) { - cssColumnSizes.push('minmax(100px, max-content)'); - } - if (showLabels) { - cssColumnSizes.push('fit-content(20%)'); - } - cssColumnSizes.push('1fr'); - const logEntriesStyle = { - gridTemplateColumns: cssColumnSizes.join(' '), - }; + // const cssColumnSizes = []; + // if (showDuplicates) { + // cssColumnSizes.push('max-content'); + // } + // // Log-level indicator line + // cssColumnSizes.push('3px'); + // if (showUtc) { + // cssColumnSizes.push('minmax(220px, max-content)'); + // } + // if (showLocalTime) { + // cssColumnSizes.push('minmax(140px, max-content)'); + // } + // if (showLabels) { + // cssColumnSizes.push('fit-content(20%)'); + // } + // cssColumnSizes.push('1fr'); + // const logEntriesStyle = { + // gridTemplateColumns: cssColumnSizes.join(' '), + // }; const scanText = scanRange ? `Scanning ${rangeUtil.describeTimeRange(scanRange)}` : 'Scanning...'; @@ -342,7 +342,7 @@ export default class Logs extends PureComponent {
-
+
{hasData && !deferLogs && // Only inject highlighterExpression in the first set for performance reasons diff --git a/public/sass/_grafana.scss b/public/sass/_grafana.scss index e4c7a9c59e1..f8de32098d1 100644 --- a/public/sass/_grafana.scss +++ b/public/sass/_grafana.scss @@ -101,6 +101,7 @@ @import 'components/delete_button'; @import 'components/add_data_source.scss'; @import 'components/page_loader'; +@import 'components/logs-rows'; // PAGES @import 'pages/login'; diff --git a/public/sass/components/_logs-rows.scss b/public/sass/components/_logs-rows.scss new file mode 100644 index 00000000000..0812be12cf9 --- /dev/null +++ b/public/sass/components/_logs-rows.scss @@ -0,0 +1,23 @@ +.logs-rows { + // display: grid; + // grid-column-gap: 1rem; + // grid-row-gap: 0.1rem; + // font-family: $font-family-monospace; + // font-size: 12px; + display: table; +} + +.logs-row { + display: table-row; + + > div { + display: table-cell; + } +} + +.logs-row__time { + white-space: nowrap; +} + +.logs-row__message { +} diff --git a/public/sass/pages/_explore.scss b/public/sass/pages/_explore.scss index 5e69b9a1f1a..fc5398553d7 100644 --- a/public/sass/pages/_explore.scss +++ b/public/sass/pages/_explore.scss @@ -289,14 +289,6 @@ top: 4px; } - .logs-entries { - display: grid; - grid-column-gap: 1rem; - grid-row-gap: 0.1rem; - font-family: $font-family-monospace; - font-size: 12px; - } - .logs-row-match-highlight { // Undoing mark styling background: inherit; @@ -319,6 +311,9 @@ opacity: 0.8; } + .logs-row__time { + } + .logs-row-level-critical, .logs-row-level-crit { background-color: #705da0; From afd7580598b40c7709f5e2d299d1e0d4abb203a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 6 Dec 2018 08:08:16 +0100 Subject: [PATCH 12/33] explore logs css refactoring, step1 --- public/app/features/explore/Logs.tsx | 37 ++-- public/sass/_grafana.scss | 2 +- public/sass/components/_logs-rows.scss | 23 --- public/sass/components/_panel_logs.scss | 223 ++++++++++++++++++++++++ public/sass/pages/_explore.scss | 201 --------------------- 5 files changed, 241 insertions(+), 245 deletions(-) delete mode 100644 public/sass/components/_logs-rows.scss create mode 100644 public/sass/components/_panel_logs.scss diff --git a/public/app/features/explore/Logs.tsx b/public/app/features/explore/Logs.tsx index 017483cccd4..76c1bd7fd33 100644 --- a/public/app/features/explore/Logs.tsx +++ b/public/app/features/explore/Logs.tsx @@ -60,15 +60,15 @@ function Row({ const previewHighlights = highlighterExpressions && !_.isEqual(highlighterExpressions, row.searchWords); const highlights = previewHighlights ? highlighterExpressions : row.searchWords; const needsHighlighter = highlights && highlights.length > 0; - const highlightClassName = classnames('logs-row-match-highlight', { - 'logs-row-match-highlight--preview': previewHighlights, + const highlightClassName = classnames('logs-row__match-highlight', { + 'logs-row__match-highlight--preview': previewHighlights, }); return (
{showDuplicates && ( -
{row.duplicates > 0 ? `${row.duplicates + 1}x` : null}
+
{row.duplicates > 0 ? `${row.duplicates + 1}x` : null}
)} -
+
{showUtc && (
{row.timestamp} @@ -80,11 +80,11 @@ function Row({
)} {showLabels && ( -
+
)} -
+
{needsHighlighter ? ( + ); @@ -112,7 +112,6 @@ function renderMetaItem(value: any, kind: LogsMetaKind) { } interface LogsProps { - className?: string; data: LogsModel; highlighterExpressions: string[]; loading: boolean; @@ -220,7 +219,6 @@ export default class Logs extends PureComponent { render() { const { - className = '', data, highlighterExpressions, loading = false, @@ -286,8 +284,8 @@ export default class Logs extends PureComponent { const scanText = scanRange ? `Scanning ${rangeUtil.describeTimeRange(scanRange)}` : 'Scanning...'; return ( -
-
+
+
{ userOptions={graphOptions} />
- -
-
+
+
@@ -330,11 +327,11 @@ export default class Logs extends PureComponent { /> {hasData && meta && ( -
+
{meta.map(item => ( -
- {item.label}: - {renderMetaItem(item.value, item.kind)} +
+ {item.label}: + {renderMetaItem(item.value, item.kind)}
))}
@@ -379,7 +376,7 @@ export default class Logs extends PureComponent { {!loading && !hasData && !scanning && ( -
+
No logs found. Scan for older logs @@ -388,7 +385,7 @@ export default class Logs extends PureComponent { )} {scanning && ( -
+
{scanText} Stop scan diff --git a/public/sass/_grafana.scss b/public/sass/_grafana.scss index f8de32098d1..3c3acf7b490 100644 --- a/public/sass/_grafana.scss +++ b/public/sass/_grafana.scss @@ -59,6 +59,7 @@ @import 'components/panel_text'; @import 'components/panel_heatmap'; @import 'components/panel_add_panel'; +@import 'components/panel_logs'; @import 'components/settings_permissions'; @import 'components/tagsinput'; @import 'components/tables_lists'; @@ -101,7 +102,6 @@ @import 'components/delete_button'; @import 'components/add_data_source.scss'; @import 'components/page_loader'; -@import 'components/logs-rows'; // PAGES @import 'pages/login'; diff --git a/public/sass/components/_logs-rows.scss b/public/sass/components/_logs-rows.scss deleted file mode 100644 index 0812be12cf9..00000000000 --- a/public/sass/components/_logs-rows.scss +++ /dev/null @@ -1,23 +0,0 @@ -.logs-rows { - // display: grid; - // grid-column-gap: 1rem; - // grid-row-gap: 0.1rem; - // font-family: $font-family-monospace; - // font-size: 12px; - display: table; -} - -.logs-row { - display: table-row; - - > div { - display: table-cell; - } -} - -.logs-row__time { - white-space: nowrap; -} - -.logs-row__message { -} diff --git a/public/sass/components/_panel_logs.scss b/public/sass/components/_panel_logs.scss new file mode 100644 index 00000000000..d3381a70d13 --- /dev/null +++ b/public/sass/components/_panel_logs.scss @@ -0,0 +1,223 @@ +.logs-panel-controls { + display: flex; + background-color: $page-bg; + padding: $panel-padding; + padding-top: 10px; + border-radius: $border-radius; + margin: 2*$panel-margin 0; + border: $panel-border; + justify-items: flex-start; + align-items: flex-start; + + > * { + margin-right: 1em; + } +} + +.logs-panel-nodata { + > * { + margin-left: 0.5em; + } +} + +.logs-panel-meta { + flex: 1; + color: $text-color-weak; + // Align first line with controls labels + margin-top: -2px; +} + +.logs-panel-meta__item { + margin-right: 1em; +} + +.logs-panel-meta__label { + margin-right: 0.5em; + font-size: 0.9em; + font-weight: 500; +} + +.logs-panel-meta__value { + font-family: $font-family-monospace; +} + +.logs-panel-meta-item__labels { + // compensate for the labels padding + position: relative; + top: 4px; +} + +.logs-rows { + // display: grid; + // grid-column-gap: 1rem; + // grid-row-gap: 0.1rem; + // font-family: $font-family-monospace; + // font-size: 12px; + display: table; +} + +.logs-row { + display: table-row; + + > div { + display: table-cell; + padding-left: 10px; + } +} + +.logs-row__time { + white-space: nowrap; +} + +.logs-row__message { + word-break: break-all; +} + +.logs-row__match-highlight { + // Undoing mark styling + background: inherit; + padding: inherit; + + color: $typeahead-selected-color; + border-bottom: 1px solid $typeahead-selected-color; + background-color: rgba($typeahead-selected-color, 0.1); + + &--preview { + background-color: rgba($typeahead-selected-color, 0.2); + border-bottom-style: dotted; + } +} + +.logs-row__level { + background-color: transparent; + margin: 2px 0; + position: relative; + opacity: 0.8; + padding: 0 3px 0 0 !important; + + &--critical, + &--crit { + background-color: #705da0; + } + + &--error, + &--err { + background-color: #e24d42; + } + + &--warning, + &--warn { + background-color: #eab839; + } + + &--info { + background-color: #7eb26d; + } + + &--debug { + background-color: #1f78c1; + } + + &--trace { + background-color: #6ed0e0; + } +} + +.logs-row__time { +} + +.logs-row__duplicates { + text-align: right; +} + +.logs-label { + display: inline-block; + padding: 0 2px; + background-color: $btn-inverse-bg; + border-radius: $border-radius; + margin: 0 4px 2px 0; + text-overflow: ellipsis; + white-space: nowrap; + position: relative; +} + +.logs-label__icon { + border-left: $panel-border; + padding: 0 2px; + cursor: pointer; + margin-left: 2px; +} + +.logs-label__stats { + position: absolute; + top: 1.25em; + left: -10px; + z-index: 100; + background-color: $popover-bg; + color: $popover-color; + border: 1px solid $popover-border-color; + padding: 10px; + border-radius: $border-radius; + justify-content: space-between; + box-shadow: $popover-shadow; +} + +.logs-row__labels { + line-height: 1.2; +} + +.logs-stats__info { + margin-bottom: $spacer / 2; +} + +.logs-stats__icon { + margin-left: 0.5em; + cursor: pointer; +} + +.logs-stats-row { + margin: $spacer/1.75 0; + + &--active { + color: $blue; + position: relative; + } + + &--active:after { + display: inline; + content: '*'; + position: absolute; + top: 0; + left: -8px; + } + + &__label { + display: flex; + margin-bottom: 1px; + } + + &__value { + flex: 1; + } + + &__count, + &__percent { + text-align: right; + margin-left: 0.5em; + } + + &__percent { + width: 3em; + } + + &__bar, + &__innerbar { + height: 4px; + overflow: hidden; + background: $text-color-faint; + } + + &__innerbar { + background-color: $blue; + } +} diff --git a/public/sass/pages/_explore.scss b/public/sass/pages/_explore.scss index fc5398553d7..37ed0bcbc92 100644 --- a/public/sass/pages/_explore.scss +++ b/public/sass/pages/_explore.scss @@ -238,207 +238,6 @@ padding-right: 0.25em; } -.explore { - .logs { - .logs-controls { - display: flex; - background-color: $page-bg; - padding: $panel-padding; - padding-top: 10px; - border-radius: $border-radius; - margin: 2*$panel-margin 0; - border: $panel-border; - justify-items: flex-start; - align-items: flex-start; - - > * { - margin-right: 1em; - } - } - - .logs-nodata { - > * { - margin-left: 0.5em; - } - } - - .logs-meta { - flex: 1; - color: $text-color-weak; - // Align first line with controls labels - margin-top: -2px; - } - - .logs-meta-item { - margin-right: 1em; - } - - .logs-meta-item__label { - margin-right: 0.5em; - font-size: 0.9em; - font-weight: 500; - } - - .logs-meta-item__value { - font-family: $font-family-monospace; - } - - .logs-meta-item__value-labels { - // compensate for the labels padding - position: relative; - top: 4px; - } - - .logs-row-match-highlight { - // Undoing mark styling - background: inherit; - padding: inherit; - - color: $typeahead-selected-color; - border-bottom: 1px solid $typeahead-selected-color; - background-color: rgba($typeahead-selected-color, 0.1); - } - - .logs-row-match-highlight--preview { - background-color: rgba($typeahead-selected-color, 0.2); - border-bottom-style: dotted; - } - - .logs-row-level { - background-color: transparent; - margin: 2px 0; - position: relative; - opacity: 0.8; - } - - .logs-row__time { - } - - .logs-row-level-critical, - .logs-row-level-crit { - background-color: #705da0; - } - - .logs-row-level-error, - .logs-row-level-err { - background-color: #e24d42; - } - - .logs-row-level-warning, - .logs-row-level-warn { - background-color: #eab839; - } - - .logs-row-level-info { - background-color: #7eb26d; - } - - .logs-row-level-debug { - background-color: #1f78c1; - } - - .logs-row-level-trace { - background-color: #6ed0e0; - } - - .logs-row-duplicates { - text-align: right; - } - - .logs-label { - display: inline-block; - padding: 0 2px; - background-color: $btn-inverse-bg; - border-radius: $border-radius; - margin: 0 4px 2px 0; - text-overflow: ellipsis; - white-space: nowrap; - position: relative; - } - - .logs-label__icon { - border-left: $panel-border; - padding: 0 2px; - cursor: pointer; - margin-left: 2px; - } - - .logs-label__stats { - position: absolute; - top: 1.25em; - left: -10px; - z-index: 100; - background-color: $popover-bg; - color: $popover-color; - border: 1px solid $popover-border-color; - padding: 10px; - border-radius: $border-radius; - justify-content: space-between; - box-shadow: $popover-shadow; - } - - .logs-row-labels { - line-height: 1.2; - } - - .logs-stats__info { - margin-bottom: $spacer / 2; - } - - .logs-stats__icon { - margin-left: 0.5em; - cursor: pointer; - } - - .logs-stats-row { - margin: $spacer/1.75 0; - - &--active { - color: $blue; - position: relative; - } - - &--active:after { - display: inline; - content: '*'; - position: absolute; - top: 0; - left: -8px; - } - - &__label { - display: flex; - margin-bottom: 1px; - } - - &__value { - flex: 1; - } - - &__count, - &__percent { - text-align: right; - margin-left: 0.5em; - } - - &__percent { - width: 3em; - } - - &__bar, - &__innerbar { - height: 4px; - overflow: hidden; - background: $text-color-faint; - } - - &__innerbar { - background-color: $blue; - } - } - } -} - // Prometheus-specifics, to be extracted to datasource soon .explore { From e884cf625022fef08c50b39b3b5aa11d767db9c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 6 Dec 2018 09:56:59 +0100 Subject: [PATCH 13/33] minor tweaks, now table renders faster and changes less on second stage rendering --- public/sass/components/_panel_logs.scss | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/public/sass/components/_panel_logs.scss b/public/sass/components/_panel_logs.scss index d3381a70d13..652c23903f8 100644 --- a/public/sass/components/_panel_logs.scss +++ b/public/sass/components/_panel_logs.scss @@ -54,6 +54,7 @@ // font-family: $font-family-monospace; // font-size: 12px; display: table; + table-layout: fixed; } .logs-row { @@ -69,8 +70,13 @@ white-space: nowrap; } +.logs-row__labels { + max-width: 20%; +} + .logs-row__message { word-break: break-all; + width: 80%; } .logs-row__match-highlight { @@ -123,9 +129,6 @@ } } -.logs-row__time { -} - .logs-row__duplicates { text-align: right; } @@ -183,7 +186,7 @@ position: relative; } - &--active:after { + &--active::after { display: inline; content: '*'; position: absolute; From 2e6690428f821ab0af72dedce97b5c079b422298 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 6 Dec 2018 12:12:43 +0100 Subject: [PATCH 14/33] fixed logs to time series calculation issue, increased bucket size, fixes #14248 --- public/app/core/logs_model.ts | 15 ++++++++++++--- public/app/core/utils/explore.ts | 22 ++++++++++------------ public/app/features/explore/Logs.tsx | 1 + public/app/types/index.ts | 2 ++ public/app/types/series.ts | 18 ++++++++++++++++++ 5 files changed, 43 insertions(+), 15 deletions(-) diff --git a/public/app/core/logs_model.ts b/public/app/core/logs_model.ts index 1efe26d28ef..1527f18e4b6 100644 --- a/public/app/core/logs_model.ts +++ b/public/app/core/logs_model.ts @@ -170,16 +170,25 @@ export function filterLogLevels(logs: LogsModel, hiddenLogLevels: Set) } export function makeSeriesForLogs(rows: LogRow[], intervalMs: number): TimeSeries[] { + // currently interval is rangeMs / resolution, which is too low for showing series as bars. + // need at least 10px per bucket, so we multiply interval by 10. Should be solved higher up the chain + // when executing queries & interval calculated and not here but this is a temporary fix. + // intervalMs = intervalMs * 10; + // Graph time series by log level const seriesByLevel = {}; - rows.forEach(row => { + const bucketSize = intervalMs * 10; + + for (const row of rows) { if (!seriesByLevel[row.logLevel]) { seriesByLevel[row.logLevel] = { lastTs: null, datapoints: [], alias: row.logLevel }; } + const levelSeries = seriesByLevel[row.logLevel]; // Bucket to nearest minute - const time = Math.round(row.timeEpochMs / intervalMs / 10) * intervalMs * 10; + const time = Math.round(row.timeEpochMs / bucketSize) * bucketSize; + // Entry for time if (time === levelSeries.lastTs) { levelSeries.datapoints[levelSeries.datapoints.length - 1][0]++; @@ -187,7 +196,7 @@ export function makeSeriesForLogs(rows: LogRow[], intervalMs: number): TimeSerie levelSeries.datapoints.push([1, time]); levelSeries.lastTs = time; } - }); + } return Object.keys(seriesByLevel).reduce((acc, level) => { if (seriesByLevel[level]) { diff --git a/public/app/core/utils/explore.ts b/public/app/core/utils/explore.ts index e268508b833..26b6a527d95 100644 --- a/public/app/core/utils/explore.ts +++ b/public/app/core/utils/explore.ts @@ -1,15 +1,15 @@ import _ from 'lodash'; import { renderUrl } from 'app/core/utils/url'; -import { ExploreState, ExploreUrlState, HistoryItem, QueryTransaction } from 'app/types/explore'; -import { DataQuery, RawTimeRange } from 'app/types/series'; - -import TableModel, { mergeTablesIntoModel } from 'app/core/table_model'; import kbn from 'app/core/utils/kbn'; -import colors from 'app/core/utils/colors'; -import TimeSeries from 'app/core/time_series2'; -import { parse as parseDate } from 'app/core/utils/datemath'; import store from 'app/core/store'; +import colors from 'app/core/utils/colors'; +import { parse as parseDate } from 'app/core/utils/datemath'; + +import TimeSeries from 'app/core/time_series2'; +import TableModel, { mergeTablesIntoModel } from 'app/core/table_model'; +import { ExploreState, ExploreUrlState, HistoryItem, QueryTransaction } from 'app/types/explore'; +import { DataQuery, RawTimeRange, IntervalValues, DataSourceApi } from 'app/types/series'; export const DEFAULT_RANGE = { from: 'now-6h', @@ -170,18 +170,16 @@ export function calculateResultsFromQueryTransactions( }; } -export function getIntervals( - range: RawTimeRange, - datasource, - resolution: number -): { interval: string; intervalMs: number } { +export function getIntervals(range: RawTimeRange, datasource: DataSourceApi, resolution: number): IntervalValues { if (!datasource || !resolution) { return { interval: '1s', intervalMs: 1000 }; } + const absoluteRange: RawTimeRange = { from: parseDate(range.from, false), to: parseDate(range.to, true), }; + return kbn.calculateInterval(absoluteRange, resolution, datasource.interval); } diff --git a/public/app/features/explore/Logs.tsx b/public/app/features/explore/Logs.tsx index 3a584cd5664..43233dce6f5 100644 --- a/public/app/features/explore/Logs.tsx +++ b/public/app/features/explore/Logs.tsx @@ -24,6 +24,7 @@ const PREVIEW_LIMIT = 100; const graphOptions = { series: { + stack: true, bars: { show: true, lineWidth: 5, diff --git a/public/app/types/index.ts b/public/app/types/index.ts index 3bf6b4c9803..30af6549097 100644 --- a/public/app/types/index.ts +++ b/public/app/types/index.ts @@ -19,6 +19,7 @@ import { DataQuery, DataQueryResponse, DataQueryOptions, + IntervalValues, } from './series'; import { PanelProps, PanelOptionsProps } from './panel'; import { PluginDashboard, PluginMeta, Plugin, PluginsState } from './plugins'; @@ -87,6 +88,7 @@ export { AppNotificationTimeout, DashboardSearchHit, UserState, + IntervalValues, }; export interface StoreState { diff --git a/public/app/types/series.ts b/public/app/types/series.ts index 18ebbc5f648..7d7d00b1a60 100644 --- a/public/app/types/series.ts +++ b/public/app/types/series.ts @@ -19,6 +19,11 @@ export interface TimeRange { raw: RawTimeRange; } +export interface IntervalValues { + interval: string; // 10s,5m + intervalMs: number; +} + export type TimeSeriesValue = string | number | null; export type TimeSeriesPoints = TimeSeriesValue[][]; @@ -89,6 +94,11 @@ export interface DataQueryOptions { } export interface DataSourceApi { + /** + * min interval range + */ + interval?: string; + /** * Imports queries from a different datasource */ @@ -97,6 +107,14 @@ export interface DataSourceApi { * Initializes a datasource after instantiation */ init?: () => void; + + /** + * Main data query method + */ query(options: DataQueryOptions): Promise; + + /** + * test data source + */ testDatasource?: () => Promise; } From 665ac229b65db4439d5561f9a55b0d7b4d1d953a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 6 Dec 2018 12:49:28 +0100 Subject: [PATCH 15/33] restoring monospace & making sure width are correct when hiding columns --- public/sass/components/_panel_logs.scss | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/public/sass/components/_panel_logs.scss b/public/sass/components/_panel_logs.scss index 652c23903f8..2d708f99d8e 100644 --- a/public/sass/components/_panel_logs.scss +++ b/public/sass/components/_panel_logs.scss @@ -48,11 +48,8 @@ } .logs-rows { - // display: grid; - // grid-column-gap: 1rem; - // grid-row-gap: 0.1rem; - // font-family: $font-family-monospace; - // font-size: 12px; + font-family: $font-family-monospace; + font-size: 12px; display: table; table-layout: fixed; } @@ -63,6 +60,7 @@ > div { display: table-cell; padding-left: 10px; + border: 1px solid transparent; } } @@ -76,7 +74,7 @@ .logs-row__message { word-break: break-all; - width: 80%; + min-width: 80%; } .logs-row__match-highlight { @@ -96,10 +94,9 @@ .logs-row__level { background-color: transparent; - margin: 2px 0; position: relative; - opacity: 0.8; - padding: 0 3px 0 0 !important; + width: 3px; + padding: 0 !important; &--critical, &--crit { From 86bb36fe9718731f3ff9e84d30bb7a1885d178eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 6 Dec 2018 13:30:48 +0100 Subject: [PATCH 16/33] wip: explore logs styling --- public/sass/components/_panel_logs.scss | 56 ++++++++++++++++++++----- 1 file changed, 45 insertions(+), 11 deletions(-) diff --git a/public/sass/components/_panel_logs.scss b/public/sass/components/_panel_logs.scss index 2d708f99d8e..4e748029e85 100644 --- a/public/sass/components/_panel_logs.scss +++ b/public/sass/components/_panel_logs.scss @@ -1,3 +1,5 @@ +$column-horizontal-spacing: 10px; + .logs-panel-controls { display: flex; background-color: $page-bg; @@ -52,6 +54,7 @@ font-size: 12px; display: table; table-layout: fixed; + margin: 0 -(nth($panel-padding, 2)); } .logs-row { @@ -59,8 +62,20 @@ > div { display: table-cell; - padding-left: 10px; - border: 1px solid transparent; + padding-right: $column-horizontal-spacing; + vertical-align: middle; + + &:first-child { + padding-left: $column-horizontal-spacing; + } + + &:last-child { + padding-left: $column-horizontal-spacing; + } + } + + &:nth-child(odd) { + background: $page-bg; } } @@ -93,36 +108,55 @@ } .logs-row__level { - background-color: transparent; position: relative; - width: 3px; - padding: 0 !important; + width: 12px; + + &::after { + content: ''; + display: block; + width: 12px; + height: 12px; + background-color: $gray-2; + border-radius: 50%; + } &--critical, &--crit { - background-color: #705da0; + &::after { + background-color: #705da0; + } } &--error, &--err { - background-color: #e24d42; + &::after { + background-color: #e24d42; + } } &--warning, &--warn { - background-color: #eab839; + &::after { + background-color: #eab839; + } } &--info { - background-color: #7eb26d; + &::after { + background-color: #7eb26d; + } } &--debug { - background-color: #1f78c1; + &::after { + background-color: #1f78c1; + } } &--trace { - background-color: #6ed0e0; + &::after { + background-color: #6ed0e0; + } } } From 114a264da4197ecc7ca7a9cca8fea1bd97e5d3f2 Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Thu, 6 Dec 2018 13:44:02 +0100 Subject: [PATCH 17/33] UserPicker and TeamPicker should use min-width instead of fixed widths to avoid overflowing form buttons. #14341 --- public/app/core/components/PermissionList/AddPermission.tsx | 2 +- public/app/features/teams/TeamMembers.tsx | 2 +- .../features/teams/__snapshots__/TeamMembers.test.tsx.snap | 6 +++--- public/sass/utils/_widths.scss | 6 ++++++ 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/public/app/core/components/PermissionList/AddPermission.tsx b/public/app/core/components/PermissionList/AddPermission.tsx index 71cc937ddfa..d6da7c68544 100644 --- a/public/app/core/components/PermissionList/AddPermission.tsx +++ b/public/app/core/components/PermissionList/AddPermission.tsx @@ -84,7 +84,7 @@ class AddPermissions extends Component { render() { const { onCancel } = this.props; const newItem = this.state; - const pickerClassName = 'width-20'; + const pickerClassName = 'min-width-20'; const isValid = this.isValid(); return (
diff --git a/public/app/features/teams/TeamMembers.tsx b/public/app/features/teams/TeamMembers.tsx index f43dc44808f..433702fa0d5 100644 --- a/public/app/features/teams/TeamMembers.tsx +++ b/public/app/features/teams/TeamMembers.tsx @@ -115,7 +115,7 @@ export class TeamMembers extends PureComponent {
Add Team Member
- + {this.state.newTeamMember && (
@@ -152,7 +152,7 @@ exports[`Render should render team members 1`] = ` className="gf-form-inline" >
@@ -372,7 +372,7 @@ exports[`Render should render team members when sync enabled 1`] = ` className="gf-form-inline" >
diff --git a/public/sass/utils/_widths.scss b/public/sass/utils/_widths.scss index 2000982f08d..b1213e6ea60 100644 --- a/public/sass/utils/_widths.scss +++ b/public/sass/utils/_widths.scss @@ -19,6 +19,12 @@ } } +@for $i from 1 through 30 { + .min-width-#{$i} { + min-width: ($spacer * $i) - $gf-form-margin !important; + } +} + @for $i from 1 through 30 { .offset-width-#{$i} { margin-left: ($spacer * $i) !important; From 02b14d33a6814589d4dbb96973c4731d12f18d2b Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Thu, 6 Dec 2018 13:45:29 +0100 Subject: [PATCH 18/33] If user login equals user email, only show the email once #14341 --- public/app/core/components/Picker/UserPicker.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/core/components/Picker/UserPicker.tsx b/public/app/core/components/Picker/UserPicker.tsx index f78cf69bf5e..f80a3fc135f 100644 --- a/public/app/core/components/Picker/UserPicker.tsx +++ b/public/app/core/components/Picker/UserPicker.tsx @@ -40,7 +40,7 @@ export class UserPicker extends Component { .then(result => { return result.map(user => ({ id: user.userId, - label: `${user.login} - ${user.email}`, + label: user.login === user.email ? user.login : `${user.login} - ${user.email}`, avatarUrl: user.avatarUrl, login: user.login, })); From e230827c062745d1cdeacb80f019826b1d993174 Mon Sep 17 00:00:00 2001 From: bergquist Date: Thu, 6 Dec 2018 13:53:11 +0100 Subject: [PATCH 19/33] more detailed error message for loki if grafana tries to access loki without promtail running it will return an error. --- public/app/plugins/datasource/loki/datasource.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/public/app/plugins/datasource/loki/datasource.ts b/public/app/plugins/datasource/loki/datasource.ts index ebe1e226a75..ebbe6bb4b56 100644 --- a/public/app/plugins/datasource/loki/datasource.ts +++ b/public/app/plugins/datasource/loki/datasource.ts @@ -136,7 +136,8 @@ export default class LokiDatasource { } return { status: 'error', - message: 'Data source connected, but no labels received. Verify that Loki is configured properly.', + message: + 'Data source connected, but no labels received. Verify that Loki and Promtail is configured properly.', }; }) .catch(err => { From b9517d110452bfb1bfed89832ac21cfabe29df92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 6 Dec 2018 14:03:57 +0100 Subject: [PATCH 20/33] wip: explore logs styling --- public/sass/components/_panel_logs.scss | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/public/sass/components/_panel_logs.scss b/public/sass/components/_panel_logs.scss index 4e748029e85..f6da51b18e5 100644 --- a/public/sass/components/_panel_logs.scss +++ b/public/sass/components/_panel_logs.scss @@ -66,7 +66,8 @@ $column-horizontal-spacing: 10px; vertical-align: middle; &:first-child { - padding-left: $column-horizontal-spacing; + padding-left: $column-horizontal-spacing - 2px; + border-left: 2px solid transparent; } &:last-child { @@ -74,6 +75,12 @@ $column-horizontal-spacing: 10px; } } + &:hover { + > div:first-child { + border-left: 2px solid $blue; + } + } + &:nth-child(odd) { background: $page-bg; } @@ -137,7 +144,7 @@ $column-horizontal-spacing: 10px; &--warning, &--warn { &::after { - background-color: #eab839; + background-color: $warn; } } From 03b5f32e071033e321a905db8f27aece68302650 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 6 Dec 2018 15:13:00 +0100 Subject: [PATCH 21/33] wip: alternative level styling & hover effect --- public/sass/components/_panel_logs.scss | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/public/sass/components/_panel_logs.scss b/public/sass/components/_panel_logs.scss index f6da51b18e5..2a3c3c96609 100644 --- a/public/sass/components/_panel_logs.scss +++ b/public/sass/components/_panel_logs.scss @@ -67,7 +67,7 @@ $column-horizontal-spacing: 10px; &:first-child { padding-left: $column-horizontal-spacing - 2px; - border-left: 2px solid transparent; + // border-left: 2px solid transparent; } &:last-child { @@ -76,13 +76,14 @@ $column-horizontal-spacing: 10px; } &:hover { - > div:first-child { - border-left: 2px solid $blue; + > div { + // border-left: 2px solid $blue; + background: $page-bg; } } &:nth-child(odd) { - background: $page-bg; + // background: $page-bg; } } @@ -116,15 +117,16 @@ $column-horizontal-spacing: 10px; .logs-row__level { position: relative; - width: 12px; + // width: 12px; &::after { content: ''; display: block; - width: 12px; - height: 12px; + position: absolute; + top: 1px; + bottom: 1px; + width: 3px; background-color: $gray-2; - border-radius: 50%; } &--critical, From 5ac3b5a561361a1160730deb51f522a70e3f3ad8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 6 Dec 2018 15:51:42 +0100 Subject: [PATCH 22/33] explore logs styling --- public/sass/components/_panel_logs.scss | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/public/sass/components/_panel_logs.scss b/public/sass/components/_panel_logs.scss index 2a3c3c96609..46a6fbe81a2 100644 --- a/public/sass/components/_panel_logs.scss +++ b/public/sass/components/_panel_logs.scss @@ -64,10 +64,11 @@ $column-horizontal-spacing: 10px; display: table-cell; padding-right: $column-horizontal-spacing; vertical-align: middle; + border-top: 1px solid transparent; + border-bottom: 1px solid transparent; &:first-child { padding-left: $column-horizontal-spacing - 2px; - // border-left: 2px solid transparent; } &:last-child { @@ -76,14 +77,7 @@ $column-horizontal-spacing: 10px; } &:hover { - > div { - // border-left: 2px solid $blue; - background: $page-bg; - } - } - - &:nth-child(odd) { - // background: $page-bg; + background: $page-bg; } } From 0d7b09b0f6cc333dfdc981cfbd4b14d93bb158b5 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Wed, 5 Dec 2018 15:35:18 +0100 Subject: [PATCH 23/33] add basic button group component, using the the same label style as is remove not used code cleanup --- .../ToggleButtonGroup/ToggleButton.tsx | 33 +++++++++++++ .../ToggleButtonGroup/ToggleButtonGroup.tsx | 46 +++++++++++++++++++ public/sass/_grafana.scss | 1 + .../sass/components/_toggle_button_group.scss | 27 +++++++++++ 4 files changed, 107 insertions(+) create mode 100644 public/app/core/components/ToggleButtonGroup/ToggleButton.tsx create mode 100644 public/app/core/components/ToggleButtonGroup/ToggleButtonGroup.tsx create mode 100644 public/sass/components/_toggle_button_group.scss diff --git a/public/app/core/components/ToggleButtonGroup/ToggleButton.tsx b/public/app/core/components/ToggleButtonGroup/ToggleButton.tsx new file mode 100644 index 00000000000..2fdf324ac44 --- /dev/null +++ b/public/app/core/components/ToggleButtonGroup/ToggleButton.tsx @@ -0,0 +1,33 @@ +import React, { PureComponent } from 'react'; + +interface ToggleButtonProps { + onChange?: (value) => void; + selected?: boolean; + value: any; + classNames?: string; +} +interface ToggleButtonState {} + +export default class ToggleButton extends PureComponent { + static defaultProps = { + classNames: '', + }; + + handleChange = () => { + const { onChange, value } = this.props; + if (onChange) { + onChange(value); + } + }; + + render() { + const { children, selected, classNames } = this.props; + const btnClassName = `btn ${classNames} ${selected ? 'active' : ''}`; + + return ( + + ); + } +} diff --git a/public/app/core/components/ToggleButtonGroup/ToggleButtonGroup.tsx b/public/app/core/components/ToggleButtonGroup/ToggleButtonGroup.tsx new file mode 100644 index 00000000000..b3063004ebd --- /dev/null +++ b/public/app/core/components/ToggleButtonGroup/ToggleButtonGroup.tsx @@ -0,0 +1,46 @@ +import React, { PureComponent, ReactElement } from 'react'; + +interface ToggleButtonGroupProps { + onChange: (value) => void; + value?: any; + label?: string; +} + +export default class ToggleButtonGroup extends PureComponent { + getValues() { + const { children } = this.props; + return React.Children.toArray(children).map(c => c['props'].value); + } + + handleToggle(toggleValue) { + const { value, onChange } = this.props; + if (value && value === toggleValue) { + return; + } + onChange(toggleValue); + } + + render() { + const { children, value, label } = this.props; + const values = this.getValues(); + const selectedValue = value || values[0]; + + const childClones = React.Children.map(children, (child: ReactElement) => { + const { value: buttonValue } = child.props; + + return React.cloneElement(child, { + selected: buttonValue === selectedValue, + onChange: this.handleToggle.bind(this), + }); + }); + + return ( +
+
+ {label && } + {childClones} +
+
+ ); + } +} diff --git a/public/sass/_grafana.scss b/public/sass/_grafana.scss index e4c7a9c59e1..b53b69a9c74 100644 --- a/public/sass/_grafana.scss +++ b/public/sass/_grafana.scss @@ -101,6 +101,7 @@ @import 'components/delete_button'; @import 'components/add_data_source.scss'; @import 'components/page_loader'; +@import 'components/toggle_button_group'; // PAGES @import 'pages/login'; diff --git a/public/sass/components/_toggle_button_group.scss b/public/sass/components/_toggle_button_group.scss new file mode 100644 index 00000000000..5107c18f1a7 --- /dev/null +++ b/public/sass/components/_toggle_button_group.scss @@ -0,0 +1,27 @@ +.toggle-button-group { + display: flex; + + .gf-form-label { + background-color: $input-label-bg; + } + + .btn { + @include buttonBackground($input-bg, $input-bg); + &.active { + background-color: lighten($input-label-bg, 5%); + color: $link-color; + &:hover { + cursor: default; + } + } + } + + &:first-child { + border-radius: 2px 0 0 2px; + margin: 0; + } + &:last-child { + border-radius: 0 2px 2px 0; + margin-left: 0 !important; + } +} From 6f8293af4ea9173e23795b380c935d241cc4fed5 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Wed, 5 Dec 2018 16:27:14 +0100 Subject: [PATCH 24/33] sort of a hacky way to figure if the small variation should be used for the label improve light theme look use new toggle button group in logs stop propagation on click event clean up styles improve support for small when a label is present refactoring - move toggle button to the group file. turn it into a stateless component improve support for light theme rename dedupe to dedup use radius variable --- .../ToggleButtonGroup/ToggleButton.tsx | 33 ----------------- .../ToggleButtonGroup/ToggleButtonGroup.tsx | 36 +++++++++++++++++-- public/app/features/explore/Logs.tsx | 32 +++++------------ .../sass/components/_toggle_button_group.scss | 32 +++++++++++------ 4 files changed, 62 insertions(+), 71 deletions(-) delete mode 100644 public/app/core/components/ToggleButtonGroup/ToggleButton.tsx diff --git a/public/app/core/components/ToggleButtonGroup/ToggleButton.tsx b/public/app/core/components/ToggleButtonGroup/ToggleButton.tsx deleted file mode 100644 index 2fdf324ac44..00000000000 --- a/public/app/core/components/ToggleButtonGroup/ToggleButton.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import React, { PureComponent } from 'react'; - -interface ToggleButtonProps { - onChange?: (value) => void; - selected?: boolean; - value: any; - classNames?: string; -} -interface ToggleButtonState {} - -export default class ToggleButton extends PureComponent { - static defaultProps = { - classNames: '', - }; - - handleChange = () => { - const { onChange, value } = this.props; - if (onChange) { - onChange(value); - } - }; - - render() { - const { children, selected, classNames } = this.props; - const btnClassName = `btn ${classNames} ${selected ? 'active' : ''}`; - - return ( - - ); - } -} diff --git a/public/app/core/components/ToggleButtonGroup/ToggleButtonGroup.tsx b/public/app/core/components/ToggleButtonGroup/ToggleButtonGroup.tsx index b3063004ebd..91b7f1538c9 100644 --- a/public/app/core/components/ToggleButtonGroup/ToggleButtonGroup.tsx +++ b/public/app/core/components/ToggleButtonGroup/ToggleButtonGroup.tsx @@ -1,4 +1,4 @@ -import React, { PureComponent, ReactElement } from 'react'; +import React, { SFC, ReactNode, PureComponent, ReactElement } from 'react'; interface ToggleButtonGroupProps { onChange: (value) => void; @@ -9,7 +9,12 @@ interface ToggleButtonGroupProps { export default class ToggleButtonGroup extends PureComponent { getValues() { const { children } = this.props; - return React.Children.toArray(children).map(c => c['props'].value); + return React.Children.toArray(children).map((c: ReactElement) => c.props.value); + } + + smallChildren() { + const { children } = this.props; + return React.Children.toArray(children).every((c: ReactElement) => c.props.className.includes('small')); } handleToggle(toggleValue) { @@ -24,6 +29,7 @@ export default class ToggleButtonGroup extends PureComponent) => { const { value: buttonValue } = child.props; @@ -37,10 +43,34 @@ export default class ToggleButtonGroup extends PureComponent
- {label && } + {label && } {childClones}
); } } + +interface ToggleButtonProps { + onChange?: (value) => void; + selected?: boolean; + value: any; + className?: string; + children: ReactNode; +} + +export const ToggleButton: SFC = ({ children, selected, className = '', value, onChange }) => { + const handleChange = event => { + event.stopPropagation(); + if (onChange) { + onChange(value); + } + }; + + const btnClassName = `btn ${className} ${selected ? 'active' : ''}`; + return ( + + ); +}; diff --git a/public/app/features/explore/Logs.tsx b/public/app/features/explore/Logs.tsx index 3a584cd5664..f7704cf340a 100644 --- a/public/app/features/explore/Logs.tsx +++ b/public/app/features/explore/Logs.tsx @@ -16,6 +16,7 @@ import { } from 'app/core/logs_model'; import { findHighlightChunksInText } from 'app/core/utils/text'; import { Switch } from 'app/core/components/Switch/Switch'; +import ToggleButtonGroup, { ToggleButton } from 'app/core/components/ToggleButtonGroup/ToggleButtonGroup'; import Graph from './Graph'; import LogLabels from './LogLabels'; @@ -304,30 +305,13 @@ export default class Logs extends PureComponent { - this.onChangeDedup(LogsDedupStrategy.none)} - small - /> - this.onChangeDedup(LogsDedupStrategy.exact)} - small - /> - this.onChangeDedup(LogsDedupStrategy.numbers)} - small - /> - this.onChangeDedup(LogsDedupStrategy.signature)} - small - /> + + {Object.keys(LogsDedupStrategy).map((dedupType, i) => ( + + {dedupType} + + ))} + {hasData && meta && (
diff --git a/public/sass/components/_toggle_button_group.scss b/public/sass/components/_toggle_button_group.scss index 5107c18f1a7..ed701a489a9 100644 --- a/public/sass/components/_toggle_button_group.scss +++ b/public/sass/components/_toggle_button_group.scss @@ -3,25 +3,35 @@ .gf-form-label { background-color: $input-label-bg; + &:first-child { + border-radius: $border-radius 0 0 $border-radius; + margin: 0; + } + &.small { + padding: ($input-padding-y / 2) ($input-padding-x / 2); + font-size: $font-size-xs; + } } .btn { - @include buttonBackground($input-bg, $input-bg); + background-color: $typeahead-selected-bg; + border-radius: 0; + color: $text-color; &.active { - background-color: lighten($input-label-bg, 5%); - color: $link-color; + background-color: $input-bg; &:hover { cursor: default; } } - } - &:first-child { - border-radius: 2px 0 0 2px; - margin: 0; - } - &:last-child { - border-radius: 0 2px 2px 0; - margin-left: 0 !important; + &:first-child { + border-radius: $border-radius 0 0 $border-radius; + margin: 0; + } + + &:last-child { + border-radius: 0 $border-radius $border-radius 0; + margin-left: 0; + } } } From b74c0997738f0b9f11caa57e7e52cd2f40bcc886 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Thu, 6 Dec 2018 14:54:26 +0100 Subject: [PATCH 25/33] use render props instead of cloneElement --- .../ToggleButtonGroup/ToggleButtonGroup.tsx | 14 +++-------- public/app/features/explore/Logs.tsx | 25 +++++++++++++------ 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/public/app/core/components/ToggleButtonGroup/ToggleButtonGroup.tsx b/public/app/core/components/ToggleButtonGroup/ToggleButtonGroup.tsx index 91b7f1538c9..1e9ae4732df 100644 --- a/public/app/core/components/ToggleButtonGroup/ToggleButtonGroup.tsx +++ b/public/app/core/components/ToggleButtonGroup/ToggleButtonGroup.tsx @@ -4,6 +4,7 @@ interface ToggleButtonGroupProps { onChange: (value) => void; value?: any; label?: string; + render: (props) => void; } export default class ToggleButtonGroup extends PureComponent { @@ -26,25 +27,16 @@ export default class ToggleButtonGroup extends PureComponent) => { - const { value: buttonValue } = child.props; - - return React.cloneElement(child, { - selected: buttonValue === selectedValue, - onChange: this.handleToggle.bind(this), - }); - }); - return (
{label && } - {childClones} + {this.props.render({ selectedValue, onChange: this.handleToggle.bind(this) })}
); diff --git a/public/app/features/explore/Logs.tsx b/public/app/features/explore/Logs.tsx index f7704cf340a..cc4dcbdcc5c 100644 --- a/public/app/features/explore/Logs.tsx +++ b/public/app/features/explore/Logs.tsx @@ -305,13 +305,24 @@ export default class Logs extends PureComponent { - - {Object.keys(LogsDedupStrategy).map((dedupType, i) => ( - - {dedupType} - - ))} - + + Object.keys(LogsDedupStrategy).map((dedupType, i) => ( + + {dedupType} + + )) + } + /> {hasData && meta && (
From 15eb3bb2f83b9d5a4b6225a2c6edd0adc3a4155d Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Thu, 6 Dec 2018 16:47:36 +0100 Subject: [PATCH 26/33] Explore: improve error handling --- public/app/features/explore/Explore.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index d2588a8ec0b..fb2f6759111 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -735,7 +735,7 @@ export class Explore extends React.PureComponent { console.error(response); - let error: string | JSX.Element = response; + let error: string | JSX.Element; if (response.data) { if (typeof response.data === 'string') { error = response.data; @@ -752,6 +752,12 @@ export class Explore extends React.PureComponent { } else { throw new Error('Could not handle error response'); } + } else if (response.message) { + error = response.message; + } else if (typeof response === 'string') { + error = response; + } else { + error = 'Unknown error during query transaction. Please check JS console logs.'; } this.setState(state => { From 9ac3821454644a38c79f0cd7e89e61ef25f4c8ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 6 Dec 2018 11:55:01 -0800 Subject: [PATCH 27/33] made unknown color theme aware and sync with graph color, some minor cleanup --- public/app/core/logs_model.ts | 4 ++-- public/app/core/utils/colors.ts | 5 +++++ public/sass/_variables.dark.scss | 3 +++ public/sass/_variables.light.scss | 3 +++ public/sass/components/_panel_logs.scss | 12 +----------- 5 files changed, 14 insertions(+), 13 deletions(-) diff --git a/public/app/core/logs_model.ts b/public/app/core/logs_model.ts index 1527f18e4b6..08d53a835d2 100644 --- a/public/app/core/logs_model.ts +++ b/public/app/core/logs_model.ts @@ -1,6 +1,6 @@ import _ from 'lodash'; import { TimeSeries } from 'app/core/core'; -import colors from 'app/core/utils/colors'; +import colors, { getThemeColor } from 'app/core/utils/colors'; export enum LogLevel { crit = 'critical', @@ -22,7 +22,7 @@ export const LogLevelColor = { [LogLevel.info]: colors[0], [LogLevel.debug]: colors[5], [LogLevel.trace]: colors[2], - [LogLevel.unkown]: '#ddd', + [LogLevel.unkown]: getThemeColor('#8e8e8e', '#dde4ed'), }; export interface LogSearchMatch { diff --git a/public/app/core/utils/colors.ts b/public/app/core/utils/colors.ts index 13e02b76e30..34508e94a9f 100644 --- a/public/app/core/utils/colors.ts +++ b/public/app/core/utils/colors.ts @@ -1,5 +1,6 @@ import _ from 'lodash'; import tinycolor from 'tinycolor2'; +import config from 'app/core/config'; export const PALETTE_ROWS = 4; export const PALETTE_COLUMNS = 14; @@ -90,5 +91,9 @@ export function hslToHex(color) { return tinycolor(color).toHexString(); } +export function getThemeColor(dark: string, light: string): string { + return config.bootData.user.lightTheme ? light : dark; +} + export let sortedColors = sortColorsByHue(colors); export default colors; diff --git a/public/sass/_variables.dark.scss b/public/sass/_variables.dark.scss index 3fc3770176d..f38349f48f4 100644 --- a/public/sass/_variables.dark.scss +++ b/public/sass/_variables.dark.scss @@ -350,3 +350,6 @@ $diff-json-icon: $gray-7; //Submenu $variable-option-bg: $blue-dark; + +// logs +$logs-color-unkown: $gray-2; diff --git a/public/sass/_variables.light.scss b/public/sass/_variables.light.scss index c70f75c9946..d76cfce85af 100644 --- a/public/sass/_variables.light.scss +++ b/public/sass/_variables.light.scss @@ -359,3 +359,6 @@ $diff-json-icon: $gray-4; //Submenu $variable-option-bg: $blue-light; + +// logs +$logs-color-unkown: $gray-5; diff --git a/public/sass/components/_panel_logs.scss b/public/sass/components/_panel_logs.scss index 46a6fbe81a2..f1a2a32af2c 100644 --- a/public/sass/components/_panel_logs.scss +++ b/public/sass/components/_panel_logs.scss @@ -54,7 +54,6 @@ $column-horizontal-spacing: 10px; font-size: 12px; display: table; table-layout: fixed; - margin: 0 -(nth($panel-padding, 2)); } .logs-row { @@ -66,14 +65,6 @@ $column-horizontal-spacing: 10px; vertical-align: middle; border-top: 1px solid transparent; border-bottom: 1px solid transparent; - - &:first-child { - padding-left: $column-horizontal-spacing - 2px; - } - - &:last-child { - padding-left: $column-horizontal-spacing; - } } &:hover { @@ -111,7 +102,6 @@ $column-horizontal-spacing: 10px; .logs-row__level { position: relative; - // width: 12px; &::after { content: ''; @@ -120,7 +110,7 @@ $column-horizontal-spacing: 10px; top: 1px; bottom: 1px; width: 3px; - background-color: $gray-2; + background-color: $logs-color-unkown; } &--critical, From e7fe0bd668e653bbf590004c209fa34f724efc4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 6 Dec 2018 12:29:55 -0800 Subject: [PATCH 28/33] fixed unit tests --- public/app/core/config.ts | 6 +++++- public/sass/components/_panel_logs.scss | 5 +---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/public/app/core/config.ts b/public/app/core/config.ts index 1473f8a91f8..13d84772ecf 100644 --- a/public/app/core/config.ts +++ b/public/app/core/config.ts @@ -54,7 +54,11 @@ export class Settings { } } -const bootData = (window as any).grafanaBootData || { settings: {} }; +const bootData = (window as any).grafanaBootData || { + settings: {}, + user: {}, +}; + const options = bootData.settings; options.bootData = bootData; diff --git a/public/sass/components/_panel_logs.scss b/public/sass/components/_panel_logs.scss index f1a2a32af2c..6f1c43cfe1d 100644 --- a/public/sass/components/_panel_logs.scss +++ b/public/sass/components/_panel_logs.scss @@ -78,6 +78,7 @@ $column-horizontal-spacing: 10px; .logs-row__labels { max-width: 20%; + line-height: 1.2; } .logs-row__message { @@ -189,10 +190,6 @@ $column-horizontal-spacing: 10px; box-shadow: $popover-shadow; } -.logs-row__labels { - line-height: 1.2; -} - .logs-stats__info { margin-bottom: $spacer / 2; } From acd52e6a93aec198b83311eac41dead61c119286 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Wed, 5 Dec 2018 12:18:51 +0100 Subject: [PATCH 29/33] Explore: Logging line parsing and field stats Lazily parse lines and display stats for field when clicked on. - log line parsers for JSON (basic), and logfmt - delayed parsing in case user moves to other line - reuse label stats for field stats --- public/app/core/logs_model.ts | 64 ++++++ public/app/core/specs/logs_model.test.ts | 121 +++++++++++- public/app/features/explore/LogLabels.tsx | 4 +- public/app/features/explore/Logs.tsx | 229 +++++++++++++++++----- public/sass/components/_panel_logs.scss | 22 +++ 5 files changed, 387 insertions(+), 53 deletions(-) diff --git a/public/app/core/logs_model.ts b/public/app/core/logs_model.ts index 08d53a835d2..09f5bb3a916 100644 --- a/public/app/core/logs_model.ts +++ b/public/app/core/logs_model.ts @@ -95,6 +95,57 @@ export enum LogsDedupStrategy { signature = 'signature', } +export interface LogsParser { + /** + * Value-agnostic matcher for a field label. + * Used to filter rows, and first capture group contains the value. + */ + buildMatcher: (label: string) => RegExp; + /** + * Regex to find a field in the log line. + * First capture group contains the label value, second capture group the value. + */ + fieldRegex: RegExp; + /** + * Function to verify if this is a valid parser for the given line. + * The parser accepts the line unless it returns undefined. + */ + test: (line: string) => any; +} + +export const LogsParsers: { [name: string]: LogsParser } = { + JSON: { + buildMatcher: label => new RegExp(`(?:{|,)\\s*"${label}"\\s*:\\s*"([^"]*)"`), + fieldRegex: /"(\w+)"\s*:\s*"([^"]*)"/, + test: line => { + try { + return JSON.parse(line); + } catch (error) {} + }, + }, + logfmt: { + buildMatcher: label => new RegExp(`(?:^|\\s)${label}=("[^"]*"|\\S+)`), + fieldRegex: /(?:^|\s)(\w+)=("[^"]*"|\S+)/, + test: line => LogsParsers.logfmt.fieldRegex.test(line), + }, +}; + +export function calculateFieldStats(rows: LogRow[], extractor: RegExp): LogsLabelStat[] { + // Consider only rows that satisfy the matcher + const rowsWithField = rows.filter(row => extractor.test(row.entry)); + const rowCount = rowsWithField.length; + + // Get field value counts for eligible rows + const countsByValue = _.countBy(rowsWithField, row => (row as LogRow).entry.match(extractor)[1]); + const sortedCounts = _.chain(countsByValue) + .map((count, value) => ({ count, value, proportion: count / rowCount })) + .sortBy('count') + .reverse() + .value(); + + return sortedCounts; +} + export function calculateLogsLabelStats(rows: LogRow[], label: string): LogsLabelStat[] { // Consider only rows that have the given label const rowsWithLabel = rows.filter(row => row.labels[label] !== undefined); @@ -151,6 +202,19 @@ export function dedupLogRows(logs: LogsModel, strategy: LogsDedupStrategy): Logs }; } +export function getParser(line: string): LogsParser { + let parser; + try { + if (LogsParsers.JSON.test(line)) { + parser = LogsParsers.JSON; + } + } catch (error) {} + if (!parser && LogsParsers.logfmt.test(line)) { + parser = LogsParsers.logfmt; + } + return parser; +} + export function filterLogLevels(logs: LogsModel, hiddenLogLevels: Set): LogsModel { if (hiddenLogLevels.size === 0) { return logs; diff --git a/public/app/core/specs/logs_model.test.ts b/public/app/core/specs/logs_model.test.ts index 22673278b13..85f75b50ed0 100644 --- a/public/app/core/specs/logs_model.test.ts +++ b/public/app/core/specs/logs_model.test.ts @@ -1,4 +1,12 @@ -import { calculateLogsLabelStats, dedupLogRows, LogsDedupStrategy, LogsModel } from '../logs_model'; +import { + calculateFieldStats, + calculateLogsLabelStats, + dedupLogRows, + getParser, + LogsDedupStrategy, + LogsModel, + LogsParsers, +} from '../logs_model'; describe('dedupLogRows()', () => { test('should return rows as is when dedup is set to none', () => { @@ -107,6 +115,50 @@ describe('dedupLogRows()', () => { }); }); +describe('calculateFieldStats()', () => { + test('should return no stats for empty rows', () => { + expect(calculateFieldStats([], /foo=(.*)/)).toEqual([]); + }); + + test('should return no stats if extractor does not match', () => { + const rows = [ + { + entry: 'foo=bar', + }, + ]; + + expect(calculateFieldStats(rows as any, /baz=(.*)/)).toEqual([]); + }); + + test('should return stats for found field', () => { + const rows = [ + { + entry: 'foo="42 + 1"', + }, + { + entry: 'foo=503 baz=foo', + }, + { + entry: 'foo="42 + 1"', + }, + { + entry: 't=2018-12-05T07:44:59+0000 foo=503', + }, + ]; + + expect(calculateFieldStats(rows as any, /foo=("[^"]*"|\S+)/)).toMatchObject([ + { + value: '"42 + 1"', + count: 2, + }, + { + value: '503', + count: 2, + }, + ]); + }); +}); + describe('calculateLogsLabelStats()', () => { test('should return no stats for empty rows', () => { expect(calculateLogsLabelStats([], '')).toEqual([]); @@ -159,3 +211,70 @@ describe('calculateLogsLabelStats()', () => { ]); }); }); + +describe('getParser()', () => { + test('should return no parser on empty line', () => { + expect(getParser('')).toBeUndefined(); + }); + + test('should return no parser on unknown line pattern', () => { + expect(getParser('To Be or not to be')).toBeUndefined(); + }); + + test('should return logfmt parser on key value patterns', () => { + expect(getParser('foo=bar baz="41 + 1')).toEqual(LogsParsers.logfmt); + }); + + test('should return JSON parser on JSON log lines', () => { + // TODO implement other JSON value types than string + expect(getParser('{"foo": "bar", "baz": "41 + 1"}')).toEqual(LogsParsers.JSON); + }); +}); + +describe('LogsParsers', () => { + describe('logfmt', () => { + const parser = LogsParsers.logfmt; + + test('should detect format', () => { + expect(parser.test('foo')).toBeFalsy(); + expect(parser.test('foo=bar')).toBeTruthy(); + }); + + test('should have a valid fieldRegex', () => { + const match = 'foo=bar'.match(parser.fieldRegex); + expect(match).toBeDefined(); + expect(match[1]).toBe('foo'); + expect(match[2]).toBe('bar'); + }); + + test('should build a valid value matcher', () => { + const matcher = parser.buildMatcher('foo'); + const match = 'foo=bar'.match(matcher); + expect(match).toBeDefined(); + expect(match[1]).toBe('bar'); + }); + }); + + describe('JSON', () => { + const parser = LogsParsers.JSON; + + test('should detect format', () => { + expect(parser.test('foo')).toBeFalsy(); + expect(parser.test('{"foo":"bar"}')).toBeTruthy(); + }); + + test('should have a valid fieldRegex', () => { + const match = '{"foo":"bar"}'.match(parser.fieldRegex); + expect(match).toBeDefined(); + expect(match[1]).toBe('foo'); + expect(match[2]).toBe('bar'); + }); + + test('should build a valid value matcher', () => { + const matcher = parser.buildMatcher('foo'); + const match = '{"foo":"bar"}'.match(matcher); + expect(match).toBeDefined(); + expect(match[1]).toBe('bar'); + }); + }); +}); diff --git a/public/app/features/explore/LogLabels.tsx b/public/app/features/explore/LogLabels.tsx index 91e2d44e517..c10ad408a42 100644 --- a/public/app/features/explore/LogLabels.tsx +++ b/public/app/features/explore/LogLabels.tsx @@ -24,7 +24,7 @@ function StatsRow({ active, count, proportion, value }: LogsLabelStat) { } const STATS_ROW_LIMIT = 5; -class Stats extends PureComponent<{ +export class Stats extends PureComponent<{ stats: LogsLabelStat[]; label: string; value: string; @@ -54,7 +54,7 @@ class Stats extends PureComponent<{
{topRows.map(stat => )} - {insertActiveRow && } + {insertActiveRow && activeRow && } {otherCount > 0 && } ); diff --git a/public/app/features/explore/Logs.tsx b/public/app/features/explore/Logs.tsx index 82995390e26..cfc109b1a7d 100644 --- a/public/app/features/explore/Logs.tsx +++ b/public/app/features/explore/Logs.tsx @@ -10,16 +10,20 @@ import { LogsModel, dedupLogRows, filterLogLevels, + getParser, LogLevel, LogsMetaKind, + LogsLabelStat, + LogsParser, LogRow, + calculateFieldStats, } from 'app/core/logs_model'; import { findHighlightChunksInText } from 'app/core/utils/text'; import { Switch } from 'app/core/components/Switch/Switch'; import ToggleButtonGroup, { ToggleButton } from 'app/core/components/ToggleButtonGroup/ToggleButtonGroup'; import Graph from './Graph'; -import LogLabels from './LogLabels'; +import LogLabels, { Stats } from './LogLabels'; const PREVIEW_LIMIT = 100; @@ -38,6 +42,19 @@ const graphOptions = { }, }; +/** + * Renders a highlighted field. + * When hovering, a stats icon is shown. + */ +const FieldHighlight = onClick => props => { + return ( + + {props.children} + onClick(props.children)} /> + + ); +}; + interface RowProps { allRows: LogRow[]; highlighterExpressions?: string[]; @@ -49,57 +66,169 @@ interface RowProps { onClickLabel?: (label: string, value: string) => void; } -function Row({ - allRows, - highlighterExpressions, - onClickLabel, - row, - showDuplicates, - showLabels, - showLocalTime, - showUtc, -}: RowProps) { - const previewHighlights = highlighterExpressions && !_.isEqual(highlighterExpressions, row.searchWords); - const highlights = previewHighlights ? highlighterExpressions : row.searchWords; - const needsHighlighter = highlights && highlights.length > 0; - const highlightClassName = classnames('logs-row__match-highlight', { - 'logs-row__match-highlight--preview': previewHighlights, - }); - return ( -
- {showDuplicates && ( -
{row.duplicates > 0 ? `${row.duplicates + 1}x` : null}
- )} -
- {showUtc && ( -
- {row.timestamp} -
- )} - {showLocalTime && ( -
- {row.timeLocal} -
- )} - {showLabels && ( -
- -
- )} -
- {needsHighlighter ? ( - - ) : ( - row.entry +interface RowState { + fieldCount: number; + fieldLabel: string; + fieldStats: LogsLabelStat[]; + fieldValue: string; + parsed: boolean; + parser: LogsParser; + parsedFieldHighlights: string[]; + showFieldStats: boolean; +} + +/** + * Renders a log line. + * + * When user hovers over it for a certain time, it lazily parses the log line. + * Once a parser is found, it will determine fields, that will be highlighted. + * When the user requests stats for a field, they will be calculated and rendered below the row. + */ +class Row extends PureComponent { + mouseMessageTimer: NodeJS.Timer; + + state = { + fieldCount: 0, + fieldLabel: null, + fieldStats: null, + fieldValue: null, + parsed: false, + parser: null, + parsedFieldHighlights: [], + showFieldStats: false, + }; + + componentWillUnmount() { + clearTimeout(this.mouseMessageTimer); + } + + onClickClose = () => { + this.setState({ showFieldStats: false }); + }; + + onClickHighlight = (fieldText: string) => { + const { allRows } = this.props; + const { parser } = this.state; + + const fieldMatch = fieldText.match(parser.fieldRegex); + if (fieldMatch) { + // Build value-agnostic row matcher based on the field label + const fieldLabel = fieldMatch[1]; + const fieldValue = fieldMatch[2]; + const matcher = parser.buildMatcher(fieldLabel); + const fieldStats = calculateFieldStats(allRows, matcher); + const fieldCount = fieldStats.reduce((sum, stat) => sum + stat.count, 0); + + this.setState({ fieldCount, fieldLabel, fieldStats, fieldValue, showFieldStats: true }); + } + }; + + onMouseOverMessage = () => { + // Don't parse right away, user might move along + this.mouseMessageTimer = setTimeout(this.parseMessage, 500); + }; + + onMouseOutMessage = () => { + clearTimeout(this.mouseMessageTimer); + this.setState({ parsed: false }); + }; + + parseMessage = () => { + if (!this.state.parsed) { + const { row } = this.props; + const parser = getParser(row.entry); + if (parser) { + // Use parser to highlight detected fields + const parsedFieldHighlights = []; + this.props.row.entry.replace(new RegExp(parser.fieldRegex, 'g'), substring => { + parsedFieldHighlights.push(substring.trim()); + return ''; + }); + this.setState({ parsedFieldHighlights, parsed: true, parser }); + } + } + }; + + render() { + const { + allRows, + highlighterExpressions, + onClickLabel, + row, + showDuplicates, + showLabels, + showLocalTime, + showUtc, + } = this.props; + const { + fieldCount, + fieldLabel, + fieldStats, + fieldValue, + parsed, + parsedFieldHighlights, + showFieldStats, + } = this.state; + const previewHighlights = highlighterExpressions && !_.isEqual(highlighterExpressions, row.searchWords); + const highlights = previewHighlights ? highlighterExpressions : row.searchWords; + const needsHighlighter = highlights && highlights.length > 0; + const highlightClassName = classnames('logs-row__match-highlight', { + 'logs-row__match-highlight--preview': previewHighlights, + }); + return ( +
+ {showDuplicates && ( +
{row.duplicates > 0 ? `${row.duplicates + 1}x` : null}
)} +
+ {showUtc && ( +
+ {row.timestamp} +
+ )} + {showLocalTime && ( +
+ {row.timeLocal} +
+ )} + {showLabels && ( +
+ +
+ )} +
+ {parsed && ( + + )} + {!parsed && + needsHighlighter && ( + + )} + {!parsed && !needsHighlighter && row.entry} + {showFieldStats && ( + + )} +
-
- ); + ); + } } function renderMetaItem(value: any, kind: LogsMetaKind) { diff --git a/public/sass/components/_panel_logs.scss b/public/sass/components/_panel_logs.scss index 6f1c43cfe1d..572e6e890d7 100644 --- a/public/sass/components/_panel_logs.scss +++ b/public/sass/components/_panel_logs.scss @@ -158,6 +158,28 @@ $column-horizontal-spacing: 10px; text-align: right; } +.logs-row__field-highlight { + // Undoing mark styling + background: inherit; + padding: inherit; + border-bottom: 1px dotted $typeahead-selected-color; + + .logs-row__field-highlight--icon { + margin-left: 0.5em; + cursor: pointer; + display: none; + } +} + +.logs-row__field-highlight:hover { + color: $typeahead-selected-color; + border-bottom-style: solid; + + .logs-row__field-highlight--icon { + display: inline; + } +} + .logs-label { display: inline-block; padding: 0 2px; From fb4925a8c20441c81882e1076cce0786b50c817e Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Fri, 7 Dec 2018 10:22:52 +0100 Subject: [PATCH 30/33] Rebase fixes --- public/app/features/explore/Logs.tsx | 18 ++++++++++-------- public/sass/components/_panel_logs.scss | 8 ++++++++ 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/public/app/features/explore/Logs.tsx b/public/app/features/explore/Logs.tsx index cfc109b1a7d..108d4f37a6e 100644 --- a/public/app/features/explore/Logs.tsx +++ b/public/app/features/explore/Logs.tsx @@ -180,7 +180,7 @@ class Row extends PureComponent { {showDuplicates && (
{row.duplicates > 0 ? `${row.duplicates + 1}x` : null}
)} -
+
{showUtc && (
{row.timestamp} @@ -217,13 +217,15 @@ class Row extends PureComponent { )} {!parsed && !needsHighlighter && row.entry} {showFieldStats && ( - +
+ +
)}
diff --git a/public/sass/components/_panel_logs.scss b/public/sass/components/_panel_logs.scss index 572e6e890d7..7a8314abcda 100644 --- a/public/sass/components/_panel_logs.scss +++ b/public/sass/components/_panel_logs.scss @@ -171,6 +171,14 @@ $column-horizontal-spacing: 10px; } } +.logs-row__stats { + margin: 5px 0; + background-color: $popover-bg; + color: $popover-color; + border: 1px solid $popover-border-color; + padding: 10px; +} + .logs-row__field-highlight:hover { color: $typeahead-selected-color; border-bottom-style: solid; From c0c06e4dd8f708891b007e34a5160afa10e11a78 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Fri, 7 Dec 2018 11:13:48 +0100 Subject: [PATCH 31/33] Prometheus: Make result transformer more robust for empty responses - some prometheus-compatible backends may return `null` instead of empty lists - test for lists before iterating - added tests for empty responses --- .../prometheus/result_transformer.ts | 6 ++--- .../specs/result_transformer.test.ts | 25 +++++++++++++++++++ 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/public/app/plugins/datasource/prometheus/result_transformer.ts b/public/app/plugins/datasource/prometheus/result_transformer.ts index c9693eaf657..3c21e0c3d51 100644 --- a/public/app/plugins/datasource/prometheus/result_transformer.ts +++ b/public/app/plugins/datasource/prometheus/result_transformer.ts @@ -16,7 +16,7 @@ export class ResultTransformer { options.valueWithRefId ), ]; - } else if (options.format === 'heatmap') { + } else if (prometheusResult && options.format === 'heatmap') { let seriesList = []; prometheusResult.sort(sortSeriesByLabel); for (const metricData of prometheusResult) { @@ -24,7 +24,7 @@ export class ResultTransformer { } seriesList = this.transformToHistogramOverTime(seriesList); return seriesList; - } else { + } else if (prometheusResult) { const seriesList = []; for (const metricData of prometheusResult) { if (response.data.data.resultType === 'matrix') { @@ -82,7 +82,7 @@ export class ResultTransformer { let i, j; const metricLabels = {}; - if (md.length === 0) { + if (!md || md.length === 0) { return table; } diff --git a/public/app/plugins/datasource/prometheus/specs/result_transformer.test.ts b/public/app/plugins/datasource/prometheus/specs/result_transformer.test.ts index 0ccb79a5d1f..d7e42237f8a 100644 --- a/public/app/plugins/datasource/prometheus/specs/result_transformer.test.ts +++ b/public/app/plugins/datasource/prometheus/specs/result_transformer.test.ts @@ -10,6 +10,31 @@ describe('Prometheus Result Transformer', () => { ctx.resultTransformer = new ResultTransformer(ctx.templateSrv); }); + describe('When nothing is returned', () => { + test('should return empty series', () => { + const response = { + status: 'success', + data: { + resultType: '', + result: null, + }, + }; + const series = ctx.resultTransformer.transform({ data: response }, {}); + expect(series).toEqual([]); + }); + test('should return empty table', () => { + const response = { + status: 'success', + data: { + resultType: '', + result: null, + }, + }; + const table = ctx.resultTransformer.transform({ data: response }, { format: 'table' }); + expect(table).toMatchObject([{ type: 'table', rows: [] }]); + }); + }); + describe('When resultFormat is table', () => { const response = { status: 'success', From 7cb456ea231b645e286006b2a96547ee382cd6b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 7 Dec 2018 11:28:22 +0100 Subject: [PATCH 32/33] Aligned styling of stats popover/box with rest of grafana & minor css refactoring --- public/app/features/explore/LogLabels.tsx | 22 +++++++---- public/sass/components/_panel_logs.scss | 45 ++++++++++++++++------- 2 files changed, 45 insertions(+), 22 deletions(-) diff --git a/public/app/features/explore/LogLabels.tsx b/public/app/features/explore/LogLabels.tsx index c10ad408a42..eb9c39050f6 100644 --- a/public/app/features/explore/LogLabels.tsx +++ b/public/app/features/explore/LogLabels.tsx @@ -48,15 +48,21 @@ export class Stats extends PureComponent<{ const otherProportion = otherCount / total; return ( - <> -
- {label}: {total} of {rowCount} rows have that label - +
+
+ + {label}: {total} of {rowCount} rows have that label + +
- {topRows.map(stat => )} - {insertActiveRow && activeRow && } - {otherCount > 0 && } - +
+ {topRows.map(stat => )} + {insertActiveRow && activeRow && } + {otherCount > 0 && ( + + )} +
+
); } } diff --git a/public/sass/components/_panel_logs.scss b/public/sass/components/_panel_logs.scss index 7a8314abcda..8220cfed878 100644 --- a/public/sass/components/_panel_logs.scss +++ b/public/sass/components/_panel_logs.scss @@ -173,10 +173,6 @@ $column-horizontal-spacing: 10px; .logs-row__stats { margin: 5px 0; - background-color: $popover-bg; - color: $popover-color; - border: 1px solid $popover-border-color; - padding: 10px; } .logs-row__field-highlight:hover { @@ -211,21 +207,42 @@ $column-horizontal-spacing: 10px; top: 1.25em; left: -10px; z-index: 100; - background-color: $popover-bg; - color: $popover-color; - border: 1px solid $popover-border-color; - padding: 10px; - border-radius: $border-radius; justify-content: space-between; box-shadow: $popover-shadow; } -.logs-stats__info { - margin-bottom: $spacer / 2; +/* +* Stats popover & message stats box +*/ +.logs-stats { + background-color: $popover-bg; + color: $popover-color; + border: 1px solid $popover-border-color; + border-radius: $border-radius; + max-width: 500px; } -.logs-stats__icon { - margin-left: 0.5em; +.logs-stats__header { + background-color: $popover-border-color; + padding: 6px 10px; + display: flex; +} + +.logs-stats__title { + font-weight: $font-weight-semi-bold; + padding-right: $spacer; + overflow: hidden; + display: inline-block; + white-space: nowrap; + text-overflow: ellipsis; + flex-grow: 1; +} + +.logs-stats__body { + padding: 20px 10px 10px 10px; +} + +.logs-stats__close { cursor: pointer; } @@ -272,6 +289,6 @@ $column-horizontal-spacing: 10px; } &__innerbar { - background-color: $blue; + background: $blue; } } From ef08b9cd2b6efefd67272178b2c214f723321bd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 7 Dec 2018 13:07:08 +0100 Subject: [PATCH 33/33] graphInterval needs to update after query execution, fixes #14364 --- public/app/features/explore/Explore.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index fb2f6759111..b9b59591bff 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -654,6 +654,7 @@ export class Explore extends React.PureComponent { ...results, queryTransactions: nextQueryTransactions, showingStartPage: false, + graphInterval: queryOptions.intervalMs, }; });