* Expose metrics under a source available license

* do not assume Cluster()

* allow metrics if licensed or dev

* temporary vet override

* simplify BULID_TAGS handling

* auto clean old imports.go

* fix license listener

* e2e test metrics & license semantics

* update from enterprise

* switch back to mattermost-govet/v2@new now

* update metrics from upstream

* Update license_spec.js

Co-authored-by: Saturnino Abril <saturnino.abril@gmail.com>

* Update license_spec.js

Co-authored-by: Saturnino Abril <saturnino.abril@gmail.com>

* Update e2e-tests/cypress/tests/integration/channels/enterprise/metrics/license_spec.js

Co-authored-by: Saturnino Abril <saturnino.abril@gmail.com>

* Update e2e-tests/cypress/tests/integration/channels/enterprise/metrics/license_spec.js

* split up specs

* require/delete license earlier in e2e test

* expanded expect to debug failures

* more logging

* Revert "more logging"

This reverts commit 0bc513fd926c07a0c6050f4b4c742e856d8dc435.

* e2e: try deleting license first

* update from enterprise

* toggleMetricsOn to work around license delete

* eslint

* ensure admin before deleting license

* update from enterprise

* updates from enterprise

* fix cypress logging

* temp: log at DEBUG for Cypress tests

---------

Co-authored-by: Saturnino Abril <saturnino.abril@gmail.com>
Co-authored-by: Mattermost Build <build@mattermost.com>
Этот коммит содержится в:
Jesse Hallam
2024-01-08 10:47:24 -04:00
коммит произвёл GitHub
родитель 6184c36e0b
Коммит b05093d508
22 изменённых файлов: 1984 добавлений и 65 удалений

39
server/enterprise/LICENSE Обычный файл
Просмотреть файл

@@ -0,0 +1,39 @@
The Mattermost Source Available License license (the “Source Available License”)
Copyright (c) 2015-present Mattermost
With regard to the Mattermost Software:
This software and associated documentation files (the "Software") may only be
used in production, if you (and any entity that you represent) have agreed to,
and are in compliance with, the Mattermost Terms of Service, available at
https://mattermost.com/enterprise-edition-terms/ (the “EE Terms”), or other
agreement governing the use of the Software, as agreed by you and Mattermost,
and otherwise have a valid Mattermost Enterprise E20 subscription for the
correct number of user seats. Subject to the foregoing sentence, you are free
to modify this Software and publish patches to the Software. You agree that
Mattermost and/or its licensors (as applicable) retain all right, title and
interest in and to all such modifications and/or patches, and all such
modifications and/or patches may only be used, copied, modified, displayed,
distributed, or otherwise exploited with a valid Mattermost Enterprise E20
Edition subscription for the correct number of user seats. Notwithstanding
the foregoing, you may copy and modify the Software for development and testing
purposes, without requiring a subscription. You agree that Mattermost and/or
its licensors (as applicable) retain all right, title and interest in and to
all such modifications. You are not granted any other rights beyond what is
expressly stated herein. Subject to the foregoing, it is forbidden to copy,
merge, publish, distribute, sublicense, and/or sell the Software.
The full text of this EE License shall be included in all copies or substantial
portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
For all third party components incorporated into the Mattermost Software, those
components are licensed under the original license provided by the owner of the
applicable component.

17
server/enterprise/README.md Обычный файл
Просмотреть файл

@@ -0,0 +1,17 @@
# Enterprise
This folder contains source available enterprise code as well as import directives for closed source enterprise code.
## Build Information
The source code in this folder is only included with builds specifying the `enterprise` or `sourceavailble` build tags. If you have a copy of https://github.com/mattermost/enterprise checked out as a peer to this repository, `enterprise` will be set automatically and the imports from both [`external_imports.go`](external_imports.go) and [`local_imports.go`](local_imports.go) will apply.
In a development environment (when `BUILD_NUMBER` is left undefined or explicitly set to `dev`), the `sourceavailable` build tag will be set automatically and only the imports from [`local_imports.go`](local_imports.go) will apply.
## License
See the [LICENSE file](LICENSE) for license rights and limitations. See also [Mattermost Source Available License](https://docs.mattermost.com/overview/faq.html#mattermost-source-available-license) to learn more.
## Contributing
Contributions to source available enterprise code are welcome. Please see [CONTRIBUTING.md](../../CONTRIBUTING.md).

45
server/enterprise/external_imports.go Обычный файл
Просмотреть файл

@@ -0,0 +1,45 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
//go:build enterprise
package enterprise
import (
// Needed to ensure the init() method in the EE gets run
_ "github.com/mattermost/enterprise/account_migration"
// Needed to ensure the init() method in the EE gets run
_ "github.com/mattermost/enterprise/cluster"
// Needed to ensure the init() method in the EE gets run
_ "github.com/mattermost/enterprise/compliance"
// Needed to ensure the init() method in the EE gets run
_ "github.com/mattermost/enterprise/data_retention"
// Needed to ensure the init() method in the EE gets run
_ "github.com/mattermost/enterprise/elasticsearch"
// Needed to ensure the init() method in the EE gets run
_ "github.com/mattermost/enterprise/ldap"
// Needed to ensure the init() method in the EE gets run
_ "github.com/mattermost/enterprise/message_export"
// Needed to ensure the init() method in the EE gets run
_ "github.com/mattermost/enterprise/cloud"
// Needed to ensure the init() method in the EE gets run
_ "github.com/mattermost/enterprise/message_export/actiance_export"
// Needed to ensure the init() method in the EE gets run
_ "github.com/mattermost/enterprise/message_export/csv_export"
// Needed to ensure the init() method in the EE gets run
_ "github.com/mattermost/enterprise/message_export/global_relay_export"
// Needed to ensure the init() method in the EE gets run
_ "github.com/mattermost/enterprise/notification"
// Needed to ensure the init() method in the EE gets run
_ "github.com/mattermost/enterprise/oauth/google"
// Needed to ensure the init() method in the EE gets run
_ "github.com/mattermost/enterprise/oauth/office365"
// Needed to ensure the init() method in the EE gets run
_ "github.com/mattermost/enterprise/saml"
// Needed to ensure the init() method in the EE gets run
_ "github.com/mattermost/enterprise/oauth/openid"
// Needed to ensure the init() method in the EE gets run
_ "github.com/mattermost/enterprise/license"
// Needed to ensure the init() method in the EE gets run
_ "github.com/mattermost/enterprise/ip_filtering"
)

11
server/enterprise/local_imports.go Обычный файл
Просмотреть файл

@@ -0,0 +1,11 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
//go:build enterprise || sourceavailable
package enterprise
import (
// Needed to ensure the init() method in the EE gets run
_ "github.com/mattermost/mattermost/server/v8/enterprise/metrics"
)

81
server/enterprise/metrics/dynamic.go Обычный файл
Просмотреть файл

@@ -0,0 +1,81 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.enterprise for license information.
package metrics
import (
"github.com/mattermost/mattermost/server/public/shared/mlog"
"github.com/prometheus/client_golang/prometheus"
)
// DynamicCounter provides a CounterVec that can create new counters via label values at runtime.
// This allows applications to create counters at runtime without locking into Prometheus as the
// metrics manager.
type DynamicCounter struct {
counter *prometheus.CounterVec
}
// NewDynamicCounter creates a new dynamic counter with corresponding labels.
func NewDynamicCounter(opts prometheus.CounterOpts, labels ...string) *DynamicCounter {
return &DynamicCounter{
counter: prometheus.NewCounterVec(opts, labels),
}
}
// GetCounter fetches a counter associated with the label values provided. An error is
// returned if the number of values differs from the number of labels provided when
// creating the DynamicCounter
func (dc *DynamicCounter) GetCounter(values ...string) (prometheus.Counter, error) {
return dc.counter.GetMetricWithLabelValues(values...)
}
// DynamicGauge provides a GaugeVec that can create new gauges via label values at runtime.
// This allows applications to create gauges at runtime without locking into Prometheus as the
// metrics manager.
type DynamicGauge struct {
gauge *prometheus.GaugeVec
}
// NewDynamicGauge creates a new dynamic gauge with corresponding labels.
func NewDynamicGauge(opts prometheus.GaugeOpts, labels ...string) *DynamicGauge {
return &DynamicGauge{
gauge: prometheus.NewGaugeVec(opts, labels),
}
}
// GetGauge fetches a gauge associated with the label values provided. An error is
// returned if the number of values differs from the number of labels provided when
// creating the DynamicGauge
func (dg *DynamicGauge) GetGauge(values ...string) (prometheus.Gauge, error) {
return dg.gauge.GetMetricWithLabelValues(values...)
}
// LoggerMetricsCollector provides counters for server logging.
// Implements Logr.MetricsCollector
type LoggerMetricsCollector struct {
queueGauge *DynamicGauge
loggedCounters *DynamicCounter
errorCounters *DynamicCounter
droppedCounters *DynamicCounter
blockedCounters *DynamicCounter
}
func (c *LoggerMetricsCollector) QueueSizeGauge(target string) (mlog.Gauge, error) {
return c.queueGauge.GetGauge(target)
}
func (c *LoggerMetricsCollector) LoggedCounter(target string) (mlog.Counter, error) {
return c.loggedCounters.GetCounter(target)
}
func (c *LoggerMetricsCollector) ErrorCounter(target string) (mlog.Counter, error) {
return c.errorCounters.GetCounter(target)
}
func (c *LoggerMetricsCollector) DroppedCounter(target string) (mlog.Counter, error) {
return c.droppedCounters.GetCounter(target)
}
func (c *LoggerMetricsCollector) BlockedCounter(target string) (mlog.Counter, error) {
return c.blockedCounters.GetCounter(target)
}

21
server/enterprise/metrics/main_test.go Обычный файл
Просмотреть файл

@@ -0,0 +1,21 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.enterprise for license information.
package metrics
import (
"testing"
"github.com/mattermost/mattermost/server/v8/channels/api4"
"github.com/mattermost/mattermost/server/v8/channels/testlib"
)
var mainHelper *testlib.MainHelper
func TestMain(m *testing.M) {
mainHelper = testlib.NewMainHelper()
defer mainHelper.Close()
api4.SetMainHelper(mainHelper)
mainHelper.Main(m)
}

1369
server/enterprise/metrics/metrics.go Обычный файл

Разница между файлами не показана из-за своего большого размера Загрузить разницу

220
server/enterprise/metrics/metrics_test.go Обычный файл
Просмотреть файл

@@ -0,0 +1,220 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.enterprise for license information.
package metrics
import (
"fmt"
"strconv"
"testing"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/v8/channels/api4"
"github.com/mattermost/mattermost/server/v8/channels/app"
"github.com/mattermost/mattermost/server/public/plugin/plugintest/mock"
"github.com/mattermost/mattermost/server/v8/channels/store/storetest/mocks"
"github.com/prometheus/client_golang/prometheus"
prometheusModels "github.com/prometheus/client_model/go"
"github.com/stretchr/testify/require"
)
func configureMetrics(th *api4.TestHelper) {
th.App.Srv().SetLicense(nil) // clear license
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.MetricsSettings.Enable = true
*cfg.MetricsSettings.ListenAddress = ":0"
})
th.App.Srv().SetLicense(model.NewTestLicense("metrics"))
}
func TestMetrics(t *testing.T) {
th := api4.SetupEnterpriseWithStoreMock(t, app.StartMetrics)
defer th.TearDown()
mockStore := th.App.Srv().Platform().Store.(*mocks.Store)
mockUserStore := mocks.UserStore{}
mockUserStore.On("Count", mock.Anything).Return(int64(10), nil)
mockPostStore := mocks.PostStore{}
mockPostStore.On("GetMaxPostSize").Return(65535, nil)
mockSystemStore := mocks.SystemStore{}
mockSystemStore.On("GetByName", "UpgradedFromTE").Return(&model.System{Name: "UpgradedFromTE", Value: "false"}, nil)
mockSystemStore.On("GetByName", "InstallationDate").Return(&model.System{Name: "InstallationDate", Value: "10"}, nil)
mockStore.On("User").Return(&mockUserStore)
mockStore.On("Post").Return(&mockPostStore)
mockStore.On("System").Return(&mockSystemStore)
mockStore.On("GetDBSchemaVersion").Return(1, nil)
configureMetrics(th)
mi := th.App.Metrics()
_, ok := mi.(*MetricsInterfaceImpl)
require.True(t, ok, fmt.Sprintf("App.Metrics is not *MetricsInterfaceImpl, but %T", mi))
mi.IncrementHTTPRequest()
mi.IncrementHTTPError()
mi.IncrementPostFileAttachment(5)
mi.IncrementPostCreate()
mi.IncrementPostSentEmail()
mi.IncrementPostSentPush()
mi.IncrementPostBroadcast()
mi.IncrementLogin()
mi.IncrementLoginFail()
mi.IncrementClusterRequest()
mi.ObserveClusterRequestDuration(2.0)
mi.IncrementClusterEventType(model.ClusterEventPublish)
loggerCollector := mi.GetLoggerMetricsCollector()
g, err := loggerCollector.QueueSizeGauge("_logr")
require.NoError(t, err)
g.Set(59)
c, err := loggerCollector.LoggedCounter("_logr")
require.NoError(t, err)
c.Inc()
c, err = loggerCollector.ErrorCounter("_logr")
require.NoError(t, err)
c.Inc()
c, err = loggerCollector.DroppedCounter("_logr")
require.NoError(t, err)
c.Inc()
c, err = loggerCollector.BlockedCounter("_logr")
require.NoError(t, err)
c.Inc()
}
func TestPluginMetrics(t *testing.T) {
th := api4.SetupEnterprise(t, app.StartMetrics)
defer th.TearDown()
configureMetrics(th)
mi := th.App.Metrics()
miImpl, ok := mi.(*MetricsInterfaceImpl)
require.True(t, ok, fmt.Sprintf("App.Metrics is not *MetricsInterfaceImpl, but %T", mi))
t.Run("test ObservePluginHookDuration", func(t *testing.T) {
pluginID := "id_"
hookName := "hook_"
elapsed := 999.1
m := &prometheusModels.Metric{}
for _, success := range []bool{true, false} {
actualMetric, err := miImpl.PluginHookTimeHistogram.GetMetricWith(prometheus.Labels{"plugin_id": pluginID, "hook_name": hookName, "success": strconv.FormatBool(success)})
require.NoError(t, err)
require.NoError(t, actualMetric.(prometheus.Histogram).Write(m))
require.Equal(t, uint64(0), m.Histogram.GetSampleCount())
require.Equal(t, 0.0, m.Histogram.GetSampleSum())
mi.ObservePluginHookDuration(pluginID, hookName, success, elapsed)
actualMetric, err = miImpl.PluginHookTimeHistogram.GetMetricWith(prometheus.Labels{"plugin_id": pluginID, "hook_name": hookName, "success": strconv.FormatBool(success)})
require.NoError(t, err)
require.NoError(t, actualMetric.(prometheus.Histogram).Write(m))
require.Equal(t, uint64(1), m.Histogram.GetSampleCount())
require.InDelta(t, elapsed, m.Histogram.GetSampleSum(), 0.001)
}
})
t.Run("test ObservePluginAPIDuration", func(t *testing.T) {
pluginID := "id_"
apiName := "api_"
elapsed := 999.1
m := &prometheusModels.Metric{}
for _, success := range []bool{true, false} {
actualMetric, err := miImpl.PluginAPITimeHistogram.GetMetricWith(prometheus.Labels{"plugin_id": pluginID, "api_name": apiName, "success": strconv.FormatBool(success)})
require.NoError(t, err)
require.NoError(t, actualMetric.(prometheus.Histogram).Write(m))
require.Equal(t, uint64(0), m.Histogram.GetSampleCount())
require.Equal(t, 0.0, m.Histogram.GetSampleSum())
mi.ObservePluginAPIDuration(pluginID, apiName, success, elapsed)
actualMetric, err = miImpl.PluginAPITimeHistogram.GetMetricWith(prometheus.Labels{"plugin_id": pluginID, "api_name": apiName, "success": strconv.FormatBool(success)})
require.NoError(t, err)
require.NoError(t, actualMetric.(prometheus.Histogram).Write(m))
require.Equal(t, uint64(1), m.Histogram.GetSampleCount())
require.InDelta(t, elapsed, m.Histogram.GetSampleSum(), 0.001)
}
})
t.Run("test ObservePluginMultiHookIterationDuration", func(t *testing.T) {
pluginID := "id_"
elapsed := 999.1
m := &prometheusModels.Metric{}
actualMetric, err := miImpl.PluginMultiHookTimeHistogram.GetMetricWith(prometheus.Labels{"plugin_id": pluginID})
require.NoError(t, err)
require.NoError(t, actualMetric.(prometheus.Histogram).Write(m))
require.Equal(t, uint64(0), m.Histogram.GetSampleCount())
require.Equal(t, 0.0, m.Histogram.GetSampleSum())
mi.ObservePluginMultiHookIterationDuration(pluginID, elapsed)
actualMetric, err = miImpl.PluginMultiHookTimeHistogram.GetMetricWith(prometheus.Labels{"plugin_id": pluginID})
require.NoError(t, err)
require.NoError(t, actualMetric.(prometheus.Histogram).Write(m))
require.Equal(t, uint64(1), m.Histogram.GetSampleCount())
require.InDelta(t, elapsed, m.Histogram.GetSampleSum(), 0.001)
})
t.Run("test ObservePluginMultiHookDuration", func(t *testing.T) {
elapsed := 50.0
m := &prometheusModels.Metric{}
require.NoError(t, miImpl.PluginMultiHookServerTimeHistogram.Write(m))
require.InDelta(t, 0.0, m.Histogram.GetSampleSum(), 0.001)
mi.ObservePluginMultiHookDuration(elapsed)
require.NoError(t, miImpl.PluginMultiHookServerTimeHistogram.Write(m))
require.InDelta(t, elapsed, m.Histogram.GetSampleSum(), 0.001)
})
}
func TestExtractDBCluster(t *testing.T) {
testCases := []struct {
description string
driver string
connectionStr string
expectedClusterName string
}{
{
description: "postgres full",
driver: "postgres",
connectionStr: "postgres://user1234:password1234@rds-cluster-multitenant-1234-postgres.cluster-abcd.us-east-1.rds.amazonaws.com:5432/cloud?connect_timeout=10",
expectedClusterName: "rds-cluster-multitenant-1234-postgres",
},
{
description: "postgres no credentials",
driver: "postgres",
connectionStr: "postgres://rds-cluster-multitenant-1234-postgres.cluster-abcd.us-east-1.rds.amazonaws.com:5432/cloud?connect_timeout=10",
expectedClusterName: "rds-cluster-multitenant-1234-postgres",
},
{
description: "mysql full",
driver: "mysql",
connectionStr: "mysql://user1234:password1234@tcp(rds-cluster-multitenant-1234-mysql.cluster-abcd.us-east-1.rds.amazonaws.com:3306)/cloud?charset=utf8mb4%2Cutf8&readTimeout=30s&writeTimeout=30s&tls=skip-verify",
expectedClusterName: "rds-cluster-multitenant-1234-mysql",
},
{
description: "mysql no credentials",
driver: "mysql",
connectionStr: "mysql://tcp(rds-cluster-multitenant-1234-mysql.cluster-abcd.us-east-1.rds.amazonaws.com:3306)/cloud?charset=utf8mb4%2Cutf8&readTimeout=30s&writeTimeout=30s&tls=skip-verify",
expectedClusterName: "rds-cluster-multitenant-1234-mysql",
},
}
for _, tc := range testCases {
t.Run(tc.description, func(t *testing.T) {
host, err := extractDBCluster(tc.driver, tc.connectionStr)
require.NoError(t, err)
require.Equal(t, tc.expectedClusterName, host)
})
}
}

5
server/enterprise/placeholder.go Обычный файл
Просмотреть файл

@@ -0,0 +1,5 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.enterprise for license information.
// Ensure this is a valid package even when build tags preclude building anything in it.
package enterprise