All eleven klog flags the pinned page says were removed at v1.26 are still documented on one component's reference page, all five generated flag tables permit only text, and the page that lists four components as supporting JSON still stamps it alpha at the release the post carries¶
Post — Introducing Structured
Logs, 4
September 2020, by Marek Siarkowicz (Google) and Nathan Beach (Google). 57 lines, three fenced
blocks and no images, which makes it one of the shortest posts in this year's walk set. It announces
structured logging in Kubernetes 1.19: two new methods on the klog library, a default text format
that stays byte-compatible with what came before, and a --logging-format=json flag the post itself
labels alpha.
The census row for this post says six years and it never left alpha. Half of that is right. Structured logging as a feature graduated to beta and then grew a successor; the JSON format did not move at all, and the pinned page that documents it still carries the release number in this post's own title. Separating those two halves is most of the work here, because the pin records them in three different places that do not agree: a concept page written by hand, a set of flag tables generated from the components' own flag sets, and a pair of generated configuration references.
As written
Two methods, and one worked pair of samples. The post's mechanism is a library change: We've
added two new methods to the klog library: InfoS and ErrorS. The call it shows is klog.InfoS("Pod
status updated", "pod", klog.KObj(pod), "status", status), and the line it says that call produces
is the block to hold on to, because it is the one piece of the post that survives in the pin
untouched:
I1025 00:15:15.525108 1 controller_utils.go:116] "Pod status updated" pod="kube-system/kubedns" status="ready"
Everything else in the post is downstream of that one call. The message becomes a quoted string, the
arguments become alternating keys and values, and klog.KObj(pod) becomes the namespace/name pair
that lets a reader find every line about one object.
The text format is presented as a compatibility promise, and the JSON one is not. The post is
explicit that nothing about the existing output changes shape: To maintain backwards compatibility,
structured logs will still be outputted as a string where the string contains representations of
those "key"="value" pairs. The very next sentence introduces the second half — Starting in alpha
in 1.19, logs can also be outputted in JSON format using a new --logging-format=json flag. One
paragraph therefore carries a format described as backward-compatible by construction and a format
described as alpha, and nothing in the post suggests their prospects will differ.
The JSON sample has four keys and no verbosity. The post's pretty-printed example is short enough to quote whole:
{
"ts": 1580306777.04728,
"msg": "Pod status updated",
"pod": {
"name": "coredns",
"namespace": "kube-system"
},
"status": "ready"
}
There is no v key, no err key, and no statement anywhere in the post of which keys are required.
A reader building a parser off this post would build it against four keys, two of which are the
post's own example payload rather than part of any contract.
The coverage claim is about volume, and the post itself says the work is not done. The opening
claims We have also updated many logging calls such that over 99% of logging volume in a typical
deployment are now migrated to the structured format, which is a statement about how many lines a
running cluster emits and not about how much of the code was converted. The closing section says the
rest out loud: While we have updated over 99% of the log entries by log volume in a typical
deployment, there are still thousands of logs to be updated. The post is asking for help finishing,
and that request is worth remembering when the pin is read.
Five benefits are listed and every one of them belongs to a consumer. The post promises that
downstream tooling can ingest the data instead of using regular expressions to parse unstructured
strings, that entries can be filtered down to only log entries referencing the particular pod,
that richer features become possible such as automated pattern recognition within logs or tighter
correlation of log and trace data, and that storage costs fall because most storage systems are
more efficiently able to compress structured key=value data than unstructured strings. Each of
those is a property of something reading the output, so each of them depends on the output being
stable enough to write a reader against. The post never says whether it is, and that is the question
the pin answers.
As it runs now
The text sample survives character for character. The line the post prints as the output of
klog.InfoS is at concepts/cluster-administration/system-logs.md:111, byte for byte, six years
later — same timestamp, same goroutine column, same controller_utils.go:116, same
pod="kube-system/kubedns" status="ready". The page also gives the shape the line follows, at
:105, as <klog header> "<message>" <key1>="<value1>" <key2>="<value2>" ..., and adds a detail
the post does not have: strings are quoted, everything else is rendered with %+v, and :114-116
warns that this may cause log messages to continue on the next line. So the format is stable
enough that a sample from 2020 is still the documentation's sample, and unstable enough that a value
can break the line the parser is reading.
The JSON sample did not survive. system-logs.md:190-201 prints the same worked example
reformatted and extended. The indentation went from two spaces to three, "pod": { lost its space,
the object moved from coredns in kube-system to nginx-1 in default, and a key appeared that
the post's sample does not have: "v": 4. :205-208 then names four keys with special meaning —
ts required and a float, v for info messages only and an int, err optional, msg required.
Two of those four are absent from the post's example, and one of the two is required.
The two halves of the post sit at different rungs, and the JSON half is stamped with this post's
own release. The page marks each section with a feature-state shortcode. Structured Logging is
{{< feature-state for_k8s_version="v1.23" state="beta" >}} at :87. Contextual Logging, which is
built on top of it, is {{< feature-state for_k8s_version="v1.30" state="beta" >}} at :125. The
JSON log format is {{< feature-state for_k8s_version="v1.19" state="alpha" >}} at :175. The
release named in that third shortcode is the release this post announces, and the pin's newest
release is v1.37.
The flag tables generated from the components' own flag sets know only text. Every one of the
five component reference pages carries exactly four flags whose name starts with --log, and they
are the same four on all five: --log-flush-frequency, --log-text-info-buffer-size,
--log-text-split-stream and --logging-format. Two of the four are named for the text format and
there is no --log-json- anything anywhere in the reference. The description of --logging-format
is identical on all five —
— at reference/command-line-tools-reference/kube-apiserver.md:790,
kube-controller-manager.md:710, kube-scheduler.md:297, kubelet.md:560 and kube-proxy.md:373.
On the kubelet the same line adds that the flag is DEPRECATED and should be set through the config
file instead. Nothing in any of the five tables offers json as a permitted value, and each table's
default is text.
Those same tables do know about the logging feature gates. The --feature-gates help on each
page enumerates every gate the binary carries, and on kube-scheduler.md:178 that list includes
kube:ContextualLogging=true|false (BETA - default=true), kube:LoggingAlphaOptions=true|false
(ALPHA - default=false) and kube:LoggingBetaOptions=true|false (BETA - default=true). The two
alpha logging flags on the same page, --log-text-info-buffer-size at :280 and
--log-text-split-stream at :287, both end with Enable the LoggingAlphaOptions feature gate to
use this. So the flag set the pages were generated from knows the gates, knows which alpha logging
options they guard, and offers no JSON option at all.
Where the JSON format is documented as a first-class thing is the configuration API, not the flag
table. reference/config-api/kubelet-config.v1beta1.md:37-52 documents logging.options.text and
logging.options.json, each marked [Alpha] and each carrying Only available when the
LoggingAlphaOptions feature gate is enabled. The wrapping logging.options field at :143-152
says the same, and so do splitStream and infoBufferSize at :204-222. The identical block
appears in kube-proxy-config.v1alpha1.md at :33-48, :141-150 and :202-220. Two files carry
the name LoggingAlphaOptions five times each and no concept or task page in the tree carries it
once. Meanwhile the format field itself, at kubelet-config.v1beta1.md:106-112, says only Format
Flag specifies the structure of log messages. default value of format is text and never names a
second permitted value, and vmodule at :135-141 is Only supported for "text" log format.
All eleven flags the page says were removed at v1.26 are still documented, on one component page
out of five. system-logs.md:32-47 states that the flags are deprecated starting with Kubernetes
v1.23 and removed in Kubernetes v1.26 and lists them. Every one of the eleven is still in the
kube-proxy.md flag table at the pin, six of them under the underscore spelling that the page's
hyphenated list does not use:
--add-dir-header kube-proxy.md:48 --add_dir_header
--alsologtostderr kube-proxy.md:55 --alsologtostderr
--log-backtrace-at kube-proxy.md:342 --log_backtrace_at
--log-dir kube-proxy.md:349 --log_dir
--log-file kube-proxy.md:356 --log_file
--log-file-max-size kube-proxy.md:363 --log_file_max_size
--logtostderr kube-proxy.md:377 --logtostderr Default: true
--one-output kube-proxy.md:412 --one_output
--skip-headers kube-proxy.md:461 --skip_headers
--skip-log-headers kube-proxy.md:468 --skip_log_headers
--stderrthreshold kube-proxy.md:475 --stderrthreshold Default: 2
None of the eleven appears on any of the other four component pages. Two more klog flags ride along
on the same page and are not in the page's list at all: --alsologtostderrthreshold at :62 and
--legacy_stderr_threshold_behavior at :314, defaulting to true. The second of those occurs
exactly twice in the whole of content/en, both times on this one page — once as its own flag and
once inside the description of --stderrthreshold, which says its threshold has no effect when
-logtostderr=true or -alsologtostderr=true unless -legacy_stderr_threshold_behavior=false. That is
a sentence about the interaction of three flags the same tree says were removed two years earlier.
The list of components that support JSON is one short, and the missing one is the one that kept
the flags. system-logs.md:210-215 gives four: kube-controller-manager, kube-apiserver,
kube-scheduler and kubelet. The flag itself is on five pages. kube-proxy is the component with
--logging-format on its reference page and no entry in the support list, and it is also the only
component still carrying the eleven removed flags.
A warning points at a list that is not there. system-logs.md:178-179 says JSON output does
not support many standard klog flags. For list of unsupported klog flags, see the [Command line tool
reference] and links /docs/reference/command-line-tools-reference/. That page is
reference/command-line-tools-reference/_index.md, and it is four lines long: the front-matter
fences, title: Component tools, and weight: 120. There is no body. No list of unsupported klog
flags exists anywhere in content/en, under that heading or any other.
The migration the post asks for help with is still described as under way.
system-logs.md:90-91 warns that Migration to structured log messages is an ongoing process. Not
all log messages are structured in this version. When parsing log files, you must also handle
unstructured log messages. The JSON section repeats the shape of the problem at :181-182: Not
all logs are guaranteed to be written in JSON format (for example, during process start). If you
intend to parse logs, make sure you can handle log lines that are not JSON as well. The post's own
closing line said there were still thousands of calls to convert; six years later the page still
tells readers to write a parser that copes with unconverted output.
The two methods the post teaches appear nowhere in the tree outside the blog archive. InfoS,
ErrorS and KObj have zero occurrences in content/en outside blog/_posts. The apparent
matches are all substrings of unrelated words — InfoSec on tasks/debug/_index.md:66, and
ErrorStream and InfoStream in the two config-API references. The contribution the post asks for,
and the API a contributor would write it in, exist only in the post.
Three separate warnings say the output is not an interface. The page opens, before its first
heading, with In contrast to the command line flags described here, the *log output* itself does
*not* fall under the Kubernetes API stability guarantees: individual log entries and their
formatting may change from one release to the next! at :21-24. The Structured Logging section
adds Log formatting and value serialization are subject to change. at :93. The JSON section adds
Field names and JSON serialization are subject to change. at :184. Every benefit the post lists
is a benefit to a reader of the output, and the page's answer, given three times, is that readers
get no guarantee.
What actually happened next is a feature the post does not mention. system-logs.md:123-171
documents Contextual Logging, beta at v1.30, gated behind the ContextualLogging feature gate and is
enabled by default, with the note at :136-138 that The infrastructure for this was added in 1.24
without modifying components. It is built on the structured calls this post introduces: the same
key-value pairs, with a logger carried through the call graph so that the pairs accumulate. The
section has aged in place — its worked examples are pinned to v1.24.0-beta.0 source, its console
samples are dated I0222 15:13:31, and :162 still reads With contextual logging disable. The
archive covers contextual logging where it was announced, in years after this one.
What this exercise does not cover, and where it lives. Container and Pod logs — what kubectl
logs reads, the on-disk layout under /var/log/pods, and what a container restart does to a stream
— are the subject of the 2015 cluster-logging
post, which also records that a Kubernetes
release no longer ships a logging stack at all. The kubelet's own metrics surface, and what replaced
the node dashboard of that era, are the 2016 node-dashboard
post. cAdvisor, Heapster and the
resource-metrics path are the 2015 resource-monitoring
post. Nothing below reads an audit log or a
metrics endpoint; this exercise reads component stdout and nothing else.
The diff, and why
One post, two claims, resolved in opposite directions — and the census row only sees one of
them. The paragraph at the top of the post carries a text format described as backward compatible
and a JSON format described as alpha. The text format is the half that moved: Structured Logging is
beta from v1.23, its worked example is now the documentation's own worked example, and a successor
feature has been built on top of it and taken to beta as well. The JSON format is the half that did
not: still alpha, still marked with v1.19, absent from every component's flag table, and documented
as a permitted value nowhere in the reference. The census row's six years and it never left alpha
is exactly right about the format and exactly wrong about the feature, and the reason it is worth
correcting is that the two halves failed differently. One was overtaken by its own successor. The
other was never wired into the surface that documents it.
Two pages in the same commit disagree about what the flag accepts, and the disagreement has a
direction. The five flag tables are generated from the components' flag sets; the concept page is
written by hand. The generated side is internally consistent — four --log flags, two of them named
text, Permitted formats: "text", and two alpha options that both say In text format — which is
what a flag set looks like when the JSON format is not registered in it. The hand-written side has a
flag invocation, a worked sample and a list of supporting components. Neither side is malformed.
What they disagree about is whether the thing the post announces is reachable by the flag the post
names, and only a running cluster can settle it, which is why Do asks a cluster rather than
reasoning from the pages. This is a different failure from a page going stale: a generated page
cannot go stale, it can only be generated from something that does not contain the feature.
The benefit the post sells is the one thing the pin refuses to promise. Every one of the five
benefits belongs to a consumer of the output, and a consumer needs a contract. The page's answer
appears three times, at :21-24, :93 and :184, and the first of the three is placed before the
first heading so that it cannot be missed: log output does not fall under the API stability
guarantees. Read together with the post, that is not a contradiction of anything the post says — the
post never claims stability — but it is a refusal of the thing the post's five benefits require. The
honest reading is that structured logging made the output parseable without making it depended
on, and the two are not the same achievement.
Removal reached the code and did not reach the reference. Eleven flags are declared removed at
v1.26. All eleven are still documented on kube-proxy.md at v1.37, six of them under a spelling the
declaration does not use, and one of them still describes its interaction with two others and with a
fourth flag that appears nowhere else in the tree. This is not a page that forgot to be updated by
hand: it is generated, so what it says is that something in the generation path still carries flags
the concept page says are gone. And kube-proxy is the same component the JSON support list omits,
which makes it the one component where both halves of the pin's logging story are wrong.
A warning that points at a list that has never existed. The JSON section tells the reader that some klog flags are unsupported and sends them to a page for the list. The page is a four-line stub. The failure is small and it is instructive, because it is the one place in this subject where the pin makes a promise about its own contents that can be checked in a second and found false — and where a reader who followed the pointer would conclude the flags are supported after all.
The census row's tag claim is wrong, and the correction matters for reading order. The row calls
this The year's only obs walk, and the tag's first since the taxonomy was frozen. The first clause
holds: 2020 has three obs rows and the other two are one skip and one read. The second does
not. Three obs rows in earlier years are walk verdicts in the same frozen census — Resource
Usage Monitoring in Kubernetes from May 2015,
Cluster Level Logging with Kubernetes from
June 2015, and Visualize Kubelet Performance with Node
Dashboard from November 2016. This
is the tag's fourth walk, not its first, and the three that precede it are the three the ceding
paragraph above hands territory to.
The ladder
No gate carries this feature's name. Of the 487 gate files at the pin, not one is named for
structured logging and not one for the JSON log format. The state of both is recorded only in the
three feature-state shortcodes on the concept page. Three gates do govern logging, all three arrived
at v1.24 — five releases after this post — and the post names none of them. Transcribed from
reference/command-line-tools-reference/feature-gates/:
ContextualLogging alpha false 1.24
beta true 1.30
LoggingAlphaOptions alpha false 1.24
LoggingBetaOptions beta true 1.24
Their bodies are one sentence each. ContextualLogging reads Enables extra details in log
output of Kubernetes components that support contextual logging. LoggingAlphaOptions reads Allow
fine-tuning of experimental, alpha-quality logging options. LoggingBetaOptions reads Allow
fine-tuning of experimental, beta-quality logging options. None of the three names a format. Two of
them name a quality tier and leave which options belong to it to be discovered from the flag
tables and the configuration references, which is where the [Alpha] markers on splitStream,
infoBufferSize and the two options sub-objects come from.
Two of the three have not moved in thirteen releases, and that is unusual. Counting across all
487 files: 52 gates have alpha as their last stage with no toVersion and no removed marker,
and 96 have beta in that position. Sorted by the release they entered that stage,
LoggingAlphaOptions is the sixth-oldest of the 52 and LoggingBetaOptions the eighth-oldest of
the 96. Only six of the surviving alpha gates and nine of the surviving beta gates date from v1.24
or earlier. So the pair that guards logging options is near the front of the queue of things that
stopped moving, and the pin's newest release is thirteen ahead of where they stopped.
The one gate that did move is the one the post does not mention. ContextualLogging is the only
logging gate with two rows, and its second row is beta true 1.30 with no toVersion and no
removed marker, so it is live and on by default at the pin. It is also the gate whose subject was
built on the post's own mechanism. The ladder therefore says something the prose sections say more
slowly: the part of this post that grew a feature gate is not the part the post is about.
Topology
One node, the solo topology, fresh. Bring the guest up
with the five provision steps, substituting
topology=solo, install Kubernetes with the node baseline
procedure, bring the cluster up as usual, and
confirm one Ready node before starting.
The component under the microscope is kube-scheduler, for three reasons. It is a static Pod, so its
flags are one file on disk and a restart is a file edit. Its log stream is bounded and predictable —
a handful of lines per scheduling decision, and a decision happens whenever you create a Pod. And
when it fails to start, the cluster keeps serving; the apiserver would not. The manifest edited
below is the same file the PodTopologySpread exercise edits,
and the backup path is deliberately the same one, /tmp/bw-scheduler.yaml, so that a half-finished
run of either exercise is recoverable the same way.
A Pod has to actually bind for the scheduler to log a binding, so the first Do step removes the
control plane's NoSchedule taint. Three Pods are created over the course of the exercise and all
three are registry.k8s.io/pause:3.10; none of them does any work, because the thing being measured
is what the scheduler says about them. The namespace is bw-slog.
Two steps run a component image as a one-shot Pod to read a binary's own --help output. That is
the only way to compare the pin's flag tables against a flag set, because the tables in the pin were
generated from a build that is not this cluster's build, and no page in the pin says which build.
The comparison is therefore a measurement and not a check: what the tables say is fixed and quoted
above, and what the cluster's binary says is whatever it says.
What one node cannot show: the kube-proxy side of the story at any scale, and the JSON output of any component other than the scheduler. Both are reachable — kube-proxy runs here as a DaemonSet with one Pod — but a single node makes the proxy's logs almost empty, so step 7 reads its configuration rather than its output.
Do
- Untaint the control plane, then ask the cluster what it admits to about logging. Two of the three
gates from The ladder are default
trueand one is defaultfalse, so all three should appear in the live gate metric with values that match the transcription. Then read the kubelet's effective logging configuration out ofconfigz, which is the one place a running component publishes the block thatkubelet-config.v1beta1.mddocuments:
CP=$(kubectl get nodes -o jsonpath='{.items[0].metadata.name}')
kubectl taint node $CP node-role.kubernetes.io/control-plane:NoSchedule-
kubectl version -o json | python3 -c 'import json,sys; print(json.load(sys.stdin)["serverVersion"]["gitVersion"])'
kubectl get --raw /metrics | grep 'kubernetes_feature_enabled.*Logging'
kubectl get --raw "/api/v1/nodes/$CP/proxy/configz" | python3 -c 'import json,sys; print(json.dumps(json.load(sys.stdin)["kubeletconfig"]["logging"], indent=2))'
- Ask the scheduler's own binary which formats it permits. Run the image the cluster is already
running, with
--helpas its only argument, and compare the four--logflags and the--logging-formatdescription againstkube-scheduler.md:273-297:
IMG=$(kubectl -n kube-system get pod -l component=kube-scheduler -o jsonpath='{.items[0].spec.containers[0].image}')
echo $IMG
kubectl run sched-help --image=$IMG --restart=Never -- --help
sleep 20
kubectl get pod sched-help
kubectl logs sched-help | grep -- '--log'
kubectl logs sched-help | grep -A1 -- '--logging-format'
- Set the flag the post names and find out which page the cluster agrees with. The backup taken here is the one restored in steps 6 and 10 and again in Teardown:
sudo cp /etc/kubernetes/manifests/kube-scheduler.yaml /tmp/bw-scheduler.yaml
sudo sed -i '/- kube-scheduler$/a\ - --logging-format=json' \
/etc/kubernetes/manifests/kube-scheduler.yaml
grep -c logging-format /etc/kubernetes/manifests/kube-scheduler.yaml
sleep 30
kubectl -n kube-system get pods | grep scheduler
sudo crictl logs --tail 8 $(sudo crictl ps -a --name kube-scheduler -q | head -1) 2>&1 | tail -8
- Whatever the previous step produced, count it. This one-liner is used again in step 9, so set it once: it reports how many lines came out, how many of them are not JSON at all, and which keys appear across the ones that are:
KEYS='import json,sys
tot=bad=0
keys={}
for l in sys.stdin:
l=l.strip()
if not l: continue
tot+=1
try: d=json.loads(l)
except Exception: bad+=1; continue
for k in d: keys[k]=keys.get(k,0)+1
print("lines", tot, "not JSON", bad)
for k in sorted(keys, key=lambda x:-keys[x]): print(keys[k], k)'
kubectl -n kube-system logs -l component=kube-scheduler --tail=200 | python3 -c "$KEYS"
- Make a scheduling decision happen and read what the post's
klog.KObjdoes to an object reference. This is the post's central claim — that every reference to a Kubernetes object is structured the same way — tested against one object whose namespace and name you chose:
kubectl create namespace bw-slog
kubectl -n bw-slog run probe --image=registry.k8s.io/pause:3.10 --restart=Never
kubectl -n bw-slog wait --for=condition=Ready pod/probe --timeout=120s
KOBJ='import json,sys
for l in sys.stdin:
l=l.strip()
if not l: continue
try: d=json.loads(l)
except Exception: continue
if "probe" not in json.dumps(d): continue
print(json.dumps(d, sort_keys=True))'
kubectl -n kube-system logs -l component=kube-scheduler --tail=800 | python3 -c "$KOBJ" | head -6
- Now offer the component a flag the pin says was removed seven releases after this post, and one
that is still documented on a sibling component's page at the pin. Read the container log before
restoring, because a static Pod that never starts leaves nothing for
kubectlto describe:
sudo sed -i '/- kube-scheduler$/a\ - --logtostderr=true' \
/etc/kubernetes/manifests/kube-scheduler.yaml
sleep 30
kubectl -n kube-system get pods | grep scheduler
sudo crictl logs --tail 10 $(sudo crictl ps -a --name kube-scheduler -q | head -1) 2>&1 | tail -10
sudo cp /tmp/bw-scheduler.yaml /etc/kubernetes/manifests/kube-scheduler.yaml
sudo sed -i '/- kube-scheduler$/a\ - --logging-format=json' \
/etc/kubernetes/manifests/kube-scheduler.yaml
sleep 30
kubectl -n kube-system get pods | grep scheduler
- Read the one component that the pin's support list leaves out. What flags kube-proxy is actually
started with, and whether its own configuration object carries a
loggingblock at all, are both answerable without restarting anything:
kubectl -n kube-system get ds kube-proxy -o json | python3 -c 'import json,sys
c = json.load(sys.stdin)["spec"]["template"]["spec"]["containers"][0]
print("command", c.get("command"))
print("args", c.get("args"))'
kubectl -n kube-system get cm kube-proxy -o jsonpath='{.data.config\.conf}' | grep -n 'logging' -A6
kubectl -n kube-system logs -l k8s-app=kube-proxy --tail=6
- Look for the replacement the pin names.
system-logs.md:49-59hands output redirection tokube-log-runner, says a prebuilt binary is included in several Kubernetes base images under the traditional name/go-runnerand askube-log-runnerin server and node release archives, and gives a four-row table of shell equivalences at:63-68. Find out whether either name is anywhere you can reach:
command -v kube-log-runner; echo "exit $?"
ls /usr/local/bin
API=$(kubectl -n kube-system get pod -l component=kube-apiserver -o jsonpath='{.items[0].spec.containers[0].image}')
kubectl run runner-probe --image=$API --restart=Never --command -- /go-runner --help
sleep 20
kubectl get pod runner-probe
kubectl logs runner-probe 2>&1 | tail -12
- Raise the verbosity and re-measure.
system-logs.md:206saysvappears on info messages and not on error messages, and:181-182says not all logs are guaranteed to be JSON(for example, during process start). Both are countable, and a restart guarantees process-start lines are in the window:
grep -n ' --v=' /etc/kubernetes/manifests/kube-scheduler.yaml; true
sudo sed -i '/ --v=/d' /etc/kubernetes/manifests/kube-scheduler.yaml
sudo sed -i '/- kube-scheduler$/a\ - --v=4' \
/etc/kubernetes/manifests/kube-scheduler.yaml
sleep 40
kubectl -n kube-system get pods | grep scheduler
kubectl -n bw-slog run probe2 --image=registry.k8s.io/pause:3.10 --restart=Never
kubectl -n bw-slog wait --for=condition=Ready pod/probe2 --timeout=120s
kubectl -n kube-system logs -l component=kube-scheduler --tail=2000 | python3 -c "$KEYS"
-
Put the manifest back and read the format the post promised would not change. Compare the line the scheduler prints about
probe3against the shape atsystem-logs.md:105and the sample at:111, and against the sample in the post itself:sudo cp /tmp/bw-scheduler.yaml /etc/kubernetes/manifests/kube-scheduler.yaml sleep 30 kubectl -n kube-system get pods | grep scheduler kubectl -n bw-slog delete pod probe probe2 --ignore-not-found kubectl -n bw-slog run probe3 --image=registry.k8s.io/pause:3.10 --restart=Never kubectl -n bw-slog wait --for=condition=Ready pod/probe3 --timeout=120s kubectl -n kube-system logs -l component=kube-scheduler --tail=400 | grep probe3
Expect
Step 1 should report a v1.37 server, and the three logging gates should appear in
kubernetes_feature_enabled with the values The ladder transcribes: ContextualLogging at 1,
LoggingBetaOptions at 1, LoggingAlphaOptions at 0. The kubelet's live logging block is the
more interesting half. kubelet-config.v1beta1.md:1572-1580 documents its default as Format:
text, and :143-152 documents an options field holding text and json sub-objects. Write down
whether configz shows an options key at all, and whether it has a json member — a defaulted
struct printing a field is not the same as a component supporting it.
Step 2 is where the exercise turns, and it has two outcomes. The scheduler's own --help prints a
--logging-format description, and that description either says Permitted formats: "text".,
matching kube-scheduler.md:297 exactly, or it names JSON as well and the generated page is
therefore not describing this binary. Record which, verbatim. Whichever it is, count the --log
flags: kube-scheduler.md:273-297 documents four, and if the binary offers a different number then
the page and the build have diverged in a second way as well. If the Pod never produces output at
all, --help on that image may write to stderr and exit non-zero, in which case kubectl logs
sched-help still has it — the Pod's phase will be Failed and the log will not be empty.
Step 3 has three outcomes and all three are findings. The scheduler may come back Running and its
log may turn into JSON, which means the binary accepts a format the reference page says is not
permitted. It may crashloop, and crictl logs will then carry an invalid-value error that usually
names the formats the binary does permit — the most direct answer the cluster can give. Or it may
come back Running and keep logging text, which would mean the flag is accepted and ignored. Write
down which, and keep the exact error text if there is one, because it is the only string in this
exercise that comes from the code rather than from a page.
Step 4 should report a line count in the low hundreds. If step 3 produced JSON, the key tally should
be led by ts and msg, which system-logs.md:205-208 marks required, and v should appear on
most lines but not on all of them — the same page says v is verbosity (only for info and not for
error messages, int). Compare that tally against the post's four-key sample, which has no v at
all, and against the pin's sample at :190-201, which has one. The not JSON count should be
greater than zero, because the restart put process-start lines in the window and :181 says Not
all logs are guaranteed to be written in JSON format (for example, during process start). If step 3
produced text, the not JSON count equals the line count, and that is the measurement.
Step 5 should print at least one entry mentioning probe, and the shape of the object reference in
it is the post's whole argument. The post's own sample renders it as pod="kube-system/kubedns" in
text, and the pin's JSON sample at :190-201 renders it as a nested object with name and
namespace. Write down which of those two shapes your line uses. If nothing matches, widen the
--tail and look for Attempting to bind or a plugin name instead of the Pod name — the scheduler
logs bindings, not Pod status, and the sample at system-logs.md:111 is a kube-controller-manager
line, not a scheduler one.
Step 6 has two outcomes and the pin predicts one of them. system-logs.md:32-47 says
--logtostderr was deprecated at v1.23 and removed at v1.26, so the scheduler should refuse to
start and crictl logs should show an unknown-flag error. Note that a static Pod that never starts
leaves no kubectl object to describe, which is why the log is read through crictl. If instead
the scheduler starts, the flag survives in a binary the pin says removed it, and the eleven rows on
kube-proxy.md stop looking like a generation artefact and start looking like a description.
manifest Either way the is restored in the same fence, so the run continues from a working cluster.
Step 7 should show kube-proxy started from a config file and not from klog flags — kubeadm passes
--config and --hostname-override, and nothing else. The question is the ConfigMap.
kube-proxy-config.v1alpha1.md:141-150 documents a logging field on KubeProxyConfiguration, and
:33-48 documents its options.json sub-object as [Alpha] and gated. Whether kubeadm writes that
block into config.conf, and whether it writes options underneath it, decides whether the JSON
format is reachable on this component at all. Hold the answer against system-logs.md:210-215,
which lists four components as supporting JSON and does not list this one.
Step 8 has two outcomes on the host and two more inside the image. command -v kube-log-runner
should fail: it is not a host tool, and system-logs.md:49-59 never claims it is. Inside the
apiserver image, /go-runner either exists and prints its options, which confirms the pin's claim
about base images for at least this image, or the Pod fails to start with an exec error, which means
the claim is about an image set this cluster does not use. Record the container's exit reason either
way. The four-row table at :63-68 is only checkable in the first case.
Step 9 should raise the line count by roughly an order of magnitude, and if the output is JSON the
v values should now spread above 2 where before they did not. The not JSON count should rise
too, by at least the process-start lines the restart produced. What matters is the ratio: :181-182
tells a reader to make sure you can handle log lines that are not JSON as well, and this step is
the only place in the exercise that puts a number on how much handling that is.
Step 10 should return the scheduler to text, and the probe3 line should have the shape
system-logs.md:105 describes and resemble the sample at :111 without matching it — that sample
is the post's sample, carried into the documentation unchanged for six years, and it is a
kube-controller-manager line about Pod status where yours is a scheduler line about binding. The
"key"="value" pairs should be quoted the way the post promised they would be. That promise is the
half of this post that held.
Read on
-
concepts/cluster-administration/system-logs.mdin full, all 316 lines. It is the only hand-written page in the pin that teaches any of this, and it teaches four features under three different feature-state stamps in one document: klog and its flags at:27-68, carrying no stamp at all, Structured Logging at:85-121marked beta at v1.23, Contextual Logging at:123-171marked beta at v1.30, and the JSON format at:173-215marked alpha at v1.19. Read the four sections in that order and then read its reviewers,dimsand44past4, neither of whom wrote the post. Decide which of the four sections a reader arriving from this post is actually looking for. -
reference/config-api/kubelet-config.v1beta1.md:37-52andreference/config-api/kube-proxy-config.v1alpha1.md:33-48, which are the only two files incontent/enwhereLoggingAlphaOptionsappears, five times each. Read thejsonandtextoption objects, then:204-222and:202-220forsplitStreamandinfoBufferSize, then go looking for a concept page or a task page that links either file. There is none. Work out what it means that the format this post announces is documented as a first-class configuration field in two generated references that nothing points at. -
The
--logblock on all five component pages:reference/command-line-tools-reference/kube-apiserver.md:766-790,kube-controller-manager.md:686-710,kube-scheduler.md:273-297,kubelet.md:536-560andkube-proxy.md:321-373. Read the four flags on each, note that two of the four sayIn text formatin their own descriptions, and then read the eleven flags that survive only onkube-proxy.md—--add_dir_header:48,--alsologtostderr:55,--log_backtrace_at:342,--log_dir:349,--log_file:356,--log_file_max_size:363,--logtostderr:377,--one_output:412,--skip_headers:461,--skip_log_headers:468,--stderrthreshold:475 — plus--alsologtostderrthreshold:62 and--legacy_stderr_threshold_behavior:314, which the removal notice atsystem-logs.md:32-47never names. -
The three gate files transcribed in The ladder, and then the population around them.
ContextualLoggingmoved once, at v1.30. The other two have not moved since v1.24. Readsystem-logs.md:136-138, which says contextual logging wasadded in 1.24 without modifying components, and then read the typo at:162—With contextual logging disable— and decide how much traffic that section gets. Then compare against the 2019 PID-limiting exercise, which reads two gates that climbed the ladder exactly as their post hoped and were deleted for it, and decide which of the two outcomes is the healthier one. -
Unanswerable from the pin: why the two halves diverged.
InfoS,ErrorSandKObj— the three identifiers the post is built on — appear nowhere incontent/enoutside the blog archive, so the pin cannot tell you whether the klog API the post announces still has those names. It cannot tell you whether--logging-format=jsonwas removed from the components, never wired into the release build the reference pages were generated from, or is present and merely undocumented; step 2 answers that for one binary on one cluster and no page in the pin answers it at all. And it cannot tell you where thelist of unsupported klog flagsatsystem-logs.md:178-179went, becausereference/command-line-tools-reference/_index.mdis four lines long and has never held a list.
Teardown
Restore the manifest, confirm the restore against the backup, and remove the two one-shot Pods in
the default namespace along with the bw-slog namespace. Re-taint the control plane if you intend
to keep the guest for another exercise; leave it untainted if the next thing you do is destroy it.
sudo cp /tmp/bw-scheduler.yaml /etc/kubernetes/manifests/kube-scheduler.yaml
sleep 30
diff /tmp/bw-scheduler.yaml /etc/kubernetes/manifests/kube-scheduler.yaml && echo "manifest restored"
kubectl -n kube-system get pods | grep scheduler
kubectl delete pod sched-help runner-probe --ignore-not-found
kubectl delete namespace bw-slog --ignore-not-found
rm -f /tmp/bw-scheduler.yaml
CP=$(kubectl get nodes -o jsonpath='{.items[0].metadata.name}')
kubectl taint node $CP node-role.kubernetes.io/control-plane=:NoSchedule || true
Then follow the teardown procedure to destroy the guest.