-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[MM-60593] Scrape Calls metrics (#35)
* Scrape Calls metrics * Use dedicated job label for Calls * Regenerate targets in non-HA case to cover other plugins * Tests
- Loading branch information
1 parent
a68ec46
commit 6d58b71
Showing
10 changed files
with
11,141 additions
and
45 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,5 @@ | ||
dist/ | ||
bin/ | ||
|
||
# Mac | ||
.DS_Store | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
quiet: False | ||
with-expecter: true | ||
dir: "server/mocks/{{.PackagePath}}" | ||
packages: | ||
github.com/mattermost/mattermost/server/public/plugin: | ||
config: | ||
interfaces: | ||
API: |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -13,6 +13,10 @@ DEFAULT_GOARCH := $(shell go env GOARCH) | |
|
||
export GO111MODULE=on | ||
|
||
# We need to export GOBIN to allow it to be set | ||
# for processes spawned from the Makefile | ||
export GOBIN ?= $(PWD)/bin | ||
|
||
# You can include assets this directory into the bundle. This can be e.g. used to include profile pictures. | ||
ASSETS_DIR ?= assets | ||
|
||
|
@@ -311,6 +315,12 @@ ifneq ($(HAS_SERVER),) | |
$(GO) tool cover -html=server/coverage.txt | ||
endif | ||
|
||
## Create plugin server mock files | ||
.PHONY: server-mocks | ||
server-mocks: | ||
$(GO) install github.com/vektra/mockery/v2/[email protected] | ||
$(GOBIN)/mockery | ||
|
||
## Extract strings for translation from the source code. | ||
.PHONY: i18n-extract | ||
i18n-extract: | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
package main | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"net" | ||
"net/url" | ||
"time" | ||
|
||
promModel "github.com/prometheus/common/model" | ||
|
||
"github.com/mattermost/mattermost/server/public/model" | ||
) | ||
|
||
const ( | ||
callsPluginID = "com.mattermost.calls" | ||
) | ||
|
||
func resolveURL(u string, timeout time.Duration) ([]net.IP, string, error) { | ||
parsed, err := url.Parse(u) | ||
if err != nil { | ||
return nil, "", fmt.Errorf("failed to parse url: %w", err) | ||
} | ||
|
||
host, port, err := net.SplitHostPort(parsed.Host) | ||
if err != nil { | ||
return nil, "", fmt.Errorf("failed to split host/port: %w", err) | ||
} | ||
|
||
ctx, cancel := context.WithTimeout(context.Background(), timeout) | ||
defer cancel() | ||
|
||
ips, err := net.DefaultResolver.LookupIP(ctx, "ip4", host) | ||
if err != nil { | ||
return nil, "", fmt.Errorf("failed to lookup ips: %w", err) | ||
} | ||
|
||
return ips, port, nil | ||
} | ||
|
||
func (p *Plugin) generateCallsTargets(appCfg *model.Config, host, port string, nodes []*model.ClusterDiscovery) ([]promModel.LabelSet, error) { | ||
// First, figure out if Calls is running. If so, add the plugin metrics endpoint to the targets. | ||
// Also, check if the external RTCD service is configured, in which case add its endpoints to targets. | ||
status, err := p.API.GetPluginStatus(callsPluginID) | ||
if err != nil { | ||
return nil, fmt.Errorf("generateCallsTargets: failed to get calls plugin status: %w", err) | ||
} | ||
|
||
if status.State != model.PluginStateRunning { | ||
p.API.LogDebug("generateCallsTargets: calls plugin is not running") | ||
return nil, nil | ||
} | ||
|
||
p.API.LogDebug("generateCallsTargets: calls plugin running, generating targets") | ||
|
||
var targets []promModel.LabelSet | ||
if len(nodes) < 2 { | ||
targets = []promModel.LabelSet{ | ||
{ | ||
promModel.AddressLabel: promModel.LabelValue(net.JoinHostPort(host, port)), | ||
promModel.MetricsPathLabel: promModel.LabelValue(fmt.Sprintf("/plugins/%s/metrics", callsPluginID)), | ||
promModel.JobLabel: "calls", | ||
}, | ||
} | ||
} else { | ||
for i := range nodes { | ||
targets = append(targets, promModel.LabelSet{ | ||
promModel.AddressLabel: promModel.LabelValue(net.JoinHostPort(nodes[i].Hostname, port)), | ||
promModel.MetricsPathLabel: promModel.LabelValue(fmt.Sprintf("/plugins/%s/metrics", callsPluginID)), | ||
promModel.JobLabel: "calls", | ||
}) | ||
} | ||
} | ||
|
||
if appCfg.PluginSettings.Plugins[callsPluginID] != nil { | ||
rtcdURL, _ := appCfg.PluginSettings.Plugins[callsPluginID]["rtcdserviceurl"].(string) | ||
if rtcdURL != "" { | ||
// Since RTCD can be DNS load balanced, we need to resolve its hostname to figure out if there's more than a single node behind it. | ||
ips, port, err := resolveURL(rtcdURL, 5*time.Second) | ||
if err != nil { | ||
p.API.LogWarn("generateCallsTargets: failed to resolve rtcd URL", "err", err.Error()) | ||
} | ||
|
||
for _, ip := range ips { | ||
targets = append(targets, promModel.LabelSet{ | ||
promModel.AddressLabel: promModel.LabelValue(net.JoinHostPort(ip.String(), port)), | ||
promModel.JobLabel: "calls", | ||
}) | ||
} | ||
} | ||
} | ||
|
||
return targets, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,134 @@ | ||
package main | ||
|
||
import ( | ||
"net/http" | ||
"testing" | ||
"time" | ||
|
||
"github.com/stretchr/testify/require" | ||
|
||
"github.com/mattermost/mattermost/server/public/model" | ||
"github.com/mattermost/mattermost/server/public/plugin" | ||
|
||
promModel "github.com/prometheus/common/model" | ||
|
||
pluginMocks "github.com/mattermost/mattermost-plugin-metrics/server/mocks/github.com/mattermost/mattermost/server/public/plugin" | ||
) | ||
|
||
func TestResolveURL(t *testing.T) { | ||
ips, port, err := resolveURL("https://localhost:8045", time.Second) | ||
require.NoError(t, err) | ||
require.NotEmpty(t, ips) | ||
require.Equal(t, "127.0.0.1", ips[0].String()) | ||
require.Equal(t, "8045", port) | ||
|
||
ips, port, err = resolveURL("http://127.0.0.1:8055", time.Second) | ||
require.NoError(t, err) | ||
require.NotEmpty(t, ips) | ||
require.Equal(t, "127.0.0.1", ips[0].String()) | ||
require.Equal(t, "8055", port) | ||
} | ||
|
||
func TestGenerateCallsTargets(t *testing.T) { | ||
mockAPI := &pluginMocks.MockAPI{} | ||
defer mockAPI.AssertExpectations(t) | ||
|
||
p := Plugin{ | ||
MattermostPlugin: plugin.MattermostPlugin{ | ||
API: mockAPI, | ||
}, | ||
} | ||
|
||
cfg := &model.Config{} | ||
cfg.SetDefaults() | ||
|
||
t.Run("plugin not installed", func(t *testing.T) { | ||
mockAPI.On("GetPluginStatus", callsPluginID).Return(&model.PluginStatus{}, model.NewAppError("GetPluginStatus", "Plugin is not installed.", nil, "", http.StatusNotFound)).Once() | ||
|
||
targets, err := p.generateCallsTargets(cfg, "localhost", "8067", nil) | ||
require.EqualError(t, err, "generateCallsTargets: failed to get calls plugin status: GetPluginStatus: Plugin is not installed.") | ||
require.Empty(t, targets) | ||
}) | ||
|
||
t.Run("plugin not running", func(t *testing.T) { | ||
mockAPI.On("GetPluginStatus", callsPluginID).Return(&model.PluginStatus{ | ||
State: model.PluginStateStarting, | ||
}, nil).Once() | ||
mockAPI.On("LogDebug", "generateCallsTargets: calls plugin is not running").Return().Once() | ||
|
||
targets, err := p.generateCallsTargets(cfg, "localhost", "8067", nil) | ||
require.NoError(t, err) | ||
require.Empty(t, targets) | ||
}) | ||
|
||
t.Run("single node", func(t *testing.T) { | ||
mockAPI.On("GetPluginStatus", callsPluginID).Return(&model.PluginStatus{ | ||
State: model.PluginStateRunning, | ||
}, nil).Once() | ||
mockAPI.On("LogDebug", "generateCallsTargets: calls plugin running, generating targets").Return().Once() | ||
|
||
targets, err := p.generateCallsTargets(cfg, "localhost", "8067", nil) | ||
require.NoError(t, err) | ||
require.Equal(t, []promModel.LabelSet{ | ||
{ | ||
promModel.AddressLabel: "localhost:8067", | ||
promModel.MetricsPathLabel: "/plugins/com.mattermost.calls/metrics", | ||
promModel.JobLabel: "calls", | ||
}, | ||
}, targets) | ||
}) | ||
|
||
t.Run("multi node", func(t *testing.T) { | ||
mockAPI.On("GetPluginStatus", callsPluginID).Return(&model.PluginStatus{ | ||
State: model.PluginStateRunning, | ||
}, nil).Once() | ||
mockAPI.On("LogDebug", "generateCallsTargets: calls plugin running, generating targets").Return().Once() | ||
|
||
targets, err := p.generateCallsTargets(cfg, "localhost", "8067", []*model.ClusterDiscovery{ | ||
{ | ||
Hostname: "192.168.1.1", | ||
}, | ||
{ | ||
Hostname: "192.168.1.2", | ||
}, | ||
}) | ||
require.NoError(t, err) | ||
require.Equal(t, []promModel.LabelSet{ | ||
{ | ||
promModel.AddressLabel: "192.168.1.1:8067", | ||
promModel.MetricsPathLabel: "/plugins/com.mattermost.calls/metrics", | ||
promModel.JobLabel: "calls", | ||
}, | ||
{ | ||
promModel.AddressLabel: "192.168.1.2:8067", | ||
promModel.MetricsPathLabel: "/plugins/com.mattermost.calls/metrics", | ||
promModel.JobLabel: "calls", | ||
}, | ||
}, targets) | ||
}) | ||
|
||
t.Run("rtcd", func(t *testing.T) { | ||
mockAPI.On("GetPluginStatus", callsPluginID).Return(&model.PluginStatus{ | ||
State: model.PluginStateRunning, | ||
}, nil).Once() | ||
mockAPI.On("LogDebug", "generateCallsTargets: calls plugin running, generating targets").Return().Once() | ||
|
||
cfg.PluginSettings.Plugins[callsPluginID] = map[string]any{ | ||
"rtcdserviceurl": "http://localhost:8045", | ||
} | ||
|
||
targets, err := p.generateCallsTargets(cfg, "localhost", "8067", nil) | ||
require.NoError(t, err) | ||
require.Equal(t, []promModel.LabelSet{ | ||
{ | ||
promModel.AddressLabel: "localhost:8067", | ||
promModel.MetricsPathLabel: "/plugins/com.mattermost.calls/metrics", | ||
promModel.JobLabel: "calls", | ||
}, | ||
{ | ||
promModel.AddressLabel: "127.0.0.1:8045", | ||
promModel.JobLabel: "calls", | ||
}, | ||
}, targets) | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.