Source available metrics (#24879)

* 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 0bc513fd92.

* 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>
This commit is contained in:
Jesse Hallam 2024-01-08 10:47:24 -04:00 committed by GitHub
parent 6184c36e0b
commit b05093d508
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 1984 additions and 65 deletions

1
LICENSE.enterprise Symbolic link
View File

@ -0,0 +1 @@
server/enterprise/LICENSE

View File

@ -0,0 +1,37 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
export const checkMetrics = (expectedStatusCode) => {
cy.apiGetConfig().then(({config}) => {
const baseURL = new URL(Cypress.config('baseUrl'));
baseURL.port = config.MetricsSettings.ListenAddress.replace(/^.*:/, '');
baseURL.pathname = '/metrics';
cy.log({name: 'Metrics License', message: `Checking metrics at ${baseURL.toString()}`});
cy.request({
headers: {'X-Requested-With': 'XMLHttpRequest'},
url: baseURL.toString(),
method: 'GET',
failOnStatusCode: false,
}).then((response) => {
expect(response.headers['Content-Type'], 'should not hit webapp').not.to.equal('text/html');
expect(response.status, 'should match expected status code').to.equal(expectedStatusCode);
});
});
};
// toggleMetricsOn turns metrics off and back on, forcing it to be tested against the current
// license. When, in the future, the product detects license removal and does this automatically,
// this helper won't be required.
export const toggleMetricsOn = () => {
cy.apiUpdateConfig({
MetricsSettings: {
Enable: false,
},
});
cy.apiUpdateConfig({
MetricsSettings: {
Enable: true,
},
});
};

View File

@ -0,0 +1,44 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// ***************************************************************
// - [#] indicates a test step (e.g. # Go to a page)
// - [*] indicates an assertion (e.g. * Check the title)
// - Use element ID when selecting an element. Create one if none.
// ***************************************************************
// Stage: @prod
// Group: @channels @enterprise @metrics @not_cloud @license_removal
import {checkMetrics, toggleMetricsOn} from './helper';
describe('Metrics > No license', () => {
before(() => {
cy.shouldNotRunOnCloudEdition();
cy.apiAdminLogin();
cy.apiDeleteLicense();
toggleMetricsOn();
});
it('should enable metrics in BUILD_NUMBER == dev environments', () => {
cy.apiGetConfig(true).then(({config}) => {
if (config.BuildNumber !== 'dev') {
Cypress.log({name: 'Metrics License', message: `Skipping test since BUILD_NUMBER = ${config.BuildNumber}`});
return;
}
checkMetrics(200);
});
});
it('should disable metrics in BUILD_NUMBER != dev environments', () => {
cy.apiGetConfig(true).then(({config}) => {
if (config.BuildNumber === 'dev') {
Cypress.log({name: 'Metrics License', message: `Skipping test since BUILD_NUMBER = ${config.BuildNumber}`});
return;
}
checkMetrics(404);
});
});
});

View File

@ -0,0 +1,43 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// ***************************************************************
// - [#] indicates a test step (e.g. # Go to a page)
// - [*] indicates an assertion (e.g. * Check the title)
// - Use element ID when selecting an element. Create one if none.
// ***************************************************************
// Stage: @prod
// Group: @channels @enterprise @metrics @not_cloud
import {checkMetrics, toggleMetricsOn} from './helper';
describe('Metrics > License', () => {
before(() => {
cy.shouldNotRunOnCloudEdition();
cy.apiRequireLicense();
toggleMetricsOn();
});
it('should enable metrics in BUILD_NUMBER == dev environments', () => {
cy.apiGetConfig(true).then(({config}) => {
if (config.BuildNumber !== 'dev') {
Cypress.log({name: 'Metrics License', message: `Skipping test since BUILD_NUMBER = ${config.BuildNumber}`});
return;
}
checkMetrics(200);
});
});
it('should enable metrics in BUILD_NUMBER != dev environments', () => {
cy.apiGetConfig(true).then(({config}) => {
if (config.BuildNumber === 'dev') {
Cypress.log({name: 'Metrics License', message: `Skipping test since BUILD_NUMBER = ${config.BuildNumber}`});
return;
}
checkMetrics(200);
});
});
});

View File

@ -162,7 +162,7 @@
"ConsoleJson": true,
"EnableColor": false,
"EnableFile": true,
"FileLevel": "INFO",
"FileLevel": "DEBUG",
"FileJson": true,
"FileLocation": "",
"EnableWebhookDebugging": true,

5
server/.gitignore vendored
View File

@ -21,9 +21,6 @@ config/active.dat
config/logging.json
/plugins
# Enterprise & products imports files
channels/imports/imports.go
# go.work file
go.work
go.work.sum
@ -104,8 +101,6 @@ api/data/*
api4/data/*
app/data/*
/enterprise
cover.out
ecover.out
mmctlcover.out

View File

@ -33,6 +33,7 @@ IS_CI ?= false
BUILD_NUMBER ?= $(BUILD_NUMBER:)
BUILD_DATE = $(shell date -u)
BUILD_HASH = $(shell git rev-parse HEAD)
BUILD_TAGS =
# Docker
@ -73,6 +74,7 @@ ifneq ($(wildcard $(BUILD_ENTERPRISE_DIR)/.),)
BUILD_ENTERPRISE_READY = true
BUILD_TYPE_NAME = enterprise
BUILD_HASH_ENTERPRISE = $(shell cd $(BUILD_ENTERPRISE_DIR) && git rev-parse HEAD)
BUILD_TAGS += enterprise
else
BUILD_ENTERPRISE_READY = false
BUILD_TYPE_NAME = team
@ -82,6 +84,16 @@ else
BUILD_TYPE_NAME = team
endif
# Clean up the old means of importing enterprise source, if it exists
ifneq ($(wildcard channels/imports/imports.go),)
IGNORE := $(shell rm -f channels/imports/imports.go)
endif
# Source available, already included with enterprise but also available during development.
ifeq ($(BUILD_NUMBER),dev)
BUILD_TAGS += sourceavailable
endif
# Webapp
BUILD_WEBAPP_DIR ?= ../webapp
@ -156,16 +168,6 @@ PLUGIN_PACKAGES += mattermost-plugin-zoom-v1.6.2
PLUGIN_PACKAGES += mattermost-plugin-apps-v1.2.2
PLUGIN_PACKAGES += focalboard-v7.11.4
# Prepares the enterprise build if exists. The IGNORE stuff is a hack to get the Makefile to execute the commands outside a target
ifeq ($(BUILD_ENTERPRISE_READY),true)
IGNORE:=$(shell echo Enterprise build selected, preparing)
IGNORE:=$(shell rm -f channels/imports/imports.go)
IGNORE:=$(shell cp $(BUILD_ENTERPRISE_DIR)/imports/imports.go channels/imports/)
IGNORE:=$(shell rm -f enterprise)
else
IGNORE:=$(shell rm -f channels/imports/imports.go)
endif
EE_PACKAGES=$(shell $(GO) list $(BUILD_ENTERPRISE_DIR)/...)
ifeq ($(BUILD_ENTERPRISE_READY),true)
@ -422,14 +424,10 @@ test-compile: gotestsum ## Compile tests.
done
modules-tidy:
@if [ -f "channels/imports/imports.go" ]; then \
mv channels/imports/imports.go channels/imports/imports.go.orig; \
fi;
mv enterprise/external_imports.go enterprise/external_imports.go.orig
-$(GO) mod tidy
-cd public && $(GO) mod tidy
@if [ -f "channels/imports/imports.go.orig" ]; then \
mv channels/imports/imports.go.orig channels/imports/imports.go; \
fi;
mv enterprise/external_imports.go.orig enterprise/external_imports.go
test-server-pre: check-prereqs-enterprise start-docker gotestsum ## Runs tests.
ifeq ($(BUILD_ENTERPRISE_READY),true)
@ -580,7 +578,7 @@ run-server: setup-go-work prepackaged-binaries validate-go-version start-docker
@echo Running mattermost for development
mkdir -p $(BUILD_WEBAPP_DIR)/channels/dist/files
$(GO) run $(GOFLAGS) -ldflags '$(LDFLAGS)' $(PLATFORM_FILES) $(RUN_IN_BACKGROUND)
$(GO) run $(GOFLAGS) -ldflags '$(LDFLAGS)' -tags '$(BUILD_TAGS)' $(PLATFORM_FILES) $(RUN_IN_BACKGROUND)
debug-server: start-docker ## Compile and start server using delve.
mkdir -p $(BUILD_WEBAPP_DIR)/channels/dist/files
@ -589,7 +587,8 @@ debug-server: start-docker ## Compile and start server using delve.
-X \"github.com/mattermost/mattermost/server/public/model.BuildDate=$(BUILD_DATE)\"\
-X github.com/mattermost/mattermost/server/public/model.BuildHash=$(BUILD_HASH)\
-X github.com/mattermost/mattermost/server/public/model.BuildHashEnterprise=$(BUILD_HASH_ENTERPRISE)\
-X github.com/mattermost/mattermost/server/public/model.BuildEnterpriseReady=$(BUILD_ENTERPRISE_READY)'"
-X github.com/mattermost/mattermost/server/public/model.BuildEnterpriseReady=$(BUILD_ENTERPRISE_READY)'\
-tags '$(BUILD_TAGS)'"
debug-server-headless: start-docker ## Debug server from within an IDE like VSCode or IntelliJ.
mkdir -p $(BUILD_WEBAPP_DIR)/channels/dist/files
@ -598,13 +597,14 @@ debug-server-headless: start-docker ## Debug server from within an IDE like VSCo
-X \"github.com/mattermost/mattermost/server/public/model.BuildDate=$(BUILD_DATE)\"\
-X github.com/mattermost/mattermost/server/public/model.BuildHash=$(BUILD_HASH)\
-X github.com/mattermost/mattermost/server/public/model.BuildHashEnterprise=$(BUILD_HASH_ENTERPRISE)\
-X github.com/mattermost/mattermost/server/public/model.BuildEnterpriseReady=$(BUILD_ENTERPRISE_READY)'"
-X github.com/mattermost/mattermost/server/public/model.BuildEnterpriseReady=$(BUILD_ENTERPRISE_READY)'\
-tags '$(BUILD_TAGS)'"
run-cli: start-docker ## Runs CLI.
@echo Running mattermost for development
@echo Example should be like 'make ARGS="-version" run-cli'
$(GO) run $(GOFLAGS) -ldflags '$(LDFLAGS)' $(PLATFORM_FILES) ${ARGS}
$(GO) run $(GOFLAGS) -ldflags '$(LDFLAGS)' -tags '$(BUILD_TAGS)' $(PLATFORM_FILES) ${ARGS}
run-client: client ## Runs the webapp.
@echo Running mattermost client for development
@ -664,7 +664,7 @@ restart-client: | stop-client run-client ## Restarts the webapp.
run-job-server: ## Runs the background job server.
@echo Running job server for development
$(GO) run $(GOFLAGS) -ldflags '$(LDFLAGS)' $(PLATFORM_FILES) jobserver &
$(GO) run $(GOFLAGS) -ldflags '$(LDFLAGS)' -tags '$(BUILD_TAGS)' $(PLATFORM_FILES) jobserver &
config-ldap: ## Configures LDAP.
@echo Setting up configuration for local LDAP
@ -730,8 +730,8 @@ update-dependencies: ## Uses go get -u to update all the dependencies while hold
@echo Updating Dependencies
ifeq ($(BUILD_ENTERPRISE_READY),true)
@echo Enterprise repository detected, temporarily removing imports.go
rm -f channels/imports/imports.go
@echo Enterprise repository detected, temporarily removing external_imports.go
mv enterprise/external_imports.go enterprise/external_imports.go.orig
endif
# Update all dependencies (does not update across major versions)
@ -741,7 +741,7 @@ endif
$(GO) mod tidy
ifeq ($(BUILD_ENTERPRISE_READY),true)
cp $(BUILD_ENTERPRISE_DIR)/imports/imports.go channels/imports/
mv enterprise/external_imports.go.orig enterprise/external_imports.go
endif
vet: ## Run mattermost go vet specific checks

View File

@ -5,85 +5,85 @@ build-linux: build-linux-amd64 build-linux-arm64
build-linux-amd64:
@echo Build Linux amd64
ifeq ($(BUILDER_GOOS_GOARCH),"linux_amd64")
env GOOS=linux GOARCH=amd64 $(GO) build -o $(GOBIN) $(GOFLAGS) -trimpath -tags production -ldflags '$(LDFLAGS)' ./...
env GOOS=linux GOARCH=amd64 $(GO) build -o $(GOBIN) $(GOFLAGS) -trimpath -tags '$(BUILD_TAGS) production' -ldflags '$(LDFLAGS)' ./...
else
mkdir -p $(GOBIN)/linux_amd64
env GOOS=linux GOARCH=amd64 $(GO) build -o $(GOBIN)/linux_amd64 $(GOFLAGS) -trimpath -tags production -ldflags '$(LDFLAGS)' ./...
env GOOS=linux GOARCH=amd64 $(GO) build -o $(GOBIN)/linux_amd64 $(GOFLAGS) -trimpath -tags '$(BUILD_TAGS) production' -ldflags '$(LDFLAGS)' ./...
endif
build-linux-arm64:
@echo Build Linux arm64
ifeq ($(BUILDER_GOOS_GOARCH),"linux_arm64")
env GOOS=linux GOARCH=arm64 $(GO) build -o $(GOBIN) $(GOFLAGS) -trimpath -tags production -ldflags '$(LDFLAGS)' ./...
env GOOS=linux GOARCH=arm64 $(GO) build -o $(GOBIN) $(GOFLAGS) -trimpath -tags '$(BUILD_TAGS) production' -ldflags '$(LDFLAGS)' ./...
else
mkdir -p $(GOBIN)/linux_arm64
env GOOS=linux GOARCH=arm64 $(GO) build -o $(GOBIN)/linux_arm64 $(GOFLAGS) -trimpath -tags production -ldflags '$(LDFLAGS)' ./...
env GOOS=linux GOARCH=arm64 $(GO) build -o $(GOBIN)/linux_arm64 $(GOFLAGS) -trimpath -tags '$(BUILD_TAGS) production' -ldflags '$(LDFLAGS)' ./...
endif
build-osx:
@echo Build OSX amd64
ifeq ($(BUILDER_GOOS_GOARCH),"darwin_amd64")
env GOOS=darwin GOARCH=amd64 $(GO) build -o $(GOBIN) $(GOFLAGS) -trimpath -tags production -ldflags '$(LDFLAGS)' ./...
env GOOS=darwin GOARCH=amd64 $(GO) build -o $(GOBIN) $(GOFLAGS) -trimpath -tags '$(BUILD_TAGS) production' -ldflags '$(LDFLAGS)' ./...
else
mkdir -p $(GOBIN)/darwin_amd64
env GOOS=darwin GOARCH=amd64 $(GO) build -o $(GOBIN)/darwin_amd64 $(GOFLAGS) -trimpath -tags production -ldflags '$(LDFLAGS)' ./...
env GOOS=darwin GOARCH=amd64 $(GO) build -o $(GOBIN)/darwin_amd64 $(GOFLAGS) -trimpath -tags '$(BUILD_TAGS) production' -ldflags '$(LDFLAGS)' ./...
endif
@echo Build OSX arm64
ifeq ($(BUILDER_GOOS_GOARCH),"darwin_arm64")
env GOOS=darwin GOARCH=arm64 $(GO) build -o $(GOBIN) $(GOFLAGS) -trimpath -tags production -ldflags '$(LDFLAGS)' ./...
env GOOS=darwin GOARCH=arm64 $(GO) build -o $(GOBIN) $(GOFLAGS) -trimpath -tags '$(BUILD_TAGS) production' -ldflags '$(LDFLAGS)' ./...
else
mkdir -p $(GOBIN)/darwin_arm64
env GOOS=darwin GOARCH=arm64 $(GO) build -o $(GOBIN)/darwin_arm64 $(GOFLAGS) -trimpath -tags production -ldflags '$(LDFLAGS)' ./...
env GOOS=darwin GOARCH=arm64 $(GO) build -o $(GOBIN)/darwin_arm64 $(GOFLAGS) -trimpath -tags '$(BUILD_TAGS) production' -ldflags '$(LDFLAGS)' ./...
endif
build-windows:
@echo Build Windows amd64
ifeq ($(BUILDER_GOOS_GOARCH),"windows_amd64")
env GOOS=windows GOARCH=amd64 $(GO) build -o $(GOBIN) $(GOFLAGS) -trimpath -tags production -ldflags '$(LDFLAGS)' ./...
env GOOS=windows GOARCH=amd64 $(GO) build -o $(GOBIN) $(GOFLAGS) -trimpath -tags '$(BUILD_TAGS) production' -ldflags '$(LDFLAGS)' ./...
else
mkdir -p $(GOBIN)/windows_amd64
env GOOS=windows GOARCH=amd64 $(GO) build -o $(GOBIN)/windows_amd64 $(GOFLAGS) -trimpath -tags production -ldflags '$(LDFLAGS)' ./...
env GOOS=windows GOARCH=amd64 $(GO) build -o $(GOBIN)/windows_amd64 $(GOFLAGS) -trimpath -tags '$(BUILD_TAGS) production' -ldflags '$(LDFLAGS)' ./...
endif
build-cmd-linux:
@echo Build CMD Linux amd64
ifeq ($(BUILDER_GOOS_GOARCH),"linux_amd64")
env GOOS=linux GOARCH=amd64 $(GO) build -o $(GOBIN) $(GOFLAGS) -trimpath -tags production -ldflags '$(LDFLAGS)' ./cmd/...
env GOOS=linux GOARCH=amd64 $(GO) build -o $(GOBIN) $(GOFLAGS) -trimpath -tags '$(BUILD_TAGS) production' -ldflags '$(LDFLAGS)' ./cmd/...
else
mkdir -p $(GOBIN)/linux_amd64
env GOOS=linux GOARCH=amd64 $(GO) build -o $(GOBIN)/linux_amd64 $(GOFLAGS) -trimpath -tags production -ldflags '$(LDFLAGS)' ./cmd/...
env GOOS=linux GOARCH=amd64 $(GO) build -o $(GOBIN)/linux_amd64 $(GOFLAGS) -trimpath -tags '$(BUILD_TAGS) production' -ldflags '$(LDFLAGS)' ./cmd/...
endif
@echo Build CMD Linux arm64
ifeq ($(BUILDER_GOOS_GOARCH),"linux_arm64")
env GOOS=linux GOARCH=arm64 $(GO) build -o $(GOBIN) $(GOFLAGS) -trimpath -tags production -ldflags '$(LDFLAGS)' ./cmd/...
env GOOS=linux GOARCH=arm64 $(GO) build -o $(GOBIN) $(GOFLAGS) -trimpath -tags '$(BUILD_TAGS) production' -ldflags '$(LDFLAGS)' ./cmd/...
else
mkdir -p $(GOBIN)/linux_arm64
env GOOS=linux GOARCH=arm64 $(GO) build -o $(GOBIN)/linux_arm64 $(GOFLAGS) -trimpath -tags production -ldflags '$(LDFLAGS)' ./cmd/...
env GOOS=linux GOARCH=arm64 $(GO) build -o $(GOBIN)/linux_arm64 $(GOFLAGS) -trimpath -tags '$(BUILD_TAGS) production' -ldflags '$(LDFLAGS)' ./cmd/...
endif
build-cmd-osx:
@echo Build CMD OSX amd64
ifeq ($(BUILDER_GOOS_GOARCH),"darwin_amd64")
env GOOS=darwin GOARCH=amd64 $(GO) build -o $(GOBIN) $(GOFLAGS) -trimpath -tags production -ldflags '$(LDFLAGS)' ./cmd/...
env GOOS=darwin GOARCH=amd64 $(GO) build -o $(GOBIN) $(GOFLAGS) -trimpath -tags '$(BUILD_TAGS) production' -ldflags '$(LDFLAGS)' ./cmd/...
else
mkdir -p $(GOBIN)/darwin_amd64
env GOOS=darwin GOARCH=amd64 $(GO) build -o $(GOBIN)/darwin_amd64 $(GOFLAGS) -trimpath -tags production -ldflags '$(LDFLAGS)' ./cmd/...
env GOOS=darwin GOARCH=amd64 $(GO) build -o $(GOBIN)/darwin_amd64 $(GOFLAGS) -trimpath -tags '$(BUILD_TAGS) production' -ldflags '$(LDFLAGS)' ./cmd/...
endif
@echo Build CMD OSX arm64
ifeq ($(BUILDER_GOOS_GOARCH),"darwin_arm64")
env GOOS=darwin GOARCH=arm64 $(GO) build -o $(GOBIN) $(GOFLAGS) -trimpath -tags production -ldflags '$(LDFLAGS)' ./cmd/...
env GOOS=darwin GOARCH=arm64 $(GO) build -o $(GOBIN) $(GOFLAGS) -trimpath -tags '$(BUILD_TAGS) production' -ldflags '$(LDFLAGS)' ./cmd/...
else
mkdir -p $(GOBIN)/darwin_arm64
env GOOS=darwin GOARCH=arm64 $(GO) build -o $(GOBIN)/darwin_arm64 $(GOFLAGS) -trimpath -tags production -ldflags '$(LDFLAGS)' ./cmd/...
env GOOS=darwin GOARCH=arm64 $(GO) build -o $(GOBIN)/darwin_arm64 $(GOFLAGS) -trimpath -tags '$(BUILD_TAGS) production' -ldflags '$(LDFLAGS)' ./cmd/...
endif
build-cmd-windows:
@echo Build CMD Windows amd64
ifeq ($(BUILDER_GOOS_GOARCH),"windows_amd64")
env GOOS=windows GOARCH=amd64 $(GO) build -o $(GOBIN) $(GOFLAGS) -trimpath -tags production -ldflags '$(LDFLAGS)' ./cmd/...
env GOOS=windows GOARCH=amd64 $(GO) build -o $(GOBIN) $(GOFLAGS) -trimpath -tags '$(BUILD_TAGS) production' -ldflags '$(LDFLAGS)' ./cmd/...
else
mkdir -p $(GOBIN)/windows_amd64
env GOOS=windows GOARCH=amd64 $(GO) build -o $(GOBIN)/windows_amd64 $(GOFLAGS) -trimpath -tags production -ldflags '$(LDFLAGS)' ./cmd/...
env GOOS=windows GOARCH=amd64 $(GO) build -o $(GOBIN)/windows_amd64 $(GOFLAGS) -trimpath -tags '$(BUILD_TAGS) production' -ldflags '$(LDFLAGS)' ./cmd/...
endif
build: setup-go-work build-client build-linux build-windows build-osx

View File

@ -323,11 +323,10 @@ func New(sc ServiceConfig, options ...Option) (*PlatformService, error) {
}
ps.AddLicenseListener(func(oldLicense, newLicense *model.License) {
if (oldLicense == nil && newLicense == nil) || !ps.startMetrics {
return
}
wasLicensed := (oldLicense != nil && *oldLicense.Features.Metrics) || (model.BuildNumber == "dev")
isLicensed := (newLicense != nil && *newLicense.Features.Metrics) || (model.BuildNumber == "dev")
if oldLicense != nil && newLicense != nil && *oldLicense.Features.Metrics == *newLicense.Features.Metrics {
if wasLicensed == isLicensed || !ps.startMetrics {
return
}

View File

@ -1,8 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// As part of the enterprise code integration machinery a go file is copied to
// this package, this file ensure the existence of the package anyway, allowing
// the rest of the code to import the package when enterprise code is there and
// when is not.
package imports

View File

@ -13,7 +13,7 @@ import (
_ "github.com/mattermost/mattermost/server/v8/channels/app/oauthproviders/gitlab"
// Enterprise Imports
_ "github.com/mattermost/mattermost/server/v8/channels/imports"
_ "github.com/mattermost/mattermost/server/v8/enterprise"
)
func main() {

View File

@ -17,8 +17,8 @@ import (
_ "github.com/hashicorp/memberlist"
_ "github.com/mattermost/gosaml2"
_ "github.com/mattermost/ldap"
_ "github.com/mattermost/mattermost/server/v8/channels/imports"
_ "github.com/mattermost/mattermost/server/v8/channels/utils/testutils"
_ "github.com/mattermost/mattermost/server/v8/enterprise"
_ "github.com/mattermost/rsc/qr"
_ "github.com/prometheus/client_golang/prometheus"
_ "github.com/prometheus/client_golang/prometheus/collectors"

39
server/enterprise/LICENSE Normal file
View File

@ -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.

View File

@ -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).

View File

@ -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"
)

View File

@ -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"
)

View File

@ -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)
}

View File

@ -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)
}

File diff suppressed because it is too large Load Diff

View File

@ -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)
})
}
}

View File

@ -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

View File

@ -51,6 +51,7 @@ require (
github.com/opentracing/opentracing-go v1.2.0
github.com/pkg/errors v0.9.1
github.com/prometheus/client_golang v1.17.0
github.com/prometheus/client_model v0.5.0
github.com/reflog/dateconstraints v0.2.1
github.com/rs/cors v1.10.1
github.com/rudderlabs/analytics-go v3.3.3+incompatible
@ -179,7 +180,6 @@ require (
github.com/philhofer/fwd v1.1.2 // indirect
github.com/pierrec/lz4/v4 v4.1.19 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_model v0.5.0 // indirect
github.com/prometheus/common v0.45.0 // indirect
github.com/prometheus/procfs v0.12.0 // indirect
github.com/redis/go-redis/v9 v9.3.0 // indirect