一、工具简洁

1、工具介绍

Kubernetes Deployment日志并行搜索工具是一个高效的命令行工具,用来在一个Deployment多个Pod中查询指定日志内容。脚本通过多线程并行处理,大大提升了在多个Pod中查找关键信息的效率。

2、核心功能介绍

  • 灵活搜索模式:支持普通文本搜索和正则表达式搜索;
  • 上下文显示:可自定义上文和下文内容;
  • 结果高亮:在终端中突出显示匹配的文本;
  • 进度显示:实时显示搜索进度;
  • 结果保存:自动将搜索结果保存到文件;
  • 日志截取:可只搜索最近N行日志
  • 并行搜索:多线程并发搜索,显著提高效率(10个线程并发搜索时间减少80%以上)。

二、 工具安装与使用

1、环境要求

(1)Python 3.6+
(2)kubectl命令行工具(已配置好Kubernetes集群访问权限)

2、安装步骤

(1)安装s命令

cd /usr/local/bin
vim s
#!/usr/bin/env python3

import argparse
import subprocess
import json
import re
import sys
import os
import time
from concurrent.futures import ThreadPoolExecutor, as_completed
from datetime import datetime
import signal

class Colors:
    RED = '\033[91m'
    GREEN = '\033[92m'
    YELLOW = '\033[93m'
    BLUE = '\033[94m'
    MAGENTA = '\033[95m'
    CYAN = '\033[96m'
    WHITE = '\033[97m'
    BOLD = '\033[1m'
    UNDERLINE = '\033[4m'
    END = '\033[0m'

class LogSearcher:
    def __init__(self, args):
        self.deployment = args.deployment
        self.namespace = args.namespace or "default"
        self.search_text = args.search_text
        self.threads = args.threads or 10
        self.before_context = args.before_context or 0
        self.after_context = args.after_context or 0
        self.context_lines = args.context or 0
        self.tail_lines = args.tail or 1000
        self.use_regex = args.regex
        self.timeout = args.timeout or 30
        self.compiled_pattern = None

        if not self.deployment:
            print(f"{Colors.RED}错误: 必须指定deployment名称{Colors.END}")
            sys.exit(1)

        if not self.search_text:
            print(f"{Colors.RED}错误: 必须指定要搜索的日志内容{Colors.END}")
            sys.exit(1)

        # 如果指定了-c参数,则覆盖before_context和after_context
        if self.context_lines > 0:
            self.before_context = self.context_lines
            self.after_context = self.context_lines

        if self.use_regex:
            try:
                self.compiled_pattern = re.compile(self.search_text, re.IGNORECASE)
            except re.error as e:
                print(f"{Colors.RED}错误: 无效的正则表达式: {e}{Colors.END}")
                sys.exit(1)

    def run_command(self, cmd: list, capture_output: bool = True):
        """执行命令并返回结果 - Python 3.6兼容版本"""
        try:
            if capture_output:
                result = subprocess.run(
                    cmd,
                    stdout=subprocess.PIPE,
                    stderr=subprocess.PIPE,
                    universal_newlines=True,
                    timeout=self.timeout
                )
            else:
                result = subprocess.run(
                    cmd,
                    universal_newlines=True,
                    timeout=self.timeout
                )

            if result.returncode == 0:
                return True, result.stdout.strip()
            else:
                return False, result.stderr.strip()
        except subprocess.TimeoutExpired:
            return False, f"命令执行超时: {' '.join(cmd)}"
        except Exception as e:
            return False, f"执行命令出错: {e}"

    def get_deployment_pods(self) -> list:
        """获取Deployment的所有Pod"""
        print(f"{Colors.BLUE}获取Deployment '{self.deployment}' 的Pod列表...{Colors.END}")

        cmd = ["kubectl", "get", "deployment", self.deployment,
               "-n", self.namespace, "-o", "json"]

        success, output = self.run_command(cmd)
        if not success:
            print(f"{Colors.RED}错误: 无法获取Deployment: {output}{Colors.END}")
            sys.exit(1)

        try:
            deployment_info = json.loads(output)
            selector = deployment_info.get("spec", {}).get("selector", {}).get("matchLabels", {})
            if not selector:
                print(f"{Colors.RED}错误: 无法获取Deployment的标签选择器{Colors.END}")
                sys.exit(1)

            selector_str = ",".join([f"{k}={v}" for k, v in selector.items()])
            print(f"{Colors.YELLOW}标签选择器: {selector_str}{Colors.END}")

            cmd = ["kubectl", "get", "pods", "-n", self.namespace,
                   "-l", selector_str, "-o", "jsonpath='{.items[*].metadata.name}'"]

            success, output = self.run_command(cmd)
            if not success:
                print(f"{Colors.RED}错误: 无法获取Pod列表: {output}{Colors.END}")
                sys.exit(1)

            if output.startswith("'") and output.endswith("'"):
                output = output[1:-1]

            pods = [pod for pod in output.split() if pod]
            if not pods:
                print(f"{Colors.RED}错误: 未找到Deployment相关的Pod{Colors.END}")
                sys.exit(1)

            print(f"{Colors.GREEN}找到 {len(pods)} 个Pod:{Colors.END}")
            for pod in pods:
                print(f"  {pod}")
            print()
            return pods

        except json.JSONDecodeError as e:
            print(f"{Colors.RED}错误: 解析Deployment信息失败: {e}{Colors.END}")
            sys.exit(1)
        except Exception as e:
            print(f"{Colors.RED}错误: {e}{Colors.END}")
            sys.exit(1)

    def get_pod_logs(self, pod_name: str):
        """获取单个Pod的日志"""
        cmd = ["kubectl", "logs", pod_name, "-n", self.namespace]

        if self.tail_lines > 0:
            cmd.extend(["--tail", str(self.tail_lines)])

        success, output = self.run_command(cmd)
        if not success:
            print(f"{Colors.YELLOW}警告: 无法获取Pod {pod_name} 的日志: {output}{Colors.END}")
            return None

        return output

    def search_in_text(self, text: str, pod_name: str) -> list:
        """在文本中搜索匹配的内容"""
        results = []
        lines = text.splitlines()
        total_lines = len(lines)

        if self.use_regex:
            # 使用正则表达式搜索
            for i, line in enumerate(lines):
                if self.compiled_pattern.search(line):
                    result = {
                        "pod": pod_name,
                        "line_number": i + 1,
                        "content": line,
                        "before_context": [],
                        "after_context": []
                    }

                    # 添加上文 (Before context)
                    if self.before_context > 0:
                        start = max(0, i - self.before_context)
                        for j in range(start, i):
                            result["before_context"].append({
                                "line": j + 1,
                                "content": lines[j]
                            })

                    # 添加下文 (After context)
                    if self.after_context > 0:
                        end = min(total_lines, i + self.after_context + 1)
                        for j in range(i + 1, end):
                            result["after_context"].append({
                                "line": j + 1,
                                "content": lines[j]
                            })

                    results.append(result)
        else:
            # 使用普通文本搜索
            search_lower = self.search_text.lower()
            for i, line in enumerate(lines):
                if search_lower in line.lower():
                    result = {
                        "pod": pod_name,
                        "line_number": i + 1,
                        "content": line,
                        "before_context": [],
                        "after_context": []
                    }

                    # 添加上文 (Before context)
                    if self.before_context > 0:
                        start = max(0, i - self.before_context)
                        for j in range(start, i):
                            result["before_context"].append({
                                "line": j + 1,
                                "content": lines[j]
                            })

                    # 添加下文 (After context)
                    if self.after_context > 0:
                        end = min(total_lines, i + self.after_context + 1)
                        for j in range(i + 1, end):
                            result["after_context"].append({
                                "line": j + 1,
                                "content": lines[j]
                            })

                    results.append(result)

        return results

    def highlight_match(self, text: str) -> str:
        """高亮显示匹配的文本"""
        if self.use_regex:
            try:
                return self.compiled_pattern.sub(
                    f"{Colors.RED}{Colors.BOLD}\\g<0>{Colors.END}",
                    text
                )
            except:
                return text
        else:
            try:
                pattern = re.compile(f"({re.escape(self.search_text)})", re.IGNORECASE)
                return pattern.sub(
                    f"{Colors.RED}{Colors.BOLD}\\1{Colors.END}",
                    text
                )
            except:
                return text

    def process_pod(self, pod_name: str):
        """处理单个Pod的搜索"""
        logs = self.get_pod_logs(pod_name)
        if logs is None:
            return pod_name, [], 0

        results = self.search_in_text(logs, pod_name)
        return pod_name, results, len(logs.splitlines())

    def search_all_pods(self, pods: list):
        """并行搜索所有Pod的日志"""
        print(f"{Colors.BLUE}开始并行搜索 (使用 {self.threads} 个线程)...{Colors.END}")
        print(f"{Colors.YELLOW}搜索内容: {self.search_text}{Colors.END}")
        if self.use_regex:
            print(f"{Colors.YELLOW}使用正则表达式模式{Colors.END}")
        if self.tail_lines > 0:
            print(f"{Colors.YELLOW}只搜索最后 {self.tail_lines} 行日志{Colors.END}")

        if self.before_context > 0 or self.after_context > 0:
            if self.before_context == self.after_context:
                print(f"{Colors.YELLOW}显示上下文: 前后各 {self.before_context}{Colors.END}")
            else:
                if self.before_context > 0:
                    print(f"{Colors.YELLOW}显示上文: {self.before_context}{Colors.END}")
                if self.after_context > 0:
                    print(f"{Colors.YELLOW}显示下文: {self.after_context}{Colors.END}")

        print()

        total_results = []
        total_matches = 0
        pods_with_matches = 0

        start_time = time.time()
        processed_count = 0

        print(f"{Colors.CYAN}正在搜索Pod日志...{Colors.END}")

        with ThreadPoolExecutor(max_workers=self.threads) as executor:
            future_to_pod = {}
            for pod in pods:
                future = executor.submit(self.process_pod, pod)
                future_to_pod[future] = pod

            for future in as_completed(future_to_pod):
                pod_name = future_to_pod[future]
                try:
                    pod_name, results, log_lines = future.result(timeout=self.timeout)

                    processed_count += 1
                    progress = processed_count / len(pods) * 100
                    sys.stdout.write(f"\r{Colors.CYAN}进度: [{processed_count}/{len(pods)}] {progress:.1f}%{Colors.END}")
                    sys.stdout.flush()

                    if results:
                        pods_with_matches += 1
                        total_matches += len(results)

                        print(f"\n{Colors.YELLOW}{'='*80}{Colors.END}")
                        print(f"{Colors.GREEN}[Pod: {pod_name}] 找到 {len(results)} 处匹配 (共{log_lines}行日志):{Colors.END}")

                        for idx, result in enumerate(results):
                            print(f"\n{Colors.MAGENTA}匹配 #{idx+1}:{Colors.END}")

                            if result["before_context"]:
                                print(f"{Colors.CYAN}上文 (前 {len(result['before_context'])} 行):{Colors.END}")
                                for ctx in result["before_context"]:
                                    print(f"  {Colors.BLUE}{ctx['line']:4d} 行:{Colors.END} {ctx['content']}")

                            print(f"{Colors.RED}{Colors.BOLD}>>> 第 {result['line_number']:4d} 行:{Colors.END} {self.highlight_match(result['content'])}")

                            if result["after_context"]:
                                print(f"{Colors.CYAN}下文 (后 {len(result['after_context'])} 行):{Colors.END}")
                                for ctx in result["after_context"]:
                                    print(f"  {Colors.BLUE}{ctx['line']:4d} 行:{Colors.END} {ctx['content']}")

                        total_results.extend(results)

                except Exception as e:
                    print(f"\n{Colors.RED}处理Pod {pod_name} 时出错: {e}{Colors.END}")

        elapsed_time = time.time() - start_time

        print(f"\n\n{Colors.YELLOW}{'='*80}{Colors.END}")
        print(f"{Colors.BLUE}搜索完成!{Colors.END}")
        print(f"{Colors.GREEN}总耗时: {elapsed_time:.2f}{Colors.END}")
        print(f"{Colors.GREEN}已检查 Pod 数量: {len(pods)}{Colors.END}")
        print(f"{Colors.GREEN}找到匹配的 Pod 数量: {pods_with_matches}{Colors.END}")
        print(f"{Colors.GREEN}总匹配行数: {total_matches}{Colors.END}")

        if total_results:
            self.save_results(total_results)

        return total_results

    def save_results(self, results: list):
        """保存搜索结果到文件"""
        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
        filename = f"search_results_{self.deployment}_{timestamp}.log"

        try:
            with open(filename, 'w', encoding='utf-8') as f:
                f.write(f"搜索时间: {datetime.now()}\n")
                f.write(f"Deployment: {self.deployment}\n")
                f.write(f"命名空间: {self.namespace}\n")
                f.write(f"搜索内容: {self.search_text}\n")
                f.write(f"使用正则: {self.use_regex}\n")
                if self.before_context > 0:
                    f.write(f"显示上文: {self.before_context} 行\n")
                if self.after_context > 0:
                    f.write(f"显示下文: {self.after_context} 行\n")
                f.write(f"搜索最后 {self.tail_lines} 行日志\n")
                f.write("=" * 80 + "\n\n")

                current_pod = None
                for result_idx, result in enumerate(results):
                    if result["pod"] != current_pod:
                        current_pod = result["pod"]
                        f.write(f"\n{'='*80}\n")
                        f.write(f"Pod: {current_pod}\n")
                        f.write(f"{'='*80}\n\n")

                    f.write(f"匹配 #{result_idx+1}:\n")

                    if result["before_context"]:
                        f.write(f"上文 (前 {len(result['before_context'])} 行):\n")
                        for ctx in result["before_context"]:
                            f.write(f"  第 {ctx['line']:4d} 行: {ctx['content']}\n")

                    f.write(f">>> 第 {result['line_number']:4d} 行: {result['content']}\n")

                    if result["after_context"]:
                        f.write(f"下文 (后 {len(result['after_context'])} 行):\n")
                        for ctx in result["after_context"]:
                            f.write(f"  第 {ctx['line']:4d} 行: {ctx['content']}\n")

                    f.write("\n")

            print(f"{Colors.GREEN}搜索结果已保存到: {filename}{Colors.END}")
        except Exception as e:
            print(f"{Colors.YELLOW}警告: 保存结果到文件失败: {e}{Colors.END}")

def signal_handler(sig, frame):
    """处理Ctrl+C信号"""
    print(f"\n\n{Colors.YELLOW}接收到中断信号,正在退出...{Colors.END}")
    sys.exit(0)

def main():
    """主函数"""
    signal.signal(signal.SIGINT, signal_handler)

    parser = argparse.ArgumentParser(
        description="Kubernetes Deployment日志搜索工具",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
示例:
  %(prog)s deployment名称 -n 命名空间 -s "error"
  %(prog)s deployment名称 -n 命名空间 -s "404" -t 20
  %(prog)s deployment名称 -n 命名空间 -s "timeout" -B 5 -A 10
  %(prog)s deployment名称 -n 命名空间 -r "error|exception|fatal" -c 3
  %(prog)s deployment名称 -n 命名空间 -s "warning" -T 5000 -B 100 -A 100
        """
    )

    parser.add_argument(
        "deployment",
        help="Kubernetes Deployment名称"
    )

    parser.add_argument(
        "-n", "--namespace",
        help="命名空间 (默认: default)"
    )

    parser.add_argument(
        "-s", "--search-text",
        required=True,
        help="要搜索的日志内容"
    )

    parser.add_argument(
        "-t", "--threads",
        type=int,
        default=10,
        help="并发线程数 (默认: 10)"
    )

    parser.add_argument(
        "-B", "--before-context",
        type=int,
        default=0,
        help="显示匹配行之前的行数 (上文)"
    )

    parser.add_argument(
        "-A", "--after-context",
        type=int,
        default=0,
        help="显示匹配行之后的行数 (下文)"
    )

    parser.add_argument(
        "-c", "--context",
        type=int,
        default=0,
        help="显示匹配行的上下文行数 (前后各c行,会被-B和-A覆盖)"
    )

    parser.add_argument(
        "-T", "--tail",
        type=int,
        default=999999999,
        help="只搜索最后N行日志 (默认: 1000)"
    )

    parser.add_argument(
        "-r", "--regex",
        action="store_true",
        help="使用正则表达式搜索"
    )

    parser.add_argument(
        "--timeout",
        type=int,
        default=30,
        help="单个Pod日志获取超时时间(秒) (默认: 30)"
    )

    args = parser.parse_args()

    try:
        result = subprocess.run(
            ["kubectl", "version", "--client"],
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            universal_newlines=True
        )
        if result.returncode != 0:
            print(f"{Colors.RED}错误: kubectl配置不正确: {result.stderr}{Colors.END}")
            sys.exit(1)
    except FileNotFoundError:
        print(f"{Colors.RED}错误: 未找到kubectl命令{Colors.END}")
        sys.exit(1)

    searcher = LogSearcher(args)
    pods = searcher.get_deployment_pods()
    searcher.search_all_pods(pods)

if __name__ == "__main__":
    main()

(2)赋予执行权限

chmod +x s

(3)测试使用

s --help

3、参数详解

参数简写说明默认值
--search-text-s要搜索的文本内容(必需)
--namespace-nKubernetes命名空间default
--threads-t并发线程数10
--before-context-B显示匹配行之前的行数0
--after-context-A显示匹配行之后的行数0
--context-c显示匹配行前后各N行0
--tail-T只搜索最后N行日志1000
--regex-r使用正则表达式搜索false
--timeout单个Pod超时时间(秒)30

4、使用案例

(1)查询指定日志

s nginx-feikong -n app -s "404"

在这里插入图片描述
(2)使用正则表达式搜索

s nginx-feikong -n app -s "(error|exception|fatal)" -r

(3)显示匹配行的上下文(前后各3行)

s nginx-feikong -n app -s "404" -c 3

(4)只搜索最近1000行日志

s nginx-feikong -n app -s "404" -T 1000

(5)增加并发线程数(适合Pod数量多的情况)

s nginx-feikong -n app -s "404" -t 20

更多推荐