-
Notifications
You must be signed in to change notification settings - Fork 4.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
HCP Telemetry Feature #17460
HCP Telemetry Feature #17460
Changes from all commits
2fdd0e9
d8927ba
27753e4
fb92de5
68e6e21
bb6cfb3
3d9fe07
8b95718
bf17ef4
1f1d5bd
1c28984
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
```release-note:feature | ||
hcp: Add new metrics sink to collect, aggregate and export server metrics to HCP in OTEL format. | ||
``` |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,7 @@ | ||
// Copyright (c) HashiCorp, Inc. | ||
// SPDX-License-Identifier: MPL-2.0 | ||
|
||
package hcp | ||
package client | ||
|
||
import ( | ||
"context" | ||
|
@@ -11,6 +11,8 @@ import ( | |
|
||
httptransport "github.com/go-openapi/runtime/client" | ||
"github.com/go-openapi/strfmt" | ||
|
||
hcptelemetry "github.com/hashicorp/hcp-sdk-go/clients/cloud-consul-telemetry-gateway/preview/2023-04-14/client/consul_telemetry_service" | ||
hcpgnm "github.com/hashicorp/hcp-sdk-go/clients/cloud-global-network-manager-service/preview/2022-02-15/client/global_network_manager_service" | ||
gnmmod "github.com/hashicorp/hcp-sdk-go/clients/cloud-global-network-manager-service/preview/2022-02-15/models" | ||
"github.com/hashicorp/hcp-sdk-go/httpclient" | ||
|
@@ -20,15 +22,34 @@ import ( | |
"github.com/hashicorp/consul/version" | ||
) | ||
|
||
// metricsGatewayPath is the default path for metrics export request on the Telemetry Gateway. | ||
const metricsGatewayPath = "/v1/metrics" | ||
|
||
// Client interface exposes HCP operations that can be invoked by Consul | ||
// | ||
//go:generate mockery --name Client --with-expecter --inpackage | ||
type Client interface { | ||
FetchBootstrap(ctx context.Context) (*BootstrapConfig, error) | ||
FetchTelemetryConfig(ctx context.Context) (*TelemetryConfig, error) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note for the future. Try not to grow interfaces as much as possible. The larger the interface the weaker the abstraction. It also stops you from making composable implementations and forces you to build a mock for FetchBootstrap, when you might only want to test FetcyTelemetryConfig. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Noted for the future! I was trying to use the same pattern, but agreed. |
||
PushServerStatus(ctx context.Context, status *ServerStatus) error | ||
DiscoverServers(ctx context.Context) ([]string, error) | ||
} | ||
|
||
// MetricsConfig holds metrics specific configuration for the TelemetryConfig. | ||
// The endpoint field overrides the TelemetryConfig endpoint. | ||
type MetricsConfig struct { | ||
Filters []string | ||
Endpoint string | ||
} | ||
|
||
// TelemetryConfig contains configuration for telemetry data forwarded by Consul servers | ||
// to the HCP Telemetry gateway. | ||
type TelemetryConfig struct { | ||
Endpoint string | ||
Labels map[string]string | ||
MetricsConfig *MetricsConfig | ||
} | ||
|
||
type BootstrapConfig struct { | ||
Name string | ||
BootstrapExpect int | ||
|
@@ -44,6 +65,7 @@ type hcpClient struct { | |
hc *httptransport.Runtime | ||
cfg config.CloudConfig | ||
gnm hcpgnm.ClientService | ||
tgw hcptelemetry.ClientService | ||
resource resource.Resource | ||
} | ||
|
||
|
@@ -64,6 +86,8 @@ func NewClient(cfg config.CloudConfig) (Client, error) { | |
} | ||
|
||
client.gnm = hcpgnm.New(client.hc, nil) | ||
client.tgw = hcptelemetry.New(client.hc, nil) | ||
|
||
return client, nil | ||
} | ||
|
||
|
@@ -79,6 +103,29 @@ func httpClient(c config.CloudConfig) (*httptransport.Runtime, error) { | |
}) | ||
} | ||
|
||
// FetchTelemetryConfig obtains telemetry configuration from the Telemetry Gateway. | ||
func (c *hcpClient) FetchTelemetryConfig(ctx context.Context) (*TelemetryConfig, error) { | ||
params := hcptelemetry.NewAgentTelemetryConfigParamsWithContext(ctx). | ||
WithLocationOrganizationID(c.resource.Organization). | ||
WithLocationProjectID(c.resource.Project). | ||
WithClusterID(c.resource.ID) | ||
|
||
resp, err := c.tgw.AgentTelemetryConfig(params, nil) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
payloadConfig := resp.Payload.TelemetryConfig | ||
return &TelemetryConfig{ | ||
Endpoint: payloadConfig.Endpoint, | ||
Labels: payloadConfig.Labels, | ||
MetricsConfig: &MetricsConfig{ | ||
Filters: payloadConfig.Metrics.IncludeList, | ||
Endpoint: payloadConfig.Metrics.Endpoint, | ||
}, | ||
}, nil | ||
} | ||
|
||
func (c *hcpClient) FetchBootstrap(ctx context.Context) (*BootstrapConfig, error) { | ||
version := version.GetHumanVersion() | ||
params := hcpgnm.NewAgentBootstrapConfigParamsWithContext(ctx). | ||
|
@@ -233,3 +280,32 @@ func (c *hcpClient) DiscoverServers(ctx context.Context) ([]string, error) { | |
|
||
return servers, nil | ||
} | ||
|
||
// Enabled verifies if telemetry is enabled by ensuring a valid endpoint has been retrieved. | ||
// It returns full metrics endpoint and true if a valid endpoint was obtained. | ||
func (t *TelemetryConfig) Enabled() (string, bool) { | ||
endpoint := t.Endpoint | ||
if override := t.MetricsConfig.Endpoint; override != "" { | ||
endpoint = override | ||
} | ||
|
||
if endpoint == "" { | ||
return "", false | ||
} | ||
|
||
// The endpoint from Telemetry Gateway is a domain without scheme, and without the metrics path, so they must be added. | ||
return endpoint + metricsGatewayPath, true | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are these just strings? Ideally we use a strong type like url. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ah yep I convert to URL here: https://github.com/hashicorp/consul/pull/17460/files/787afd8667a5f55019bb51ae67d91fcb9ac167cd#diff-d652bfc7ebdcf8edacd32ad13fd9059b666981a2a76ef9bc5d985be7edac5601R69 but maybe this should have been done earlier. |
||
} | ||
|
||
// DefaultLabels returns a set of <key, value> string pairs that must be added as attributes to all exported telemetry data. | ||
func (t *TelemetryConfig) DefaultLabels(nodeID string) map[string]string { | ||
labels := map[string]string{ | ||
"node_id": nodeID, // used to delineate Consul nodes in graphs | ||
} | ||
|
||
for k, v := range t.Labels { | ||
labels[k] = v | ||
} | ||
|
||
return labels | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
package client | ||
|
||
import ( | ||
"context" | ||
"testing" | ||
|
||
"github.com/stretchr/testify/mock" | ||
"github.com/stretchr/testify/require" | ||
) | ||
|
||
func TestFetchTelemetryConfig(t *testing.T) { | ||
t.Parallel() | ||
for name, test := range map[string]struct { | ||
metricsEndpoint string | ||
expect func(*MockClient) | ||
disabled bool | ||
}{ | ||
"success": { | ||
expect: func(mockClient *MockClient) { | ||
mockClient.EXPECT().FetchTelemetryConfig(mock.Anything).Return(&TelemetryConfig{ | ||
Endpoint: "https://test.com", | ||
MetricsConfig: &MetricsConfig{ | ||
Endpoint: "", | ||
}, | ||
}, nil) | ||
}, | ||
metricsEndpoint: "https://test.com/v1/metrics", | ||
}, | ||
"overrideMetricsEndpoint": { | ||
expect: func(mockClient *MockClient) { | ||
mockClient.EXPECT().FetchTelemetryConfig(mock.Anything).Return(&TelemetryConfig{ | ||
Endpoint: "https://test.com", | ||
MetricsConfig: &MetricsConfig{ | ||
Endpoint: "https://test.com", | ||
}, | ||
}, nil) | ||
}, | ||
metricsEndpoint: "https://test.com/v1/metrics", | ||
}, | ||
"disabledWithEmptyEndpoint": { | ||
expect: func(mockClient *MockClient) { | ||
mockClient.EXPECT().FetchTelemetryConfig(mock.Anything).Return(&TelemetryConfig{ | ||
Endpoint: "", | ||
MetricsConfig: &MetricsConfig{ | ||
Endpoint: "", | ||
}, | ||
}, nil) | ||
}, | ||
disabled: true, | ||
}, | ||
} { | ||
test := test | ||
t.Run(name, func(t *testing.T) { | ||
t.Parallel() | ||
|
||
mock := NewMockClient(t) | ||
test.expect(mock) | ||
|
||
telemetryCfg, err := mock.FetchTelemetryConfig(context.Background()) | ||
require.NoError(t, err) | ||
|
||
if test.disabled { | ||
endpoint, ok := telemetryCfg.Enabled() | ||
require.False(t, ok) | ||
require.Empty(t, endpoint) | ||
return | ||
} | ||
|
||
endpoint, ok := telemetryCfg.Enabled() | ||
|
||
require.True(t, ok) | ||
require.Equal(t, test.metricsEndpoint, endpoint) | ||
}) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any thoughts on this changelog writeup?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done! Thanks :D