diff --git a/LICENSE.enterprise b/LICENSE.enterprise new file mode 120000 index 00000000000..c3dc0c6eb13 --- /dev/null +++ b/LICENSE.enterprise @@ -0,0 +1 @@ +server/enterprise/LICENSE \ No newline at end of file diff --git a/e2e-tests/cypress/tests/integration/channels/enterprise/metrics/helper.js b/e2e-tests/cypress/tests/integration/channels/enterprise/metrics/helper.js new file mode 100644 index 00000000000..e713bb37f9f --- /dev/null +++ b/e2e-tests/cypress/tests/integration/channels/enterprise/metrics/helper.js @@ -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, + }, + }); +}; diff --git a/e2e-tests/cypress/tests/integration/channels/enterprise/metrics/license_no_license_spec.js b/e2e-tests/cypress/tests/integration/channels/enterprise/metrics/license_no_license_spec.js new file mode 100644 index 00000000000..aff4a98901a --- /dev/null +++ b/e2e-tests/cypress/tests/integration/channels/enterprise/metrics/license_no_license_spec.js @@ -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); + }); + }); +}); diff --git a/e2e-tests/cypress/tests/integration/channels/enterprise/metrics/license_spec.js b/e2e-tests/cypress/tests/integration/channels/enterprise/metrics/license_spec.js new file mode 100644 index 00000000000..530da66f015 --- /dev/null +++ b/e2e-tests/cypress/tests/integration/channels/enterprise/metrics/license_spec.js @@ -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); + }); + }); +}); diff --git a/e2e-tests/cypress/tests/support/api/on_prem_default_config.json b/e2e-tests/cypress/tests/support/api/on_prem_default_config.json index a5a09a73fb5..8d4d196e4fb 100644 --- a/e2e-tests/cypress/tests/support/api/on_prem_default_config.json +++ b/e2e-tests/cypress/tests/support/api/on_prem_default_config.json @@ -162,7 +162,7 @@ "ConsoleJson": true, "EnableColor": false, "EnableFile": true, - "FileLevel": "INFO", + "FileLevel": "DEBUG", "FileJson": true, "FileLocation": "", "EnableWebhookDebugging": true, diff --git a/server/.gitignore b/server/.gitignore index f965b70690f..cafb6408e9b 100644 --- a/server/.gitignore +++ b/server/.gitignore @@ -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 diff --git a/server/Makefile b/server/Makefile index 807a8390802..41d7d794427 100644 --- a/server/Makefile +++ b/server/Makefile @@ -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 diff --git a/server/build/release.mk b/server/build/release.mk index 41f1b4b14e9..9c0e08efd04 100644 --- a/server/build/release.mk +++ b/server/build/release.mk @@ -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 diff --git a/server/channels/app/platform/service.go b/server/channels/app/platform/service.go index bb5fe914dc1..0aa732ad01a 100644 --- a/server/channels/app/platform/service.go +++ b/server/channels/app/platform/service.go @@ -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 } diff --git a/server/channels/imports/placeholder.go b/server/channels/imports/placeholder.go deleted file mode 100644 index f12e7166143..00000000000 --- a/server/channels/imports/placeholder.go +++ /dev/null @@ -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 diff --git a/server/cmd/mattermost/main.go b/server/cmd/mattermost/main.go index 4ff658560e1..5fb4b4a005b 100644 --- a/server/cmd/mattermost/main.go +++ b/server/cmd/mattermost/main.go @@ -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() { diff --git a/server/cmd/mmctl/commands/enterprise.go b/server/cmd/mmctl/commands/enterprise.go index da02110f8eb..568a18a8ad9 100644 --- a/server/cmd/mmctl/commands/enterprise.go +++ b/server/cmd/mmctl/commands/enterprise.go @@ -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" diff --git a/server/enterprise/LICENSE b/server/enterprise/LICENSE new file mode 100644 index 00000000000..c698c02052e --- /dev/null +++ b/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. diff --git a/server/enterprise/README.md b/server/enterprise/README.md new file mode 100644 index 00000000000..38ae3037b15 --- /dev/null +++ b/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). diff --git a/server/enterprise/external_imports.go b/server/enterprise/external_imports.go new file mode 100644 index 00000000000..ea6bef7c39f --- /dev/null +++ b/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" +) diff --git a/server/enterprise/local_imports.go b/server/enterprise/local_imports.go new file mode 100644 index 00000000000..0890e060eaf --- /dev/null +++ b/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" +) diff --git a/server/enterprise/metrics/dynamic.go b/server/enterprise/metrics/dynamic.go new file mode 100644 index 00000000000..a2bccff3832 --- /dev/null +++ b/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) +} diff --git a/server/enterprise/metrics/main_test.go b/server/enterprise/metrics/main_test.go new file mode 100644 index 00000000000..f411847a5f3 --- /dev/null +++ b/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) +} diff --git a/server/enterprise/metrics/metrics.go b/server/enterprise/metrics/metrics.go new file mode 100644 index 00000000000..7440b138e44 --- /dev/null +++ b/server/enterprise/metrics/metrics.go @@ -0,0 +1,1369 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.enterprise for license information. + +package metrics + +import ( + "database/sql" + "math" + "net/url" + "os" + "strconv" + "strings" + + "github.com/go-sql-driver/mysql" + + "github.com/pkg/errors" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/collectors" + "github.com/prometheus/client_golang/prometheus/promhttp" + + "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/public/shared/mlog" + "github.com/mattermost/mattermost/server/v8/channels/app/platform" + "github.com/mattermost/mattermost/server/v8/einterfaces" +) + +const ( + MetricsNamespace = "mattermost" + MetricsSubsystemPosts = "post" + MetricsSubsystemDB = "db" + MetricsSubsystemAPI = "api" + MetricsSubsystemPlugin = "plugin" + MetricsSubsystemHTTP = "http" + MetricsSubsystemCluster = "cluster" + MetricsSubsystemLogin = "login" + MetricsSubsystemCaching = "cache" + MetricsSubsystemWebsocket = "websocket" + MetricsSubsystemSearch = "search" + MetricsSubsystemLogging = "logging" + MetricsSubsystemRemoteCluster = "remote_cluster" + MetricsSubsystemSystem = "system" + MetricsSubsystemJobs = "jobs" + MetricsCloudInstallationLabel = "installationId" + MetricsCloudDatabaseClusterLabel = "databaseClusterName" + MetricsCloudInstallationGroupLabel = "installationGroupId" +) + +type MetricsInterfaceImpl struct { + Platform *platform.PlatformService + + Registry *prometheus.Registry + + DbMasterConnectionsGauge prometheus.GaugeFunc + DbReadConnectionsGauge prometheus.GaugeFunc + DbSearchConnectionsGauge prometheus.GaugeFunc + DbReplicaLagGaugeAbs *prometheus.GaugeVec + DbReplicaLagGaugeTime *prometheus.GaugeVec + + PostCreateCounter prometheus.Counter + WebhookPostCounter prometheus.Counter + PostSentEmailCounter prometheus.Counter + PostSentPushCounter prometheus.Counter + PostBroadcastCounter prometheus.Counter + PostFileAttachCounter prometheus.Counter + + HTTPRequestsCounter prometheus.Counter + HTTPErrorsCounter prometheus.Counter + HTTPWebsocketsGauge prometheus.GaugeFunc + + ClusterRequestsDuration prometheus.Histogram + ClusterRequestsCounter prometheus.Counter + + ClusterHealthGauge prometheus.GaugeFunc + + ClusterEventTypeCounters *prometheus.CounterVec + ClusterEventTypePublish prometheus.Counter + ClusterEventTypeStatus prometheus.Counter + ClusterEventTypeInvAll prometheus.Counter + ClusterEventTypeInvReactions prometheus.Counter + ClusterEventTypeInvWebhook prometheus.Counter + ClusterEventTypeInvChannelPosts prometheus.Counter + ClusterEventTypeInvChannelMembersNotifyProps prometheus.Counter + ClusterEventTypeInvChannelMembers prometheus.Counter + ClusterEventTypeInvChannelByName prometheus.Counter + ClusterEventTypeInvChannel prometheus.Counter + ClusterEventTypeInvUser prometheus.Counter + ClusterEventTypeInvSessions prometheus.Counter + ClusterEventTypeInvRoles prometheus.Counter + ClusterEventTypeOther prometheus.Counter + + LoginCounter prometheus.Counter + LoginFailCounter prometheus.Counter + + EtagMissCounters *prometheus.CounterVec + EtagHitCounters *prometheus.CounterVec + + MemCacheMissCounters *prometheus.CounterVec + MemCacheHitCounters *prometheus.CounterVec + MemCacheInvalidationCounters *prometheus.CounterVec + + MemCacheHitCounterSession prometheus.Counter + MemCacheMissCounterSession prometheus.Counter + MemCacheInvalidationCounterSession prometheus.Counter + + WebsocketEventCounters *prometheus.CounterVec + + WebSocketBroadcastCounters *prometheus.CounterVec + WebSocketBroadcastTyping prometheus.Counter + WebSocketBroadcastChannelViewed prometheus.Counter + WebSocketBroadcastPosted prometheus.Counter + WebSocketBroadcastNewUser prometheus.Counter + WebSocketBroadcastUserAdded prometheus.Counter + WebSocketBroadcastUserUpdated prometheus.Counter + WebSocketBroadcastUserRemoved prometheus.Counter + WebSocketBroadcastPreferenceChanged prometheus.Counter + WebSocketBroadcastephemeralMessage prometheus.Counter + WebSocketBroadcastStatusChange prometheus.Counter + WebSocketBroadcastHello prometheus.Counter + WebSocketBroadcastResponse prometheus.Counter + WebsocketBroadcastPostEdited prometheus.Counter + WebsocketBroadcastPostDeleted prometheus.Counter + WebsocketBroadcastPostUnread prometheus.Counter + WebsocketBroadcastChannelConverted prometheus.Counter + WebsocketBroadcastChannelCreated prometheus.Counter + WebsocketBroadcastChannelDeleted prometheus.Counter + WebsocketBroadcastChannelRestored prometheus.Counter + WebsocketBroadcastChannelUpdated prometheus.Counter + WebsocketBroadcastChannelMemberUpdated prometheus.Counter + WebsocketBroadcastChannelSchemeUpdated prometheus.Counter + WebsocketBroadcastDirectAdded prometheus.Counter + WebsocketBroadcastGroupAdded prometheus.Counter + WebsocketBroadcastAddedToTeam prometheus.Counter + WebsocketBroadcastLeaveTeam prometheus.Counter + WebsocketBroadcastUpdateTeam prometheus.Counter + WebsocketBroadcastDeleteTeam prometheus.Counter + WebsocketBroadcastRestoreTeam prometheus.Counter + WebsocketBroadcastUpdateTeamScheme prometheus.Counter + WebsocketBroadcastUserRoleUpdated prometheus.Counter + WebsocketBroadcastMemberroleUpdated prometheus.Counter + WebsocketBroadcastPreferencesChanged prometheus.Counter + WebsocketBroadcastPreferencesDeleted prometheus.Counter + WebsocketBroadcastReactionAdded prometheus.Counter + WebsocketBroadcastReactionRemoved prometheus.Counter + WebsocketBroadcastGroupMemberDelete prometheus.Counter + WebsocketBroadcastGroupMemberAdd prometheus.Counter + WebsocketBroadcastSidebarCategoryCreated prometheus.Counter + WebsocketBroadcastSidebarCategoryUpdated prometheus.Counter + WebsocketBroadcastSidebarCategoryDeleted prometheus.Counter + WebsocketBroadcastSidebarCategoryOrderUpdated prometheus.Counter + WebsocketBroadcastThreadUpdated prometheus.Counter + WebsocketBroadcastThreadFollowChanged prometheus.Counter + WebsocketBroadcastThreadReadChanged prometheus.Counter + WebsocketBroadcastDraftCreated prometheus.Counter + WebsocketBroadcastDraftUpdated prometheus.Counter + WebsocketBroadcastDraftDeleted prometheus.Counter + + WebSocketBroadcastOther prometheus.Counter + WebSocketBroadcastBufferGauge *prometheus.GaugeVec + WebSocketBroadcastBufferUsersRegisteredGauge *prometheus.GaugeVec + WebSocketReconnectCounter *prometheus.CounterVec + + SearchPostSearchesCounter prometheus.Counter + SearchPostSearchesDuration prometheus.Histogram + SearchFileSearchesCounter prometheus.Counter + SearchFileSearchesDuration prometheus.Histogram + StoreTimesHistograms *prometheus.HistogramVec + APITimesHistograms *prometheus.HistogramVec + SearchPostIndexCounter prometheus.Counter + SearchFileIndexCounter prometheus.Counter + SearchUserIndexCounter prometheus.Counter + SearchChannelIndexCounter prometheus.Counter + ActiveUsers prometheus.Gauge + + PluginHookTimeHistogram *prometheus.HistogramVec + PluginMultiHookTimeHistogram *prometheus.HistogramVec + PluginMultiHookServerTimeHistogram prometheus.Histogram + PluginAPITimeHistogram *prometheus.HistogramVec + + LoggerQueueGauge *DynamicGauge + LoggerLoggedCounters *DynamicCounter + LoggerErrorCounters *DynamicCounter + LoggerDroppedCounters *DynamicCounter + LoggerBlockedCounters *DynamicCounter + + RemoteClusterMsgSentCounters *prometheus.CounterVec + RemoteClusterMsgReceivedCounters *prometheus.CounterVec + RemoteClusterMsgErrorsCounter *prometheus.CounterVec + RemoteClusterPingTimesHistograms *prometheus.HistogramVec + RemoteClusterClockSkewHistograms *prometheus.HistogramVec + RemoteClusterConnStateChangeCounter *prometheus.CounterVec + + ServerStartTime prometheus.Gauge + + JobsActive *prometheus.GaugeVec +} + +func init() { + platform.RegisterMetricsInterface(func(ps *platform.PlatformService, driver, dataSource string) einterfaces.MetricsInterface { + return New(ps, driver, dataSource) + }) +} + +// New creates a new MetricsInterface. The driver and datasoruce parameters are added during +// migrating configuration store to the new platform service. Once the store and license are migrated, +// we will be able to remove server dependency and lean on platform service during initialization. +func New(ps *platform.PlatformService, driver, dataSource string) *MetricsInterfaceImpl { + m := &MetricsInterfaceImpl{ + Platform: ps, + } + + m.Registry = prometheus.NewRegistry() + options := collectors.ProcessCollectorOpts{ + Namespace: MetricsNamespace, + } + m.Registry.MustRegister(collectors.NewProcessCollector(options)) + m.Registry.MustRegister(collectors.NewGoCollector()) + + additionalLabels := map[string]string{} + if os.Getenv("MM_CLOUD_INSTALLATION_ID") != "" { + additionalLabels[MetricsCloudInstallationLabel] = os.Getenv("MM_CLOUD_INSTALLATION_ID") + if os.Getenv("MM_CLOUD_GROUP_ID") != "" { + additionalLabels[MetricsCloudInstallationGroupLabel] = os.Getenv("MM_CLOUD_GROUP_ID") + } + cluster, err := extractDBCluster(driver, dataSource) + if err != nil { + ps.Log().Warn("Failed to extract DB Cluster label", mlog.Err(err)) + } else { + additionalLabels[MetricsCloudDatabaseClusterLabel] = cluster + } + } + // Posts Subsystem + + m.PostCreateCounter = prometheus.NewCounter(prometheus.CounterOpts{ + Namespace: MetricsNamespace, + Subsystem: MetricsSubsystemPosts, + Name: "total", + Help: "The total number of posts created.", + ConstLabels: additionalLabels, + }) + m.Registry.MustRegister(m.PostCreateCounter) + + m.WebhookPostCounter = prometheus.NewCounter(prometheus.CounterOpts{ + Namespace: MetricsNamespace, + Subsystem: MetricsSubsystemPosts, + Name: "webhooks_total", + Help: "Total number of webhook posts created.", + ConstLabels: additionalLabels, + }) + m.Registry.MustRegister(m.WebhookPostCounter) + + m.PostSentEmailCounter = prometheus.NewCounter(prometheus.CounterOpts{ + Namespace: MetricsNamespace, + Subsystem: MetricsSubsystemPosts, + Name: "emails_sent_total", + Help: "The total number of emails sent because a post was created.", + ConstLabels: additionalLabels, + }) + m.Registry.MustRegister(m.PostSentEmailCounter) + + m.PostSentPushCounter = prometheus.NewCounter(prometheus.CounterOpts{ + Namespace: MetricsNamespace, + Subsystem: MetricsSubsystemPosts, + Name: "pushes_sent_total", + Help: "The total number of mobile push notifications sent because a post was created.", + ConstLabels: additionalLabels, + }) + m.Registry.MustRegister(m.PostSentPushCounter) + + m.PostBroadcastCounter = prometheus.NewCounter(prometheus.CounterOpts{ + Namespace: MetricsNamespace, + Subsystem: MetricsSubsystemPosts, + Name: "broadcasts_total", + Help: "The total number of websocket broadcasts sent because a post was created.", + ConstLabels: additionalLabels, + }) + m.Registry.MustRegister(m.PostBroadcastCounter) + + m.PostFileAttachCounter = prometheus.NewCounter(prometheus.CounterOpts{ + Namespace: MetricsNamespace, + Subsystem: MetricsSubsystemPosts, + Name: "file_attachments_total", + Help: "The total number of file attachments created because a post was created.", + ConstLabels: additionalLabels, + }) + m.Registry.MustRegister(m.PostFileAttachCounter) + + // Database Subsystem + + m.DbMasterConnectionsGauge = prometheus.NewGaugeFunc(prometheus.GaugeOpts{ + Namespace: MetricsNamespace, + Subsystem: MetricsSubsystemDB, + Name: "master_connections_total", + Help: "The total number of connections to the master database.", + ConstLabels: additionalLabels, + }, func() float64 { return float64(m.Platform.Store.TotalMasterDbConnections()) }) + m.Registry.MustRegister(m.DbMasterConnectionsGauge) + + m.DbReadConnectionsGauge = prometheus.NewGaugeFunc(prometheus.GaugeOpts{ + Namespace: MetricsNamespace, + Subsystem: MetricsSubsystemDB, + Name: "read_replica_connections_total", + Help: "The total number of connections to all the read replica databases.", + ConstLabels: additionalLabels, + }, func() float64 { + // We use the event hook for total read_replica connections to populate + // the replica lag metrics. + // The reason for doing it this way is that the replica lag metrics need the node + // as a label value, which means we need to populate the metric ourselves. Since this + // is not an event based metric, we would need to maintain a poller goroutine ourselves + // to do that. Therefore using the Prometheus in-built metric writer interface helps us + // to avoid writing that code. + if m.Platform.IsLeader() { + err := m.Platform.Store.ReplicaLagAbs() + if err != nil { + m.Platform.Log().Warn("ReplicaLagAbs query returned error", mlog.Err(err)) + } + err = m.Platform.Store.ReplicaLagTime() + if err != nil { + m.Platform.Log().Warn("ReplicaLagTime query returned error", mlog.Err(err)) + } + } + + return float64(m.Platform.Store.TotalReadDbConnections()) + }) + m.Registry.MustRegister(m.DbReadConnectionsGauge) + + m.DbSearchConnectionsGauge = prometheus.NewGaugeFunc(prometheus.GaugeOpts{ + Namespace: MetricsNamespace, + Subsystem: MetricsSubsystemDB, + Name: "search_replica_connections_total", + Help: "The total number of connections to the search replica database.", + ConstLabels: additionalLabels, + }, func() float64 { return float64(m.Platform.Store.TotalSearchDbConnections()) }) + m.Registry.MustRegister(m.DbSearchConnectionsGauge) + + m.DbReplicaLagGaugeAbs = prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Namespace: MetricsNamespace, + Subsystem: MetricsSubsystemDB, + Name: "replica_lag_abs", + Help: "An abstract unit for measuring replica lag.", + ConstLabels: additionalLabels, + }, + []string{"node"}, + ) + m.Registry.MustRegister(m.DbReplicaLagGaugeAbs) + + m.DbReplicaLagGaugeTime = prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Namespace: MetricsNamespace, + Subsystem: MetricsSubsystemDB, + Name: "replica_lag_time", + Help: "A time unit for measuring replica lag.", + ConstLabels: additionalLabels, + }, + []string{"node"}, + ) + m.Registry.MustRegister(m.DbReplicaLagGaugeTime) + + // HTTP Subsystem + + m.HTTPWebsocketsGauge = prometheus.NewGaugeFunc(prometheus.GaugeOpts{ + Namespace: MetricsNamespace, + Subsystem: MetricsSubsystemHTTP, + Name: "websockets_total", + Help: "The total number of websocket connections to this server.", + ConstLabels: additionalLabels, + }, func() float64 { return float64(m.Platform.TotalWebsocketConnections()) }) + m.Registry.MustRegister(m.HTTPWebsocketsGauge) + + m.HTTPRequestsCounter = prometheus.NewCounter(prometheus.CounterOpts{ + Namespace: MetricsNamespace, + Subsystem: MetricsSubsystemHTTP, + Name: "requests_total", + Help: "The total number of http API requests.", + ConstLabels: additionalLabels, + }) + m.Registry.MustRegister(m.HTTPRequestsCounter) + + m.HTTPErrorsCounter = prometheus.NewCounter(prometheus.CounterOpts{ + Namespace: MetricsNamespace, + Subsystem: MetricsSubsystemHTTP, + Name: "errors_total", + Help: "The total number of http API errors.", + ConstLabels: additionalLabels, + }) + m.Registry.MustRegister(m.HTTPErrorsCounter) + + // Cluster Subsystem + + m.ClusterHealthGauge = prometheus.NewGaugeFunc(prometheus.GaugeOpts{ + Namespace: MetricsNamespace, + Subsystem: MetricsSubsystemCluster, + Name: "cluster_health_score", + Help: "A score that gives an idea of how well it is meeting the soft-real time requirements of the gossip protocol.", + ConstLabels: additionalLabels, + }, func() float64 { + if m.Platform.Cluster() == nil { + return 0 + } + + return float64(m.Platform.Cluster().HealthScore()) + }) + m.Registry.MustRegister(m.ClusterHealthGauge) + + m.ClusterRequestsCounter = prometheus.NewCounter(prometheus.CounterOpts{ + Namespace: MetricsNamespace, + Subsystem: MetricsSubsystemCluster, + Name: "cluster_requests_total", + Help: "The total number of inter-node requests.", + ConstLabels: additionalLabels, + }) + m.Registry.MustRegister(m.ClusterRequestsCounter) + + m.ClusterRequestsDuration = prometheus.NewHistogram(prometheus.HistogramOpts{ + Namespace: MetricsNamespace, + Subsystem: MetricsSubsystemCluster, + Name: "cluster_request_duration_seconds", + Help: "The total duration in seconds of the inter-node cluster requests.", + ConstLabels: additionalLabels, + }) + m.Registry.MustRegister(m.ClusterRequestsDuration) + + m.ClusterEventTypeCounters = prometheus.NewCounterVec( + prometheus.CounterOpts{ + Namespace: MetricsNamespace, + Subsystem: MetricsSubsystemCluster, + Name: "cluster_event_type_totals", + Help: "The total number of cluster requests sent for any type.", + ConstLabels: additionalLabels, + }, + []string{"name"}, + ) + m.Registry.MustRegister(m.ClusterEventTypeCounters) + m.ClusterEventTypePublish = m.ClusterEventTypeCounters.With(prometheus.Labels{"name": string(model.ClusterEventPublish)}) + m.ClusterEventTypeStatus = m.ClusterEventTypeCounters.With(prometheus.Labels{"name": string(model.ClusterEventUpdateStatus)}) + m.ClusterEventTypeInvAll = m.ClusterEventTypeCounters.With(prometheus.Labels{"name": string(model.ClusterEventInvalidateAllCaches)}) + m.ClusterEventTypeInvReactions = m.ClusterEventTypeCounters.With(prometheus.Labels{"name": string(model.ClusterEventInvalidateCacheForReactions)}) + m.ClusterEventTypeInvChannelMembersNotifyProps = m.ClusterEventTypeCounters.With(prometheus.Labels{"name": string(model.ClusterEventInvalidateCacheForChannelMembersNotifyProps)}) + m.ClusterEventTypeInvChannelByName = m.ClusterEventTypeCounters.With(prometheus.Labels{"name": string(model.ClusterEventInvalidateCacheForChannelByName)}) + m.ClusterEventTypeInvChannel = m.ClusterEventTypeCounters.With(prometheus.Labels{"name": string(model.ClusterEventInvalidateCacheForChannel)}) + m.ClusterEventTypeInvUser = m.ClusterEventTypeCounters.With(prometheus.Labels{"name": string(model.ClusterEventInvalidateCacheForUser)}) + m.ClusterEventTypeInvSessions = m.ClusterEventTypeCounters.With(prometheus.Labels{"name": string(model.ClusterEventClearSessionCacheForUser)}) + m.ClusterEventTypeInvRoles = m.ClusterEventTypeCounters.With(prometheus.Labels{"name": string(model.ClusterEventInvalidateCacheForRoles)}) + m.ClusterEventTypeOther = m.ClusterEventTypeCounters.With(prometheus.Labels{"name": "other"}) + + // Login Subsystem + + m.LoginCounter = prometheus.NewCounter(prometheus.CounterOpts{ + Namespace: MetricsNamespace, + Subsystem: MetricsSubsystemLogin, + Name: "logins_total", + Help: "The total number of successful logins.", + ConstLabels: additionalLabels, + }) + m.Registry.MustRegister(m.LoginCounter) + + m.LoginFailCounter = prometheus.NewCounter(prometheus.CounterOpts{ + Namespace: MetricsNamespace, + Subsystem: MetricsSubsystemLogin, + Name: "logins_fail_total", + Help: "The total number of failed logins.", + ConstLabels: additionalLabels, + }) + m.Registry.MustRegister(m.LoginFailCounter) + + // Caching Subsystem + + m.EtagMissCounters = prometheus.NewCounterVec( + prometheus.CounterOpts{ + Namespace: MetricsNamespace, + Subsystem: MetricsSubsystemCaching, + Name: "etag_miss_total", + Help: "Total number of etag misses", + ConstLabels: additionalLabels, + }, + []string{"route"}, + ) + m.Registry.MustRegister(m.EtagMissCounters) + + m.EtagHitCounters = prometheus.NewCounterVec( + prometheus.CounterOpts{ + Namespace: MetricsNamespace, + Subsystem: MetricsSubsystemCaching, + Name: "etag_hit_total", + Help: "Total number of etag hits (304)", + ConstLabels: additionalLabels, + }, + []string{"route"}, + ) + m.Registry.MustRegister(m.EtagHitCounters) + + m.MemCacheMissCounters = prometheus.NewCounterVec( + prometheus.CounterOpts{ + Namespace: MetricsNamespace, + Subsystem: MetricsSubsystemCaching, + Name: "mem_miss_total", + Help: "Total number of memory cache misses", + ConstLabels: additionalLabels, + }, + []string{"name"}, + ) + m.Registry.MustRegister(m.MemCacheMissCounters) + m.MemCacheMissCounterSession = m.MemCacheMissCounters.With(prometheus.Labels{"name": "Session"}) + + m.MemCacheHitCounters = prometheus.NewCounterVec( + prometheus.CounterOpts{ + Namespace: MetricsNamespace, + Subsystem: MetricsSubsystemCaching, + Name: "mem_hit_total", + Help: "Total number of memory cache hits", + ConstLabels: additionalLabels, + }, + []string{"name"}, + ) + m.Registry.MustRegister(m.MemCacheHitCounters) + m.MemCacheHitCounterSession = m.MemCacheHitCounters.With(prometheus.Labels{"name": "Session"}) + + m.MemCacheInvalidationCounters = prometheus.NewCounterVec( + prometheus.CounterOpts{ + Namespace: MetricsNamespace, + Subsystem: MetricsSubsystemCaching, + Name: "mem_invalidation_total", + Help: "Total number of memory cache invalidations", + ConstLabels: additionalLabels, + }, + []string{"name"}, + ) + m.Registry.MustRegister(m.MemCacheInvalidationCounters) + m.MemCacheInvalidationCounterSession = m.MemCacheInvalidationCounters.With(prometheus.Labels{"name": "Session"}) + + // Websocket Subsystem + + m.WebSocketBroadcastCounters = prometheus.NewCounterVec( + prometheus.CounterOpts{ + Namespace: MetricsNamespace, + Subsystem: MetricsSubsystemWebsocket, + Name: "broadcasts_total", + Help: "The total number of websocket broadcasts sent for any type.", + ConstLabels: additionalLabels, + }, + []string{"name"}, + ) + m.Registry.MustRegister(m.WebSocketBroadcastCounters) + m.WebSocketBroadcastTyping = m.WebSocketBroadcastCounters.With(prometheus.Labels{"name": string(model.WebsocketEventTyping)}) + m.WebSocketBroadcastChannelViewed = m.WebSocketBroadcastCounters.With(prometheus.Labels{"name": string(model.WebsocketEventMultipleChannelsViewed)}) + m.WebSocketBroadcastPosted = m.WebSocketBroadcastCounters.With(prometheus.Labels{"name": string(model.WebsocketEventPosted)}) + m.WebSocketBroadcastNewUser = m.WebSocketBroadcastCounters.With(prometheus.Labels{"name": string(model.WebsocketEventNewUser)}) + m.WebSocketBroadcastUserAdded = m.WebSocketBroadcastCounters.With(prometheus.Labels{"name": string(model.WebsocketEventUserAdded)}) + m.WebSocketBroadcastUserUpdated = m.WebSocketBroadcastCounters.With(prometheus.Labels{"name": string(model.WebsocketEventUserUpdated)}) + m.WebSocketBroadcastUserRemoved = m.WebSocketBroadcastCounters.With(prometheus.Labels{"name": string(model.WebsocketEventUserRemoved)}) + m.WebSocketBroadcastPreferenceChanged = m.WebSocketBroadcastCounters.With(prometheus.Labels{"name": string(model.WebsocketEventPreferenceChanged)}) + m.WebSocketBroadcastephemeralMessage = m.WebSocketBroadcastCounters.With(prometheus.Labels{"name": string(model.WebsocketEventEphemeralMessage)}) + m.WebSocketBroadcastStatusChange = m.WebSocketBroadcastCounters.With(prometheus.Labels{"name": string(model.WebsocketEventStatusChange)}) + m.WebSocketBroadcastHello = m.WebSocketBroadcastCounters.With(prometheus.Labels{"name": string(model.WebsocketEventHello)}) + m.WebSocketBroadcastResponse = m.WebSocketBroadcastCounters.With(prometheus.Labels{"name": string(model.WebsocketEventResponse)}) + m.WebsocketBroadcastPostEdited = m.WebSocketBroadcastCounters.With(prometheus.Labels{"name": string(model.WebsocketEventPostEdited)}) + m.WebsocketBroadcastPostDeleted = m.WebSocketBroadcastCounters.With(prometheus.Labels{"name": string(model.WebsocketEventPostDeleted)}) + m.WebsocketBroadcastPostUnread = m.WebSocketBroadcastCounters.With(prometheus.Labels{"name": string(model.WebsocketEventPostUnread)}) + m.WebsocketBroadcastChannelConverted = m.WebSocketBroadcastCounters.With(prometheus.Labels{"name": string(model.WebsocketEventChannelConverted)}) + m.WebsocketBroadcastChannelCreated = m.WebSocketBroadcastCounters.With(prometheus.Labels{"name": string(model.WebsocketEventChannelCreated)}) + m.WebsocketBroadcastChannelDeleted = m.WebSocketBroadcastCounters.With(prometheus.Labels{"name": string(model.WebsocketEventChannelDeleted)}) + m.WebsocketBroadcastChannelRestored = m.WebSocketBroadcastCounters.With(prometheus.Labels{"name": string(model.WebsocketEventChannelRestored)}) + m.WebsocketBroadcastChannelUpdated = m.WebSocketBroadcastCounters.With(prometheus.Labels{"name": string(model.WebsocketEventChannelUpdated)}) + m.WebsocketBroadcastChannelMemberUpdated = m.WebSocketBroadcastCounters.With(prometheus.Labels{"name": string(model.WebsocketEventChannelMemberUpdated)}) + m.WebsocketBroadcastChannelSchemeUpdated = m.WebSocketBroadcastCounters.With(prometheus.Labels{"name": string(model.WebsocketEventChannelSchemeUpdated)}) + m.WebsocketBroadcastDirectAdded = m.WebSocketBroadcastCounters.With(prometheus.Labels{"name": string(model.WebsocketEventDirectAdded)}) + m.WebsocketBroadcastGroupAdded = m.WebSocketBroadcastCounters.With(prometheus.Labels{"name": string(model.WebsocketEventGroupAdded)}) + m.WebsocketBroadcastAddedToTeam = m.WebSocketBroadcastCounters.With(prometheus.Labels{"name": string(model.WebsocketEventAddedToTeam)}) + m.WebsocketBroadcastLeaveTeam = m.WebSocketBroadcastCounters.With(prometheus.Labels{"name": string(model.WebsocketEventLeaveTeam)}) + m.WebsocketBroadcastUpdateTeam = m.WebSocketBroadcastCounters.With(prometheus.Labels{"name": string(model.WebsocketEventUpdateTeam)}) + m.WebsocketBroadcastDeleteTeam = m.WebSocketBroadcastCounters.With(prometheus.Labels{"name": string(model.WebsocketEventDeleteTeam)}) + m.WebsocketBroadcastRestoreTeam = m.WebSocketBroadcastCounters.With(prometheus.Labels{"name": string(model.WebsocketEventRestoreTeam)}) + m.WebsocketBroadcastUpdateTeamScheme = m.WebSocketBroadcastCounters.With(prometheus.Labels{"name": string(model.WebsocketEventUpdateTeamScheme)}) + m.WebsocketBroadcastUserRoleUpdated = m.WebSocketBroadcastCounters.With(prometheus.Labels{"name": string(model.WebsocketEventUserRoleUpdated)}) + m.WebsocketBroadcastMemberroleUpdated = m.WebSocketBroadcastCounters.With(prometheus.Labels{"name": string(model.WebsocketEventMemberroleUpdated)}) + m.WebsocketBroadcastPreferencesChanged = m.WebSocketBroadcastCounters.With(prometheus.Labels{"name": string(model.WebsocketEventPreferencesChanged)}) + m.WebsocketBroadcastPreferencesDeleted = m.WebSocketBroadcastCounters.With(prometheus.Labels{"name": string(model.WebsocketEventPreferencesDeleted)}) + m.WebsocketBroadcastReactionAdded = m.WebSocketBroadcastCounters.With(prometheus.Labels{"name": string(model.WebsocketEventReactionAdded)}) + m.WebsocketBroadcastReactionRemoved = m.WebSocketBroadcastCounters.With(prometheus.Labels{"name": string(model.WebsocketEventReactionRemoved)}) + m.WebsocketBroadcastGroupMemberDelete = m.WebSocketBroadcastCounters.With(prometheus.Labels{"name": string(model.WebsocketEventGroupMemberDelete)}) + m.WebsocketBroadcastGroupMemberAdd = m.WebSocketBroadcastCounters.With(prometheus.Labels{"name": string(model.WebsocketEventGroupMemberAdd)}) + m.WebsocketBroadcastSidebarCategoryCreated = m.WebSocketBroadcastCounters.With(prometheus.Labels{"name": string(model.WebsocketEventSidebarCategoryCreated)}) + m.WebsocketBroadcastSidebarCategoryUpdated = m.WebSocketBroadcastCounters.With(prometheus.Labels{"name": string(model.WebsocketEventSidebarCategoryUpdated)}) + m.WebsocketBroadcastSidebarCategoryDeleted = m.WebSocketBroadcastCounters.With(prometheus.Labels{"name": string(model.WebsocketEventSidebarCategoryDeleted)}) + m.WebsocketBroadcastSidebarCategoryOrderUpdated = m.WebSocketBroadcastCounters.With(prometheus.Labels{"name": string(model.WebsocketEventSidebarCategoryOrderUpdated)}) + m.WebsocketBroadcastThreadUpdated = m.WebSocketBroadcastCounters.With(prometheus.Labels{"name": string(model.WebsocketEventThreadUpdated)}) + m.WebsocketBroadcastThreadFollowChanged = m.WebSocketBroadcastCounters.With(prometheus.Labels{"name": string(model.WebsocketEventThreadFollowChanged)}) + m.WebsocketBroadcastThreadReadChanged = m.WebSocketBroadcastCounters.With(prometheus.Labels{"name": string(model.WebsocketEventThreadReadChanged)}) + m.WebsocketBroadcastDraftCreated = m.WebSocketBroadcastCounters.With(prometheus.Labels{"name": string(model.WebsocketEventDraftCreated)}) + m.WebsocketBroadcastDraftUpdated = m.WebSocketBroadcastCounters.With(prometheus.Labels{"name": string(model.WebsocketEventDraftUpdated)}) + m.WebsocketBroadcastDraftDeleted = m.WebSocketBroadcastCounters.With(prometheus.Labels{"name": string(model.WebsocketEventDraftDeleted)}) + m.WebSocketBroadcastOther = m.WebSocketBroadcastCounters.With(prometheus.Labels{"name": "other"}) + + m.WebsocketEventCounters = prometheus.NewCounterVec( + prometheus.CounterOpts{ + Namespace: MetricsNamespace, + Subsystem: MetricsSubsystemWebsocket, + Name: "event_total", + Help: "Total number of websocket events", + ConstLabels: additionalLabels, + }, + []string{"type"}, + ) + m.Registry.MustRegister(m.WebsocketEventCounters) + + m.WebSocketBroadcastBufferGauge = prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Namespace: MetricsNamespace, + Subsystem: MetricsSubsystemWebsocket, + Name: "broadcast_buffer_size", + Help: "Number of events in the websocket broadcasts buffer waiting to be processed", + ConstLabels: additionalLabels, + }, + []string{"hub"}, + ) + m.Registry.MustRegister(m.WebSocketBroadcastBufferGauge) + + m.WebSocketBroadcastBufferUsersRegisteredGauge = prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Namespace: MetricsNamespace, + Subsystem: MetricsSubsystemWebsocket, + Name: "broadcast_buffer_users_registered", + Help: "Number of users registered in a broadcast buffer hub", + ConstLabels: additionalLabels, + }, + []string{"hub"}, + ) + m.Registry.MustRegister(m.WebSocketBroadcastBufferUsersRegisteredGauge) + + m.WebSocketReconnectCounter = prometheus.NewCounterVec( + prometheus.CounterOpts{ + Namespace: MetricsNamespace, + Subsystem: MetricsSubsystemWebsocket, + Name: "reconnects_total", + Help: "Total number of websocket reconnect attempts", + ConstLabels: additionalLabels, + }, + []string{"type"}, + ) + m.Registry.MustRegister(m.WebSocketReconnectCounter) + + // Search Subsystem + + m.SearchPostSearchesCounter = prometheus.NewCounter(prometheus.CounterOpts{ + Namespace: MetricsNamespace, + Subsystem: MetricsSubsystemSearch, + Name: "posts_searches_total", + Help: "The total number of post searches carried out.", + ConstLabels: additionalLabels, + }) + m.Registry.MustRegister(m.SearchPostSearchesCounter) + + m.SearchPostSearchesDuration = prometheus.NewHistogram(prometheus.HistogramOpts{ + Namespace: MetricsNamespace, + Subsystem: MetricsSubsystemSearch, + Name: "posts_searches_duration_seconds", + Help: "The total duration in seconds of post searches.", + ConstLabels: additionalLabels, + }) + m.Registry.MustRegister(m.SearchPostSearchesDuration) + + m.SearchFileSearchesCounter = prometheus.NewCounter(prometheus.CounterOpts{ + Namespace: MetricsNamespace, + Subsystem: MetricsSubsystemSearch, + Name: "files_searches_total", + Help: "The total number of file searches carried out.", + ConstLabels: additionalLabels, + }) + m.Registry.MustRegister(m.SearchFileSearchesCounter) + + m.SearchFileSearchesDuration = prometheus.NewHistogram(prometheus.HistogramOpts{ + Namespace: MetricsNamespace, + Subsystem: MetricsSubsystemSearch, + Name: "files_searches_duration_seconds", + Help: "The total duration in seconds of file searches.", + ConstLabels: additionalLabels, + }) + m.Registry.MustRegister(m.SearchFileSearchesDuration) + + m.ActiveUsers = prometheus.NewGauge(prometheus.GaugeOpts{ + Namespace: MetricsNamespace, + Subsystem: MetricsSubsystemDB, + Name: "active_users", + Help: "The total number of active users.", + ConstLabels: additionalLabels, + }) + m.Registry.MustRegister(m.ActiveUsers) + + m.StoreTimesHistograms = prometheus.NewHistogramVec( + prometheus.HistogramOpts{ + Namespace: MetricsNamespace, + Subsystem: MetricsSubsystemDB, + Name: "store_time", + Help: "Time to execute the store method", + ConstLabels: additionalLabels, + }, + []string{"method", "success"}, + ) + m.Registry.MustRegister(m.StoreTimesHistograms) + + m.APITimesHistograms = prometheus.NewHistogramVec( + prometheus.HistogramOpts{ + Namespace: MetricsNamespace, + Subsystem: MetricsSubsystemAPI, + Name: "time", + Help: "Time to execute the api handler", + ConstLabels: additionalLabels, + }, + []string{"handler", "method", "status_code", "origin_client", "page_load_context"}, + ) + m.Registry.MustRegister(m.APITimesHistograms) + + m.SearchPostIndexCounter = prometheus.NewCounter(prometheus.CounterOpts{ + Namespace: MetricsNamespace, + Subsystem: MetricsSubsystemSearch, + Name: "post_index_total", + Help: "The total number of posts indexes carried out.", + ConstLabels: additionalLabels, + }) + m.Registry.MustRegister(m.SearchPostIndexCounter) + + m.SearchFileIndexCounter = prometheus.NewCounter(prometheus.CounterOpts{ + Namespace: MetricsNamespace, + Subsystem: MetricsSubsystemSearch, + Name: "file_index_total", + Help: "The total number of files indexes carried out.", + ConstLabels: additionalLabels, + }) + m.Registry.MustRegister(m.SearchFileIndexCounter) + + m.SearchUserIndexCounter = prometheus.NewCounter(prometheus.CounterOpts{ + Namespace: MetricsNamespace, + Subsystem: MetricsSubsystemSearch, + Name: "user_index_total", + Help: "The total number of user indexes carried out.", + ConstLabels: additionalLabels, + }) + m.Registry.MustRegister(m.SearchUserIndexCounter) + + m.SearchChannelIndexCounter = prometheus.NewCounter(prometheus.CounterOpts{ + Namespace: MetricsNamespace, + Subsystem: MetricsSubsystemSearch, + Name: "channel_index_total", + Help: "The total number of channel indexes carried out.", + ConstLabels: additionalLabels, + }) + m.Registry.MustRegister(m.SearchChannelIndexCounter) + + // Plugin Subsystem + + m.PluginHookTimeHistogram = prometheus.NewHistogramVec( + prometheus.HistogramOpts{ + Namespace: MetricsNamespace, + Subsystem: MetricsSubsystemPlugin, + Name: "hook_time", + Help: "Time to execute plugin hook handler in seconds.", + ConstLabels: additionalLabels, + }, + []string{"plugin_id", "hook_name", "success"}, + ) + m.Registry.MustRegister(m.PluginHookTimeHistogram) + + m.PluginMultiHookTimeHistogram = prometheus.NewHistogramVec( + prometheus.HistogramOpts{ + Namespace: MetricsNamespace, + Subsystem: MetricsSubsystemPlugin, + Name: "multi_hook_time", + Help: "Time to execute multiple plugin hook handler in seconds.", + ConstLabels: additionalLabels, + }, + []string{"plugin_id"}, + ) + m.Registry.MustRegister(m.PluginMultiHookTimeHistogram) + + m.PluginMultiHookServerTimeHistogram = prometheus.NewHistogram( + prometheus.HistogramOpts{ + Namespace: MetricsNamespace, + Subsystem: MetricsSubsystemPlugin, + Name: "multi_hook_server_time", + Help: "Time for the server to execute multiple plugin hook handlers in seconds.", + ConstLabels: additionalLabels, + }, + ) + m.Registry.MustRegister(m.PluginMultiHookServerTimeHistogram) + + m.PluginAPITimeHistogram = prometheus.NewHistogramVec( + prometheus.HistogramOpts{ + Namespace: MetricsNamespace, + Subsystem: MetricsSubsystemPlugin, + Name: "api_time", + Help: "Time to execute plugin API handlers in seconds.", + ConstLabels: additionalLabels, + }, + []string{"plugin_id", "api_name", "success"}, + ) + m.Registry.MustRegister(m.PluginAPITimeHistogram) + + // Logging subsystem + + m.LoggerQueueGauge = NewDynamicGauge( + prometheus.GaugeOpts{ + Namespace: MetricsNamespace, + Subsystem: MetricsSubsystemLogging, + Name: "logger_queue_used", + Help: "Number of records in log target queue.", + }, + "target", + ) + m.Registry.MustRegister(m.LoggerQueueGauge.gauge) + + m.LoggerLoggedCounters = NewDynamicCounter( + prometheus.CounterOpts{ + Namespace: MetricsNamespace, + Subsystem: MetricsSubsystemLogging, + Name: "logger_logged_total", + Help: "The total number of records logged.", + }, + "target", + ) + m.Registry.MustRegister(m.LoggerLoggedCounters.counter) + + m.LoggerErrorCounters = NewDynamicCounter( + prometheus.CounterOpts{ + Namespace: MetricsNamespace, + Subsystem: MetricsSubsystemLogging, + Name: "logger_error_total", + Help: "The total number of logger errors.", + }, + "target", + ) + m.Registry.MustRegister(m.LoggerErrorCounters.counter) + + m.LoggerDroppedCounters = NewDynamicCounter( + prometheus.CounterOpts{ + Namespace: MetricsNamespace, + Subsystem: MetricsSubsystemLogging, + Name: "logger_dropped_total", + Help: "The total number of dropped log records.", + }, + "target", + ) + m.Registry.MustRegister(m.LoggerDroppedCounters.counter) + + m.LoggerBlockedCounters = NewDynamicCounter( + prometheus.CounterOpts{ + Namespace: MetricsNamespace, + Subsystem: MetricsSubsystemLogging, + Name: "logger_blocked_total", + Help: "The total number of log records that were blocked/delayed.", + }, + "target", + ) + m.Registry.MustRegister(m.LoggerBlockedCounters.counter) + + // Remote Cluster subsystem + + m.RemoteClusterMsgSentCounters = prometheus.NewCounterVec( + prometheus.CounterOpts{ + Namespace: MetricsNamespace, + Subsystem: MetricsSubsystemRemoteCluster, + Name: "msg_sent_total", + Help: "Total number of messages sent to the remote cluster", + ConstLabels: additionalLabels, + }, + []string{"remote_id"}, + ) + m.Registry.MustRegister(m.RemoteClusterMsgSentCounters) + + m.RemoteClusterMsgReceivedCounters = prometheus.NewCounterVec( + prometheus.CounterOpts{ + Namespace: MetricsNamespace, + Subsystem: MetricsSubsystemRemoteCluster, + Name: "msg_received_total", + Help: "Total number of messages received from the remote cluster", + ConstLabels: additionalLabels, + }, + []string{"remote_id"}, + ) + m.Registry.MustRegister(m.RemoteClusterMsgReceivedCounters) + + m.RemoteClusterMsgErrorsCounter = prometheus.NewCounterVec( + prometheus.CounterOpts{ + Namespace: MetricsNamespace, + Subsystem: MetricsSubsystemRemoteCluster, + Name: "msg_errors_total", + Help: "Total number of message errors", + ConstLabels: additionalLabels, + }, + []string{"remote_id", "timeout"}, + ) + m.Registry.MustRegister(m.RemoteClusterMsgErrorsCounter) + + m.RemoteClusterPingTimesHistograms = prometheus.NewHistogramVec( + prometheus.HistogramOpts{ + Namespace: MetricsNamespace, + Subsystem: MetricsSubsystemRemoteCluster, + Name: "ping_time", + Help: "The ping roundtrip times to the remote cluster", + ConstLabels: additionalLabels, + }, + []string{"remote_id"}, + ) + m.Registry.MustRegister(m.RemoteClusterPingTimesHistograms) + + m.RemoteClusterClockSkewHistograms = prometheus.NewHistogramVec( + prometheus.HistogramOpts{ + Namespace: MetricsNamespace, + Subsystem: MetricsSubsystemRemoteCluster, + Name: "clock_skew", + Help: "An approximated value for clock skew between clusters", + ConstLabels: additionalLabels, + }, + []string{"remote_id"}, + ) + m.Registry.MustRegister(m.RemoteClusterClockSkewHistograms) + + m.RemoteClusterConnStateChangeCounter = prometheus.NewCounterVec( + prometheus.CounterOpts{ + Namespace: MetricsNamespace, + Subsystem: MetricsSubsystemRemoteCluster, + Name: "conn_state_change_total", + Help: "Total number of connection state changes", + ConstLabels: additionalLabels, + }, + []string{"remote_id", "online"}, + ) + m.Registry.MustRegister(m.RemoteClusterConnStateChangeCounter) + + m.ServerStartTime = prometheus.NewGauge(prometheus.GaugeOpts{ + Namespace: MetricsNamespace, + Subsystem: MetricsSubsystemSystem, + Name: "server_start_time", + Help: "The time the server started.", + ConstLabels: additionalLabels, + }) + m.ServerStartTime.SetToCurrentTime() + m.Registry.MustRegister(m.ServerStartTime) + + m.JobsActive = prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Namespace: MetricsNamespace, + Subsystem: MetricsSubsystemJobs, + Name: "active", + Help: "Number of active jobs", + ConstLabels: additionalLabels, + }, + []string{"type"}, + ) + m.Registry.MustRegister(m.JobsActive) + return m +} + +func (mi *MetricsInterfaceImpl) isLicensed() bool { + license := mi.Platform.License() + return (license != nil && *license.Features.Metrics) || (model.BuildNumber == "dev") +} + +func (mi *MetricsInterfaceImpl) Register() { + if !mi.isLicensed() { + return + } + + mi.Platform.HandleMetrics("/metrics", promhttp.HandlerFor(mi.Registry, promhttp.HandlerOpts{})) + mi.Platform.Logger().Info("Metrics endpoint is initiated", mlog.String("address", *mi.Platform.Config().MetricsSettings.ListenAddress)) +} + +func (mi *MetricsInterfaceImpl) RegisterDBCollector(db *sql.DB, name string) { + mi.Registry.MustRegister(collectors.NewDBStatsCollector(db, name)) +} + +func (mi *MetricsInterfaceImpl) UnregisterDBCollector(db *sql.DB, name string) { + mi.Registry.Unregister(collectors.NewDBStatsCollector(db, name)) +} + +func (mi *MetricsInterfaceImpl) IncrementPostCreate() { + mi.PostCreateCounter.Inc() +} + +func (mi *MetricsInterfaceImpl) IncrementWebhookPost() { + mi.WebhookPostCounter.Inc() +} + +func (mi *MetricsInterfaceImpl) IncrementPostSentEmail() { + mi.PostSentEmailCounter.Inc() +} + +func (mi *MetricsInterfaceImpl) IncrementPostSentPush() { + mi.PostSentPushCounter.Inc() +} + +func (mi *MetricsInterfaceImpl) IncrementPostBroadcast() { + mi.PostBroadcastCounter.Inc() +} + +func (mi *MetricsInterfaceImpl) IncrementWebSocketBroadcast(eventType model.WebsocketEventType) { + switch eventType { + case model.WebsocketEventPosted: + mi.IncrementPostBroadcast() + mi.WebSocketBroadcastPosted.Inc() + case model.WebsocketEventTyping: + mi.WebSocketBroadcastTyping.Inc() + case model.WebsocketEventMultipleChannelsViewed: + mi.WebSocketBroadcastChannelViewed.Inc() + case model.WebsocketEventNewUser: + mi.WebSocketBroadcastNewUser.Inc() + case model.WebsocketEventUserAdded: + mi.WebSocketBroadcastUserAdded.Inc() + case model.WebsocketEventUserUpdated: + mi.WebSocketBroadcastUserUpdated.Inc() + case model.WebsocketEventUserRemoved: + mi.WebSocketBroadcastUserRemoved.Inc() + case model.WebsocketEventPreferenceChanged: + mi.WebSocketBroadcastPreferenceChanged.Inc() + case model.WebsocketEventEphemeralMessage: + mi.WebSocketBroadcastephemeralMessage.Inc() + case model.WebsocketEventStatusChange: + mi.WebSocketBroadcastStatusChange.Inc() + case model.WebsocketEventHello: + mi.WebSocketBroadcastHello.Inc() + case model.WebsocketEventResponse: + mi.WebSocketBroadcastResponse.Inc() + case model.WebsocketEventPostEdited: + mi.WebsocketBroadcastPostEdited.Inc() + case model.WebsocketEventPostDeleted: + mi.WebsocketBroadcastPostDeleted.Inc() + case model.WebsocketEventPostUnread: + mi.WebsocketBroadcastPostUnread.Inc() + case model.WebsocketEventChannelConverted: + mi.WebsocketBroadcastChannelConverted.Inc() + case model.WebsocketEventChannelCreated: + mi.WebsocketBroadcastChannelCreated.Inc() + case model.WebsocketEventChannelDeleted: + mi.WebsocketBroadcastChannelDeleted.Inc() + case model.WebsocketEventChannelRestored: + mi.WebsocketBroadcastChannelRestored.Inc() + case model.WebsocketEventChannelUpdated: + mi.WebsocketBroadcastChannelUpdated.Inc() + case model.WebsocketEventChannelMemberUpdated: + mi.WebsocketBroadcastChannelMemberUpdated.Inc() + case model.WebsocketEventChannelSchemeUpdated: + mi.WebsocketBroadcastChannelSchemeUpdated.Inc() + case model.WebsocketEventDirectAdded: + mi.WebsocketBroadcastDirectAdded.Inc() + case model.WebsocketEventGroupAdded: + mi.WebsocketBroadcastGroupAdded.Inc() + case model.WebsocketEventAddedToTeam: + mi.WebsocketBroadcastAddedToTeam.Inc() + case model.WebsocketEventLeaveTeam: + mi.WebsocketBroadcastLeaveTeam.Inc() + case model.WebsocketEventUpdateTeam: + mi.WebsocketBroadcastUpdateTeam.Inc() + case model.WebsocketEventDeleteTeam: + mi.WebsocketBroadcastDeleteTeam.Inc() + case model.WebsocketEventRestoreTeam: + mi.WebsocketBroadcastRestoreTeam.Inc() + case model.WebsocketEventUpdateTeamScheme: + mi.WebsocketBroadcastUpdateTeamScheme.Inc() + case model.WebsocketEventUserRoleUpdated: + mi.WebsocketBroadcastUserRoleUpdated.Inc() + case model.WebsocketEventMemberroleUpdated: + mi.WebsocketBroadcastMemberroleUpdated.Inc() + case model.WebsocketEventPreferencesChanged: + mi.WebsocketBroadcastPreferencesChanged.Inc() + case model.WebsocketEventPreferencesDeleted: + mi.WebsocketBroadcastPreferencesDeleted.Inc() + case model.WebsocketEventReactionAdded: + mi.WebsocketBroadcastReactionAdded.Inc() + case model.WebsocketEventReactionRemoved: + mi.WebsocketBroadcastReactionRemoved.Inc() + case model.WebsocketEventGroupMemberDelete: + mi.WebsocketBroadcastGroupMemberDelete.Inc() + case model.WebsocketEventGroupMemberAdd: + mi.WebsocketBroadcastGroupMemberAdd.Inc() + case model.WebsocketEventSidebarCategoryCreated: + mi.WebsocketBroadcastSidebarCategoryCreated.Inc() + case model.WebsocketEventSidebarCategoryUpdated: + mi.WebsocketBroadcastSidebarCategoryUpdated.Inc() + case model.WebsocketEventSidebarCategoryDeleted: + mi.WebsocketBroadcastSidebarCategoryDeleted.Inc() + case model.WebsocketEventSidebarCategoryOrderUpdated: + mi.WebsocketBroadcastSidebarCategoryOrderUpdated.Inc() + case model.WebsocketEventThreadUpdated: + mi.WebsocketBroadcastThreadUpdated.Inc() + case model.WebsocketEventThreadFollowChanged: + mi.WebsocketBroadcastThreadFollowChanged.Inc() + case model.WebsocketEventThreadReadChanged: + mi.WebsocketBroadcastThreadReadChanged.Inc() + case model.WebsocketEventDraftCreated: + mi.WebsocketBroadcastDraftCreated.Inc() + case model.WebsocketEventDraftUpdated: + mi.WebsocketBroadcastDraftUpdated.Inc() + case model.WebsocketEventDraftDeleted: + mi.WebsocketBroadcastDraftDeleted.Inc() + default: + mi.WebSocketBroadcastOther.Inc() + } +} + +func (mi *MetricsInterfaceImpl) IncrementPostFileAttachment(count int) { + mi.PostFileAttachCounter.Add(float64(count)) +} + +func (mi *MetricsInterfaceImpl) IncrementHTTPRequest() { + mi.HTTPRequestsCounter.Inc() +} + +func (mi *MetricsInterfaceImpl) IncrementHTTPError() { + mi.HTTPErrorsCounter.Inc() +} + +func (mi *MetricsInterfaceImpl) IncrementClusterRequest() { + mi.ClusterRequestsCounter.Inc() +} + +func (mi *MetricsInterfaceImpl) ObserveClusterRequestDuration(elapsed float64) { + mi.ClusterRequestsDuration.Observe(elapsed) +} + +func (mi *MetricsInterfaceImpl) ObserveStoreMethodDuration(method string, success string, elapsed float64) { + mi.StoreTimesHistograms.With(prometheus.Labels{"method": method, "success": success}).Observe(elapsed) +} + +func (mi *MetricsInterfaceImpl) ObserveAPIEndpointDuration(handler, method, statusCode, originClient, pageLoadContext string, elapsed float64) { + mi.APITimesHistograms.With(prometheus.Labels{"handler": handler, "method": method, "status_code": statusCode, "origin_client": originClient, "page_load_context": pageLoadContext}).Observe(elapsed) +} + +func (mi *MetricsInterfaceImpl) IncrementClusterEventType(eventType model.ClusterEvent) { + switch eventType { + case model.ClusterEventPublish: + mi.ClusterEventTypePublish.Inc() + case model.ClusterEventUpdateStatus: + mi.ClusterEventTypeStatus.Inc() + case model.ClusterEventInvalidateAllCaches: + mi.ClusterEventTypeInvAll.Inc() + case model.ClusterEventInvalidateCacheForReactions: + mi.ClusterEventTypeInvReactions.Inc() + case model.ClusterEventInvalidateCacheForChannelMembersNotifyProps: + mi.ClusterEventTypeInvChannelMembersNotifyProps.Inc() + case model.ClusterEventInvalidateCacheForChannelByName: + mi.ClusterEventTypeInvChannelByName.Inc() + case model.ClusterEventInvalidateCacheForChannel: + mi.ClusterEventTypeInvChannel.Inc() + case model.ClusterEventInvalidateCacheForUser: + mi.ClusterEventTypeInvUser.Inc() + case model.ClusterEventClearSessionCacheForUser: + mi.ClusterEventTypeInvSessions.Inc() + case model.ClusterEventInvalidateCacheForRoles: + mi.ClusterEventTypeInvRoles.Inc() + default: + mi.ClusterEventTypeOther.Inc() + } +} + +func (mi *MetricsInterfaceImpl) IncrementLogin() { + mi.LoginCounter.Inc() +} + +func (mi *MetricsInterfaceImpl) IncrementLoginFail() { + mi.LoginFailCounter.Inc() +} + +func (mi *MetricsInterfaceImpl) IncrementEtagMissCounter(route string) { + mi.EtagMissCounters.With(prometheus.Labels{"route": route}).Inc() +} + +func (mi *MetricsInterfaceImpl) IncrementEtagHitCounter(route string) { + mi.EtagHitCounters.With(prometheus.Labels{"route": route}).Inc() +} + +func (mi *MetricsInterfaceImpl) IncrementMemCacheMissCounter(cacheName string) { + mi.MemCacheMissCounters.With(prometheus.Labels{"name": cacheName}).Inc() +} + +func (mi *MetricsInterfaceImpl) IncrementMemCacheHitCounter(cacheName string) { + mi.MemCacheHitCounters.With(prometheus.Labels{"name": cacheName}).Inc() +} + +func (mi *MetricsInterfaceImpl) IncrementMemCacheInvalidationCounter(cacheName string) { + mi.MemCacheInvalidationCounters.With(prometheus.Labels{"name": cacheName}).Inc() +} + +func (mi *MetricsInterfaceImpl) IncrementMemCacheMissCounterSession() { + mi.MemCacheMissCounterSession.Inc() +} + +func (mi *MetricsInterfaceImpl) IncrementMemCacheHitCounterSession() { + mi.MemCacheHitCounterSession.Inc() +} + +func (mi *MetricsInterfaceImpl) IncrementMemCacheInvalidationCounterSession() { + mi.MemCacheInvalidationCounterSession.Inc() +} + +func (mi *MetricsInterfaceImpl) AddMemCacheMissCounter(cacheName string, amount float64) { + mi.MemCacheMissCounters.With(prometheus.Labels{"name": cacheName}).Add(amount) +} + +func (mi *MetricsInterfaceImpl) AddMemCacheHitCounter(cacheName string, amount float64) { + mi.MemCacheHitCounters.With(prometheus.Labels{"name": cacheName}).Add(amount) +} + +func (mi *MetricsInterfaceImpl) IncrementWebsocketEvent(eventType model.WebsocketEventType) { + mi.WebsocketEventCounters.With(prometheus.Labels{"type": string(eventType)}).Inc() +} + +func (mi *MetricsInterfaceImpl) IncrementWebsocketReconnectEvent(eventType string) { + mi.WebSocketReconnectCounter.With(prometheus.Labels{"type": eventType}).Inc() +} + +func (mi *MetricsInterfaceImpl) IncrementWebSocketBroadcastBufferSize(hub string, amount float64) { + mi.WebSocketBroadcastBufferGauge.With(prometheus.Labels{"hub": hub}).Add(math.Abs(amount)) +} + +func (mi *MetricsInterfaceImpl) DecrementWebSocketBroadcastBufferSize(hub string, amount float64) { + mi.WebSocketBroadcastBufferGauge.With(prometheus.Labels{"hub": hub}).Add(-math.Abs(amount)) +} + +func (mi *MetricsInterfaceImpl) IncrementWebSocketBroadcastUsersRegistered(hub string, amount float64) { + mi.WebSocketBroadcastBufferUsersRegisteredGauge.With(prometheus.Labels{"hub": hub}).Add(math.Abs(amount)) +} + +func (mi *MetricsInterfaceImpl) DecrementWebSocketBroadcastUsersRegistered(hub string, amount float64) { + mi.WebSocketBroadcastBufferUsersRegisteredGauge.With(prometheus.Labels{"hub": hub}).Add(-math.Abs(amount)) +} + +func (mi *MetricsInterfaceImpl) IncrementPostsSearchCounter() { + mi.SearchPostSearchesCounter.Inc() +} + +func (mi *MetricsInterfaceImpl) IncrementFilesSearchCounter() { + mi.SearchFileSearchesCounter.Inc() +} + +func (mi *MetricsInterfaceImpl) ObserveEnabledUsers(users int64) { + mi.ActiveUsers.Set(float64(users)) +} + +func (mi *MetricsInterfaceImpl) ObservePostsSearchDuration(elapsed float64) { + mi.SearchPostSearchesDuration.Observe(elapsed) +} + +func (mi *MetricsInterfaceImpl) ObserveFilesSearchDuration(elapsed float64) { + mi.SearchFileSearchesDuration.Observe(elapsed) +} + +func (mi *MetricsInterfaceImpl) IncrementPostIndexCounter() { + mi.SearchPostIndexCounter.Inc() +} + +func (mi *MetricsInterfaceImpl) IncrementFileIndexCounter() { + mi.SearchFileIndexCounter.Inc() +} + +func (mi *MetricsInterfaceImpl) IncrementUserIndexCounter() { + mi.SearchUserIndexCounter.Inc() +} + +func (mi *MetricsInterfaceImpl) IncrementChannelIndexCounter() { + mi.SearchChannelIndexCounter.Inc() +} + +func (mi *MetricsInterfaceImpl) ObservePluginHookDuration(pluginID, hookName string, success bool, elapsed float64) { + mi.PluginHookTimeHistogram.With(prometheus.Labels{"plugin_id": pluginID, "hook_name": hookName, "success": strconv.FormatBool(success)}).Observe(elapsed) +} + +func (mi *MetricsInterfaceImpl) ObservePluginMultiHookIterationDuration(pluginID string, elapsed float64) { + mi.PluginMultiHookTimeHistogram.With(prometheus.Labels{"plugin_id": pluginID}).Observe(elapsed) +} + +func (mi *MetricsInterfaceImpl) ObservePluginMultiHookDuration(elapsed float64) { + mi.PluginMultiHookServerTimeHistogram.Observe(elapsed) +} + +func (mi *MetricsInterfaceImpl) ObservePluginAPIDuration(pluginID, apiName string, success bool, elapsed float64) { + mi.PluginAPITimeHistogram.With(prometheus.Labels{"plugin_id": pluginID, "api_name": apiName, "success": strconv.FormatBool(success)}).Observe(elapsed) +} + +func (mi *MetricsInterfaceImpl) GetLoggerMetricsCollector() mlog.MetricsCollector { + return &LoggerMetricsCollector{ + queueGauge: mi.LoggerQueueGauge, + loggedCounters: mi.LoggerLoggedCounters, + errorCounters: mi.LoggerErrorCounters, + droppedCounters: mi.LoggerDroppedCounters, + blockedCounters: mi.LoggerBlockedCounters, + } +} + +func (mi *MetricsInterfaceImpl) IncrementRemoteClusterMsgSentCounter(remoteID string) { + mi.RemoteClusterMsgSentCounters.With(prometheus.Labels{"remote_id": remoteID}).Inc() +} + +func (mi *MetricsInterfaceImpl) IncrementRemoteClusterMsgReceivedCounter(remoteID string) { + mi.RemoteClusterMsgReceivedCounters.With(prometheus.Labels{"remote_id": remoteID}).Inc() +} + +func (mi *MetricsInterfaceImpl) IncrementRemoteClusterMsgErrorsCounter(remoteID string, timeout bool) { + mi.RemoteClusterMsgErrorsCounter.With(prometheus.Labels{ + "remote_id": remoteID, + "timeout": strconv.FormatBool(timeout), + }).Inc() +} + +func (mi *MetricsInterfaceImpl) ObserveRemoteClusterPingDuration(remoteID string, elapsed float64) { + mi.RemoteClusterPingTimesHistograms.With(prometheus.Labels{"remote_id": remoteID}).Observe(elapsed) +} + +func (mi *MetricsInterfaceImpl) ObserveRemoteClusterClockSkew(remoteID string, skew float64) { + mi.RemoteClusterClockSkewHistograms.With(prometheus.Labels{"remote_id": remoteID}).Observe(skew) +} + +func (mi *MetricsInterfaceImpl) IncrementJobActive(jobType string) { + mi.JobsActive.With(prometheus.Labels{"type": jobType}).Inc() +} + +func (mi *MetricsInterfaceImpl) DecrementJobActive(jobType string) { + mi.JobsActive.With(prometheus.Labels{"type": jobType}).Dec() +} + +func (mi *MetricsInterfaceImpl) IncrementRemoteClusterConnStateChangeCounter(remoteID string, online bool) { + mi.RemoteClusterConnStateChangeCounter.With(prometheus.Labels{ + "remote_id": remoteID, + "online": strconv.FormatBool(online), + }).Inc() +} + +// SetReplicaLagAbsolute sets the absolute replica lag for a given node. +func (mi *MetricsInterfaceImpl) SetReplicaLagAbsolute(node string, value float64) { + mi.DbReplicaLagGaugeAbs.With(prometheus.Labels{"node": node}).Set(value) +} + +// SetReplicaLagTime sets the time-based replica lag for a given node. +func (mi *MetricsInterfaceImpl) SetReplicaLagTime(node string, value float64) { + mi.DbReplicaLagGaugeTime.With(prometheus.Labels{"node": node}).Set(value) +} + +func extractDBCluster(driver, connectionString string) (string, error) { + host, err := extractHost(driver, connectionString) + if err != nil { + return "", err + } + + clusterEnd := strings.Index(host, ".") + if clusterEnd == -1 { + return host, nil + } + + return host[:clusterEnd], nil +} + +func extractHost(driver, connectionString string) (string, error) { + switch driver { + case model.DatabaseDriverPostgres: + parsedURL, err := url.Parse(connectionString) + if err != nil { + return "", errors.Wrap(err, "failed to parse postgres connection string") + } + return parsedURL.Host, nil + case model.DatabaseDriverMysql: + config, err := mysql.ParseDSN(connectionString) + if err != nil { + return "", errors.Wrap(err, "failed to parse mysql connection string") + } + host := strings.Split(config.Addr, ":")[0] + + return host, nil + } + return "", errors.Errorf("unsupported database driver: %q", driver) +} diff --git a/server/enterprise/metrics/metrics_test.go b/server/enterprise/metrics/metrics_test.go new file mode 100644 index 00000000000..95173490ddc --- /dev/null +++ b/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) + }) + } +} diff --git a/server/enterprise/placeholder.go b/server/enterprise/placeholder.go new file mode 100644 index 00000000000..85287cecac2 --- /dev/null +++ b/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 diff --git a/server/go.mod b/server/go.mod index 6c726c50a3c..bf55ff1bdd8 100644 --- a/server/go.mod +++ b/server/go.mod @@ -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