Repository navigation
Conversation
44e02e2 to
2265070
Compare
Adds an MVP for posting metrics scraped from local daemons and the agent itself to a central in-cluster node telemetry API.
| close(registered) | ||
|
|
||
| if monitorConfig.TelemetryEnabled() { | ||
| go runTelemetry(ctx, logger.WithName("telemetry"), monitorConfig.Telemetry, monitorCfg, monitoringKubeClient, monitorMgr) |
There was a problem hiding this comment.
can we switch this to use serviceaccount in rbac rather than node in rbac? I'll start a thread internally as to why
| ) | ||
|
|
||
| func runTelemetry(ctx context.Context, logger logr.Logger, cfg *telemetryconfig.Config, restCfg *rest.Config, kubeClient client.Client, source telemetry.FatalSource) { | ||
| sender, err := nodetelemetry.NewClient(restCfg) |
There was a problem hiding this comment.
This effectively renders telemetry off for the lifetime of this NMA process. We will want to be more resilient here and periodically retry construction of the client.
There was a problem hiding this comment.
there's no retryable error in the call chain AFAICT: nodetelemetry.NewClient() -> rest.HTTPClientFor -> rest.TransportFor -> config.TransportConfig and transport.New(), which both error on static validation. in practice, this path can't really error since we're reusing the same config controller runtime manager was created with and went through the same call chain for.
I'd went with a fail-open approach with a log line for that edge case so that NMA could still monitor, but perhaps failing closed makes more sense here so that if this can ever produce an error in the future, it could be caught and stopped in a DaemonSet update rollout rather than letting every node silently stop reporting
There was a problem hiding this comment.
i think fail open approach may work for now, but it'll probably bite us down the road if we ever ship a bug that breaks this telemetry thread.
There was a problem hiding this comment.
fair, I think this could actually be entirely avoided by just re-using the manager's already constructed client since we're already re-using its config. done in 4570f7b, lmk what you think
this should be equivalent to before since client-go's transport is a cached lookup based on the config (link), and the config used between the controller runtime manager and the telemetry manager was already the same for all the keyed fields
| if err := wait.PollUntilContextCancel(ctx, 30*time.Second, true, func(ctx context.Context) (bool, error) { | ||
| return kubeClient.Get(ctx, types.NamespacedName{Name: hostname}, &node) == nil, nil | ||
| }); err != nil { | ||
| return |
There was a problem hiding this comment.
same concern, we should retry here periodically instead of just turning off telemetry.
There was a problem hiding this comment.
this one actually does poll indefinitely because the context passed here is the manager's, which is only canceled on shutdown, though telemetry does end up being blocked until the node object can be fetched.
more broadly, on this note, I was thinking one of the following approaches could be better :
- refactoring it into a generic runtime context bootstrapper, like the HybridNodesBootstrapper but ran in the background until it can fetch the node object, that way telemetry can be started earlier and attributes can be added into the reports as they are discovered
- dropping the k8s Get() API call entirely by taking a hard dependency on IMDS for all attributes. the kubeletVersion cannot be fetched this way, but it can already be inferred from the AMI ID (if it's a public AMI). alternative local options for the kubelet version could be
kubelet --version(if permissions allow) or reading/etc/eks/kubelet-version.txton AL2023 AMIs link, which we could also get added to BR
There was a problem hiding this comment.
+1 to using IMDS we already use elsewhere in NMA, so it's a not a new dependency. having a single source of truth is a nice property too.
that way telemetry can be started earlier and attributes can be added into the reports as they are discovered
I am not sure about, it may be confusing or provide wrong metrics if we lazily load these attributes. For example, getting some metrics without the kubelet version would potentially lead to incomplete metrics.
There was a problem hiding this comment.
IMDS it is then, that also addresses a I was also seeing a race condition that particularly impacted Auto (NMA as a system daemon) where the first fetched Node object may not include the desired labels since they're applied post-registration by CP controllers. I'll make that change
There was a problem hiding this comment.
I made this change in 4cb9fc7:
- k8s version is still through a k8s Get(), should be safe & quick since the kubelet version should be in the first version of the node object that the kubelet creates
- runtime.GOARCH for the host arch: should hold true except for the unlikely situation where NMA's ran in an emulated environment. could use
kubernetes.io/arch, which is set bykubeletin the initial node object using the same source: link, but I'd prefer to avoid using the k8s API except where it's actually needed to make future cleanups easier - all else from IMDS. IMDS checks are skipped on hybrid nodes, so those attributes (like instance id and AZ) are left empty. hybrid nodes are expected to use their node name as their instance ID in the provider ID (link), so there's not much value add for these additional attributes for hybrid
| func NodeAttrsFromNode(node *corev1.Node) nodetelemetry.NodeAttrs { | ||
| return nodetelemetry.NodeAttrs{ | ||
| NodeName: node.Name, | ||
| HostID: InstanceID(node.Spec.ProviderID), |
There was a problem hiding this comment.
I don't think this is guaranteed to be the the instance-id. We should be defensive here for self-hosted NMA
There was a problem hiding this comment.
for self-hosted/hybrid nodes, I don't know if it's safe to take this field, so I've currently left it to drop the instance ID from the report. as far as I know, there's nothing that would prevent a self-provided instance from having the same ID as an EC2 instance part of the same cluster. this should be guaranteed as the instance ID for cloud-provider-aws/ccm managed ec2 instances though
There was a problem hiding this comment.
I think it's just a bit misleading rn, because if I have an "abnormal" provider ID then I set the HostID to "". Which is probably correct.
There was a problem hiding this comment.
I think 4cb9fc7 makes this a bit more readable by only adding the attributes when they can be resolved. assuming this comment was on the InstanceId() function returning an empty string when the ID doesn't match the expected pattern
| md := pmetric.NewMetrics() | ||
| rm := md.ResourceMetrics().AppendEmpty() | ||
| a := rm.Resource().Attributes() | ||
| for k, v := range map[string]string{ |
There was a problem hiding this comment.
nit: you can just a singleton instead of re-creating this map everytime.
There was a problem hiding this comment.
agreed on this in the current format, I'm going to hold off on this though pending your thoughts on the point about starting telemetry early with a subset of attributes. that would benefit from re-constructing the attributes on each report
There was a problem hiding this comment.
done in 4cb9fc7, there's now a single call to construct a new "Resource" which is used across all reports
| } | ||
| } | ||
|
|
||
| func (b *Baselines) Unreachable() { |
There was a problem hiding this comment.
I don't follow what unreachable means here.
There was a problem hiding this comment.
I went ahead and renamed this in db944af, it's used as a catch-all for all scrape failures so I renamed it to that. it covers everything from us dropping the scrape because the response is too large to the scrape target being down/unresponsive
| NotReportingAttrReason = "reason" | ||
| ) | ||
|
|
||
| const MaxPointsPerMetric = 50 |
There was a problem hiding this comment.
Shouldn't this be gathered from the metric configuration rather than hardcoded?
There was a problem hiding this comment.
I intended this as the client-side limit to match what the API's expected to accept, so it also applies as a ceiling to the maxSeries value defined in the user-configuration. I also made an update to make that ceiling explicit at config assembly rather than just at scrape time, done in 5a9ee2c. let me know if that makes sense
| } | ||
|
|
||
| func NewClient(cfg *rest.Config) (*Client, error) { | ||
| hc, err := rest.HTTPClientFor(cfg) |
There was a problem hiding this comment.
Do we need to add a timeout to the rest config?
There was a problem hiding this comment.
that would probably be prudent to add, but I'm not sure if it's necessary. none of the rest configs used throughout have a configured timeout, including the default one generated for the controller-runtime. I'm thinking that's b/c the default configured http health check in client go sets a 30s ReadIdleTimeout and a 15s PingTimeout(link), so a dropped connection would be caught within about 45s. the kube-apiserver handles a stuck request by returning a 504 after a 60s timeout
for completeness, I added a 75s timeout in 8834820. that should never take effect in the expected path, but an API server configured with a higher timeout may open up this path. lmk what you think
| for _, s := range series { | ||
| key := seriesKey(metric, s.Labels) | ||
| seen[key] = true | ||
| base, ok := b.series[key] |
There was a problem hiding this comment.
Reset detection compares the latest counter against the last acknowledged baseline, rather than the last observed value.
Step | Counter | Behavior
-- | -- | --
Establish baseline | 10 | Baseline becomes 10
Scrape; upload returns 503 | 100 | Delta 90 held; baseline remains 10
Process resets; next scrape | 20 | Incorrectly reports delta 10
The observed decrease from 100 to 20 should trigger reset handling. Instead, it is missed because 20 exceeds the acknowledged baseline of 10. I reproduced this with a focused regression test.
Fix: track the last observed value separately from the acknowledged baseline, and explicitly handle pending deltas across resets.
There was a problem hiding this comment.
good catch, added in lastScraped and carry fields to track the last scraped value and the delta not acknowledged by the extension at a reset, respectively. I also added a test case for this specific example to ensure it measures a reset as starting at 0 and reports 110, done in d07b793
edit: oops, forgot we'd want to establish a new baseline after a reset rather than assuming a 0-start. fixed that in eb2d2ca so it'll report 90 instead, and if it increases from 20 again it'll add that in the next delta (it increments an extra 5 in the test case)
0f1d078 to
8f4ae63
Compare
| } | ||
|
|
||
| // addComputeType sets ComputeType from the runtime tags | ||
| func addComputeType(attrs *nodetelemetry.NodeAttrs, tags []string) { |
There was a problem hiding this comment.
can we stay consistent and set compute type for regular eks-ec2 nodes as well?
There was a problem hiding this comment.
we can, I just felt less comfortable giving that one a label since it's not really tied to a check, but rather the omission of other categories. eks-auto specifically means the node is an auto mode one and hybrid means it's a hybrid node, those two things are mutually exclusive. eks-ec2 would currently just be the case where neither of those labels were applied, and that's totally fine, but it's different from the others since that definition isn't tied to anything specific and could therefore change over time, e.g. if NMA identified and tagged nodes managed by EKS MNG
There was a problem hiding this comment.
I see your point yea. It would be tied to visibility service-side atm but if we are going to have a change like you mentioned, worth holdign off for now
Description of changes:
This adds support posting metrics scraped from local daemons and the agent itself to an in-cluster telemetry API. The new feature is disabled by default, but can be enabled through a dedicated sub-section in the existing monitor-config ConfigMap. Enabling telemetry leads the agent to, at the configured interval:
This is a minimal implementation of the feature intended to span the necessary functionality. There are several TODOs scattered throughout to refine the logic, including:
Testing Done:
Deployed an API server to to handle the posted metrics. Confirmed that:
By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.