This is an automated email from the ASF dual-hosted git repository.
klesh pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/devlake.git
The following commit(s) were added to refs/heads/main by this push:
new 1cbd8357e feat(sonarqube): collect historical project metrics and
Grafana trends (#8983)
1cbd8357e is described below
commit 1cbd8357e87cbc25c5566f58f3070640e85f294f
Author: Joshua Smith <[email protected]>
AuthorDate: Wed Jul 15 02:53:49 2026 -0600
feat(sonarqube): collect historical project metrics and Grafana trends
(#8983)
* feat(sonarqube): add project-level metrics history collection
Add collector, extractor, and convertor subtasks that pull historical
metric snapshots from the SonarQube measures/search_history and
project_analyses/search APIs into a new cq_project_metrics_history
domain table, enabling trend analysis for coverage, bugs, code smells,
complexity, vulnerabilities, and other quality indicators over time.
Signed-off-by: Joshua Smith <[email protected]>
* fix(sonarqube): Require user token at test
* Reject Global and Project analysis tokens on SonarQube Server before
authentication validate runs.
* Skip prefix validation for SonarCloud endpoints where token prefixes
differ.
* feat(grafana): add historical trends to SonarQube dashboards
* Add collapsed "Historical Trends" row with coverage, bugs,
vulnerabilities, code smells, and duplication time-series panels to both Server
and Cloud dashboards
* Panels query cq_project_metrics_history and honor the Grafana time
picker via $__timeFilter
Signed-off-by: Joshua Smith <[email protected]>
* style(sonarqube): Fix gofmt formatting in code quality files
* Align struct fields and ApiCollectorArgs literals to satisfy
golangci-lint gofmt checks.
* Updates cq_project_metrics_history and SonarQube collector tasks
flagged in CI.
---------
Signed-off-by: Joshua Smith <[email protected]>
---
.../codequality/cq_project_metrics_history.go | 46 ++++
.../models/domainlayer/domaininfo/domaininfo.go | 1 +
.../20260707_add_cq_project_metrics_history.go} | 45 ++--
.../archived/cq_project_metrics_history.go} | 45 ++--
backend/core/models/migrationscripts/register.go | 1 +
backend/plugins/sonarqube/api/connection_api.go | 3 +
backend/plugins/sonarqube/impl/impl.go | 7 +
backend/plugins/sonarqube/models/connection.go | 42 +++-
.../plugins/sonarqube/models/connection_test.go | 92 +++++++
.../20260707_add_project_metrics_history.go | 77 ++++++
.../sonarqube/models/migrationscripts/register.go | 1 +
.../register.go => sonarqube_project_analyses.go} | 43 ++--
...ter.go => sonarqube_project_metrics_history.go} | 40 ++-
.../sonarqube/tasks/project_analyses_collector.go | 92 +++++++
.../sonarqube/tasks/project_analyses_extractor.go | 84 +++++++
.../tasks/project_metrics_history_collector.go | 94 +++++++
.../tasks/project_metrics_history_convertor.go | 158 ++++++++++++
.../tasks/project_metrics_history_extractor.go | 89 +++++++
grafana/dashboards/mysql/Sonarqube.json | 261 +++++++++++++++++++-
grafana/dashboards/mysql/sonar-qube-cloud.json | 261 +++++++++++++++++++-
grafana/dashboards/postgresql/Sonarqube.json | 269 ++++++++++++++++++++-
.../dashboards/postgresql/sonar-qube-cloud.json | 269 ++++++++++++++++++++-
22 files changed, 1919 insertions(+), 101 deletions(-)
diff --git
a/backend/core/models/domainlayer/codequality/cq_project_metrics_history.go
b/backend/core/models/domainlayer/codequality/cq_project_metrics_history.go
new file mode 100644
index 000000000..90f64ce66
--- /dev/null
+++ b/backend/core/models/domainlayer/codequality/cq_project_metrics_history.go
@@ -0,0 +1,46 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements. See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package codequality
+
+import (
+ "time"
+
+ "github.com/apache/incubator-devlake/core/models/domainlayer"
+)
+
+type CqProjectMetricsHistory struct {
+ domainlayer.DomainEntity
+ ProjectKey string `gorm:"index;type:varchar(500)"`
+ AnalysisDate time.Time `gorm:"index"`
+ Coverage *float64
+ Ncloc *int
+ Bugs *int
+ ReliabilityRating string `gorm:"type:varchar(5)"`
+ CodeSmells *int
+ SqaleRating string `gorm:"type:varchar(5)"`
+ Complexity *int
+ CognitiveComplexity *int
+ Vulnerabilities *int
+ SecurityRating string `gorm:"type:varchar(5)"`
+ SecurityHotspots *int
+ DuplicatedLinesDensity *float64
+}
+
+func (CqProjectMetricsHistory) TableName() string {
+ return "cq_project_metrics_history"
+}
diff --git a/backend/core/models/domainlayer/domaininfo/domaininfo.go
b/backend/core/models/domainlayer/domaininfo/domaininfo.go
index b2cc5431f..b88289e8d 100644
--- a/backend/core/models/domainlayer/domaininfo/domaininfo.go
+++ b/backend/core/models/domainlayer/domaininfo/domaininfo.go
@@ -56,6 +56,7 @@ func GetDomainTablesInfo() []dal.Tabler {
&codequality.CqIssue{},
&codequality.CqIssueImpact{},
&codequality.CqProject{},
+ &codequality.CqProjectMetricsHistory{},
// crossdomain
&crossdomain.Account{},
&crossdomain.BoardRepo{},
diff --git a/backend/plugins/sonarqube/models/migrationscripts/register.go
b/backend/core/models/migrationscripts/20260707_add_cq_project_metrics_history.go
similarity index 52%
copy from backend/plugins/sonarqube/models/migrationscripts/register.go
copy to
backend/core/models/migrationscripts/20260707_add_cq_project_metrics_history.go
index 7c48de842..de66ae829 100644
--- a/backend/plugins/sonarqube/models/migrationscripts/register.go
+++
b/backend/core/models/migrationscripts/20260707_add_cq_project_metrics_history.go
@@ -17,27 +17,26 @@ limitations under the License.
package migrationscripts
-import "github.com/apache/incubator-devlake/core/plugin"
-
-// All return all the migration scripts
-func All() []plugin.MigrationScript {
- return []plugin.MigrationScript{
- new(addInitTables),
- new(modifyCharacterSet),
- new(expandProjectKey20230206),
- new(addRawParamTableForScope),
- new(addScopeConfigIdToProject),
- new(modifyFileMetricsKeyLength),
- new(modifyComponentLength),
- new(addSonarQubeScopeConfig20231214),
- new(modifyCommitCharacterType),
- new(modifyCommitCharacterType0508),
- new(updateSonarQubeScopeConfig20240614),
- new(modifyNameLength),
- new(changeIssueComponentType),
- new(increaseProjectKeyLength),
- new(addOrgToConn),
- new(addIssueImpacts),
- new(extendSonarqubeFieldSize),
- }
+import (
+ "github.com/apache/incubator-devlake/core/context"
+ "github.com/apache/incubator-devlake/core/errors"
+
"github.com/apache/incubator-devlake/core/models/migrationscripts/archived"
+ "github.com/apache/incubator-devlake/helpers/migrationhelper"
+)
+
+type addCqProjectMetricsHistory struct{}
+
+func (u *addCqProjectMetricsHistory) Up(basicRes context.BasicRes)
errors.Error {
+ return migrationhelper.AutoMigrateTables(
+ basicRes,
+ &archived.CqProjectMetricsHistory{},
+ )
+}
+
+func (*addCqProjectMetricsHistory) Version() uint64 {
+ return 20260707153201
+}
+
+func (*addCqProjectMetricsHistory) Name() string {
+ return "add cq_project_metrics_history domain table"
}
diff --git a/backend/plugins/sonarqube/models/migrationscripts/register.go
b/backend/core/models/migrationscripts/archived/cq_project_metrics_history.go
similarity index 51%
copy from backend/plugins/sonarqube/models/migrationscripts/register.go
copy to
backend/core/models/migrationscripts/archived/cq_project_metrics_history.go
index 7c48de842..934627368 100644
--- a/backend/plugins/sonarqube/models/migrationscripts/register.go
+++
b/backend/core/models/migrationscripts/archived/cq_project_metrics_history.go
@@ -15,29 +15,28 @@ See the License for the specific language governing
permissions and
limitations under the License.
*/
-package migrationscripts
+package archived
-import "github.com/apache/incubator-devlake/core/plugin"
+import "time"
-// All return all the migration scripts
-func All() []plugin.MigrationScript {
- return []plugin.MigrationScript{
- new(addInitTables),
- new(modifyCharacterSet),
- new(expandProjectKey20230206),
- new(addRawParamTableForScope),
- new(addScopeConfigIdToProject),
- new(modifyFileMetricsKeyLength),
- new(modifyComponentLength),
- new(addSonarQubeScopeConfig20231214),
- new(modifyCommitCharacterType),
- new(modifyCommitCharacterType0508),
- new(updateSonarQubeScopeConfig20240614),
- new(modifyNameLength),
- new(changeIssueComponentType),
- new(increaseProjectKeyLength),
- new(addOrgToConn),
- new(addIssueImpacts),
- new(extendSonarqubeFieldSize),
- }
+type CqProjectMetricsHistory struct {
+ DomainEntity
+ ProjectKey string `gorm:"index;type:varchar(500)"`
+ AnalysisDate time.Time `gorm:"index"`
+ Coverage *float64
+ Ncloc *int
+ Bugs *int
+ ReliabilityRating string `gorm:"type:varchar(5)"`
+ CodeSmells *int
+ SqaleRating string `gorm:"type:varchar(5)"`
+ Complexity *int
+ CognitiveComplexity *int
+ Vulnerabilities *int
+ SecurityRating string `gorm:"type:varchar(5)"`
+ SecurityHotspots *int
+ DuplicatedLinesDensity *float64
+}
+
+func (CqProjectMetricsHistory) TableName() string {
+ return "cq_project_metrics_history"
}
diff --git a/backend/core/models/migrationscripts/register.go
b/backend/core/models/migrationscripts/register.go
index b4596154e..76f2e9683 100644
--- a/backend/core/models/migrationscripts/register.go
+++ b/backend/core/models/migrationscripts/register.go
@@ -145,5 +145,6 @@ func All() []plugin.MigrationScript {
new(modifyCicdDeploymentsToText),
new(increaseCqIssuesProjectKeyLength),
new(addAuthSessions),
+ new(addCqProjectMetricsHistory),
}
}
diff --git a/backend/plugins/sonarqube/api/connection_api.go
b/backend/plugins/sonarqube/api/connection_api.go
index feb1905a6..d387e5d46 100644
--- a/backend/plugins/sonarqube/api/connection_api.go
+++ b/backend/plugins/sonarqube/api/connection_api.go
@@ -44,6 +44,9 @@ func testConnection(ctx context.Context, connection
models.SonarqubeConn) (*plug
return nil, errors.Default.Wrap(err, "error validating
target")
}
}
+ if err := connection.ValidateUserTokenPrefix(); err != nil {
+ return nil, err
+ }
apiClient, err := api.NewApiClientFromConnection(ctx, basicRes,
&connection)
if err != nil {
return nil, err
diff --git a/backend/plugins/sonarqube/impl/impl.go
b/backend/plugins/sonarqube/impl/impl.go
index a6d6bd258..85ee668ee 100644
--- a/backend/plugins/sonarqube/impl/impl.go
+++ b/backend/plugins/sonarqube/impl/impl.go
@@ -86,6 +86,8 @@ func (p Sonarqube) GetTablesInfo() []dal.Tabler {
&models.SonarqubeFileMetrics{},
&models.SonarqubeAccount{},
&models.SonarqubeScopeConfig{},
+ &models.SonarqubeProjectMetricsHistory{},
+ &models.SonarqubeProjectAnalysis{},
}
}
@@ -108,6 +110,11 @@ func (p Sonarqube) SubTaskMetas() []plugin.SubTaskMeta {
tasks.ConvertHotspotsMeta,
tasks.ConvertFileMetricsMeta,
tasks.ConvertAccountsMeta,
+ tasks.CollectProjectMetricsHistoryMeta,
+ tasks.ExtractProjectMetricsHistoryMeta,
+ tasks.CollectProjectAnalysesMeta,
+ tasks.ExtractProjectAnalysesMeta,
+ tasks.ConvertProjectMetricsHistoryMeta,
}
}
diff --git a/backend/plugins/sonarqube/models/connection.go
b/backend/plugins/sonarqube/models/connection.go
index 66c32f57c..0bbe124d5 100644
--- a/backend/plugins/sonarqube/models/connection.go
+++ b/backend/plugins/sonarqube/models/connection.go
@@ -21,6 +21,7 @@ import (
"encoding/base64"
"fmt"
"net/http"
+ "strings"
"github.com/apache/incubator-devlake/core/utils"
@@ -29,6 +30,12 @@ import (
helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
)
+const (
+ userTokenPrefix = "squ_"
+ globalAnalysisTokenPrefix = "sqa_"
+ projectAnalysisTokenPrefix = "sqp_"
+)
+
type SonarqubeAccessToken helper.AccessToken
// SetupAuthentication sets up the HTTP Request Authentication
@@ -92,7 +99,40 @@ func (connection *SonarqubeConnection)
MergeFromRequest(target *SonarqubeConnect
return nil
}
-func (connection *SonarqubeConnection) IsCloud() bool {
+// ValidateUserTokenPrefix ensures the token is a SonarQube User token on
Server
+// instances. Global (sqa_) and Project (sqp_) analysis tokens authenticate but
+// cannot call read APIs such as measures/component_tree.
+func (connection SonarqubeConn) ValidateUserTokenPrefix() errors.Error {
+ if connection.IsCloud() {
+ return nil
+ }
+ token := strings.TrimSpace(connection.Token)
+ if token == "" {
+ return errors.BadInput.New("token is required")
+ }
+ switch {
+ case strings.HasPrefix(token, globalAnalysisTokenPrefix):
+ return errors.BadInput.New(
+ "DevLake requires a User token (squ_ prefix). " +
+ "Global Analysis tokens (sqa_) can push scan
results but cannot read project metrics via the Web API. " +
+ "Create a User token under My Account >
Security in SonarQube.",
+ )
+ case strings.HasPrefix(token, projectAnalysisTokenPrefix):
+ return errors.BadInput.New(
+ "DevLake requires a User token (squ_ prefix). " +
+ "Project Analysis tokens (sqp_) can push scan
results but cannot read project metrics via the Web API. " +
+ "Create a User token under My Account >
Security in SonarQube.",
+ )
+ case !strings.HasPrefix(token, userTokenPrefix):
+ return errors.BadInput.New(
+ "DevLake requires a User token (squ_ prefix) for
SonarQube Server. " +
+ "Create one under My Account > Security in
SonarQube.",
+ )
+ }
+ return nil
+}
+
+func (connection SonarqubeConn) IsCloud() bool {
return connection.Endpoint == "https://sonarcloud.io/api/"
}
diff --git a/backend/plugins/sonarqube/models/connection_test.go
b/backend/plugins/sonarqube/models/connection_test.go
new file mode 100644
index 000000000..e92284ad5
--- /dev/null
+++ b/backend/plugins/sonarqube/models/connection_test.go
@@ -0,0 +1,92 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements. See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package models
+
+import (
+ "testing"
+
+ helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
+)
+
+func TestValidateUserTokenPrefix(t *testing.T) {
+ t.Parallel()
+
+ serverEndpoint := "https://rad-sonar.example.com/api/"
+ cloudEndpoint := "https://sonarcloud.io/api/"
+
+ tests := []struct {
+ name string
+ conn SonarqubeConn
+ wantErr bool
+ }{
+ {
+ name: "user token on server",
+ conn: SonarqubeConn{
+ RestConnection:
helper.RestConnection{Endpoint: serverEndpoint},
+ SonarqubeAccessToken:
SonarqubeAccessToken{Token: "squ_abc123"},
+ },
+ },
+ {
+ name: "global analysis token on server",
+ conn: SonarqubeConn{
+ RestConnection:
helper.RestConnection{Endpoint: serverEndpoint},
+ SonarqubeAccessToken:
SonarqubeAccessToken{Token: "sqa_abc123"},
+ },
+ wantErr: true,
+ },
+ {
+ name: "project analysis token on server",
+ conn: SonarqubeConn{
+ RestConnection:
helper.RestConnection{Endpoint: serverEndpoint},
+ SonarqubeAccessToken:
SonarqubeAccessToken{Token: "sqp_abc123"},
+ },
+ wantErr: true,
+ },
+ {
+ name: "unknown prefix on server",
+ conn: SonarqubeConn{
+ RestConnection:
helper.RestConnection{Endpoint: serverEndpoint},
+ SonarqubeAccessToken:
SonarqubeAccessToken{Token: "legacy-token"},
+ },
+ wantErr: true,
+ },
+ {
+ name: "sonarcloud skips prefix check",
+ conn: SonarqubeConn{
+ RestConnection:
helper.RestConnection{Endpoint: cloudEndpoint},
+ SonarqubeAccessToken:
SonarqubeAccessToken{Token: "sqa_abc123"},
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ t.Parallel()
+ err := tt.conn.ValidateUserTokenPrefix()
+ if tt.wantErr {
+ if err == nil {
+ t.Fatal("expected error, got nil")
+ }
+ return
+ }
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ })
+ }
+}
diff --git
a/backend/plugins/sonarqube/models/migrationscripts/20260707_add_project_metrics_history.go
b/backend/plugins/sonarqube/models/migrationscripts/20260707_add_project_metrics_history.go
new file mode 100644
index 000000000..d060f4631
--- /dev/null
+++
b/backend/plugins/sonarqube/models/migrationscripts/20260707_add_project_metrics_history.go
@@ -0,0 +1,77 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements. See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package migrationscripts
+
+import (
+ "time"
+
+ "github.com/apache/incubator-devlake/core/context"
+ "github.com/apache/incubator-devlake/core/errors"
+
"github.com/apache/incubator-devlake/core/models/migrationscripts/archived"
+ "github.com/apache/incubator-devlake/core/plugin"
+ "github.com/apache/incubator-devlake/helpers/migrationhelper"
+)
+
+var _ plugin.MigrationScript = (*addProjectMetricsHistory)(nil)
+
+type projectMetricsHistory20260707 struct {
+ ConnectionId uint64 `gorm:"primaryKey"`
+ ProjectKey string `gorm:"primaryKey;type:varchar(255)"`
+ AnalysisDate time.Time `gorm:"primaryKey"`
+ MetricKey string `gorm:"primaryKey;type:varchar(100)"`
+ MetricValue string `gorm:"type:varchar(50)"`
+ archived.NoPKModel
+}
+
+func (projectMetricsHistory20260707) TableName() string {
+ return "_tool_sonarqube_project_metrics_history"
+}
+
+type projectAnalyses20260707 struct {
+ ConnectionId uint64 `gorm:"primaryKey"`
+ ProjectKey string `gorm:"primaryKey;type:varchar(255)"`
+ AnalysisKey string `gorm:"primaryKey;type:varchar(255)"`
+ AnalysisDate time.Time `gorm:"index"`
+ ProjectVersion string `gorm:"type:varchar(255)"`
+ Revision string `gorm:"type:varchar(255)"`
+ BuildString string `gorm:"type:varchar(255)"`
+ DetectedCI string `gorm:"type:varchar(100)"`
+ archived.NoPKModel
+}
+
+func (projectAnalyses20260707) TableName() string {
+ return "_tool_sonarqube_project_analyses"
+}
+
+type addProjectMetricsHistory struct{}
+
+func (script *addProjectMetricsHistory) Up(basicRes context.BasicRes)
errors.Error {
+ return migrationhelper.AutoMigrateTables(
+ basicRes,
+ &projectMetricsHistory20260707{},
+ &projectAnalyses20260707{},
+ )
+}
+
+func (*addProjectMetricsHistory) Version() uint64 {
+ return 20260707153200
+}
+
+func (*addProjectMetricsHistory) Name() string {
+ return "add project_metrics_history and project_analyses tables"
+}
diff --git a/backend/plugins/sonarqube/models/migrationscripts/register.go
b/backend/plugins/sonarqube/models/migrationscripts/register.go
index 7c48de842..113341921 100644
--- a/backend/plugins/sonarqube/models/migrationscripts/register.go
+++ b/backend/plugins/sonarqube/models/migrationscripts/register.go
@@ -39,5 +39,6 @@ func All() []plugin.MigrationScript {
new(addOrgToConn),
new(addIssueImpacts),
new(extendSonarqubeFieldSize),
+ new(addProjectMetricsHistory),
}
}
diff --git a/backend/plugins/sonarqube/models/migrationscripts/register.go
b/backend/plugins/sonarqube/models/sonarqube_project_analyses.go
similarity index 51%
copy from backend/plugins/sonarqube/models/migrationscripts/register.go
copy to backend/plugins/sonarqube/models/sonarqube_project_analyses.go
index 7c48de842..55dac5ff7 100644
--- a/backend/plugins/sonarqube/models/migrationscripts/register.go
+++ b/backend/plugins/sonarqube/models/sonarqube_project_analyses.go
@@ -15,29 +15,26 @@ See the License for the specific language governing
permissions and
limitations under the License.
*/
-package migrationscripts
+package models
-import "github.com/apache/incubator-devlake/core/plugin"
+import (
+ "time"
-// All return all the migration scripts
-func All() []plugin.MigrationScript {
- return []plugin.MigrationScript{
- new(addInitTables),
- new(modifyCharacterSet),
- new(expandProjectKey20230206),
- new(addRawParamTableForScope),
- new(addScopeConfigIdToProject),
- new(modifyFileMetricsKeyLength),
- new(modifyComponentLength),
- new(addSonarQubeScopeConfig20231214),
- new(modifyCommitCharacterType),
- new(modifyCommitCharacterType0508),
- new(updateSonarQubeScopeConfig20240614),
- new(modifyNameLength),
- new(changeIssueComponentType),
- new(increaseProjectKeyLength),
- new(addOrgToConn),
- new(addIssueImpacts),
- new(extendSonarqubeFieldSize),
- }
+ "github.com/apache/incubator-devlake/core/models/common"
+)
+
+type SonarqubeProjectAnalysis struct {
+ ConnectionId uint64 `gorm:"primaryKey"`
+ ProjectKey string `gorm:"primaryKey;type:varchar(255)"`
+ AnalysisKey string `gorm:"primaryKey;type:varchar(255)"`
+ AnalysisDate time.Time `gorm:"index"`
+ ProjectVersion string `gorm:"type:varchar(255)"`
+ Revision string `gorm:"type:varchar(255)"`
+ BuildString string `gorm:"type:varchar(255)"`
+ DetectedCI string `gorm:"type:varchar(100)"`
+ common.NoPKModel
+}
+
+func (SonarqubeProjectAnalysis) TableName() string {
+ return "_tool_sonarqube_project_analyses"
}
diff --git a/backend/plugins/sonarqube/models/migrationscripts/register.go
b/backend/plugins/sonarqube/models/sonarqube_project_metrics_history.go
similarity index 51%
copy from backend/plugins/sonarqube/models/migrationscripts/register.go
copy to backend/plugins/sonarqube/models/sonarqube_project_metrics_history.go
index 7c48de842..ebd555252 100644
--- a/backend/plugins/sonarqube/models/migrationscripts/register.go
+++ b/backend/plugins/sonarqube/models/sonarqube_project_metrics_history.go
@@ -15,29 +15,23 @@ See the License for the specific language governing
permissions and
limitations under the License.
*/
-package migrationscripts
+package models
-import "github.com/apache/incubator-devlake/core/plugin"
+import (
+ "time"
-// All return all the migration scripts
-func All() []plugin.MigrationScript {
- return []plugin.MigrationScript{
- new(addInitTables),
- new(modifyCharacterSet),
- new(expandProjectKey20230206),
- new(addRawParamTableForScope),
- new(addScopeConfigIdToProject),
- new(modifyFileMetricsKeyLength),
- new(modifyComponentLength),
- new(addSonarQubeScopeConfig20231214),
- new(modifyCommitCharacterType),
- new(modifyCommitCharacterType0508),
- new(updateSonarQubeScopeConfig20240614),
- new(modifyNameLength),
- new(changeIssueComponentType),
- new(increaseProjectKeyLength),
- new(addOrgToConn),
- new(addIssueImpacts),
- new(extendSonarqubeFieldSize),
- }
+ "github.com/apache/incubator-devlake/core/models/common"
+)
+
+type SonarqubeProjectMetricsHistory struct {
+ ConnectionId uint64 `gorm:"primaryKey"`
+ ProjectKey string `gorm:"primaryKey;type:varchar(255)"`
+ AnalysisDate time.Time `gorm:"primaryKey"`
+ MetricKey string `gorm:"primaryKey;type:varchar(100)"`
+ MetricValue string `gorm:"type:varchar(50)"`
+ common.NoPKModel
+}
+
+func (SonarqubeProjectMetricsHistory) TableName() string {
+ return "_tool_sonarqube_project_metrics_history"
}
diff --git a/backend/plugins/sonarqube/tasks/project_analyses_collector.go
b/backend/plugins/sonarqube/tasks/project_analyses_collector.go
new file mode 100644
index 000000000..37682fd7c
--- /dev/null
+++ b/backend/plugins/sonarqube/tasks/project_analyses_collector.go
@@ -0,0 +1,92 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements. See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package tasks
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "net/url"
+
+ "github.com/apache/incubator-devlake/core/errors"
+ "github.com/apache/incubator-devlake/core/plugin"
+ helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
+ "github.com/apache/incubator-devlake/plugins/sonarqube/models"
+)
+
+const RAW_PROJECT_ANALYSES_TABLE = "sonarqube_api_project_analyses"
+
+var _ plugin.SubTaskEntryPoint = CollectProjectAnalyses
+
+func CollectProjectAnalyses(taskCtx plugin.SubTaskContext) errors.Error {
+ logger := taskCtx.GetLogger()
+ logger.Info("collect project analyses")
+
+ data := taskCtx.GetData().(*SonarqubeTaskData)
+ apiCollector, err :=
helper.NewStatefulApiCollector(helper.RawDataSubTaskArgs{
+ Ctx: taskCtx,
+ Params: models.SonarqubeApiParams{
+ ConnectionId: data.Options.ConnectionId,
+ ProjectKey: data.Options.ProjectKey,
+ },
+ Table: RAW_PROJECT_ANALYSES_TABLE,
+ })
+ if err != nil {
+ return err
+ }
+
+ err = apiCollector.InitCollector(helper.ApiCollectorArgs{
+ ApiClient: data.ApiClient,
+ PageSize: 500,
+ UrlTemplate: "project_analyses/search",
+ Query: func(reqData *helper.RequestData) (url.Values,
errors.Error) {
+ query := url.Values{}
+ query.Set("project", data.Options.ProjectKey)
+ query.Set("ps", fmt.Sprintf("%v", reqData.Pager.Size))
+ query.Set("p", fmt.Sprintf("%v", reqData.Pager.Page))
+ if apiCollector.GetSince() != nil {
+ query.Set("from",
apiCollector.GetSince().UTC().Format("2006-01-02"))
+ }
+ return query, nil
+ },
+ GetTotalPages: GetTotalPagesFromResponse,
+ ResponseParser: func(res *http.Response) ([]json.RawMessage,
errors.Error) {
+ var resData struct {
+ Analyses []json.RawMessage `json:"analyses"`
+ }
+ err := helper.UnmarshalResponse(res, &resData)
+ if err != nil {
+ return nil, err
+ }
+ return resData.Analyses, nil
+ },
+ })
+ if err != nil {
+ return err
+ }
+
+ return apiCollector.Execute()
+}
+
+var CollectProjectAnalysesMeta = plugin.SubTaskMeta{
+ Name: "CollectProjectAnalyses",
+ EntryPoint: CollectProjectAnalyses,
+ EnabledByDefault: true,
+ Description: "Collect project analysis metadata from SonarQube
project_analyses/search API",
+ DomainTypes: []string{plugin.DOMAIN_TYPE_CODE_QUALITY},
+}
diff --git a/backend/plugins/sonarqube/tasks/project_analyses_extractor.go
b/backend/plugins/sonarqube/tasks/project_analyses_extractor.go
new file mode 100644
index 000000000..ac10d8d6e
--- /dev/null
+++ b/backend/plugins/sonarqube/tasks/project_analyses_extractor.go
@@ -0,0 +1,84 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements. See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package tasks
+
+import (
+ "encoding/json"
+ "time"
+
+ "github.com/apache/incubator-devlake/core/errors"
+ "github.com/apache/incubator-devlake/core/plugin"
+ helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
+ "github.com/apache/incubator-devlake/plugins/sonarqube/models"
+)
+
+var _ plugin.SubTaskEntryPoint = ExtractProjectAnalyses
+
+type projectAnalysisResponse struct {
+ Key string `json:"key"`
+ Date string `json:"date"`
+ ProjectVersion string `json:"projectVersion"`
+ Revision string `json:"revision"`
+ BuildString string `json:"buildString"`
+ DetectedCI string `json:"detectedCI"`
+}
+
+func ExtractProjectAnalyses(taskCtx plugin.SubTaskContext) errors.Error {
+ rawDataSubTaskArgs, data := CreateRawDataSubTaskArgs(taskCtx,
RAW_PROJECT_ANALYSES_TABLE)
+
+ extractor, err := helper.NewApiExtractor(helper.ApiExtractorArgs{
+ RawDataSubTaskArgs: *rawDataSubTaskArgs,
+ Extract: func(resData *helper.RawData) ([]interface{},
errors.Error) {
+ body := &projectAnalysisResponse{}
+ err := errors.Convert(json.Unmarshal(resData.Data,
body))
+ if err != nil {
+ return nil, err
+ }
+
+ analysisDate, parseErr :=
time.Parse("2006-01-02T15:04:05-0700", body.Date)
+ if parseErr != nil {
+ return nil,
errors.Default.Wrap(errors.Convert(parseErr), "failed to parse analysis date")
+ }
+
+ analysis := &models.SonarqubeProjectAnalysis{
+ ConnectionId: data.Options.ConnectionId,
+ ProjectKey: data.Options.ProjectKey,
+ AnalysisKey: body.Key,
+ AnalysisDate: analysisDate,
+ ProjectVersion: body.ProjectVersion,
+ Revision: body.Revision,
+ BuildString: body.BuildString,
+ DetectedCI: body.DetectedCI,
+ }
+ return []interface{}{analysis}, nil
+ },
+ })
+ if err != nil {
+ return err
+ }
+
+ return extractor.Execute()
+}
+
+var ExtractProjectAnalysesMeta = plugin.SubTaskMeta{
+ Name: "ExtractProjectAnalyses",
+ EntryPoint: ExtractProjectAnalyses,
+ EnabledByDefault: true,
+ Description: "Extract raw project analyses data into tool layer
table",
+ DomainTypes: []string{plugin.DOMAIN_TYPE_CODE_QUALITY},
+}
diff --git
a/backend/plugins/sonarqube/tasks/project_metrics_history_collector.go
b/backend/plugins/sonarqube/tasks/project_metrics_history_collector.go
new file mode 100644
index 000000000..fa2b02633
--- /dev/null
+++ b/backend/plugins/sonarqube/tasks/project_metrics_history_collector.go
@@ -0,0 +1,94 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements. See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package tasks
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "net/url"
+
+ "github.com/apache/incubator-devlake/core/errors"
+ "github.com/apache/incubator-devlake/core/plugin"
+ helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
+ "github.com/apache/incubator-devlake/plugins/sonarqube/models"
+)
+
+const RAW_PROJECT_METRICS_HISTORY_TABLE =
"sonarqube_api_project_metrics_history"
+
+const metricsToCollect =
"coverage,ncloc,bugs,vulnerabilities,code_smells,security_hotspots," +
+
"duplicated_lines_density,sqale_rating,reliability_rating,security_rating,complexity,cognitive_complexity"
+
+var _ plugin.SubTaskEntryPoint = CollectProjectMetricsHistory
+
+func CollectProjectMetricsHistory(taskCtx plugin.SubTaskContext) errors.Error {
+ logger := taskCtx.GetLogger()
+ logger.Info("collect project metrics history")
+
+ data := taskCtx.GetData().(*SonarqubeTaskData)
+ apiCollector, err :=
helper.NewStatefulApiCollector(helper.RawDataSubTaskArgs{
+ Ctx: taskCtx,
+ Params: models.SonarqubeApiParams{
+ ConnectionId: data.Options.ConnectionId,
+ ProjectKey: data.Options.ProjectKey,
+ },
+ Table: RAW_PROJECT_METRICS_HISTORY_TABLE,
+ })
+ if err != nil {
+ return err
+ }
+
+ err = apiCollector.InitCollector(helper.ApiCollectorArgs{
+ ApiClient: data.ApiClient,
+ PageSize: 1000,
+ UrlTemplate: "measures/search_history",
+ Query: func(reqData *helper.RequestData) (url.Values,
errors.Error) {
+ query := url.Values{}
+ query.Set("component", data.Options.ProjectKey)
+ query.Set("metrics", metricsToCollect)
+ query.Set("ps", fmt.Sprintf("%v", reqData.Pager.Size))
+ query.Set("p", fmt.Sprintf("%v", reqData.Pager.Page))
+ if apiCollector.GetSince() != nil {
+ query.Set("from",
apiCollector.GetSince().UTC().Format("2006-01-02"))
+ }
+ return query, nil
+ },
+ GetTotalPages: GetTotalPagesFromResponse,
+ ResponseParser: func(res *http.Response) ([]json.RawMessage,
errors.Error) {
+ var body json.RawMessage
+ err := helper.UnmarshalResponse(res, &body)
+ if err != nil {
+ return nil, err
+ }
+ return []json.RawMessage{body}, nil
+ },
+ })
+ if err != nil {
+ return err
+ }
+
+ return apiCollector.Execute()
+}
+
+var CollectProjectMetricsHistoryMeta = plugin.SubTaskMeta{
+ Name: "CollectProjectMetricsHistory",
+ EntryPoint: CollectProjectMetricsHistory,
+ EnabledByDefault: true,
+ Description: "Collect project-level metric history from SonarQube
measures/search_history API",
+ DomainTypes: []string{plugin.DOMAIN_TYPE_CODE_QUALITY},
+}
diff --git
a/backend/plugins/sonarqube/tasks/project_metrics_history_convertor.go
b/backend/plugins/sonarqube/tasks/project_metrics_history_convertor.go
new file mode 100644
index 000000000..bf59aa1fe
--- /dev/null
+++ b/backend/plugins/sonarqube/tasks/project_metrics_history_convertor.go
@@ -0,0 +1,158 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements. See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package tasks
+
+import (
+ "fmt"
+ "reflect"
+ "strconv"
+ "time"
+
+ "github.com/apache/incubator-devlake/core/dal"
+ "github.com/apache/incubator-devlake/core/errors"
+ "github.com/apache/incubator-devlake/core/models/domainlayer"
+
"github.com/apache/incubator-devlake/core/models/domainlayer/codequality"
+ "github.com/apache/incubator-devlake/core/models/domainlayer/didgen"
+ "github.com/apache/incubator-devlake/core/plugin"
+ helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
+ sonarqubeModels
"github.com/apache/incubator-devlake/plugins/sonarqube/models"
+)
+
+var ConvertProjectMetricsHistoryMeta = plugin.SubTaskMeta{
+ Name: "convertProjectMetricsHistory",
+ EntryPoint: ConvertProjectMetricsHistory,
+ EnabledByDefault: true,
+ Description: "Convert tool layer project metrics history into
domain layer table cq_project_metrics_history",
+ DomainTypes: []string{plugin.DOMAIN_TYPE_CODE_QUALITY},
+}
+
+func ConvertProjectMetricsHistory(taskCtx plugin.SubTaskContext) errors.Error {
+ db := taskCtx.GetDal()
+ _, data := CreateRawDataSubTaskArgs(taskCtx,
RAW_PROJECT_METRICS_HISTORY_TABLE)
+
+ // Query all narrow metric rows ordered so we can group-pivot them
+ cursor, err := db.Cursor(
+ dal.From(sonarqubeModels.SonarqubeProjectMetricsHistory{}),
+ dal.Where("connection_id = ? AND project_key = ?",
data.Options.ConnectionId, data.Options.ProjectKey),
+ dal.Orderby("analysis_date"),
+ )
+ if err != nil {
+ return err
+ }
+ defer cursor.Close()
+
+ projectIdGen :=
didgen.NewDomainIdGenerator(&sonarqubeModels.SonarqubeProject{})
+ domainProjectKey := projectIdGen.Generate(data.Options.ConnectionId,
data.Options.ProjectKey)
+
+ batchSave, err := helper.NewBatchSave(taskCtx,
reflect.TypeOf(&codequality.CqProjectMetricsHistory{}), 200)
+ if err != nil {
+ return err
+ }
+ defer batchSave.Close()
+
+ // Group narrow rows by analysis_date, pivot into wide domain rows
+ var currentDate *time.Time
+ var currentDomain *codequality.CqProjectMetricsHistory
+
+ flushCurrent := func() errors.Error {
+ if currentDomain != nil {
+ return batchSave.Add(currentDomain)
+ }
+ return nil
+ }
+
+ for cursor.Next() {
+ row := &sonarqubeModels.SonarqubeProjectMetricsHistory{}
+ err = db.Fetch(cursor, row)
+ if err != nil {
+ return err
+ }
+
+ if currentDate == nil || !currentDate.Equal(row.AnalysisDate) {
+ if flushErr := flushCurrent(); flushErr != nil {
+ return flushErr
+ }
+ domainId := fmt.Sprintf("%s:%s",
+ domainProjectKey,
+
row.AnalysisDate.UTC().Format("2006-01-02T15:04:05Z"),
+ )
+ currentDomain = &codequality.CqProjectMetricsHistory{
+ DomainEntity: domainlayer.DomainEntity{Id:
domainId},
+ ProjectKey: domainProjectKey,
+ AnalysisDate: row.AnalysisDate,
+ }
+ t := row.AnalysisDate
+ currentDate = &t
+ }
+
+ applyMetricValue(currentDomain, row.MetricKey, row.MetricValue)
+ }
+
+ if flushErr := flushCurrent(); flushErr != nil {
+ return flushErr
+ }
+
+ return batchSave.Close()
+}
+
+func applyMetricValue(d *codequality.CqProjectMetricsHistory, metricKey, value
string) {
+ switch metricKey {
+ case "coverage":
+ if v, err := strconv.ParseFloat(value, 64); err == nil {
+ d.Coverage = &v
+ }
+ case "ncloc":
+ if v, err := strconv.Atoi(value); err == nil {
+ d.Ncloc = &v
+ }
+ case "bugs":
+ if v, err := strconv.Atoi(value); err == nil {
+ d.Bugs = &v
+ }
+ case "reliability_rating":
+ d.ReliabilityRating = alphabetMap[value]
+ case "code_smells":
+ if v, err := strconv.Atoi(value); err == nil {
+ d.CodeSmells = &v
+ }
+ case "sqale_rating":
+ d.SqaleRating = alphabetMap[value]
+ case "complexity":
+ if v, err := strconv.Atoi(value); err == nil {
+ d.Complexity = &v
+ }
+ case "cognitive_complexity":
+ if v, err := strconv.Atoi(value); err == nil {
+ d.CognitiveComplexity = &v
+ }
+ case "vulnerabilities":
+ if v, err := strconv.Atoi(value); err == nil {
+ d.Vulnerabilities = &v
+ }
+ case "security_rating":
+ d.SecurityRating = alphabetMap[value]
+ case "security_hotspots":
+ if v, err := strconv.Atoi(value); err == nil {
+ d.SecurityHotspots = &v
+ }
+ case "duplicated_lines_density":
+ if v, err := strconv.ParseFloat(value, 64); err == nil {
+ d.DuplicatedLinesDensity = &v
+ }
+ }
+}
diff --git
a/backend/plugins/sonarqube/tasks/project_metrics_history_extractor.go
b/backend/plugins/sonarqube/tasks/project_metrics_history_extractor.go
new file mode 100644
index 000000000..430ff196b
--- /dev/null
+++ b/backend/plugins/sonarqube/tasks/project_metrics_history_extractor.go
@@ -0,0 +1,89 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements. See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package tasks
+
+import (
+ "encoding/json"
+ "time"
+
+ "github.com/apache/incubator-devlake/core/errors"
+ "github.com/apache/incubator-devlake/core/plugin"
+ helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
+ "github.com/apache/incubator-devlake/plugins/sonarqube/models"
+)
+
+var _ plugin.SubTaskEntryPoint = ExtractProjectMetricsHistory
+
+type metricsHistoryResponse struct {
+ Measures []struct {
+ Metric string `json:"metric"`
+ History []struct {
+ Date string `json:"date"`
+ Value string `json:"value"`
+ } `json:"history"`
+ } `json:"measures"`
+}
+
+func ExtractProjectMetricsHistory(taskCtx plugin.SubTaskContext) errors.Error {
+ rawDataSubTaskArgs, data := CreateRawDataSubTaskArgs(taskCtx,
RAW_PROJECT_METRICS_HISTORY_TABLE)
+
+ extractor, err := helper.NewApiExtractor(helper.ApiExtractorArgs{
+ RawDataSubTaskArgs: *rawDataSubTaskArgs,
+ Extract: func(resData *helper.RawData) ([]interface{},
errors.Error) {
+ body := &metricsHistoryResponse{}
+ err := errors.Convert(json.Unmarshal(resData.Data,
body))
+ if err != nil {
+ return nil, err
+ }
+
+ var results []interface{}
+ for _, measure := range body.Measures {
+ for _, entry := range measure.History {
+ if entry.Value == "" {
+ continue
+ }
+ analysisDate, parseErr :=
time.Parse("2006-01-02T15:04:05-0700", entry.Date)
+ if parseErr != nil {
+ return nil,
errors.Default.Wrap(errors.Convert(parseErr), "failed to parse analysis date")
+ }
+ results = append(results,
&models.SonarqubeProjectMetricsHistory{
+ ConnectionId:
data.Options.ConnectionId,
+ ProjectKey:
data.Options.ProjectKey,
+ AnalysisDate: analysisDate,
+ MetricKey: measure.Metric,
+ MetricValue: entry.Value,
+ })
+ }
+ }
+ return results, nil
+ },
+ })
+ if err != nil {
+ return err
+ }
+
+ return extractor.Execute()
+}
+
+var ExtractProjectMetricsHistoryMeta = plugin.SubTaskMeta{
+ Name: "ExtractProjectMetricsHistory",
+ EntryPoint: ExtractProjectMetricsHistory,
+ EnabledByDefault: true,
+ Description: "Extract raw project metrics history into tool layer
table",
+ DomainTypes: []string{plugin.DOMAIN_TYPE_CODE_QUALITY},
+}
diff --git a/grafana/dashboards/mysql/Sonarqube.json
b/grafana/dashboards/mysql/Sonarqube.json
index d8708f36d..2dac28c9f 100644
--- a/grafana/dashboards/mysql/Sonarqube.json
+++ b/grafana/dashboards/mysql/Sonarqube.json
@@ -46,7 +46,7 @@
"showLineNumbers": false,
"showMiniMap": false
},
- "content": "- Use Cases: This dashboard shows the code quality metrics
from SonarQube.\n- Data Source Required: SonarQube v8.2+\n- This dashboard does
not honor the time filter on the top-right side as SonarQube metrics are all
from the latest scan.",
+ "content": "- Use Cases: This dashboard shows the code quality metrics
from SonarQube.\n- Data Source Required: SonarQube v8.2+\n- Snapshot panels
(Reliability, Security, Test, etc.) show the latest scan and do not honor the
time filter.\n- **Historical Trends** panels at the bottom respond to the time
range selector and show metric history over time.",
"mode": "markdown"
},
"pluginVersion": "11.2.0",
@@ -1023,6 +1023,263 @@
],
"title": "Code Quality Metrics by Files (Top 20 order by Bugs)",
"type": "table"
+ },
+ {
+ "collapsed": true,
+ "datasource": {
+ "type": "datasource",
+ "uid": "grafana"
+ },
+ "gridPos": {
+ "h": 1,
+ "w": 24,
+ "x": 0,
+ "y": 30
+ },
+ "id": 20,
+ "panels": [
+ {
+ "datasource": "mysql",
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "custom": {
+ "lineWidth": 2,
+ "fillOpacity": 10,
+ "spanNulls": true,
+ "axisBorderShow": false,
+ "axisCenteredZero": false,
+ "axisColorMode": "text",
+ "axisLabel": "",
+ "scaleDistribution": {
+ "type": "linear"
+ },
+ "showPoints": "auto",
+ "pointSize": 5
+ },
+ "unit": "percent"
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 0,
+ "y": 31
+ },
+ "id": 21,
+ "options": {
+ "legend": {
+ "calcs": ["lastNotNull"],
+ "displayMode": "list",
+ "placement": "bottom",
+ "showLegend": true
+ },
+ "tooltip": {
+ "mode": "multi",
+ "sort": "none"
+ }
+ },
+ "pluginVersion": "11.2.0",
+ "targets": [
+ {
+ "datasource": "mysql",
+ "editorMode": "code",
+ "format": "time_series",
+ "rawQuery": true,
+ "rawSql": "SELECT\n pmh.analysis_date AS time,\n pmh.coverage
AS 'Coverage %'\nFROM cq_project_metrics_history pmh\nWHERE\n pmh.project_key
in (${project_id})\n AND $__timeFilter(pmh.analysis_date)\nORDER BY
pmh.analysis_date ASC",
+ "refId": "A"
+ }
+ ],
+ "title": "Coverage Trend",
+ "type": "timeseries"
+ },
+ {
+ "datasource": "mysql",
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "custom": {
+ "lineWidth": 2,
+ "fillOpacity": 10,
+ "spanNulls": true,
+ "axisBorderShow": false,
+ "axisCenteredZero": false,
+ "axisColorMode": "text",
+ "axisLabel": "",
+ "scaleDistribution": {
+ "type": "linear"
+ },
+ "showPoints": "auto",
+ "pointSize": 5
+ }
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 12,
+ "y": 31
+ },
+ "id": 22,
+ "options": {
+ "legend": {
+ "calcs": ["lastNotNull"],
+ "displayMode": "list",
+ "placement": "bottom",
+ "showLegend": true
+ },
+ "tooltip": {
+ "mode": "multi",
+ "sort": "none"
+ }
+ },
+ "pluginVersion": "11.2.0",
+ "targets": [
+ {
+ "datasource": "mysql",
+ "editorMode": "code",
+ "format": "time_series",
+ "rawQuery": true,
+ "rawSql": "SELECT\n pmh.analysis_date AS time,\n pmh.bugs AS
'Bugs',\n pmh.vulnerabilities AS 'Vulnerabilities'\nFROM
cq_project_metrics_history pmh\nWHERE\n pmh.project_key in (${project_id})\n
AND $__timeFilter(pmh.analysis_date)\nORDER BY pmh.analysis_date ASC",
+ "refId": "A"
+ }
+ ],
+ "title": "Bugs & Vulnerabilities Trend",
+ "type": "timeseries"
+ },
+ {
+ "datasource": "mysql",
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "custom": {
+ "lineWidth": 2,
+ "fillOpacity": 10,
+ "spanNulls": true,
+ "axisBorderShow": false,
+ "axisCenteredZero": false,
+ "axisColorMode": "text",
+ "axisLabel": "",
+ "scaleDistribution": {
+ "type": "linear"
+ },
+ "showPoints": "auto",
+ "pointSize": 5
+ }
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 0,
+ "y": 39
+ },
+ "id": 23,
+ "options": {
+ "legend": {
+ "calcs": ["lastNotNull"],
+ "displayMode": "list",
+ "placement": "bottom",
+ "showLegend": true
+ },
+ "tooltip": {
+ "mode": "multi",
+ "sort": "none"
+ }
+ },
+ "pluginVersion": "11.2.0",
+ "targets": [
+ {
+ "datasource": "mysql",
+ "editorMode": "code",
+ "format": "time_series",
+ "rawQuery": true,
+ "rawSql": "SELECT\n pmh.analysis_date AS time,\n
pmh.code_smells AS 'Code Smells'\nFROM cq_project_metrics_history pmh\nWHERE\n
pmh.project_key in (${project_id})\n AND
$__timeFilter(pmh.analysis_date)\nORDER BY pmh.analysis_date ASC",
+ "refId": "A"
+ }
+ ],
+ "title": "Code Smells Trend",
+ "type": "timeseries"
+ },
+ {
+ "datasource": "mysql",
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "custom": {
+ "lineWidth": 2,
+ "fillOpacity": 10,
+ "spanNulls": true,
+ "axisBorderShow": false,
+ "axisCenteredZero": false,
+ "axisColorMode": "text",
+ "axisLabel": "",
+ "scaleDistribution": {
+ "type": "linear"
+ },
+ "showPoints": "auto",
+ "pointSize": 5
+ },
+ "unit": "percent"
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 12,
+ "y": 39
+ },
+ "id": 24,
+ "options": {
+ "legend": {
+ "calcs": ["lastNotNull"],
+ "displayMode": "list",
+ "placement": "bottom",
+ "showLegend": true
+ },
+ "tooltip": {
+ "mode": "multi",
+ "sort": "none"
+ }
+ },
+ "pluginVersion": "11.2.0",
+ "targets": [
+ {
+ "datasource": "mysql",
+ "editorMode": "code",
+ "format": "time_series",
+ "rawQuery": true,
+ "rawSql": "SELECT\n pmh.analysis_date AS time,\n
pmh.duplicated_lines_density AS 'Duplication %'\nFROM
cq_project_metrics_history pmh\nWHERE\n pmh.project_key in (${project_id})\n
AND $__timeFilter(pmh.analysis_date)\nORDER BY pmh.analysis_date ASC",
+ "refId": "A"
+ }
+ ],
+ "title": "Duplication Trend",
+ "type": "timeseries"
+ }
+ ],
+ "targets": [
+ {
+ "datasource": {
+ "type": "datasource",
+ "uid": "grafana"
+ },
+ "refId": "A"
+ }
+ ],
+ "title": "Historical Trends",
+ "type": "row"
}
],
"refresh": "",
@@ -1086,7 +1343,7 @@
]
},
"time": {
- "from": "now",
+ "from": "now-6M",
"to": "now"
},
"timepicker": {},
diff --git a/grafana/dashboards/mysql/sonar-qube-cloud.json
b/grafana/dashboards/mysql/sonar-qube-cloud.json
index 1a2907038..8cb1939a5 100644
--- a/grafana/dashboards/mysql/sonar-qube-cloud.json
+++ b/grafana/dashboards/mysql/sonar-qube-cloud.json
@@ -47,7 +47,7 @@
"showLineNumbers": false,
"showMiniMap": false
},
- "content": "- Use Cases: This dashboard shows the code quality metrics
from SonarCloud.\n- Data Source Required: SonarCloud\n- This dashboard does not
honor the time filter on the top-right side as SonarQube metrics are all from
the latest scan.",
+ "content": "- Use Cases: This dashboard shows the code quality metrics
from SonarCloud.\n- Data Source Required: SonarCloud\n- Snapshot panels
(Software Quality, Security Review, Test, etc.) show the latest scan and do not
honor the time filter.\n- **Historical Trends** panels at the bottom respond to
the time range selector and show metric history over time.",
"mode": "markdown"
},
"pluginVersion": "11.2.0",
@@ -1428,6 +1428,263 @@
],
"title": "Code Quality Metrics by Files (Top 20 order by Bugs)",
"type": "table"
+ },
+ {
+ "collapsed": true,
+ "datasource": {
+ "type": "datasource",
+ "uid": "grafana"
+ },
+ "gridPos": {
+ "h": 1,
+ "w": 24,
+ "x": 0,
+ "y": 44
+ },
+ "id": 27,
+ "panels": [
+ {
+ "datasource": "mysql",
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "custom": {
+ "lineWidth": 2,
+ "fillOpacity": 10,
+ "spanNulls": true,
+ "axisBorderShow": false,
+ "axisCenteredZero": false,
+ "axisColorMode": "text",
+ "axisLabel": "",
+ "scaleDistribution": {
+ "type": "linear"
+ },
+ "showPoints": "auto",
+ "pointSize": 5
+ },
+ "unit": "percent"
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 0,
+ "y": 45
+ },
+ "id": 28,
+ "options": {
+ "legend": {
+ "calcs": ["lastNotNull"],
+ "displayMode": "list",
+ "placement": "bottom",
+ "showLegend": true
+ },
+ "tooltip": {
+ "mode": "multi",
+ "sort": "none"
+ }
+ },
+ "pluginVersion": "11.2.0",
+ "targets": [
+ {
+ "datasource": "mysql",
+ "editorMode": "code",
+ "format": "time_series",
+ "rawQuery": true,
+ "rawSql": "SELECT\n pmh.analysis_date AS time,\n pmh.coverage
AS 'Coverage %'\nFROM cq_project_metrics_history pmh\nWHERE\n pmh.project_key
in (${project_id})\n AND $__timeFilter(pmh.analysis_date)\nORDER BY
pmh.analysis_date ASC",
+ "refId": "A"
+ }
+ ],
+ "title": "Coverage Trend",
+ "type": "timeseries"
+ },
+ {
+ "datasource": "mysql",
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "custom": {
+ "lineWidth": 2,
+ "fillOpacity": 10,
+ "spanNulls": true,
+ "axisBorderShow": false,
+ "axisCenteredZero": false,
+ "axisColorMode": "text",
+ "axisLabel": "",
+ "scaleDistribution": {
+ "type": "linear"
+ },
+ "showPoints": "auto",
+ "pointSize": 5
+ }
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 12,
+ "y": 45
+ },
+ "id": 29,
+ "options": {
+ "legend": {
+ "calcs": ["lastNotNull"],
+ "displayMode": "list",
+ "placement": "bottom",
+ "showLegend": true
+ },
+ "tooltip": {
+ "mode": "multi",
+ "sort": "none"
+ }
+ },
+ "pluginVersion": "11.2.0",
+ "targets": [
+ {
+ "datasource": "mysql",
+ "editorMode": "code",
+ "format": "time_series",
+ "rawQuery": true,
+ "rawSql": "SELECT\n pmh.analysis_date AS time,\n pmh.bugs AS
'Bugs',\n pmh.vulnerabilities AS 'Vulnerabilities'\nFROM
cq_project_metrics_history pmh\nWHERE\n pmh.project_key in (${project_id})\n
AND $__timeFilter(pmh.analysis_date)\nORDER BY pmh.analysis_date ASC",
+ "refId": "A"
+ }
+ ],
+ "title": "Bugs & Vulnerabilities Trend",
+ "type": "timeseries"
+ },
+ {
+ "datasource": "mysql",
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "custom": {
+ "lineWidth": 2,
+ "fillOpacity": 10,
+ "spanNulls": true,
+ "axisBorderShow": false,
+ "axisCenteredZero": false,
+ "axisColorMode": "text",
+ "axisLabel": "",
+ "scaleDistribution": {
+ "type": "linear"
+ },
+ "showPoints": "auto",
+ "pointSize": 5
+ }
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 0,
+ "y": 53
+ },
+ "id": 30,
+ "options": {
+ "legend": {
+ "calcs": ["lastNotNull"],
+ "displayMode": "list",
+ "placement": "bottom",
+ "showLegend": true
+ },
+ "tooltip": {
+ "mode": "multi",
+ "sort": "none"
+ }
+ },
+ "pluginVersion": "11.2.0",
+ "targets": [
+ {
+ "datasource": "mysql",
+ "editorMode": "code",
+ "format": "time_series",
+ "rawQuery": true,
+ "rawSql": "SELECT\n pmh.analysis_date AS time,\n
pmh.code_smells AS 'Code Smells'\nFROM cq_project_metrics_history pmh\nWHERE\n
pmh.project_key in (${project_id})\n AND
$__timeFilter(pmh.analysis_date)\nORDER BY pmh.analysis_date ASC",
+ "refId": "A"
+ }
+ ],
+ "title": "Code Smells Trend",
+ "type": "timeseries"
+ },
+ {
+ "datasource": "mysql",
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "custom": {
+ "lineWidth": 2,
+ "fillOpacity": 10,
+ "spanNulls": true,
+ "axisBorderShow": false,
+ "axisCenteredZero": false,
+ "axisColorMode": "text",
+ "axisLabel": "",
+ "scaleDistribution": {
+ "type": "linear"
+ },
+ "showPoints": "auto",
+ "pointSize": 5
+ },
+ "unit": "percent"
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 12,
+ "y": 53
+ },
+ "id": 31,
+ "options": {
+ "legend": {
+ "calcs": ["lastNotNull"],
+ "displayMode": "list",
+ "placement": "bottom",
+ "showLegend": true
+ },
+ "tooltip": {
+ "mode": "multi",
+ "sort": "none"
+ }
+ },
+ "pluginVersion": "11.2.0",
+ "targets": [
+ {
+ "datasource": "mysql",
+ "editorMode": "code",
+ "format": "time_series",
+ "rawQuery": true,
+ "rawSql": "SELECT\n pmh.analysis_date AS time,\n
pmh.duplicated_lines_density AS 'Duplication %'\nFROM
cq_project_metrics_history pmh\nWHERE\n pmh.project_key in (${project_id})\n
AND $__timeFilter(pmh.analysis_date)\nORDER BY pmh.analysis_date ASC",
+ "refId": "A"
+ }
+ ],
+ "title": "Duplication Trend",
+ "type": "timeseries"
+ }
+ ],
+ "targets": [
+ {
+ "datasource": {
+ "type": "datasource",
+ "uid": "grafana"
+ },
+ "refId": "A"
+ }
+ ],
+ "title": "Historical Trends",
+ "type": "row"
}
],
"refresh": "",
@@ -1491,7 +1748,7 @@
]
},
"time": {
- "from": "now",
+ "from": "now-6M",
"to": "now"
},
"timepicker": {},
diff --git a/grafana/dashboards/postgresql/Sonarqube.json
b/grafana/dashboards/postgresql/Sonarqube.json
index 02d9ccbc2..1bae97b37 100644
--- a/grafana/dashboards/postgresql/Sonarqube.json
+++ b/grafana/dashboards/postgresql/Sonarqube.json
@@ -46,7 +46,7 @@
"showLineNumbers": false,
"showMiniMap": false
},
- "content": "- Use Cases: This dashboard shows the code quality metrics
from SonarQube.\n- Data Source Required: SonarQube v8.2+\n- This dashboard does
not honor the time filter on the top-right side as SonarQube metrics are all
from the latest scan.",
+ "content": "- Use Cases: This dashboard shows the code quality metrics
from SonarQube.\n- Data Source Required: SonarQube v8.2+\n- Snapshot panels
(Reliability, Security, Test, etc.) show the latest scan and do not honor the
time filter.\n- **Historical Trends** panels at the bottom respond to the time
range selector and show metric history over time.",
"mode": "markdown"
},
"pluginVersion": "11.2.0",
@@ -1023,6 +1023,271 @@
],
"title": "Code Quality Metrics by Files (Top 20 order by Bugs)",
"type": "table"
+ },
+ {
+ "collapsed": true,
+ "datasource": {
+ "type": "datasource",
+ "uid": "grafana"
+ },
+ "gridPos": {
+ "h": 1,
+ "w": 24,
+ "x": 0,
+ "y": 30
+ },
+ "id": 20,
+ "panels": [
+ {
+ "datasource": "postgresql",
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "custom": {
+ "lineWidth": 2,
+ "fillOpacity": 10,
+ "spanNulls": true,
+ "axisBorderShow": false,
+ "axisCenteredZero": false,
+ "axisColorMode": "text",
+ "axisLabel": "",
+ "scaleDistribution": {
+ "type": "linear"
+ },
+ "showPoints": "auto",
+ "pointSize": 5
+ },
+ "unit": "percent"
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 0,
+ "y": 31
+ },
+ "id": 21,
+ "options": {
+ "legend": {
+ "calcs": [
+ "lastNotNull"
+ ],
+ "displayMode": "list",
+ "placement": "bottom",
+ "showLegend": true
+ },
+ "tooltip": {
+ "mode": "multi",
+ "sort": "none"
+ }
+ },
+ "pluginVersion": "11.2.0",
+ "targets": [
+ {
+ "datasource": "postgresql",
+ "editorMode": "code",
+ "format": "time_series",
+ "rawQuery": true,
+ "rawSql": "SELECT pmh.analysis_date AS time, pmh.coverage AS
\"Coverage %\" FROM cq_project_metrics_history AS pmh WHERE
('${project_id:csv}' = '' OR pmh.project_key::text =
ANY(ARRAY[${project_id:singlequote}]::text[])) AND
$__timeFilter(pmh.analysis_date) ORDER BY pmh.analysis_date ASC NULLS FIRST",
+ "refId": "A"
+ }
+ ],
+ "title": "Coverage Trend",
+ "type": "timeseries"
+ },
+ {
+ "datasource": "postgresql",
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "custom": {
+ "lineWidth": 2,
+ "fillOpacity": 10,
+ "spanNulls": true,
+ "axisBorderShow": false,
+ "axisCenteredZero": false,
+ "axisColorMode": "text",
+ "axisLabel": "",
+ "scaleDistribution": {
+ "type": "linear"
+ },
+ "showPoints": "auto",
+ "pointSize": 5
+ }
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 12,
+ "y": 31
+ },
+ "id": 22,
+ "options": {
+ "legend": {
+ "calcs": [
+ "lastNotNull"
+ ],
+ "displayMode": "list",
+ "placement": "bottom",
+ "showLegend": true
+ },
+ "tooltip": {
+ "mode": "multi",
+ "sort": "none"
+ }
+ },
+ "pluginVersion": "11.2.0",
+ "targets": [
+ {
+ "datasource": "postgresql",
+ "editorMode": "code",
+ "format": "time_series",
+ "rawQuery": true,
+ "rawSql": "SELECT pmh.analysis_date AS time, pmh.bugs AS
\"Bugs\", pmh.vulnerabilities AS \"Vulnerabilities\" FROM
cq_project_metrics_history AS pmh WHERE ('${project_id:csv}' = '' OR
pmh.project_key::text = ANY(ARRAY[${project_id:singlequote}]::text[])) AND
$__timeFilter(pmh.analysis_date) ORDER BY pmh.analysis_date ASC NULLS FIRST",
+ "refId": "A"
+ }
+ ],
+ "title": "Bugs & Vulnerabilities Trend",
+ "type": "timeseries"
+ },
+ {
+ "datasource": "postgresql",
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "custom": {
+ "lineWidth": 2,
+ "fillOpacity": 10,
+ "spanNulls": true,
+ "axisBorderShow": false,
+ "axisCenteredZero": false,
+ "axisColorMode": "text",
+ "axisLabel": "",
+ "scaleDistribution": {
+ "type": "linear"
+ },
+ "showPoints": "auto",
+ "pointSize": 5
+ }
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 0,
+ "y": 39
+ },
+ "id": 23,
+ "options": {
+ "legend": {
+ "calcs": [
+ "lastNotNull"
+ ],
+ "displayMode": "list",
+ "placement": "bottom",
+ "showLegend": true
+ },
+ "tooltip": {
+ "mode": "multi",
+ "sort": "none"
+ }
+ },
+ "pluginVersion": "11.2.0",
+ "targets": [
+ {
+ "datasource": "postgresql",
+ "editorMode": "code",
+ "format": "time_series",
+ "rawQuery": true,
+ "rawSql": "SELECT pmh.analysis_date AS time, pmh.code_smells AS
\"Code Smells\" FROM cq_project_metrics_history AS pmh WHERE
('${project_id:csv}' = '' OR pmh.project_key::text =
ANY(ARRAY[${project_id:singlequote}]::text[])) AND
$__timeFilter(pmh.analysis_date) ORDER BY pmh.analysis_date ASC NULLS FIRST",
+ "refId": "A"
+ }
+ ],
+ "title": "Code Smells Trend",
+ "type": "timeseries"
+ },
+ {
+ "datasource": "postgresql",
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "custom": {
+ "lineWidth": 2,
+ "fillOpacity": 10,
+ "spanNulls": true,
+ "axisBorderShow": false,
+ "axisCenteredZero": false,
+ "axisColorMode": "text",
+ "axisLabel": "",
+ "scaleDistribution": {
+ "type": "linear"
+ },
+ "showPoints": "auto",
+ "pointSize": 5
+ },
+ "unit": "percent"
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 12,
+ "y": 39
+ },
+ "id": 24,
+ "options": {
+ "legend": {
+ "calcs": [
+ "lastNotNull"
+ ],
+ "displayMode": "list",
+ "placement": "bottom",
+ "showLegend": true
+ },
+ "tooltip": {
+ "mode": "multi",
+ "sort": "none"
+ }
+ },
+ "pluginVersion": "11.2.0",
+ "targets": [
+ {
+ "datasource": "postgresql",
+ "editorMode": "code",
+ "format": "time_series",
+ "rawQuery": true,
+ "rawSql": "SELECT pmh.analysis_date AS time,
pmh.duplicated_lines_density AS \"Duplication %\" FROM
cq_project_metrics_history AS pmh WHERE ('${project_id:csv}' = '' OR
pmh.project_key::text = ANY(ARRAY[${project_id:singlequote}]::text[])) AND
$__timeFilter(pmh.analysis_date) ORDER BY pmh.analysis_date ASC NULLS FIRST",
+ "refId": "A"
+ }
+ ],
+ "title": "Duplication Trend",
+ "type": "timeseries"
+ }
+ ],
+ "targets": [
+ {
+ "datasource": {
+ "type": "datasource",
+ "uid": "grafana"
+ },
+ "refId": "A"
+ }
+ ],
+ "title": "Historical Trends",
+ "type": "row"
}
],
"refresh": "",
@@ -1086,7 +1351,7 @@
]
},
"time": {
- "from": "now",
+ "from": "now-6M",
"to": "now"
},
"timepicker": {},
diff --git a/grafana/dashboards/postgresql/sonar-qube-cloud.json
b/grafana/dashboards/postgresql/sonar-qube-cloud.json
index 6719a3e2d..08cd7359e 100644
--- a/grafana/dashboards/postgresql/sonar-qube-cloud.json
+++ b/grafana/dashboards/postgresql/sonar-qube-cloud.json
@@ -47,7 +47,7 @@
"showLineNumbers": false,
"showMiniMap": false
},
- "content": "- Use Cases: This dashboard shows the code quality metrics
from SonarCloud.\n- Data Source Required: SonarCloud\n- This dashboard does not
honor the time filter on the top-right side as SonarQube metrics are all from
the latest scan.",
+ "content": "- Use Cases: This dashboard shows the code quality metrics
from SonarCloud.\n- Data Source Required: SonarCloud\n- Snapshot panels
(Software Quality, Security Review, Test, etc.) show the latest scan and do not
honor the time filter.\n- **Historical Trends** panels at the bottom respond to
the time range selector and show metric history over time.",
"mode": "markdown"
},
"pluginVersion": "11.2.0",
@@ -1428,6 +1428,271 @@
],
"title": "Code Quality Metrics by Files (Top 20 order by Bugs)",
"type": "table"
+ },
+ {
+ "collapsed": true,
+ "datasource": {
+ "type": "datasource",
+ "uid": "grafana"
+ },
+ "gridPos": {
+ "h": 1,
+ "w": 24,
+ "x": 0,
+ "y": 44
+ },
+ "id": 27,
+ "panels": [
+ {
+ "datasource": "postgresql",
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "custom": {
+ "lineWidth": 2,
+ "fillOpacity": 10,
+ "spanNulls": true,
+ "axisBorderShow": false,
+ "axisCenteredZero": false,
+ "axisColorMode": "text",
+ "axisLabel": "",
+ "scaleDistribution": {
+ "type": "linear"
+ },
+ "showPoints": "auto",
+ "pointSize": 5
+ },
+ "unit": "percent"
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 0,
+ "y": 45
+ },
+ "id": 28,
+ "options": {
+ "legend": {
+ "calcs": [
+ "lastNotNull"
+ ],
+ "displayMode": "list",
+ "placement": "bottom",
+ "showLegend": true
+ },
+ "tooltip": {
+ "mode": "multi",
+ "sort": "none"
+ }
+ },
+ "pluginVersion": "11.2.0",
+ "targets": [
+ {
+ "datasource": "postgresql",
+ "editorMode": "code",
+ "format": "time_series",
+ "rawQuery": true,
+ "rawSql": "SELECT pmh.analysis_date AS time, pmh.coverage AS
\"Coverage %\" FROM cq_project_metrics_history AS pmh WHERE
('${project_id:csv}' = '' OR pmh.project_key::text =
ANY(ARRAY[${project_id:singlequote}]::text[])) AND
$__timeFilter(pmh.analysis_date) ORDER BY pmh.analysis_date ASC NULLS FIRST",
+ "refId": "A"
+ }
+ ],
+ "title": "Coverage Trend",
+ "type": "timeseries"
+ },
+ {
+ "datasource": "postgresql",
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "custom": {
+ "lineWidth": 2,
+ "fillOpacity": 10,
+ "spanNulls": true,
+ "axisBorderShow": false,
+ "axisCenteredZero": false,
+ "axisColorMode": "text",
+ "axisLabel": "",
+ "scaleDistribution": {
+ "type": "linear"
+ },
+ "showPoints": "auto",
+ "pointSize": 5
+ }
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 12,
+ "y": 45
+ },
+ "id": 29,
+ "options": {
+ "legend": {
+ "calcs": [
+ "lastNotNull"
+ ],
+ "displayMode": "list",
+ "placement": "bottom",
+ "showLegend": true
+ },
+ "tooltip": {
+ "mode": "multi",
+ "sort": "none"
+ }
+ },
+ "pluginVersion": "11.2.0",
+ "targets": [
+ {
+ "datasource": "postgresql",
+ "editorMode": "code",
+ "format": "time_series",
+ "rawQuery": true,
+ "rawSql": "SELECT pmh.analysis_date AS time, pmh.bugs AS
\"Bugs\", pmh.vulnerabilities AS \"Vulnerabilities\" FROM
cq_project_metrics_history AS pmh WHERE ('${project_id:csv}' = '' OR
pmh.project_key::text = ANY(ARRAY[${project_id:singlequote}]::text[])) AND
$__timeFilter(pmh.analysis_date) ORDER BY pmh.analysis_date ASC NULLS FIRST",
+ "refId": "A"
+ }
+ ],
+ "title": "Bugs & Vulnerabilities Trend",
+ "type": "timeseries"
+ },
+ {
+ "datasource": "postgresql",
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "custom": {
+ "lineWidth": 2,
+ "fillOpacity": 10,
+ "spanNulls": true,
+ "axisBorderShow": false,
+ "axisCenteredZero": false,
+ "axisColorMode": "text",
+ "axisLabel": "",
+ "scaleDistribution": {
+ "type": "linear"
+ },
+ "showPoints": "auto",
+ "pointSize": 5
+ }
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 0,
+ "y": 53
+ },
+ "id": 30,
+ "options": {
+ "legend": {
+ "calcs": [
+ "lastNotNull"
+ ],
+ "displayMode": "list",
+ "placement": "bottom",
+ "showLegend": true
+ },
+ "tooltip": {
+ "mode": "multi",
+ "sort": "none"
+ }
+ },
+ "pluginVersion": "11.2.0",
+ "targets": [
+ {
+ "datasource": "postgresql",
+ "editorMode": "code",
+ "format": "time_series",
+ "rawQuery": true,
+ "rawSql": "SELECT pmh.analysis_date AS time, pmh.code_smells AS
\"Code Smells\" FROM cq_project_metrics_history AS pmh WHERE
('${project_id:csv}' = '' OR pmh.project_key::text =
ANY(ARRAY[${project_id:singlequote}]::text[])) AND
$__timeFilter(pmh.analysis_date) ORDER BY pmh.analysis_date ASC NULLS FIRST",
+ "refId": "A"
+ }
+ ],
+ "title": "Code Smells Trend",
+ "type": "timeseries"
+ },
+ {
+ "datasource": "postgresql",
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "custom": {
+ "lineWidth": 2,
+ "fillOpacity": 10,
+ "spanNulls": true,
+ "axisBorderShow": false,
+ "axisCenteredZero": false,
+ "axisColorMode": "text",
+ "axisLabel": "",
+ "scaleDistribution": {
+ "type": "linear"
+ },
+ "showPoints": "auto",
+ "pointSize": 5
+ },
+ "unit": "percent"
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 12,
+ "y": 53
+ },
+ "id": 31,
+ "options": {
+ "legend": {
+ "calcs": [
+ "lastNotNull"
+ ],
+ "displayMode": "list",
+ "placement": "bottom",
+ "showLegend": true
+ },
+ "tooltip": {
+ "mode": "multi",
+ "sort": "none"
+ }
+ },
+ "pluginVersion": "11.2.0",
+ "targets": [
+ {
+ "datasource": "postgresql",
+ "editorMode": "code",
+ "format": "time_series",
+ "rawQuery": true,
+ "rawSql": "SELECT pmh.analysis_date AS time,
pmh.duplicated_lines_density AS \"Duplication %\" FROM
cq_project_metrics_history AS pmh WHERE ('${project_id:csv}' = '' OR
pmh.project_key::text = ANY(ARRAY[${project_id:singlequote}]::text[])) AND
$__timeFilter(pmh.analysis_date) ORDER BY pmh.analysis_date ASC NULLS FIRST",
+ "refId": "A"
+ }
+ ],
+ "title": "Duplication Trend",
+ "type": "timeseries"
+ }
+ ],
+ "targets": [
+ {
+ "datasource": {
+ "type": "datasource",
+ "uid": "grafana"
+ },
+ "refId": "A"
+ }
+ ],
+ "title": "Historical Trends",
+ "type": "row"
}
],
"refresh": "",
@@ -1491,7 +1756,7 @@
]
},
"time": {
- "from": "now",
+ "from": "now-6M",
"to": "now"
},
"timepicker": {},