Skip to content

Commit

Permalink
[MM-60593] Scrape Calls metrics (#35)
Browse files Browse the repository at this point in the history
* Scrape Calls metrics

* Use dedicated job label for Calls

* Regenerate targets in non-HA case to cover other plugins

* Tests
  • Loading branch information
streamer45 authored Oct 9, 2024
1 parent a68ec46 commit 6d58b71
Show file tree
Hide file tree
Showing 10 changed files with 11,141 additions and 45 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
dist/
bin/

# Mac
.DS_Store
Expand Down
8 changes: 8 additions & 0 deletions .mockery.yaml
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:
10 changes: 10 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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/...@v2.40.3
$(GOBIN)/mockery

## Extract strings for translation from the source code.
.PHONY: i18n-extract
i18n-extract:
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ require (
github.com/rogpeppe/go-internal v1.11.0 // indirect
github.com/rs/xid v1.5.0 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/stretchr/objx v0.5.1 // indirect
github.com/tinylib/msgp v1.1.9 // indirect
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
Expand Down
5 changes: 5 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -501,14 +501,19 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.1 h1:4VhoImhV/Bm0ToFkXFi8hXNXwpDRZ/ynw3amt82mzq0=
github.com/stretchr/objx v0.5.1/go.mod h1:/iHQpkQwBD6DLUmQ4pE+s1TXdob1mORJ4/UFdrifcy0=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
Expand Down
94 changes: 94 additions & 0 deletions server/calls_targets.go
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
}
134 changes: 134 additions & 0 deletions server/calls_targets_test.go
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)
})
}
32 changes: 16 additions & 16 deletions server/configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -232,36 +232,36 @@ func (p *Plugin) isHA() bool {
return cfg.ClusterSettings.Enable != nil && *cfg.ClusterSettings.Enable
}

func generateTargetGroup(appCfg *model.Config, nodes []*model.ClusterDiscovery) (map[string][]*targetgroup.Group, error) {
func (p *Plugin) generateTargetGroup(appCfg *model.Config, nodes []*model.ClusterDiscovery) (map[string][]*targetgroup.Group, error) {
host, port, err := net.SplitHostPort(*appCfg.MetricsSettings.ListenAddress)
if err != nil {
return nil, fmt.Errorf("could not parse the listen address %q", *appCfg.MetricsSettings.ListenAddress)
}

sync := make(map[string][]*targetgroup.Group)
if nodes == nil || len(nodes) < 2 {
var targets []promModel.LabelSet
if len(nodes) < 2 {
if host == "" {
host = "localhost"
}

sync["prometheus"] = []*targetgroup.Group{
targets = []promModel.LabelSet{
{
Targets: []promModel.LabelSet{
{
promModel.AddressLabel: promModel.LabelValue(net.JoinHostPort(host, port)),
},
},
promModel.AddressLabel: promModel.LabelValue(net.JoinHostPort(host, port)),
},
}

return sync, nil
} else {
targets = make([]promModel.LabelSet, len(nodes))
for i, node := range nodes {
targets[i] = promModel.LabelSet{
promModel.AddressLabel: promModel.LabelValue(net.JoinHostPort(node.Hostname, port)),
}
}
}

targets := make([]promModel.LabelSet, len(nodes))
for i, node := range nodes {
targets[i] = promModel.LabelSet{
promModel.AddressLabel: promModel.LabelValue(net.JoinHostPort(node.Hostname, port)),
}
if callsTargets, err := p.generateCallsTargets(appCfg, host, port, nodes); err != nil {
p.API.LogWarn("failed to generate calls targets", "err", err.Error())
} else {
targets = append(targets, callsTargets...)
}

sync["prometheus"] = []*targetgroup.Group{
Expand Down
Loading

0 comments on commit 6d58b71

Please sign in to comment.