feat(cse): emit kubelet active settings as a structured JSON Kusto event - #9034
feat(cse): emit kubelet active settings as a structured JSON Kusto event#9034abigailliang-aks-sig-node wants to merge 22 commits into
Conversation
Add logKubeletActiveFlags() which parses the flags kubelet actually started with -- the "FLAG: --name=value" lines kubelet's klog prints at startup (journalctl -u kubelet) -- de-duplicates them across restarts, and emits them as a single structured, greppable event line (AKS_KUBELET_CONFIG event=kubelet_active_flags ...). It is invoked at the end of ensureKubelet via logs_to_events so the effective per-node kubelet configuration lands in GuestAgentGenericLogs and is queryable from Kusto without SSHing into each node. This is a fleet-wide rollout-verification probe for the flags-to-config-file migration (enable-kubelet-config-file toggle): it makes it easy to confirm whether kubelet is running with --config=<file> versus inline flags across all nodes. The probe is best-effort and non-blocking: kubelet is started with --no-block so it polls the journal briefly (bounded, breaking as soon as flags appear) and never fails node provisioning. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 240e2517-2ad6-4925-90fa-4377e01798e6
|
Tracking work item: AB#38936519 |
Verified against GuestAgentGenericLogs (azcore.centralus / FA): logs_to_events records the event Message as "Completed: $*" -> Context1, and CSE stdout echoes do NOT land in that table. So wrapping a bare `logKubeletActiveFlags` would only store "Completed: logKubeletActiveFlags" -- the flags payload would not be queryable. Rework into getKubeletActiveFlagsSummary (emits a compact `uses_config_file=<bool> config_path=<path> flag_count=<n> found=<bool>` summary from journalctl) plus a reportKubeletActiveFlags pass-through emitter. The summary is handed to logs_to_events as arguments, so it lands in the event Message (Context1) and is queryable from Kusto. The summary is intentionally small (not the full flag dump) to stay well under the ~3KB Context1 cap. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 240e2517-2ad6-4925-90fa-4377e01798e6
Windows Unit Test Results 3 files 11 suites 47s ⏱️ Results for commit d95ede2. ♻️ This comment has been updated with latest results. |
AgentBaker Linux gate detectiveRun: 173691175 Detective summary: VMSS/node provisioning, SSH, validation pod, MANA PCI, driver, and VF bonding all passed; the failure occurred when Kubernetes exec from the debug pod returned apiserver-to-kubelet proxy 502 Bad Gateway before the curl traffic check could prove a datapath issue. Likely cause / signature: Transient kubelet streaming/exec transport failure; reusing existing wiki signature wireserver-exec-context-deadline-kubelet-streaming. Confidence: High Recommended owner/action: E2E/test-infra owner should track under the kubelet-streaming flake and consider retrying exec on 502/500/context-deadline validation failures; no PR-author action recommended from this signal alone. Strongest alternative: Real MANA datapath regression, but less likely because MANA setup validations passed and the observed error is the Kubernetes exec proxy transport failing before curl results were available. Evidence: timeline failed task exit 1; failed test run 545377067; task log 562 around line 799; build metadata and PR #9034 metadata. |
Rework the kubelet active-settings event to emit a structured JSON payload
(found, uses_config_file, config_path, flag_count, flags{...}) directly into
GuestAgentGenericLogs.Context1, mirroring AKS.Runtime.memory_telemetry_cgroupv2
so it is consumable with parse_json() from Kusto and fits existing config/
version-tracking dashboard templates.
- Replace the flat key=value summary with getKubeletActiveFlagsJSON (jq-built).
- Surface curated key flag values (--config, cgroup-driver, kube-reserved,
max-pods, rotate-certificates, etc.) instead of just a boolean + count.
- Write the event file directly (like the AKS.Runtime cgroup/TLS telemetry)
instead of via logs_to_events, which would prefix Message with "Completed: "
and break parse_json().
- Update ShellSpec tests to assert the JSON output and event emission.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 240e2517-2ad6-4925-90fa-4377e01798e6
AgentBaker Linux gate detectiveRun: 173784179 Detective summary: Ubuntu 22.04 ArtifactStreaming ARM64 provisioned successfully, reached VM validation, then kernel-log validation flagged an early boot Call trace/PANIC_CRASH after node Ready, SSH, validation pod, KSCR/bootstrap, and NIC/rx checks passed. Likely cause / signature: Likely transient/platform kernel call-trace validation noise; the kubelet-active-flags JSON PR is unlikely causal because the failure is in kernel log scan after unrelated provisioning/validation succeeded. Confidence: Medium Recommended owner/action: Node Lifecycle/E2E owners should track as a new Ubuntu2204 ArtifactStreaming ARM64 kernel-calltrace signature and inspect scenario kernel logs if it recurs. Strongest alternative: A PR-induced kubelet/config output instability, but less likely because the PR surface is telemetry/config formatting and the node was otherwise healthy through validation. Evidence: timeline failed task exit 1; failed test run 546207590; task log 562 around line 772; build/PR metadata. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (2)
aks-node-controller/helpers/kubelet_flags.go:18
- The TaskName used for the scriptless kubelet-active-flags event ("AKS.AKSNodeController.ensureKubelet.kubeletActiveFlags") does not match the TaskName referenced in the PR description/query example ("ensureKubelet.kubeletActiveFlags"), so the documented Kusto query will miss events emitted by aks-node-controller. Either update the query/docs everywhere this is consumed, or align the TaskName here.
// kubeletActiveFlagsTaskName is the event TaskName for the kubelet active flags telemetry.
// Scriptless (aks-node-controller) and shell CSE use different TaskName values; keep this in sync
// with the query patterns documented in the PR description.
kubeletActiveFlagsTaskName = "AKS.AKSNodeController.ensureKubelet.kubeletActiveFlags"
// kubeletFlagsPollMaxAttempts is how many times to poll journalctl for FLAG lines.
aks-node-controller/app.go:193
- EmitKubeletActiveFlagsEvent() can block for up to ~20s (10 attempts × 2s interval) while polling journalctl, but it is called synchronously on the success path. This contradicts the PR description’s claim that the scriptless implementation runs as a goroutine and can also delay the provision command/service completion unnecessarily. Consider moving this to an asynchronous execution path (or otherwise shortening/bounding the poll) while ensuring the process lifetime still allows the event to be written.
a.eventLogger.LogEvent("Provision", "Completed", helpers.EventLevelInformational, startTime, endTime)
slog.Info("aks-node-controller finished successfully.")
// Emit kubelet active flags as a structured event for Kusto querying.
// Best-effort: telemetry failures do not fail provisioning.
a.eventLogger.EmitKubeletActiveFlagsEvent()
}
Shell CSE (cse_config.sh emitKubeletActiveFlagsEvent) already covers both traditional and scriptless paths — in scriptless phase 2, aks-node-controller still executes the shell scripts which include ensureKubelet(). The Go-side implementation was redundant. Removes: - aks-node-controller/helpers/kubelet_flags.go - aks-node-controller/helpers/kubelet_flags_test.go - EmitKubeletActiveFlagsEvent() call in app.go - 3-event test assertions in app_test.go (back to 2)
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (1)
parts/linux/cloud-init/artifacts/cse_config.sh:1016
- The PR description states the Go/aks-node-controller path is the “primary” implementation (with TaskName
AKS.AKSNodeController.ensureKubelet.kubeletActiveFlagsand helper code underaks-node-controller/helpers/kubelet_flags.go), but the current changeset only adds the shell fallback (AKS.CSE.ensureKubelet.kubeletActiveFlags). This mismatch makes it unclear whether the scriptless/phase-2 path is actually covered by this PR or whether the description needs updating.
# Emit kubelet's effective startup configuration as a structured JSON event so it is queryable
# per node from Kusto (rollout verification for the flags-to-config-file migration, and config/
# version tracking dashboards). The JSON lands verbatim in GuestAgentGenericLogs.Context1 and is
# consumable with parse_json(). Best-effort: never fail node provisioning on this.
(emitKubeletActiveFlagsEvent || true) &
…ally started by systemd via `WantedBy=kubelet.service`.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 18 out of 18 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (2)
parts/linux/cloud-init/artifacts/emit-kubelet-active-flags.sh:52
- Flag parsing only handles
--key=valuetokens. Several kubelet flags are passed in--key valueform (e.g.--config /etc/default/kubeletconfig.json,--kubeconfig ...,--bootstrap-kubeconfig ...), so they’ll be silently dropped from the emitted JSON.
# Parse --key=value pairs from the flags string
local key value
for token in ${kubelet_flags}; do
case "${token}" in
--*=*)
parts/linux/cloud-init/artifacts/emit-kubelet-active-flags.sh:28
- The script currently only reads
KUBELET_FLAGSfrom/etc/default/kubelet, but kubelet’s effective startup args also include flags coming from other env vars and systemd drop-ins (e.g.KUBELET_CONTAINERD_FLAGS,KUBELET_CONTAINER_RUNTIME_FLAG,KUBELET_CGROUP_FLAGS,KUBELET_TLS_BOOTSTRAP_FLAGS, and--node-labels). As-is, the emitted event won’t reflect the full effective kubelet startup configuration.
# Read KUBELET_FLAGS from /etc/default/kubelet
if [ -f "${KUBELET_DEFAULT_FILE}" ]; then
kubelet_flags="$(grep '^KUBELET_FLAGS=' "${KUBELET_DEFAULT_FILE}" | sed 's/^KUBELET_FLAGS=//')"
fi
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 15 out of 15 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (1)
parts/linux/cloud-init/artifacts/emit-kubelet-active-flags.sh:80
- The size guard only drops
config_fileonce; if the payload is still > 3000 bytes (e.g., unusually longKUBELET_FLAGSvalues), GuestAgentGenericLogs.Context1 will still be truncated andparse_json()will fail. Consider re-checking size after droppingconfig_file, and if still oversized, also drop (or truncate)flagsto guarantee the emitted payload remains valid JSON.
if [ "${payload_bytes}" -gt "${max_message_bytes}" ]; then
echo "kubelet active settings payload exceeded ${max_message_bytes} bytes (${payload_bytes}), dropping config_file content" >&2
payload="$(jq -cn \
--argjson uses_config_file "${uses_config_file}" \
--arg config_path "${config_path}" \
| # before kubelet starts), so After=kubelet.service guarantees the files exist. | ||
| # The /run marker (tmpfs, cleared on reboot) keeps it to one event per boot so a crashlooping | ||
| # kubelet cannot flood the guest agent event directory. | ||
| ConditionPathExists=!/run/emit-kubelet-active-flags.done |
There was a problem hiding this comment.
This is oneshot service even if it crash loops, it will still be executed only once isnt it? since you also set Restart=no, may be you can get rid of it.
|
|
||
| [Install] | ||
| WantedBy=kubelet.service |
There was a problem hiding this comment.
seems we also need After=kubelet.service
Without After=kubelet.service, systemd may start both services concurrently:
kubelet ───────► starting
collector ─────► reads settings immediately
With After=:
kubelet ───────► start job finishes
└──► collector starts
You can verify if this is really needed.
There was a problem hiding this comment.
But since kubelet configs is always ready before kubelet starts, this is probably fine. Just food for thought.
Summary
Emit kubelet's effective runtime configuration (CLI flags + config file content) as a structured JSON event in
GuestAgentGenericLogs, queryable viaparse_json()from Kusto.Motivation
There is currently no easy way to query kubelet's active settings across nodes in Kusto. Debugging requires SSH-ing into individual nodes. This event enables a single Kusto query to surface kubelet configuration for all nodes in a cluster.
Design: one event per boot is sufficient
Architecture: async oneshot systemd service
The telemetry runs as an independent systemd oneshot service (
emit-kubelet-active-flags.service), completely decoupled from the CSE critical path:WantedBy=kubelet.service, enabled at VHD build time — systemd automatically starts it whenever kubelet starts; nosystemctl startcall needed incse_config.shAfter=kubelet.service— guarantees/etc/default/kubeletand/etc/default/kubeletconfig.jsonare already writtenConditionPathExists=!/run/emit-kubelet-active-flags.done+ExecStartPost=/bin/touch /run/emit-kubelet-active-flags.done— limits to one event per boot (prevents flooding if kubelet crashloops;/runis tmpfs, cleared on reboot)cse_config.shis not modified; the service is triggered entirely by systemd dependency orderingThis follows the same pattern as
measure-tls-bootstrapping-latency.service.Why not inline in CSE?
Even with
&and FD redirection, a background subshell inherits the CSE pipe FDs. In scriptless phase 2, Go'scmd.Wait()blocks until all pipe write ends close — so the background process still delays CSE completion. A systemd oneshot service runs in its own scope with independent FDs, fully decoupled.Why not a generic node-telemetry service?
This unit is deliberately kubelet-scoped rather than a shared "node config telemetry" service, because
the repo already establishes one unit per telemetry domain, each with a different trigger:
cgroup-memory-telemetry.timer—OnBootSec=0+ every 5 mincgroup-pressure-telemetry.timer— periodicaks-check-networkAfter=network-online.targetemit-kubelet-active-flagsWantedBy=kubelet.serviceFolding other domains (e.g. OS configuration: sysctl, THP, cgroup version, ulimits) into this unit would
be wrong on two counts:
kubelet.servicemeans a node whose kubelet fails to start emits no OS telemetry, which is exactlywhen it is most needed for diagnosis.
config_filewhen itexceeds 3000. A second domain in the same Message would crowd out the kubelet data, so it has to be a
separate event (separate TaskName) anyway.
Separate event + separate trigger implies a separate unit. What is worth sharing when a second consumer
appears is the event-writing boilerplate (the
jq -nenvelope, the size cap, the once-per-boot/runmarker) — currently duplicated across ~8 scripts in
parts/linux/cloud-init/artifacts/. Note this scriptis VHD-baked and intentionally does not source
cse_helpers.sh, which is what keeps it off the CSE path.Data sources: files, not journalctl
Reads directly from kubelet's config files on disk (instantaneous, no polling needed):
/etc/default/kubeletKUBELET_FLAGS=--cloud-provider=external --max-pods=110 ...(command-line flags)/etc/default/kubeletconfig.jsonmaxPods,clusterDNS,featureGates, etc.)Both files are written by
ensureKubelet()before kubelet starts, so they are guaranteed to exist when this service runs (After=kubelet.service).Together these cover all RP-configurable kubelet settings: the 39 flags translated to config file + the command-line flags (notably
--cloud-provider).Size guard
GuestAgentGenericLogs.Context1 is hard-truncated at 3072 bytes (3 KiB), verified empirically (16M events, max Context1 length = 3072). We cap at 3000 bytes. If exceeded,
config_fileis dropped and replaced with"truncated:exceeded_size_cap", with a log warning.Files
parts/linux/cloud-init/artifacts/emit-kubelet-active-flags.shparts/linux/cloud-init/artifacts/emit-kubelet-active-flags.serviceAfter=kubelet.service,WantedBy=kubelet.service, once-per-boot guardvhdbuilder/packer/packer_source.shvhdbuilder/packer/vhd-image-builder-*.jsonvhdbuilder/packer/pre-install-dependencies.shsystemctl enable emit-kubelet-active-flags.service— creates thekubelet.service.wants/symlink that makesWantedBy=take effectvhdbuilder/packer/imagecustomizer/azlosguard/azlosguard.ymlNo changes to
cse_config.shorapp.go— the service is fully self-contained.Event payload
{ "uses_config_file": true, "config_path": "/etc/default/kubeletconfig.json", "kubelet_flags_count": 5, "kubelet_flags": { "cloud-provider": "external", "node-ip": "10.224.0.5" }, "config_file": { "maxPods": 110, "clusterDNS": ["10.0.0.10"] } }kubelet_flagscomes from/etc/default/kubelet,config_filefrom/etc/default/kubeletconfig.json— sokubelet_flags_countdoubles as a migration-progress metric (how many settings have not moved into the config file yet).Kusto query example
Test plan
make generateproduces no diff[Unofficial][Orchestration] AgentBaker + Linux VHD Full E2E Testspipeline (bake + apply with current branch)systemctl status emit-kubelet-active-flagsshows completed/var/log/azure/Microsoft.Azure.Extensions.CustomScript/events/withkubeletActiveFlagsTaskName