All checks were successful
deploy / deploy (push) Successful in 41s
Proof: Successful trades only dashboard is empty Assumptions: The downtime resulted in a massive gap we can attribute Still fake: Some outcomes may be heuristic
212 lines
6 KiB
Python
Executable file
212 lines
6 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import signal
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
|
|
from common import DEFAULT_NAMESPACE, build_env, kafka_brokers, raw_near_topic
|
|
|
|
|
|
DEFAULT_OUTPUT_FORMAT = (
|
|
"%d{go[2006-01-02T15:04:05Z07:00]} topic=%t partition=%p offset=%o\n"
|
|
"%v\n"
|
|
)
|
|
|
|
|
|
def parse_args() -> argparse.Namespace:
|
|
parser = argparse.ArgumentParser(
|
|
description="Live-inspect raw NEAR intents arriving in Redpanda."
|
|
)
|
|
parser.add_argument(
|
|
"--namespace",
|
|
default=DEFAULT_NAMESPACE,
|
|
help=f"Kubernetes namespace to inspect (default: {DEFAULT_NAMESPACE})",
|
|
)
|
|
parser.add_argument(
|
|
"--brokers",
|
|
default="",
|
|
help="Override Kafka brokers instead of reading them from unrip-config.",
|
|
)
|
|
parser.add_argument(
|
|
"--topic",
|
|
default="",
|
|
help="Topic to consume. Defaults to KAFKA_TOPIC_RAW_NEAR_INTENTS_QUOTE from unrip-config.",
|
|
)
|
|
offset_group = parser.add_mutually_exclusive_group()
|
|
offset_group.add_argument(
|
|
"--offset",
|
|
default="end",
|
|
help=(
|
|
"Kafka offset selector passed to rpk. "
|
|
"The default 'end' means live-tail from now, not replay history."
|
|
),
|
|
)
|
|
offset_group.add_argument(
|
|
"--last",
|
|
type=int,
|
|
default=0,
|
|
help=(
|
|
"Read the most recent N retained records and exit. "
|
|
"Equivalent to '--offset -N --num N'."
|
|
),
|
|
)
|
|
parser.add_argument(
|
|
"--num",
|
|
type=int,
|
|
default=0,
|
|
help=(
|
|
"Number of records to consume before exiting, starting from the chosen "
|
|
"offset. 0 means unbounded. With '--offset end' this waits for new records."
|
|
),
|
|
)
|
|
parser.add_argument(
|
|
"--timeout",
|
|
type=float,
|
|
default=0,
|
|
help="Stop after this many seconds even if the stream is still open.",
|
|
)
|
|
parser.add_argument(
|
|
"--value-only",
|
|
action="store_true",
|
|
help="Print only the record value without topic/partition/offset metadata.",
|
|
)
|
|
parser.add_argument(
|
|
"--rpk-json",
|
|
action="store_true",
|
|
help="Use rpk's built-in JSON record formatter instead of the default metadata+value output.",
|
|
)
|
|
parser.add_argument(
|
|
"--output",
|
|
choices=("json", "jsonl", "text"),
|
|
default="json",
|
|
help="Output format (default: json).",
|
|
)
|
|
return parser.parse_args()
|
|
|
|
|
|
def build_command(args: argparse.Namespace, brokers: str, topic: str) -> list[str]:
|
|
offset = args.offset
|
|
num = args.num
|
|
if args.last:
|
|
offset = f"-{args.last}"
|
|
num = args.last
|
|
|
|
command = [
|
|
"kubectl",
|
|
"exec",
|
|
"-i",
|
|
"-n",
|
|
args.namespace,
|
|
"deploy/redpanda",
|
|
"--",
|
|
"rpk",
|
|
"topic",
|
|
"consume",
|
|
topic,
|
|
"--brokers",
|
|
brokers,
|
|
"--offset",
|
|
offset,
|
|
"--num",
|
|
str(num),
|
|
]
|
|
|
|
if args.output in {"json", "jsonl"} or args.rpk_json:
|
|
command.extend(["--format", "json", "--pretty-print=false"])
|
|
elif args.rpk_json:
|
|
command.extend(["--format", "json", "--pretty-print=false"])
|
|
elif args.value_only:
|
|
command.extend(["--format", "%v\n"])
|
|
else:
|
|
command.extend(["--format", DEFAULT_OUTPUT_FORMAT])
|
|
|
|
return command
|
|
|
|
|
|
def main() -> int:
|
|
args = parse_args()
|
|
brokers = args.brokers or kafka_brokers(namespace=args.namespace)
|
|
topic = args.topic or raw_near_topic(namespace=args.namespace)
|
|
command = build_command(args, brokers, topic)
|
|
effective_offset = args.offset if not args.last else f"-{args.last}"
|
|
effective_num = args.num if not args.last else args.last
|
|
|
|
if args.output == "text":
|
|
process = subprocess.Popen(command, env=build_env())
|
|
try:
|
|
process.wait(timeout=args.timeout if args.timeout and args.timeout > 0 else None)
|
|
except subprocess.TimeoutExpired:
|
|
process.send_signal(signal.SIGINT)
|
|
try:
|
|
process.wait(timeout=5)
|
|
except subprocess.TimeoutExpired:
|
|
process.kill()
|
|
process.wait(timeout=5)
|
|
return 124
|
|
except KeyboardInterrupt:
|
|
process.send_signal(signal.SIGINT)
|
|
return process.wait()
|
|
return process.returncode
|
|
|
|
try:
|
|
result = subprocess.run(
|
|
command,
|
|
env=build_env(),
|
|
text=True,
|
|
capture_output=True,
|
|
timeout=args.timeout if args.timeout and args.timeout > 0 else None,
|
|
check=False,
|
|
)
|
|
stdout = result.stdout
|
|
return_code = result.returncode
|
|
except subprocess.TimeoutExpired as exc:
|
|
stdout = exc.stdout or ""
|
|
return_code = 124
|
|
|
|
records = parse_rpk_json_lines(stdout, value_only=args.value_only)
|
|
if args.output == "json":
|
|
print(json.dumps(records, indent=2))
|
|
else:
|
|
for record in records:
|
|
print(json.dumps(record, separators=(",", ":")))
|
|
return return_code
|
|
|
|
|
|
def parse_rpk_json_lines(stdout: str, *, value_only: bool) -> list[object]:
|
|
records: list[object] = []
|
|
for raw_line in stdout.splitlines():
|
|
line = raw_line.strip()
|
|
if not line:
|
|
continue
|
|
record = json.loads(line)
|
|
if value_only:
|
|
record = decode_jsonish(record.get("value"))
|
|
else:
|
|
if "value" in record:
|
|
record["value"] = decode_jsonish(record.get("value"))
|
|
records.append(record)
|
|
return records
|
|
|
|
|
|
def decode_jsonish(value: object) -> object:
|
|
if not isinstance(value, str):
|
|
return value
|
|
stripped = value.strip()
|
|
if not stripped:
|
|
return value
|
|
if stripped[0] not in "{[":
|
|
return value
|
|
try:
|
|
return json.loads(stripped)
|
|
except json.JSONDecodeError:
|
|
return value
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|