Skip to content

Commit ebbbd33

Browse files
committed
fixed the logs script
1 parent cbeed9c commit ebbbd33

1 file changed

Lines changed: 180 additions & 174 deletions

File tree

deploy/scripts/logs.sh

Lines changed: 180 additions & 174 deletions
Original file line numberDiff line numberDiff line change
@@ -1,174 +1,180 @@
1-
#!/usr/bin/env bash
2-
set -euo pipefail
3-
4-
namespace="serge"
5-
release="serge"
6-
since="2h"
7-
follow=0
8-
grep_pattern=""
9-
expected_context=""
10-
last_error=0
11-
12-
usage() {
13-
cat <<'EOF'
14-
Usage: deploy/scripts/logs.sh [options]
15-
16-
Options:
17-
-n, --namespace NAME Kubernetes namespace (default: serge)
18-
-r, --release NAME Helm release/app label (default: serge)
19-
--since DURATION Log window, e.g. 30m, 2h (default: 2h)
20-
-f, --follow Follow logs
21-
--grep PATTERN Filter logs with grep -Ei
22-
--last-error Print the last ERROR/Traceback block from recent logs
23-
--context NAME Require this kubectl context before reading logs
24-
-h, --help Show this help
25-
EOF
26-
}
27-
28-
while [[ $# -gt 0 ]]; do
29-
case "$1" in
30-
-n|--namespace)
31-
namespace="$2"
32-
shift 2
33-
;;
34-
-r|--release)
35-
release="$2"
36-
shift 2
37-
;;
38-
--since)
39-
since="$2"
40-
shift 2
41-
;;
42-
-f|--follow)
43-
follow=1
44-
shift
45-
;;
46-
--grep)
47-
grep_pattern="$2"
48-
shift 2
49-
;;
50-
--last-error)
51-
last_error=1
52-
shift
53-
;;
54-
--context)
55-
expected_context="$2"
56-
shift 2
57-
;;
58-
-h|--help)
59-
usage
60-
exit 0
61-
;;
62-
*)
63-
echo "unknown option: $1" >&2
64-
usage >&2
65-
exit 2
66-
;;
67-
esac
68-
done
69-
70-
if ! command -v kubectl >/dev/null 2>&1; then
71-
echo "missing required command: kubectl" >&2
72-
exit 1
73-
fi
74-
75-
if [[ -n "${grep_pattern}" ]] && ! command -v grep >/dev/null 2>&1; then
76-
echo "missing required command: grep" >&2
77-
exit 1
78-
fi
79-
80-
current_context="$(kubectl config current-context)"
81-
if [[ -n "${expected_context}" && "${current_context}" != "${expected_context}" ]]; then
82-
echo "refusing to read logs from context '${current_context}' (expected '${expected_context}')" >&2
83-
exit 1
84-
fi
85-
86-
pod="$(
87-
kubectl get pods -n "${namespace}" \
88-
-l "app=${release}" \
89-
--field-selector=status.phase=Running \
90-
--sort-by=.metadata.creationTimestamp \
91-
-o jsonpath='{range .items[*]}{.metadata.name}{"\n"}{end}' \
92-
| tail -n 1
93-
)"
94-
95-
if [[ -z "${pod}" ]]; then
96-
echo "no running pod found in namespace '${namespace}' with label app=${release}" >&2
97-
exit 1
98-
fi
99-
100-
pod_started="$(kubectl get pod "${pod}" -n "${namespace}" -o jsonpath='{.status.startTime}')"
101-
102-
echo "Context: ${current_context}" >&2
103-
echo "Namespace: ${namespace}" >&2
104-
echo "Pod: ${pod}" >&2
105-
echo "Pod started: ${pod_started}" >&2
106-
echo "Since: ${since}" >&2
107-
108-
args=(logs -n "${namespace}" "${pod}" --since="${since}")
109-
if [[ "${follow}" -eq 1 ]]; then
110-
args+=(-f)
111-
fi
112-
113-
if [[ "${last_error}" -eq 1 ]]; then
114-
if [[ "${follow}" -eq 1 ]]; then
115-
echo "--last-error cannot be combined with --follow" >&2
116-
exit 2
117-
fi
118-
kubectl "${args[@]}" | awk '
119-
function flush() {
120-
if (in_block && block != "") {
121-
last = block
122-
}
123-
block = ""
124-
in_block = 0
125-
}
126-
127-
/^[0-9-]+ [0-9:,]+ ERROR / {
128-
flush()
129-
in_block = 1
130-
block = $0 "\n"
131-
next
132-
}
133-
134-
/^Traceback \(most recent call last\):/ {
135-
if (!in_block) {
136-
in_block = 1
137-
block = $0 "\n"
138-
} else {
139-
block = block $0 "\n"
140-
}
141-
next
142-
}
143-
144-
in_block {
145-
if ($0 ~ /^[0-9-]+ [0-9:,]+ (INFO|WARNING|ERROR|DEBUG) / || $0 ~ /^INFO:/) {
146-
flush()
147-
}
148-
}
149-
150-
in_block {
151-
block = block $0 "\n"
152-
}
153-
154-
END {
155-
flush()
156-
if (last != "") {
157-
printf "%s", last
158-
} else {
159-
exit 1
160-
}
161-
}
162-
' || {
163-
echo "no ERROR/Traceback block found for pod ${pod} in the last ${since}" >&2
164-
echo "note: this only covers logs retained for the current pod, which started at ${pod_started}" >&2
165-
exit 1
166-
}
167-
exit 0
168-
fi
169-
170-
if [[ -n "${grep_pattern}" ]]; then
171-
kubectl "${args[@]}" | grep -Ei "${grep_pattern}"
172-
else
173-
kubectl "${args[@]}"
174-
fi
1+
#!/usr/bin/env python3
2+
import argparse
3+
import re
4+
import shutil
5+
import subprocess
6+
import sys
7+
8+
9+
class CommandError(Exception):
10+
def __init__(self, return_code):
11+
self.return_code = return_code
12+
13+
14+
def run_text(args):
15+
try:
16+
return subprocess.check_output(args, text=True).strip()
17+
except subprocess.CalledProcessError as exc:
18+
raise CommandError(exc.returncode) from exc
19+
20+
21+
def stream_logs(args, grep_pattern):
22+
proc = subprocess.Popen(args, stdout=subprocess.PIPE, text=True)
23+
assert proc.stdout is not None
24+
25+
pattern = re.compile(grep_pattern, re.IGNORECASE) if grep_pattern else None
26+
try:
27+
for line in proc.stdout:
28+
if pattern is None or pattern.search(line):
29+
print(line, end="")
30+
finally:
31+
if proc.stdout:
32+
proc.stdout.close()
33+
34+
return proc.wait()
35+
36+
37+
def latest_error_block(args, pod, pod_started, since):
38+
proc = subprocess.Popen(args, stdout=subprocess.PIPE, text=True)
39+
assert proc.stdout is not None
40+
41+
log_line_re = re.compile(r"^[0-9-]+ [0-9:,]+ (INFO|WARNING|ERROR|DEBUG) ")
42+
error_re = re.compile(r"^[0-9-]+ [0-9:,]+ ERROR ")
43+
last = []
44+
block = []
45+
46+
def flush():
47+
nonlocal last, block
48+
if block:
49+
last = block
50+
block = []
51+
52+
for line in proc.stdout:
53+
if error_re.match(line):
54+
flush()
55+
block = [line]
56+
continue
57+
58+
if line.startswith("Traceback (most recent call last):"):
59+
block.append(line)
60+
continue
61+
62+
if block and (log_line_re.match(line) or line.startswith("INFO:")):
63+
flush()
64+
65+
if block:
66+
block.append(line)
67+
68+
proc.stdout.close()
69+
return_code = proc.wait()
70+
if return_code != 0:
71+
return return_code
72+
73+
flush()
74+
if last:
75+
print("".join(last), end="")
76+
return 0
77+
78+
print(f"no ERROR/Traceback block found for pod {pod} in the last {since}", file=sys.stderr)
79+
print(
80+
f"note: this only covers logs retained for the current pod, which started at {pod_started}",
81+
file=sys.stderr,
82+
)
83+
return 1
84+
85+
86+
def main():
87+
parser = argparse.ArgumentParser(
88+
prog="deploy/scripts/logs.sh",
89+
description="Find the current running Serge pod and print recent logs.",
90+
)
91+
parser.add_argument("-n", "--namespace", default="serge", help="Kubernetes namespace")
92+
parser.add_argument("-r", "--release", default="serge", help="Helm release/app label")
93+
parser.add_argument("--since", default="2h", help="Log window, e.g. 30m, 2h")
94+
parser.add_argument("-f", "--follow", action="store_true", help="Follow logs")
95+
parser.add_argument("--grep", dest="grep_pattern", help="Filter logs with grep -Ei semantics")
96+
parser.add_argument("--last-error", action="store_true", help="Print the last ERROR/Traceback block")
97+
parser.add_argument("--context", dest="expected_context", help="Required kubectl context")
98+
args = parser.parse_args()
99+
100+
if args.last_error and args.follow:
101+
print("--last-error cannot be combined with --follow", file=sys.stderr)
102+
return 2
103+
104+
if shutil.which("kubectl") is None:
105+
print("missing required command: kubectl", file=sys.stderr)
106+
return 1
107+
108+
try:
109+
current_context = run_text(["kubectl", "config", "current-context"])
110+
except CommandError as exc:
111+
return exc.return_code
112+
if args.expected_context and current_context != args.expected_context:
113+
print(
114+
f"refusing to read logs from context '{current_context}' "
115+
f"(expected '{args.expected_context}')",
116+
file=sys.stderr,
117+
)
118+
return 1
119+
120+
try:
121+
pod_output = run_text(
122+
[
123+
"kubectl",
124+
"get",
125+
"pods",
126+
"-n",
127+
args.namespace,
128+
"-l",
129+
f"app={args.release}",
130+
"--field-selector=status.phase=Running",
131+
"--sort-by=.metadata.creationTimestamp",
132+
"-o",
133+
'jsonpath={range .items[*]}{.metadata.name}{"\\n"}{end}',
134+
]
135+
)
136+
except CommandError as exc:
137+
return exc.return_code
138+
pods = [line for line in pod_output.splitlines() if line]
139+
if not pods:
140+
print(
141+
f"no running pod found in namespace '{args.namespace}' with label app={args.release}",
142+
file=sys.stderr,
143+
)
144+
return 1
145+
146+
pod = pods[-1]
147+
try:
148+
pod_started = run_text(
149+
[
150+
"kubectl",
151+
"get",
152+
"pod",
153+
pod,
154+
"-n",
155+
args.namespace,
156+
"-o",
157+
"jsonpath={.status.startTime}",
158+
]
159+
)
160+
except CommandError as exc:
161+
return exc.return_code
162+
163+
print(f"Context: {current_context}", file=sys.stderr)
164+
print(f"Namespace: {args.namespace}", file=sys.stderr)
165+
print(f"Pod: {pod}", file=sys.stderr)
166+
print(f"Pod started: {pod_started}", file=sys.stderr)
167+
print(f"Since: {args.since}", file=sys.stderr)
168+
169+
log_args = ["kubectl", "logs", "-n", args.namespace, pod, f"--since={args.since}"]
170+
if args.follow:
171+
log_args.append("-f")
172+
173+
if args.last_error:
174+
return latest_error_block(log_args, pod, pod_started, args.since)
175+
176+
return stream_logs(log_args, args.grep_pattern)
177+
178+
179+
if __name__ == "__main__":
180+
sys.exit(main())

0 commit comments

Comments
 (0)