├── assets ├── healthz ├── filebeat │ ├── filebeat.tpl │ └── config.filebeat └── entrypoint ├── .gitignore ├── pkg ├── runtime │ ├── docker.go │ ├── container.go │ └── types.go ├── tools │ ├── utils.go │ └── format.go ├── ctx │ └── ctx.go └── provider │ ├── filebeat_types.go │ └── filebeat.go ├── deploy └── kubernetes │ ├── nginx.yaml │ └── watchlog.yaml ├── log ├── nodeInfo │ └── n.go ├── config │ └── logConfig.go └── log.go ├── Dockerfile ├── .github └── workflows │ └── ci.yaml ├── main.go ├── README.md ├── go.mod ├── controller ├── runtime.go ├── docker.go └── containerd.go └── go.sum /assets/healthz: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | 4 | point=$(ps aux | grep -v grep | grep point) 5 | 6 | if [ -z "point" ]; then 7 | exit 1 8 | else 9 | exit 0 10 | fi -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *~ 3 | #* 4 | *# 5 | .#* 6 | .classpath 7 | .project 8 | .settings/ 9 | .springBeans 10 | target/ 11 | bin/ 12 | _site/ 13 | .idea 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .factorypath 18 | *.log 19 | .shelf 20 | *.swp 21 | *.swo 22 | .vscode/ 23 | .flattened-pom.xml 24 | go.sum 25 | -------------------------------------------------------------------------------- /pkg/runtime/docker.go: -------------------------------------------------------------------------------- 1 | package runtime 2 | 3 | import ( 4 | "fmt" 5 | docker "github.com/docker/docker/client" 6 | ) 7 | 8 | func NewDockerClient() *docker.Client { 9 | cli, err := docker.NewEnvClient() 10 | if err != nil { 11 | panic(fmt.Sprintf("Error: Create docker client failed, %s", err.Error())) 12 | } 13 | 14 | return cli 15 | } 16 | -------------------------------------------------------------------------------- /pkg/tools/utils.go: -------------------------------------------------------------------------------- 1 | package tools 2 | 3 | import ( 4 | "io/ioutil" 5 | "strings" 6 | ) 7 | 8 | // ReadFile return string list separated by separator 9 | func ReadFile(path string, separator string) ([]string, error) { 10 | data, err := ioutil.ReadFile(path) 11 | if err != nil { 12 | return nil, err 13 | } 14 | 15 | return strings.Split(string(data), separator), nil 16 | } 17 | -------------------------------------------------------------------------------- /pkg/runtime/container.go: -------------------------------------------------------------------------------- 1 | package runtime 2 | 3 | import ( 4 | "fmt" 5 | "github.com/containerd/containerd" 6 | ) 7 | 8 | const sock = "/run/containerd/containerd.sock" 9 | 10 | func NewContainerClient() *containerd.Client { 11 | cli, err := containerd.New(sock) 12 | if err != nil { 13 | panic(fmt.Sprintf("Error: Create container client failed, %s", err.Error())) 14 | } 15 | 16 | return cli 17 | } 18 | -------------------------------------------------------------------------------- /assets/filebeat/filebeat.tpl: -------------------------------------------------------------------------------- 1 | {{range .configList}} 2 | - type: container 3 | enabled: true 4 | paths: 5 | - {{ .HostDir }}/{{ .File }} 6 | exclude_files: ['\.gz$'] 7 | scan_frequency: 10s 8 | harvester_limit: 1024 9 | fields_under_root: true 10 | fields: 11 | {{range $key, $value := .Tags}} 12 | {{ $key }}: {{ $value }} 13 | {{end}} 14 | {{range $key, $value := $.container}} 15 | {{ $key }}: {{ $value }} 16 | {{end}} 17 | tail_files: false 18 | close_inactive: 2h 19 | close_eof: false 20 | close_removed: true 21 | clean_removed: true 22 | close_renamed: false 23 | {{end}} -------------------------------------------------------------------------------- /deploy/kubernetes/nginx.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | labels: 5 | app: nginx 6 | app.kubernetes.io/name: nginx 7 | name: nginx 8 | spec: 9 | replicas: 1 10 | selector: 11 | matchLabels: 12 | app: nginx 13 | strategy: 14 | rollingUpdate: 15 | maxSurge: 25% 16 | maxUnavailable: 25% 17 | type: RollingUpdate 18 | template: 19 | metadata: 20 | labels: 21 | app: nginx 22 | spec: 23 | containers: 24 | - env: 25 | # 配置日志采集前缀标志 watchlog_{xxx} 26 | - name: watchlog_default-nginx 27 | value: stdout 28 | image: nginx:1.18.0 29 | imagePullPolicy: IfNotPresent 30 | name: nginx 31 | resources: {} -------------------------------------------------------------------------------- /log/nodeInfo/n.go: -------------------------------------------------------------------------------- 1 | package nodeInfo 2 | 3 | import "fmt" 4 | 5 | // LogInfoNode 表示树结构中的一个节点 6 | type LogInfoNode struct { 7 | Value string 8 | Children map[string]*LogInfoNode 9 | } 10 | 11 | // NewLogInfoNode 创建一个新的节点 12 | func NewLogInfoNode(value string) *LogInfoNode { 13 | return &LogInfoNode{ 14 | Value: value, 15 | Children: make(map[string]*LogInfoNode), 16 | } 17 | } 18 | 19 | // Insert 插入新值,如果必要会自动创建缺失的父节点 20 | func (node *LogInfoNode) Insert(key string, value string) error { 21 | if len(key) == 0 { 22 | return fmt.Errorf("键不能为空") 23 | } 24 | 25 | node.Children[key] = NewLogInfoNode(value) 26 | return nil 27 | } 28 | 29 | // Get 根据键获取对应的值 30 | func (node *LogInfoNode) Get(key string) string { 31 | if child, ok := node.Children[key]; ok { 32 | return child.Value 33 | } 34 | return "" 35 | } 36 | -------------------------------------------------------------------------------- /pkg/runtime/types.go: -------------------------------------------------------------------------------- 1 | package runtime 2 | 3 | import "os" 4 | 5 | const ( 6 | KubernetesPodName = "io.kubernetes.pod.name" 7 | KubernetesContainerName = "io.kubernetes.container.name" 8 | KubernetesContainerNamespace = "io.kubernetes.pod.namespace" 9 | ) 10 | 11 | func putIfNotEmpty(store map[string]string, key, value string) { 12 | if key == "" || value == "" { 13 | return 14 | } 15 | store[key] = value 16 | } 17 | 18 | func BuildContainerLabels(labels map[string]string) map[string]string { 19 | c := make(map[string]string) 20 | putIfNotEmpty(c, "k8s_pod", labels[KubernetesPodName]) 21 | putIfNotEmpty(c, "k8s_pod_namespace", labels[KubernetesContainerNamespace]) 22 | putIfNotEmpty(c, "k8s_container_name", labels[KubernetesContainerName]) 23 | putIfNotEmpty(c, "k8s_node_name", os.Getenv("NODE_NAME")) 24 | return c 25 | } 26 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM registry.cn-hangzhou.aliyuncs.com/opsre/golang:1.21.9-alpine3.19 AS build 2 | 3 | ENV GO111MODULE=on \ 4 | CGO_ENABLED=0 \ 5 | GOOS=linux \ 6 | GOARCH=amd64 \ 7 | GOPROXY=https://goproxy.cn,direct 8 | 9 | COPY . /workspace 10 | 11 | WORKDIR /workspace 12 | 13 | RUN go mod tidy && \ 14 | CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o watchlog ./main.go && \ 15 | chmod 777 watchlog 16 | 17 | FROM registry.js.design/library/filebeat:7.17.10_python2 18 | 19 | COPY --from=build /workspace/watchlog /usr/share/filebeat/watchlog/watchlog 20 | 21 | COPY assets/entrypoint assets/filebeat/ assets/healthz /usr/share/filebeat/watchlog/ 22 | 23 | RUN /usr/bin/chmod +x /usr/share/filebeat/watchlog/watchlog /usr/share/filebeat/watchlog/healthz /usr/share/filebeat/watchlog/config.filebeat 24 | 25 | HEALTHCHECK CMD /usr/share/filebeat/healthz 26 | 27 | WORKDIR /usr/share/filebeat/ 28 | 29 | ENV PILOT_TYPE=filebeat 30 | 31 | ENTRYPOINT ["python2", "/usr/share/filebeat/watchlog/entrypoint"] 32 | -------------------------------------------------------------------------------- /pkg/ctx/ctx.go: -------------------------------------------------------------------------------- 1 | package ctx 2 | 3 | import ( 4 | "context" 5 | "github.com/containerd/containerd" 6 | "github.com/docker/docker/client" 7 | "os" 8 | "sync" 9 | "watchlog/pkg/provider" 10 | "watchlog/pkg/runtime" 11 | ) 12 | 13 | type Context struct { 14 | context.Context 15 | // 采集器 16 | FilebeatPointer provider.FilebeatPointer 17 | // 日志前缀 18 | LogPrefix string 19 | BaseDir string 20 | DockerCli *client.Client 21 | ContainerdCli *containerd.Client 22 | sync.Mutex 23 | } 24 | 25 | func NewContext(baseDir, logPrefix string, f provider.FilebeatPointer) *Context { 26 | dockerCli := new(client.Client) 27 | containerCli := new(containerd.Client) 28 | 29 | switch os.Getenv("RUNTIME_TYPE") { 30 | case "docker": 31 | dockerCli = runtime.NewDockerClient() 32 | case "containerd": 33 | containerCli = runtime.NewContainerClient() 34 | } 35 | 36 | return &Context{ 37 | Context: context.Background(), 38 | FilebeatPointer: f, 39 | LogPrefix: logPrefix, 40 | BaseDir: baseDir, 41 | DockerCli: dockerCli, 42 | ContainerdCli: containerCli, 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /pkg/provider/filebeat_types.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | // RegistryState represents log offsets 9 | type RegistryState struct { 10 | K string `json:"k"` 11 | V RegistryV `json:"v"` 12 | } 13 | 14 | type RegistryV struct { 15 | Source string `json:"source"` 16 | Offset int64 `json:"offset"` 17 | Timestamp []time.Time `json:"timestamp"` 18 | TTL time.Duration `json:"ttl"` 19 | Type string `json:"type"` 20 | FileStateOS FileInode 21 | } 22 | 23 | type FileInode struct { 24 | Inode uint64 `json:"inode,"` 25 | Device uint64 `json:"device,"` 26 | } 27 | 28 | const ( 29 | FilebeatBaseConf = "/usr/share/filebeat" 30 | FilebeatExecCmd = FilebeatBaseConf + "/filebeat" 31 | FilebeatConfFile = FilebeatBaseConf + "/filebeat.yml" 32 | FilebeatConfDir = FilebeatBaseConf + "/inputs.d" 33 | FilebeatRegistry = FilebeatBaseConf + "/data/registry/filebeat/log.json" 34 | ) 35 | 36 | // GetConfPath get configuration path FilebeatConfDir/${container}.yaml 37 | func (f FilebeatPointer) GetConfPath(container string) string { 38 | return fmt.Sprintf("%s/%s.yml", FilebeatConfDir, container) 39 | } 40 | 41 | // GetBaseConf returns plugin root directory 42 | func (f FilebeatPointer) GetBaseConf() string { 43 | return FilebeatBaseConf 44 | } 45 | 46 | // GetConfHome returns configuration directory 47 | func (f FilebeatPointer) GetConfHome() string { 48 | return FilebeatConfDir 49 | } 50 | -------------------------------------------------------------------------------- /assets/entrypoint: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # coding: utf-8 3 | 4 | import os 5 | import os.path 6 | import subprocess 7 | 8 | base = '/host' 9 | pilot_filebeat = "filebeat" 10 | ENV_PILOT_TYPE = "PILOT_TYPE" 11 | 12 | 13 | def umount(volume): 14 | subprocess.check_call('umount -l %s' % volume, shell=True) 15 | 16 | 17 | def mount_points(): 18 | with open('/proc/self/mountinfo', 'r') as f: 19 | mounts = f.read().decode('utf-8') 20 | 21 | points = set() 22 | for line in mounts.split('\n'): 23 | mtab = line.split() 24 | if len(mtab) > 1 and mtab[4].startswith(base + '/') and mtab[4].endswith('shm') and 'containers' in mtab[4]: 25 | points.add(mtab[4]) 26 | return points 27 | 28 | 29 | def cleanup(): 30 | umounts = mount_points() 31 | for volume in sorted(umounts, reverse=True): 32 | umount(volume) 33 | 34 | 35 | def run(): 36 | pilot_type = os.environ.get(ENV_PILOT_TYPE) 37 | if pilot_filebeat == pilot_type: 38 | tpl_config = "/usr/share/filebeat/watchlog/filebeat.tpl" 39 | 40 | os.execve('/usr/share/filebeat/watchlog/watchlog', ['/usr/share/filebeat/watchlog/watchlog', '-template', tpl_config], 41 | os.environ) 42 | 43 | 44 | def config(): 45 | pilot_type = os.environ.get(ENV_PILOT_TYPE) 46 | if pilot_filebeat == pilot_type: 47 | print "start log-pilot:", pilot_filebeat 48 | subprocess.check_call(['/usr/share/filebeat/watchlog/config.filebeat']) 49 | 50 | 51 | if __name__ == '__main__': 52 | config() 53 | cleanup() 54 | run() -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout code 14 | uses: actions/checkout@v3 15 | 16 | - name: Set up QEMU 17 | uses: docker/setup-qemu-action@v3 18 | 19 | - name: Inject slug/short variables 20 | uses: rlespinasse/github-slug-action@v4 21 | 22 | - name: Set up Docker Buildx 23 | id: buildx 24 | uses: docker/setup-buildx-action@v3 25 | 26 | - name: Available platforms 27 | run: echo ${{ steps.buildx.outputs.platforms }} 28 | 29 | - name: Login to Docker Hub 30 | uses: docker/login-action@v3 31 | with: 32 | username: ${{ secrets.DOCKERHUB_USER }} 33 | password: ${{ secrets.DOCKERHUB_TOKEN }} 34 | 35 | - name: Set env variables 36 | id: set_env 37 | run: | 38 | echo "BRANCH_NAME=${GITHUB_REF#refs/heads/}" >> $GITHUB_ENV 39 | echo "SHORT_SHA=${GITHUB_SHA:0:4}" >> $GITHUB_ENV 40 | echo "DATE=$(TZ=Asia/Shanghai date +%Y-%m-%d.%H-%M-%S)" >> $GITHUB_ENV 41 | 42 | - name: Build and push 43 | uses: docker/build-push-action@v6 44 | with: 45 | context: . 46 | file: ./Dockerfile 47 | platforms: linux/arm64,linux/amd64 48 | push: ${{ github.event_name != 'pull_request' }} 49 | build-args: | 50 | VERSION=${{ env.DATE }} 51 | tags: | 52 | cairry/watchlog:latest 53 | cairry/watchlog:${{ env.BRANCH_NAME }}.${{ env.DATE }}.${{ env.SHORT_SHA }} -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "github.com/zeromicro/go-zero/core/logc" 7 | "os" 8 | "watchlog/log" 9 | ) 10 | 11 | func main() { 12 | // Command-line flags 13 | template := flag.String("template", "", "Template filepath for fluentd or filebeat.") 14 | flag.Parse() 15 | 16 | // Set default Docker API version if not set 17 | if err := setDefaultDockerAPIVersion(); err != nil { 18 | logc.Errorf(context.Background(), err.Error()) 19 | return 20 | } 21 | 22 | // Validate runtime type 23 | if os.Getenv("RUNTIME_TYPE") == "" { 24 | panic("Please set service type, (docker|containerd)") 25 | } 26 | 27 | // Validate template 28 | if *template == "" { 29 | panic("template file cannot be empty") 30 | } 31 | 32 | // Run log processing 33 | if err := log.Run(*template, getLogPrefix(), getBaseDir()); err != nil { 34 | logc.Errorf(context.Background(), err.Error()) 35 | } 36 | } 37 | 38 | // setDefaultDockerAPIVersion sets the default Docker API version if not already set. 39 | func setDefaultDockerAPIVersion() error { 40 | if os.Getenv("DOCKER_API_VERSION") == "" { 41 | return os.Setenv("DOCKER_API_VERSION", "1.24") 42 | } 43 | return nil 44 | } 45 | 46 | // getLogPrefix retrieves the log prefix from the environment or defaults to "watchlog". 47 | func getLogPrefix() string { 48 | if lp := os.Getenv("LOG_PREFIX"); len(lp) > 0 { 49 | return lp 50 | } 51 | return "watchlog" 52 | } 53 | 54 | // getBaseDir get log base store dir or defaults to "/var/log/containers" 55 | func getBaseDir() string { 56 | if lbd := os.Getenv("LOG_BASE_DIR"); len(lbd) > 0 { 57 | return lbd 58 | } 59 | return "/var/log/containers" 60 | } 61 | -------------------------------------------------------------------------------- /log/config/logConfig.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "path/filepath" 6 | "strings" 7 | ) 8 | 9 | // LogConfig log configuration 10 | type LogConfig struct { 11 | Name string 12 | HostDir string 13 | ContainerDir string 14 | Format string 15 | FormatConfig map[string]string 16 | File string 17 | Tags map[string]string 18 | EstimateTime bool 19 | Stdout bool 20 | } 21 | 22 | const LabelServiceLogsTmpl = "%s_" 23 | 24 | func GetLogConfigs(logPrefix string, jsonLogPath string, labels map[string]string) ([]LogConfig, error) { 25 | var ret []LogConfig 26 | for label, _ := range labels { 27 | p := fmt.Sprintf(LabelServiceLogsTmpl, logPrefix) 28 | logTopicName := strings.TrimPrefix(label, p) // watchlog_default, logTopicName = default 29 | logConfig, err := parseLogConfig(logTopicName, labels[label], jsonLogPath) 30 | if err != nil { 31 | return nil, err 32 | } 33 | ret = append(ret, logConfig) 34 | } 35 | return ret, nil 36 | } 37 | 38 | //func getLabelNames(logPrefix string, labels map[string]string) []string { 39 | // var labelNames []string 40 | // for k := range labels { 41 | // if strings.HasPrefix(k, logPrefix) { 42 | // labelNames = append(labelNames, k) 43 | // } 44 | // } 45 | // //sort keys 46 | // sort.Strings(labelNames) 47 | // return labelNames 48 | //} 49 | 50 | func parseLogConfig(label, value string, jsonLogPath string) (LogConfig, error) { 51 | cfg := new(LogConfig) 52 | if value == "" { 53 | return *cfg, fmt.Errorf("env %s value don't is null", label) 54 | } 55 | 56 | // 标准输出日志 57 | if value == "stdout" { 58 | logFile := filepath.Base(jsonLogPath) 59 | cfg = &LogConfig{ 60 | File: logFile, 61 | Name: label, 62 | HostDir: filepath.Dir(jsonLogPath), 63 | Tags: map[string]string{ 64 | "index": label, 65 | "topic": label, 66 | }, 67 | } 68 | } 69 | return *cfg, nil 70 | } 71 | -------------------------------------------------------------------------------- /pkg/tools/format.go: -------------------------------------------------------------------------------- 1 | package tools 2 | 3 | import ( 4 | "fmt" 5 | "watchlog/log/nodeInfo" 6 | ) 7 | 8 | // FormatConverter converts node info to map 9 | type FormatConverter func(info *nodeInfo.LogInfoNode) (map[string]string, error) 10 | 11 | var converters = make(map[string]FormatConverter) 12 | 13 | // Register format converter instance 14 | func Register(format string, converter FormatConverter) { 15 | converters[format] = converter 16 | } 17 | 18 | // Convert convert node info to map 19 | func Convert(info *nodeInfo.LogInfoNode) (map[string]string, error) { 20 | converter := converters[info.Value] 21 | if converter == nil { 22 | return nil, fmt.Errorf("unsupported log format: %s", info.Value) 23 | } 24 | return converter(info) 25 | } 26 | 27 | // SimpleConverter simple format converter 28 | type SimpleConverter struct { 29 | properties map[string]bool 30 | } 31 | 32 | func init() { 33 | simpleConverter := func(properties []string) FormatConverter { 34 | return func(info *nodeInfo.LogInfoNode) (map[string]string, error) { 35 | validProperties := make(map[string]bool) 36 | for _, property := range properties { 37 | validProperties[property] = true 38 | } 39 | ret := make(map[string]string) 40 | for k, v := range info.Children { 41 | if _, ok := validProperties[k]; !ok { 42 | return nil, fmt.Errorf("%s is not a valid properties for format %s", k, info.Value) 43 | } 44 | ret[k] = v.Value 45 | } 46 | return ret, nil 47 | } 48 | } 49 | 50 | Register("nonex", simpleConverter([]string{})) 51 | Register("csv", simpleConverter([]string{"time_key", "time_format", "keys"})) 52 | Register("json", simpleConverter([]string{"time_key", "time_format"})) 53 | Register("regexp", simpleConverter([]string{"time_key", "time_format"})) 54 | Register("apache2", simpleConverter([]string{})) 55 | Register("apache_error", simpleConverter([]string{})) 56 | Register("nginx", simpleConverter([]string{})) 57 | Register("regexp", func(info *nodeInfo.LogInfoNode) (map[string]string, error) { 58 | ret, err := simpleConverter([]string{"pattern", "time_format"})(info) 59 | if err != nil { 60 | return ret, err 61 | } 62 | if ret["pattern"] == "" { 63 | return nil, fmt.Errorf("regex pattern can not be empty") 64 | } 65 | return ret, nil 66 | }) 67 | } 68 | -------------------------------------------------------------------------------- /log/log.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "context" 5 | "github.com/docker/docker/api/types/filters" 6 | "github.com/zeromicro/go-zero/core/logc" 7 | "io/ioutil" 8 | "os" 9 | "os/signal" 10 | "syscall" 11 | "text/template" 12 | "watchlog/controller" 13 | "watchlog/pkg/ctx" 14 | "watchlog/pkg/provider" 15 | ) 16 | 17 | // Run starts the log pilot. 18 | func Run(tmplPath, logPrefix, baseDir string) error { 19 | tmpl, err := loadTemplate(tmplPath) 20 | if err != nil { 21 | return err 22 | } 23 | 24 | p := provider.NewFilebeatPointer(tmpl, baseDir) 25 | c := ctx.NewContext(baseDir, logPrefix, p) 26 | return startWorker(c) 27 | } 28 | 29 | // loadTemplate reads and parses the template file. 30 | func loadTemplate(path string) (*template.Template, error) { 31 | data, err := ioutil.ReadFile(path) 32 | if err != nil { 33 | return nil, err 34 | } 35 | return template.New("watchalert").Parse(string(data)) 36 | } 37 | 38 | // startWorker initiates the worker process. 39 | func startWorker(c *ctx.Context) error { 40 | if err := c.FilebeatPointer.CleanConfigs(); err != nil { 41 | return err 42 | } 43 | 44 | if err := c.FilebeatPointer.Start(); err != nil { 45 | return err 46 | } 47 | 48 | if err := processContainers(c); err != nil { 49 | return err 50 | } 51 | 52 | waitForShutdown() 53 | logc.Infof(context.Background(), "Program Stop Successful!!!") 54 | return nil 55 | } 56 | 57 | // processContainers handles container processing based on the runtime type. 58 | func processContainers(c *ctx.Context) error { 59 | switch os.Getenv("RUNTIME_TYPE") { 60 | case "docker": 61 | logc.Infof(context.Background(), "Processing Docker runtime") 62 | filter := filters.NewArgs() 63 | filter.Add("type", "container") 64 | dockerController := controller.NewDockerInterface(c, filter) 65 | return dockerController.ProcessContainers() 66 | case "containerd": 67 | logc.Infof(context.Background(), "Processing container runtime") 68 | containerController := controller.NewContainerInterface(c) 69 | return containerController.ProcessContainers() 70 | default: 71 | return nil 72 | } 73 | } 74 | 75 | // waitForShutdown listens for OS signals to gracefully shut down the program. 76 | func waitForShutdown() { 77 | sig := make(chan os.Signal, 1) 78 | signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM) 79 | <-sig 80 | } 81 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | WatchLog 2 | ======== 3 | ## 🎉 项目介绍 4 | `WatchLog`是一个云原生容器日志采集工具。你可以使用它来收集`Docker`、`Containerd`的容器日志并发送到集中式日志管理系统中,例如`elasticsearch` `kafka` `redis`等。 5 | 6 | ## ♻️ 版本兼容 7 | 8 | **Input** 9 | 10 | | Service | Version | 11 | |------------|------------| 12 | | Docker | 推荐 20.x ➕ | 13 | | Containerd | 推荐 1.2.x ➕ | 14 | 15 | **Output** 16 | 17 | | Service | Version | 18 | |---------------|--------------| 19 | | Elasticsearch | 推荐 7.10.x ➕ | 20 | | Kafka | 推荐 2.x ➕ | 21 | | Redis | 推荐 6.x ➕ | 22 | 23 | ## 🚀 快速开始 24 | ### 确定参数配置 25 | - LOG_PREFIX:日志前缀标识, 默认是watchlog, 支持自定义 26 | - LOG_BASE_DIR:日志存储目录(挂载到WatchLog容器内的路径),默认 `/var/log/containers` 27 | - RUNTIME_TYPE:运行时类型,支持`docker` `containerd` 28 | - LOGGING_OUTPUT:日志输出类型,支持主流的`kafka` `elasticsearch` `redis` `file`等 29 | 30 | **LOG_PREFIX 详细** 31 | ```yaml 32 | - name: LOG_PREFIX 33 | value: watchlog 34 | ``` 35 | 36 | **LOG_BASE_DIR 详细** 37 | ```yaml 38 | - name: LOG_BASE_DIR 39 | value: "/var/log/containers" 40 | ``` 41 | 42 | **RUNTIME_TYPE 详细** 43 | ```yaml 44 | - name: RUNTIME_TYPE 45 | value: docker 46 | ``` 47 | 48 | **LOGGING_OUTPUT 详细配置** 49 | 50 | - kafka 51 | ```yaml 52 | - name: LOGGING_OUTPUT 53 | value: kafka 54 | - name: KAFKA_BROKERS 55 | value: 192.168.1.190:9092 56 | ``` 57 | - elasticsearch 58 | ```yaml 59 | - name: LOGGING_OUTPUT 60 | value: elasticsearch 61 | - name: ELASTICSEARCH_HOST 62 | value: "192.168.1.190" 63 | - name: ELASTICSEARCH_PORT 64 | value: "9200" 65 | ``` 66 | - redis 67 | ```yaml 68 | - name: LOGGING_OUTPUT 69 | value: redis 70 | - name: REDIS_HOST 71 | value: "192.168.1.190" 72 | - name: REDIS_PORT 73 | value: "6379" 74 | - name: REDIS_PASSWORD 75 | value: "redis@123." 76 | ``` 77 | - file 78 | ```yaml 79 | - name: LOGGING_OUTPUT 80 | value: file 81 | - name: FILE_PATH 82 | value: "/tmp/filebeat" 83 | - name: FILE_NAME 84 | value: "filebeat" 85 | ``` 86 | ### 启动服务 87 | ```bash 88 | kubectl apply -f ./deploy/kubernetes/watchlog.yaml 89 | ``` 90 | 91 | ### 运行测试用例 92 | #### 前提条件 93 | 需要为每个被收集的`Controller`/`Pod`中, 注入日志采集前缀标志`watchlog_{xxx}`的环境变量, 前缀标识取决于 WatchLog 服务的环境变量 LOG_PREFIX, 默认情况下是 watchlog. 94 | ```yaml 95 | - env: 96 | - name: watchlog_default-nginx 97 | value: stdout 98 | ``` 99 | #### 启动服务 100 | ```bash 101 | kubectl apply -f ./deploy/kubernetes/nginx.yaml 102 | ``` 103 | 104 | ## 🎸 支持 105 | - 如果你觉得 WatchLog 还不错,可以通过 Star 来表示你的喜欢 106 | - 在公司或个人项目中使用 WatchLog,并帮忙推广给伙伴使用 107 | 108 | ## 🧑‍💻 交流渠道 109 | - [点击我](https://cairry.github.io/docs/#%E4%BA%A4%E6%B5%81%E7%BE%A4-%E8%81%94%E7%B3%BB%E6%88%91) -------------------------------------------------------------------------------- /deploy/kubernetes/watchlog.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: DaemonSet 3 | metadata: 4 | labels: 5 | k8s-app: watchlog 6 | kubernetes.io/cluster-service: "true" 7 | name: watchlog 8 | namespace: kube-system 9 | 10 | spec: 11 | selector: 12 | matchLabels: 13 | k8s-app: watchlog 14 | 15 | template: 16 | metadata: 17 | creationTimestamp: null 18 | labels: 19 | k8s-app: watchlog 20 | kubernetes.io/cluster-service: "true" 21 | 22 | spec: 23 | containers: 24 | - env: 25 | - name: RUNTIME_TYPE 26 | value: docker 27 | - name: LOGGING_OUTPUT 28 | value: elasticsearch 29 | - name: ELASTICSEARCH_HOST 30 | value: "192.168.1.190" 31 | - name: ELASTICSEARCH_PORT 32 | value: "9200" 33 | - name: NODE_NAME 34 | valueFrom: 35 | fieldRef: 36 | apiVersion: v1 37 | fieldPath: spec.nodeName 38 | image: docker.io/cairry/watchlog:latest 39 | imagePullPolicy: Always 40 | name: watchlog 41 | 42 | resources: 43 | limits: 44 | cpu: "1" 45 | memory: 1000Mi 46 | requests: 47 | cpu: 50m 48 | memory: 100Mi 49 | 50 | securityContext: 51 | privileged: true 52 | capabilities: 53 | add: 54 | - SYS_ADMIN 55 | 56 | volumeMounts: 57 | - mountPath: /var/run/containerd/containerd.sock 58 | name: containersock 59 | - mountPath: /var/run/docker.sock 60 | name: sock 61 | - mountPath: /var/log/containers 62 | name: var-log-contaienrs 63 | - mountPath: /var/log/pods 64 | name: var-log-pods 65 | - mountPath: /var/lib/filebeat 66 | name: var-lib-filebeat 67 | - mountPath: /var/log/filebeat 68 | name: var-log-filebeat 69 | 70 | restartPolicy: Always 71 | 72 | tolerations: 73 | - effect: NoSchedule 74 | operator: Exists 75 | 76 | volumes: 77 | - hostPath: 78 | path: /run/k3s/containerd/containerd.sock 79 | type: "" 80 | name: containersock 81 | - hostPath: 82 | path: /var/run/docker.sock 83 | type: "" 84 | name: sock 85 | - hostPath: 86 | path: /var/log/containers 87 | type: "" 88 | name: var-log-contaienrs 89 | - hostPath: 90 | path: /var/log/pods 91 | type: "" 92 | name: var-log-pods 93 | - hostPath: 94 | path: /var/lib/filebeat 95 | type: DirectoryOrCreate 96 | name: var-lib-filebeat 97 | - hostPath: 98 | path: /var/log/filebeat 99 | type: DirectoryOrCreate 100 | name: var-log-filebeat -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module watchlog 2 | 3 | go 1.21 4 | 5 | require ( 6 | github.com/containerd/containerd v1.7.7 7 | github.com/docker/docker v23.0.3+incompatible 8 | github.com/elastic/go-ucfg v0.8.8 9 | github.com/sirupsen/logrus v1.9.3 10 | github.com/zeromicro/go-zero v1.7.4 11 | ) 12 | 13 | require ( 14 | github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 // indirect 15 | github.com/AdamKorcz/go-118-fuzz-build v0.0.0-20230306123547-8075edf89bb0 // indirect 16 | github.com/Microsoft/go-winio v0.6.1 // indirect 17 | github.com/Microsoft/hcsshim v0.11.4 // indirect 18 | github.com/containerd/cgroups v1.1.0 // indirect 19 | github.com/containerd/continuity v0.4.2 // indirect 20 | github.com/containerd/fifo v1.1.0 // indirect 21 | github.com/containerd/log v0.1.0 // indirect 22 | github.com/containerd/ttrpc v1.2.2 // indirect 23 | github.com/containerd/typeurl/v2 v2.1.1 // indirect 24 | github.com/docker/distribution v2.8.1+incompatible // indirect 25 | github.com/docker/go-connections v0.5.0 // indirect 26 | github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c // indirect 27 | github.com/docker/go-units v0.5.0 // indirect 28 | github.com/fatih/color v1.18.0 // indirect 29 | github.com/go-logr/logr v1.4.2 // indirect 30 | github.com/go-logr/stdr v1.2.2 // indirect 31 | github.com/gogo/protobuf v1.3.2 // indirect 32 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 33 | github.com/google/go-cmp v0.6.0 // indirect 34 | github.com/google/uuid v1.6.0 // indirect 35 | github.com/klauspost/compress v1.17.9 // indirect 36 | github.com/mattn/go-colorable v0.1.13 // indirect 37 | github.com/mattn/go-isatty v0.0.20 // indirect 38 | github.com/moby/locker v1.0.1 // indirect 39 | github.com/moby/sys/mountinfo v0.6.2 // indirect 40 | github.com/moby/sys/sequential v0.5.0 // indirect 41 | github.com/moby/sys/signal v0.7.0 // indirect 42 | github.com/moby/term v0.5.0 // indirect 43 | github.com/morikuni/aec v1.0.0 // indirect 44 | github.com/opencontainers/go-digest v1.0.0 // indirect 45 | github.com/opencontainers/image-spec v1.1.0 // indirect 46 | github.com/opencontainers/runc v1.1.5 // indirect 47 | github.com/opencontainers/runtime-spec v1.1.0 // indirect 48 | github.com/opencontainers/selinux v1.11.0 // indirect 49 | github.com/pkg/errors v0.9.1 // indirect 50 | github.com/spaolacci/murmur3 v1.1.0 // indirect 51 | go.opencensus.io v0.24.0 // indirect 52 | go.uber.org/automaxprocs v1.6.0 // indirect 53 | golang.org/x/mod v0.17.0 // indirect 54 | golang.org/x/net v0.31.0 // indirect 55 | golang.org/x/sync v0.9.0 // indirect 56 | golang.org/x/sys v0.27.0 // indirect 57 | golang.org/x/text v0.20.0 // indirect 58 | golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect 59 | google.golang.org/genproto v0.0.0-20230711160842-782d3b101e98 // indirect 60 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 // indirect 61 | google.golang.org/grpc v1.65.0 // indirect 62 | google.golang.org/protobuf v1.35.2 // indirect 63 | gopkg.in/yaml.v2 v2.4.0 // indirect 64 | gotest.tools/v3 v3.5.1 // indirect 65 | ) 66 | -------------------------------------------------------------------------------- /controller/runtime.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/zeromicro/go-zero/core/logc" 7 | "io/ioutil" 8 | "os" 9 | "path/filepath" 10 | "strings" 11 | logtypes "watchlog/log/config" 12 | "watchlog/pkg/ctx" 13 | "watchlog/pkg/runtime" 14 | ) 15 | 16 | type InterRuntime interface { 17 | ProcessContainers() error 18 | } 19 | 20 | // Collect 判断是否需要收集日志 21 | func Collect(env []string, logPrefix string) bool { 22 | var exist bool 23 | for _, e := range env { 24 | if strings.HasPrefix(e, logPrefix) { 25 | exist = true 26 | } 27 | } 28 | 29 | return exist 30 | } 31 | 32 | // Exists 判断采集容器日志的配置是否存在 33 | func Exists(ctx *ctx.Context, containId string) bool { 34 | if _, err := os.Stat(ctx.FilebeatPointer.GetConfPath(containId)); os.IsNotExist(err) { 35 | return false 36 | } 37 | return true 38 | } 39 | 40 | // DelContainerLogFile 销毁采集容器日志文件 41 | func DelContainerLogFile(ctx *ctx.Context, id string) error { 42 | logc.Infof(context.Background(), "Try removing log config %s", id) 43 | if err := os.Remove(ctx.FilebeatPointer.GetConfPath(id)); err != nil { 44 | return fmt.Errorf("removing %s log config failure, err: %s", id, err.Error()) 45 | } 46 | 47 | return nil 48 | } 49 | 50 | type CollectFields struct { 51 | Id string 52 | Env []string 53 | Labels map[string]string 54 | LogPath string 55 | } 56 | 57 | // NewCollectFile 创建Filebeat采集配置 58 | func NewCollectFile(ctx *ctx.Context, cf CollectFields) error { 59 | id := cf.Id 60 | env := cf.Env 61 | labels := cf.Labels 62 | jsonLogPath := cf.LogPath 63 | ct := runtime.BuildContainerLabels(labels) 64 | logEnvs := getLogEnvs(env) 65 | 66 | logPath := filepath.Join(ctx.BaseDir, jsonLogPath) // /host/var/lib/containerd/log/pods/intl_diagon-alley-5cf4c7cddc-7nd94_*/diagon-alley/*.log 67 | logConfigs, err := logtypes.GetLogConfigs(ctx.LogPrefix, logPath, logEnvs) 68 | if err != nil { 69 | return fmt.Errorf("GetLogConfigs failed, err: %s", err.Error()) 70 | } 71 | 72 | if len(logConfigs) == 0 { 73 | return nil 74 | } 75 | 76 | //生成 filebeat 采集配置 77 | logConfig, err := ctx.FilebeatPointer.RenderLogConfig(id, ct, logConfigs) 78 | if err != nil { 79 | return fmt.Errorf("RenderLogConfig failed, err: %s", err.Error()) 80 | } 81 | 82 | //TODO validate config before save 83 | logc.Infof(context.Background(), fmt.Sprintf("Write Log config, path: %s", ctx.FilebeatPointer.GetConfPath(id))) 84 | if err = ioutil.WriteFile(ctx.FilebeatPointer.GetConfPath(id), []byte(logConfig), os.FileMode(0644)); err != nil { 85 | return fmt.Errorf("WriteFile failed, err: %s", err.Error()) 86 | } 87 | 88 | return nil 89 | } 90 | 91 | // getLogEnvs 获取关键 Envs 92 | func getLogEnvs(env []string) map[string]string { 93 | var logEnv = map[string]string{} // map[aliyun_logs_tencent-prod-diagon-alley:stdout] 94 | for _, e := range env { 95 | envLabel := strings.SplitN(e, "=", 2) // [aliyun_logs_tencent-prod-diagon-alley stdout] 2 96 | if len(envLabel) == 2 { 97 | logEnv[envLabel[0]] = envLabel[1] // aliyun_logs_tencent-prod-diagon-alley stdout 98 | } 99 | } 100 | return logEnv 101 | } 102 | -------------------------------------------------------------------------------- /controller/docker.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/docker/docker/api/types" 7 | "github.com/docker/docker/api/types/events" 8 | "github.com/docker/docker/api/types/filters" 9 | "github.com/zeromicro/go-zero/core/logc" 10 | "io" 11 | "strings" 12 | "watchlog/pkg/ctx" 13 | "watchlog/pkg/runtime" 14 | ) 15 | 16 | type Docker struct { 17 | ctx *ctx.Context 18 | f filters.Args 19 | } 20 | 21 | // NewDockerInterface creates a new Docker interface. 22 | func NewDockerInterface(ctx *ctx.Context, f filters.Args) InterRuntime { 23 | return &Docker{ctx: ctx, f: f} 24 | } 25 | 26 | // ProcessContainers processes the Docker containers and logs. 27 | func (d *Docker) ProcessContainers() error { 28 | d.ctx.Lock() 29 | defer d.ctx.Unlock() 30 | 31 | d.watchEvent(d.f) 32 | containers, err := d.listContainers() 33 | if err != nil { 34 | return err 35 | } 36 | 37 | for _, c := range containers { 38 | if c.State == "removing" { 39 | continue 40 | } 41 | 42 | if Exists(d.ctx, c.ID) { 43 | continue 44 | } 45 | 46 | if err := d.processContainer(c.ID); err != nil { 47 | logc.Errorf(context.Background(), fmt.Sprintf("Error processing container %s: %v", c.ID, err)) 48 | } 49 | } 50 | return nil 51 | } 52 | 53 | // listContainers retrieves the list of Docker containers. 54 | func (d *Docker) listContainers() ([]types.Container, error) { 55 | opts := types.ContainerListOptions{} 56 | containers, err := d.ctx.DockerCli.ContainerList(d.ctx, opts) 57 | if err != nil { 58 | logc.Errorf(context.Background(), fmt.Sprintf("Failed to list containers: %s", err.Error())) 59 | return nil, err 60 | } 61 | return containers, nil 62 | } 63 | 64 | // processContainer inspects and processes an individual container. 65 | func (d *Docker) processContainer(containerID string) error { 66 | containerJSON, err := d.ctx.DockerCli.ContainerInspect(d.ctx, containerID) 67 | if err != nil { 68 | logc.Errorf(context.Background(), fmt.Sprintf("Failed to inspect container %s: %v", containerID, err)) 69 | return err 70 | } 71 | 72 | if !Collect(containerJSON.Config.Env, d.ctx.LogPrefix) { 73 | return nil 74 | } 75 | 76 | // 符合条件的 Env 77 | var logEnvs []string 78 | for _, envVar := range containerJSON.Config.Env { 79 | // LogPrefix: aliyun_logs_tencent-prod-hermione=stdout ,envVar: aliyun_logs 80 | if strings.HasPrefix(envVar, d.ctx.LogPrefix) { 81 | logEnvs = append(logEnvs, envVar) 82 | } 83 | } 84 | 85 | fields := CollectFields{ 86 | Id: containerJSON.ID, 87 | Env: logEnvs, 88 | Labels: containerJSON.Config.Labels, 89 | LogPath: fmt.Sprintf("%s_%s_%s-*.log", containerJSON.Config.Labels[runtime.KubernetesPodName], containerJSON.Config.Labels[runtime.KubernetesContainerNamespace], containerJSON.Config.Labels[runtime.KubernetesContainerName]), 90 | } 91 | return NewCollectFile(d.ctx, fields) 92 | } 93 | 94 | // watchEvent listens for Docker events and processes them. 95 | func (d *Docker) watchEvent(filter filters.Args) { 96 | options := types.EventsOptions{Filters: filter} 97 | msgs, errs := d.ctx.DockerCli.Events(d.ctx.Context, options) 98 | 99 | go func() { 100 | logc.Infof(context.Background(), "Beginning to watch docker events") 101 | for { 102 | select { 103 | case msg := <-msgs: 104 | if err := d.processEvent(msg); err != nil { 105 | logc.Errorf(context.Background(), fmt.Sprintf("Error processing event: %v", err)) 106 | } 107 | case err := <-errs: 108 | logc.Errorf(context.Background(), fmt.Sprintf("Error in event stream: %v", err)) 109 | if err == io.EOF || err == io.ErrUnexpectedEOF { 110 | return 111 | } 112 | } 113 | } 114 | }() 115 | } 116 | 117 | // processEvent handles Docker events for containers. 118 | func (d *Docker) processEvent(msg events.Message) error { 119 | containerID := msg.Actor.ID 120 | switch msg.Action { 121 | case "start", "restart": 122 | return d.handleStartRestartEvent(containerID) 123 | case "destroy", "die": 124 | return d.handleDestroyDieEvent(containerID) 125 | default: 126 | return nil 127 | } 128 | } 129 | 130 | // handleStartRestartEvent processes container start/restart events. 131 | func (d *Docker) handleStartRestartEvent(containerID string) error { 132 | logc.Debugf(context.Background(), "Processing container start/restart event: %s", containerID) 133 | if Exists(d.ctx, containerID) { 134 | logc.Debugf(context.Background(), "Container %s already exists, skipping", containerID) 135 | return nil 136 | } 137 | return d.processContainer(containerID) 138 | } 139 | 140 | // handleDestroyDieEvent processes container destroy/die events. 141 | func (d *Docker) handleDestroyDieEvent(containerID string) error { 142 | if !Exists(d.ctx, containerID) { 143 | return nil 144 | } 145 | 146 | logc.Debugf(context.Background(), "Processing container destroy event: %s", containerID) 147 | return DelContainerLogFile(d.ctx, containerID) 148 | } 149 | -------------------------------------------------------------------------------- /controller/containerd.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "github.com/containerd/containerd" 8 | "github.com/containerd/containerd/containers" 9 | "github.com/containerd/containerd/errdefs" 10 | "github.com/containerd/containerd/events" 11 | "github.com/containerd/containerd/namespaces" 12 | "github.com/containerd/containerd/oci" 13 | "github.com/zeromicro/go-zero/core/logc" 14 | "io" 15 | "regexp" 16 | "strings" 17 | "watchlog/pkg/ctx" 18 | "watchlog/pkg/runtime" 19 | ) 20 | 21 | var ( 22 | create = "containerd.events.ContainerCreate" 23 | delete = "containerd.events.ContainerDelete" 24 | ) 25 | 26 | type Containerd struct { 27 | ctx *ctx.Context 28 | } 29 | 30 | func NewContainerInterface(ctx *ctx.Context) InterRuntime { 31 | return &Containerd{ 32 | ctx: ctx, 33 | } 34 | } 35 | 36 | func (c Containerd) ProcessContainers() error { 37 | c.ctx.Lock() 38 | defer c.ctx.Unlock() 39 | 40 | containerCtx := namespaces.WithNamespace(c.ctx.Context, "k8s.io") 41 | c.watchEvent(c.ctx, containerCtx) 42 | 43 | containers, err := c.ctx.ContainerdCli.Containers(containerCtx) 44 | if err != nil { 45 | logc.Errorf(context.Background(), fmt.Sprintf("get containers failed, %s", err.Error())) 46 | return err 47 | } 48 | 49 | for _, container := range containers { 50 | if err := c.processContainer(containerCtx, container); err != nil { 51 | logc.Errorf(context.Background(), "process container failed: %v", err) 52 | } 53 | } 54 | 55 | return nil 56 | } 57 | 58 | func (c Containerd) processContainer(containerCtx context.Context, container containerd.Container) error { 59 | meta, err := container.Info(containerCtx) 60 | if err != nil { 61 | return fmt.Errorf("get container meta info failed: %s", err.Error()) 62 | } 63 | 64 | spec, err := container.Spec(containerCtx) 65 | if err != nil { 66 | return fmt.Errorf("get container spec failed: %s", err.Error()) 67 | } 68 | 69 | return processCollectFile(c.ctx, spec.Process.Env, meta) 70 | } 71 | 72 | func (c Containerd) watchEvent(ctx *ctx.Context, containerCtx context.Context) { 73 | msgs, errs := c.ctx.ContainerdCli.EventService().Subscribe(containerCtx, "") 74 | 75 | go func() { 76 | defer logc.Info(context.Background(), "finish to watch containerd event") 77 | logc.Infof(context.Background(), "begin to watch containerd event") 78 | 79 | for { 80 | select { 81 | case msg := <-msgs: 82 | if err := c.processEvent(ctx, containerCtx, msg); err != nil { 83 | logc.Errorf(context.Background(), "process event failed: %v", err) 84 | } 85 | case err := <-errs: 86 | if err == io.EOF || err == io.ErrUnexpectedEOF { 87 | return 88 | } 89 | logc.Errorf(context.Background(), "event subscription error: %v", err) 90 | } 91 | } 92 | }() 93 | } 94 | 95 | func (c Containerd) processEvent(ctx *ctx.Context, containerCtx context.Context, msg *events.Envelope) error { 96 | v := string(msg.Event.GetValue()) 97 | s := strings.TrimPrefix(v, "\n@") 98 | containerId := removeSpecialChars(s) 99 | containerId = strings.Split(containerId, "-")[0] 100 | 101 | t := msg.Event.GetTypeUrl() 102 | switch t { 103 | case create: 104 | if Exists(ctx, containerId) { 105 | return nil 106 | } 107 | 108 | _, err := ctx.ContainerdCli.LoadContainer(containerCtx, containerId) 109 | if err != nil { 110 | if errdefs.IsNotFound(err) { 111 | _, err = ctx.ContainerdCli.LoadContainer(containerCtx, containerId) 112 | } 113 | 114 | return err 115 | } 116 | 117 | meta, err := ctx.ContainerdCli.ContainerService().Get(containerCtx, containerId) 118 | if err != nil { 119 | return err 120 | } 121 | 122 | var spec oci.Spec 123 | err = json.Unmarshal(meta.Spec.GetValue(), &spec) 124 | if err != nil { 125 | return err 126 | } 127 | 128 | err = processCollectFile(ctx, spec.Process.Env, meta) 129 | if err != nil { 130 | return err 131 | } 132 | 133 | return err 134 | 135 | case delete: 136 | logc.Infof(context.Background(), "Process container destroy event: %s", containerId) 137 | 138 | err := DelContainerLogFile(ctx, containerId) 139 | if err != nil { 140 | logc.Errorf(context.Background(), fmt.Sprintf("Process container destroy event error: %s, %s", containerId, err.Error())) 141 | } 142 | } 143 | return nil 144 | } 145 | 146 | func processCollectFile(c *ctx.Context, envs []string, meta containers.Container) error { 147 | // 符合条件的 Env 148 | var logEnvs []string 149 | for _, envVar := range envs { 150 | // LogPrefix: aliyun_logs_tencent-prod-hermione=stdout ,envVar: aliyun_logs 151 | if strings.HasPrefix(envVar, c.LogPrefix) { 152 | logEnvs = append(logEnvs, envVar) 153 | } 154 | } 155 | 156 | fields := CollectFields{ 157 | Id: meta.ID, 158 | Env: logEnvs, 159 | Labels: meta.Labels, 160 | LogPath: fmt.Sprintf("%s_%s_%s-*.log", meta.Labels[runtime.KubernetesPodName], meta.Labels[runtime.KubernetesContainerNamespace], meta.Labels[runtime.KubernetesContainerName]), 161 | } 162 | return NewCollectFile(c, fields) 163 | } 164 | 165 | func removeSpecialChars(str string) string { 166 | re := regexp.MustCompile(`[^a-zA-Z0-9]+`) 167 | return re.ReplaceAllString(str, "-") 168 | } 169 | -------------------------------------------------------------------------------- /pkg/provider/filebeat.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "fmt" 8 | "github.com/elastic/go-ucfg" 9 | "github.com/elastic/go-ucfg/yaml" 10 | "github.com/zeromicro/go-zero/core/logc" 11 | "io/ioutil" 12 | "os" 13 | "os/exec" 14 | "path/filepath" 15 | "strings" 16 | "text/template" 17 | logtypes "watchlog/log/config" 18 | ) 19 | 20 | // FilebeatPointer Filebeat 插件 21 | type FilebeatPointer struct { 22 | cmd *exec.Cmd 23 | Name string 24 | Tmpl *template.Template 25 | BaseDir string 26 | } 27 | 28 | func NewFilebeatPointer(Tmpl *template.Template, BaseDir string) FilebeatPointer { 29 | return FilebeatPointer{ 30 | Name: "Filebeat", 31 | Tmpl: Tmpl, 32 | BaseDir: BaseDir, 33 | } 34 | } 35 | 36 | // Start 启动采集器 37 | func (f FilebeatPointer) Start() error { 38 | if f.cmd != nil { 39 | pid := f.cmd.Process.Pid 40 | return fmt.Errorf("Filebeat process is exists, PID: %d", pid) 41 | } 42 | 43 | f.cmd = exec.Command(FilebeatExecCmd, "-c", FilebeatConfFile) 44 | f.cmd.Stderr = os.Stderr 45 | f.cmd.Stdout = os.Stdout 46 | err := f.cmd.Start() 47 | if err != nil { 48 | logc.Errorf(context.Background(), "Filebeat start fail: %s", err) 49 | } 50 | 51 | go func() { 52 | logc.Infof(context.Background(), "Starting Filebeat pid: %v", f.cmd.Process.Pid) 53 | err := f.cmd.Wait() 54 | if err != nil { 55 | logc.Errorf(context.Background(), "Filebeat exited: %v", err) 56 | if exitError, ok := err.(*exec.ExitError); ok { 57 | processState := exitError.ProcessState 58 | logc.Errorf(context.Background(), "Filebeat exited pid: %v", processState.Pid()) 59 | } 60 | } 61 | 62 | // try to restart filebeat 63 | logc.Debugf(context.Background(), "Filebeat exited and try to restart") 64 | f.cmd = nil 65 | err = f.cmd.Start() 66 | if err != nil { 67 | return 68 | } 69 | }() 70 | 71 | return err 72 | } 73 | 74 | // GetRegistryState 获取 filebeat 仓库中容器日志的基本信息 75 | func (f FilebeatPointer) GetRegistryState() (map[string]RegistryState, error) { 76 | file, err := os.Open(FilebeatRegistry) 77 | if err != nil { 78 | return nil, err 79 | } 80 | defer file.Close() 81 | 82 | decoder := json.NewDecoder(file) 83 | var state RegistryState 84 | err = decoder.Decode(&state) 85 | if err != nil { 86 | return nil, err 87 | } 88 | 89 | statesMap := make(map[string]RegistryState, 0) 90 | if _, ok := statesMap[state.V.Source]; !ok { 91 | statesMap[state.V.Source] = state 92 | } 93 | 94 | return statesMap, nil 95 | } 96 | 97 | // LoadConfigPaths 加载容器config, path 98 | func (f FilebeatPointer) LoadConfigPaths() map[string]string { 99 | paths := make(map[string]string, 0) 100 | // 读取 inputs.d 目录下所有配置 101 | confs, _ := ioutil.ReadDir(FilebeatConfDir) 102 | for _, conf := range confs { 103 | // get file name 104 | container := strings.TrimRight(conf.Name(), ".yml") 105 | config, err := f.ParseConfig(container) 106 | if err != nil || config == nil { 107 | continue 108 | } 109 | 110 | for _, path := range config.Paths { 111 | if _, ok := paths[path]; !ok { 112 | paths[path] = container 113 | } 114 | } 115 | } 116 | return paths 117 | } 118 | 119 | type Config struct { 120 | Paths []string `config:"paths"` 121 | } 122 | 123 | var configOpts = []ucfg.Option{ 124 | ucfg.PathSep("."), 125 | ucfg.ResolveEnv, 126 | ucfg.VarExp, 127 | } 128 | 129 | // ParseConfig 解析容器信息配置,获取path信息 130 | func (f FilebeatPointer) ParseConfig(container string) (*Config, error) { 131 | // get config full path, /etc/filebeat/inputs.d/*.yml 132 | confPath := f.GetConfPath(container) 133 | c, err := yaml.NewConfigWithFile(confPath, configOpts...) 134 | if err != nil { 135 | logc.Errorf(context.Background(), "read %s.yml log config error: %v", container, err) 136 | return nil, err 137 | } 138 | 139 | var config Config 140 | if err := c.Unpack(&config); err != nil { 141 | logc.Errorf(context.Background(), "parse %s.yml log config error: %v", container, err) 142 | return nil, err 143 | } 144 | return &config, nil 145 | } 146 | 147 | // RenderLogConfig 生成日志采集配置文件 148 | func (f FilebeatPointer) RenderLogConfig(containerId string, container map[string]string, configList []logtypes.LogConfig) (string, error) { 149 | for _, config := range configList { 150 | logc.Infof(context.Background(), "logs: %s = %v", containerId, config) 151 | } 152 | 153 | var buf bytes.Buffer 154 | m := map[string]interface{}{ 155 | "containerId": containerId, 156 | "configList": configList, 157 | "container": container, 158 | "output": "FILEBEAT_OUTPUT", 159 | } 160 | if err := f.Tmpl.Execute(&buf, m); err != nil { 161 | return "", err 162 | } 163 | 164 | return buf.String(), nil 165 | } 166 | 167 | // CleanConfigs 清理旧配置 168 | func (f FilebeatPointer) CleanConfigs() error { 169 | confDir := f.GetConfHome() 170 | d, err := os.Open(confDir) 171 | if err != nil { 172 | return err 173 | } 174 | defer d.Close() 175 | 176 | // 获取目录下所有数据, 包括目录和文件 177 | names, err := d.Readdirnames(-1) 178 | if err != nil { 179 | return err 180 | } 181 | 182 | for _, name := range names { 183 | conf := filepath.Join(confDir, name) 184 | stat, err := os.Stat(filepath.Join(confDir, name)) 185 | if err != nil { 186 | return err 187 | } 188 | // 是否为普通文件 189 | if stat.Mode().IsRegular() { 190 | if err := os.Remove(conf); err != nil { 191 | return err 192 | } 193 | } 194 | } 195 | return nil 196 | } 197 | -------------------------------------------------------------------------------- /assets/filebeat/config.filebeat: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | BIN="/usr/bin" 6 | PATH="/usr/share/filebeat" 7 | 8 | FILEBEAT_CONFIG="${PATH}/filebeat.yml" 9 | if [ -f "$FILEBEAT_CONFIG" ]; then 10 | ${BIN}/rm -rf ${FILEBEAT_CONFIG}; 11 | fi 12 | 13 | INPUTS_DIR="${PATH}/inputs.d" 14 | if [ ! -d ${INPUTS_DIR} ]; then 15 | ${BIN}/mkdir -p ${INPUTS_DIR}; 16 | fi 17 | 18 | assert_not_empty() { 19 | arg=$1 20 | shift 21 | if [ -z "$arg" ]; then 22 | ${BIN}/echo "ERROR $@" 23 | exit 1 24 | fi 25 | } 26 | 27 | cd $(${BIN}/dirname $0) 28 | 29 | base() { 30 | ${BIN}/cat >> $FILEBEAT_CONFIG << EOF 31 | processors: 32 | - add_cloud_metadata: ~ 33 | 34 | filebeat.config: 35 | modules: 36 | path: ${PATH}/modules.d/*.yml 37 | reload.enabled: false 38 | 39 | inputs: 40 | path: ${PATH}/inputs.d/*.yml 41 | reload.enabled: true 42 | EOF 43 | } 44 | 45 | es() { 46 | if [ -f "/run/secrets/es_credential" ]; then 47 | ELASTICSEARCH_USER=$(${BIN}/cat /run/secrets/es_credential | ${BIN}/awk -F":" '{ print $1 }') 48 | ELASTICSEARCH_PASSWORD=$(${BIN}/cat /run/secrets/es_credential | ${BIN}/awk -F":" '{ print $2 }') 49 | fi 50 | 51 | if [ -n "$ELASTICSEARCH_HOSTS" ]; then 52 | ELASTICSEARCH_HOSTS=$(${BIN}/echo $ELASTICSEARCH_HOSTS| ${BIN}/awk -F, '{for(i=1;i<=NF;i++){printf "\"%s\",", $i}}') 53 | ELASTICSEARCH_HOSTS=${ELASTICSEARCH_HOSTS%,} 54 | else 55 | assert_not_empty "$ELASTICSEARCH_HOST" "ELASTICSEARCH_HOST required" 56 | assert_not_empty "$ELASTICSEARCH_PORT" "ELASTICSEARCH_PORT required" 57 | ELASTICSEARCH_HOSTS="\"$ELASTICSEARCH_HOST:$ELASTICSEARCH_PORT\"" 58 | fi 59 | 60 | ${BIN}/cat >> $FILEBEAT_CONFIG << EOF 61 | $(base) 62 | output.elasticsearch: 63 | hosts: [$ELASTICSEARCH_HOSTS] 64 | ${ELASTICSEARCH_SCHEME:+protocol: ${ELASTICSEARCH_SCHEME}} 65 | ${ELASTICSEARCH_USER:+username: ${ELASTICSEARCH_USER}} 66 | ${ELASTICSEARCH_PASSWORD:+password: ${ELASTICSEARCH_PASSWORD}} 67 | ${ELASTICSEARCH_WORKER:+worker: ${ELASTICSEARCH_WORKER}} 68 | ${ELASTICSEARCH_PATH:+path: ${ELASTICSEARCH_PATH}} 69 | ${ELASTICSEARCH_BULK_MAX_SIZE:+bulk_max_size: ${ELASTICSEARCH_BULK_MAX_SIZE}} 70 | indices: 71 | - index: "${LOG_PREFIX:-watchlog}-%{+yyy-MM.dd}" 72 | EOF 73 | } 74 | 75 | default() { 76 | ${BIN}/echo "use default output" 77 | ${BIN}/cat >> $FILEBEAT_CONFIG << EOF 78 | $(base) 79 | output.console: 80 | pretty: ${CONSOLE_PRETTY:-false} 81 | EOF 82 | } 83 | 84 | file() { 85 | assert_not_empty "$FILE_PATH" "FILE_PATH required" 86 | 87 | ${BIN}/cat >> $FILEBEAT_CONFIG << EOF 88 | $(base) 89 | output.file: 90 | path: $FILE_PATH 91 | ${FILE_NAME:+filename: ${FILE_NAME}} 92 | ${FILE_ROTATE_SIZE:+rotate_every_kb: ${FILE_ROTATE_SIZE}} 93 | ${FILE_NUMBER_OF_FILES:+number_of_files: ${FILE_NUMBER_OF_FILES}} 94 | ${FILE_PERMISSIONS:+permissions: ${FILE_PERMISSIONS}} 95 | EOF 96 | } 97 | 98 | logstash() { 99 | assert_not_empty "$LOGSTASH_HOST" "LOGSTASH_HOST required" 100 | assert_not_empty "$LOGSTASH_PORT" "LOGSTASH_PORT required" 101 | 102 | ${BIN}/cat >> $FILEBEAT_CONFIG << EOF 103 | $(base) 104 | output.logstash: 105 | hosts: ["$LOGSTASH_HOST:$LOGSTASH_PORT"] 106 | index: ${FILEBEAT_INDEX:-filebeat}-%{+yyyy.MM.dd} 107 | ${LOGSTASH_WORKER:+worker: ${LOGSTASH_WORKER}} 108 | ${LOGSTASH_LOADBALANCE:+loadbalance: ${LOGSTASH_LOADBALANCE}} 109 | ${LOGSTASH_BULK_MAX_SIZE:+bulk_max_size: ${LOGSTASH_BULK_MAX_SIZE}} 110 | ${LOGSTASH_SLOW_START:+slow_start: ${LOGSTASH_SLOW_START}} 111 | EOF 112 | } 113 | 114 | redis() { 115 | assert_not_empty "$REDIS_HOST" "REDIS_HOST required" 116 | assert_not_empty "$REDIS_PORT" "REDIS_PORT required" 117 | 118 | ${BIN}/cat >> $FILEBEAT_CONFIG << EOF 119 | $(base) 120 | output.redis: 121 | hosts: ["$REDIS_HOST:$REDIS_PORT"] 122 | key: "%{[fields.topic]:filebeat}" 123 | ${REDIS_WORKER:+worker: ${REDIS_WORKER}} 124 | ${REDIS_PASSWORD:+password: ${REDIS_PASSWORD}} 125 | ${REDIS_DATATYPE:+datatype: ${REDIS_DATATYPE}} 126 | ${REDIS_LOADBALANCE:+loadbalance: ${REDIS_LOADBALANCE}} 127 | ${REDIS_TIMEOUT:+timeout: ${REDIS_TIMEOUT}} 128 | ${REDIS_BULK_MAX_SIZE:+bulk_max_size: ${REDIS_BULK_MAX_SIZE}} 129 | EOF 130 | } 131 | 132 | kafka() { 133 | assert_not_empty "$KAFKA_BROKERS" "KAFKA_BROKERS required" 134 | KAFKA_BROKERS=$(${BIN}/echo $KAFKA_BROKERS| ${BIN}/awk -F, '{for(i=1;i<=NF;i++){printf "\"%s\",", $i}}') 135 | KAFKA_BROKERS=${KAFKA_BROKERS%,} 136 | 137 | ${BIN}/cat >> $FILEBEAT_CONFIG << EOF 138 | $(base) 139 | output.kafka: 140 | hosts: [$KAFKA_BROKERS] 141 | topic: '%{[topic]}' 142 | ${KAFKA_VERSION:+version: ${KAFKA_VERSION}} 143 | ${KAFKA_USERNAME:+username: ${KAFKA_USERNAME}} 144 | ${KAFKA_PASSWORD:+password: ${KAFKA_PASSWORD}} 145 | ${KAFKA_WORKER:+worker: ${KAFKA_WORKER}} 146 | ${KAFKA_PARTITION_KEY:+key: ${KAFKA_PARTITION_KEY}} 147 | ${KAFKA_PARTITION:+partition: ${KAFKA_PARTITION}} 148 | ${KAFKA_CLIENT_ID:+client_id: ${KAFKA_CLIENT_ID}} 149 | ${KAFKA_METADATA:+metadata: ${KAFKA_METADATA}} 150 | ${KAFKA_BULK_MAX_SIZE:+bulk_max_size: ${KAFKA_BULK_MAX_SIZE}} 151 | ${KAFKA_BROKER_TIMEOUT:+broker_timeout: ${KAFKA_BROKER_TIMEOUT}} 152 | ${KAFKA_CHANNEL_BUFFER_SIZE:+channel_buffer_size: ${KAFKA_CHANNEL_BUFFER_SIZE}} 153 | ${KAFKA_KEEP_ALIVE:+keep_alive ${KAFKA_KEEP_ALIVE}} 154 | ${KAFKA_MAX_MESSAGE_BYTES:+max_message_bytes: ${KAFKA_MAX_MESSAGE_BYTES}} 155 | ${KAFKA_REQUIRE_ACKS:+required_acks: ${KAFKA_REQUIRE_ACKS}} 156 | EOF 157 | } 158 | 159 | count(){ 160 | ${BIN}/cat >> $FILEBEAT_CONFIG << EOF 161 | $(base) 162 | output.count: 163 | EOF 164 | } 165 | 166 | if [ -n "$FILEBEAT_OUTPUT" ]; then 167 | LOGGING_OUTPUT=$FILEBEAT_OUTPUT 168 | fi 169 | 170 | case "$LOGGING_OUTPUT" in 171 | elasticsearch) 172 | es;; 173 | logstash) 174 | logstash;; 175 | file) 176 | file;; 177 | redis) 178 | redis;; 179 | kafka) 180 | kafka;; 181 | count) 182 | count;; 183 | *) 184 | default 185 | esac 186 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU= 3 | github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= 4 | github.com/AdamKorcz/go-118-fuzz-build v0.0.0-20230306123547-8075edf89bb0 h1:59MxjQVfjXsBpLy+dbd2/ELV5ofnUkUZBvWSC85sheA= 5 | github.com/AdamKorcz/go-118-fuzz-build v0.0.0-20230306123547-8075edf89bb0/go.mod h1:OahwfttHWG6eJ0clwcfBAHoDI6X/LV/15hx/wlMZSrU= 6 | github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= 7 | github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= 8 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 9 | github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= 10 | github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= 11 | github.com/Microsoft/hcsshim v0.11.4 h1:68vKo2VN8DE9AdN4tnkWnmdhqdbpUFM8OF3Airm7fz8= 12 | github.com/Microsoft/hcsshim v0.11.4/go.mod h1:smjE4dvqPX9Zldna+t5FG3rnoHhaB7QYxPRqGcpAD9w= 13 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 14 | github.com/checkpoint-restore/go-criu/v5 v5.3.0/go.mod h1:E/eQpaFtUKGOOSEBZgmKAcn+zUUwWxqcaKZlF54wK8E= 15 | github.com/cilium/ebpf v0.7.0/go.mod h1:/oI2+1shJiTGAMgl6/RgJr36Eo1jzrRcAWbcXO2usCA= 16 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 17 | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= 18 | github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM= 19 | github.com/containerd/cgroups v1.1.0/go.mod h1:6ppBcbh/NOOUU+dMKrykgaBnK9lCIBxHqJDGwsa1mIw= 20 | github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= 21 | github.com/containerd/containerd v1.7.7 h1:QOC2K4A42RQpcrZyptP6z9EJZnlHfHJUfZrAAHe15q4= 22 | github.com/containerd/containerd v1.7.7/go.mod h1:3c4XZv6VeT9qgf9GMTxNTMFxGJrGpI2vz1yk4ye+YY8= 23 | github.com/containerd/continuity v0.4.2 h1:v3y/4Yz5jwnvqPKJJ+7Wf93fyWoCB3F5EclWG023MDM= 24 | github.com/containerd/continuity v0.4.2/go.mod h1:F6PTNCKepoxEaXLQp3wDAjygEnImnZ/7o4JzpodfroQ= 25 | github.com/containerd/fifo v1.1.0 h1:4I2mbh5stb1u6ycIABlBw9zgtlK8viPI9QkQNRQEEmY= 26 | github.com/containerd/fifo v1.1.0/go.mod h1:bmC4NWMbXlt2EZ0Hc7Fx7QzTFxgPID13eH0Qu+MAb2o= 27 | github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= 28 | github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= 29 | github.com/containerd/ttrpc v1.2.2 h1:9vqZr0pxwOF5koz6N0N3kJ0zDHokrcPxIR/ZR2YFtOs= 30 | github.com/containerd/ttrpc v1.2.2/go.mod h1:sIT6l32Ph/H9cvnJsfXM5drIVzTr5A2flTf1G5tYZak= 31 | github.com/containerd/typeurl/v2 v2.1.1 h1:3Q4Pt7i8nYwy2KmQWIw2+1hTvwTE/6w9FqcttATPO/4= 32 | github.com/containerd/typeurl/v2 v2.1.1/go.mod h1:IDp2JFvbwZ31H8dQbEIY7sDl2L3o3HZj1hsSQlywkQ0= 33 | github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= 34 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 35 | github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= 36 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 37 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 38 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 39 | github.com/docker/distribution v2.8.1+incompatible h1:Q50tZOPR6T/hjNsyc9g8/syEs6bk8XXApsHjKukMl68= 40 | github.com/docker/distribution v2.8.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= 41 | github.com/docker/docker v23.0.3+incompatible h1:9GhVsShNWz1hO//9BNg/dpMnZW25KydO4wtVxWAIbho= 42 | github.com/docker/docker v23.0.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= 43 | github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= 44 | github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= 45 | github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c h1:+pKlWGMw7gf6bQ+oDZB4KHQFypsfjYlq/C4rfL7D3g8= 46 | github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= 47 | github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= 48 | github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= 49 | github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= 50 | github.com/elastic/go-ucfg v0.8.8 h1:54KIF/2zFKfl0MzsSOCGOsZ3O2bnjFQJ0nDJcLhviyk= 51 | github.com/elastic/go-ucfg v0.8.8/go.mod h1:4E8mPOLSUV9hQ7sgLEJ4bvt0KhMuDJa8joDT2QGAEKA= 52 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 53 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 54 | github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= 55 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 56 | github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= 57 | github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= 58 | github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= 59 | github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 60 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 61 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 62 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 63 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 64 | github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 65 | github.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 66 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 67 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 68 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 69 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 70 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= 71 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 72 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 73 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 74 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 75 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 76 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 77 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 78 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 79 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 80 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 81 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 82 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 83 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 84 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 85 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 86 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 87 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 88 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 89 | github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 90 | github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 91 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 92 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 93 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 94 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 95 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 96 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 97 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 98 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 99 | github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= 100 | github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= 101 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 102 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 103 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 104 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 105 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 106 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 107 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 108 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 109 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 110 | github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg= 111 | github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc= 112 | github.com/moby/sys/mountinfo v0.5.0/go.mod h1:3bMD3Rg+zkqx8MRYPi7Pyb0Ie97QEBmdxbhnCLlSvSU= 113 | github.com/moby/sys/mountinfo v0.6.2 h1:BzJjoreD5BMFNmD9Rus6gdd1pLuecOFPt8wC+Vygl78= 114 | github.com/moby/sys/mountinfo v0.6.2/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI= 115 | github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc= 116 | github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo= 117 | github.com/moby/sys/signal v0.7.0 h1:25RW3d5TnQEoKvRbEKUGay6DCQ46IxAVTT9CUMgmsSI= 118 | github.com/moby/sys/signal v0.7.0/go.mod h1:GQ6ObYZfqacOwTtlXvcmh9A26dVRul/hbOZn88Kg8Tg= 119 | github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= 120 | github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= 121 | github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= 122 | github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= 123 | github.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ= 124 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= 125 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 126 | github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= 127 | github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= 128 | github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= 129 | github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= 130 | github.com/opencontainers/runc v1.1.5 h1:L44KXEpKmfWDcS02aeGm8QNTFXTo2D+8MYGDIJ/GDEs= 131 | github.com/opencontainers/runc v1.1.5/go.mod h1:1J5XiS+vdZ3wCyZybsuxXZWGrgSr8fFJHLXuG2PsnNg= 132 | github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= 133 | github.com/opencontainers/runtime-spec v1.1.0 h1:HHUyrt9mwHUjtasSbXSMvs4cyFxh+Bll4AjJ9odEGpg= 134 | github.com/opencontainers/runtime-spec v1.1.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= 135 | github.com/opencontainers/selinux v1.10.0/go.mod h1:2i0OySw99QjzBBQByd1Gr9gSjvuho1lHsJxIJ3gGbJI= 136 | github.com/opencontainers/selinux v1.11.0 h1:+5Zbo97w3Lbmb3PeqQtpmTkMwsW5nRI3YaLpt7tQ7oU= 137 | github.com/opencontainers/selinux v1.11.0/go.mod h1:E5dMC3VPuVvVHDYmi78qvhJp8+M586T4DlDRYpFkyec= 138 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 139 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 140 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 141 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 142 | github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= 143 | github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= 144 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 145 | github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= 146 | github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= 147 | github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= 148 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 149 | github.com/seccomp/libseccomp-golang v0.9.2-0.20220502022130-f33da4d89646/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg= 150 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 151 | github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= 152 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 153 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 154 | github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= 155 | github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 156 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 157 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 158 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 159 | github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= 160 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 161 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 162 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 163 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 164 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 165 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 166 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 167 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 168 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 169 | github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= 170 | github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= 171 | github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE= 172 | github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU= 173 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 174 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 175 | github.com/zeromicro/go-zero v1.7.4 h1:lyIUsqbpVRzM4NmXu5pRM3XrdRdUuWOkQmHiNmJF0VU= 176 | github.com/zeromicro/go-zero v1.7.4/go.mod h1:jmv4hTdUBkDn6kxgI+WrKQw0q6LKxDElGPMfCLOeeEY= 177 | go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= 178 | go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= 179 | go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo= 180 | go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo= 181 | go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI= 182 | go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco= 183 | go.opentelemetry.io/otel/sdk v1.24.0 h1:YMPPDNymmQN3ZgczicBY3B6sf9n62Dlj9pWD3ucgoDw= 184 | go.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35HoYiQWGDFg= 185 | go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI= 186 | go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= 187 | go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= 188 | go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= 189 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 190 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 191 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 192 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 193 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 194 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 195 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 196 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 197 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 198 | golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= 199 | golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 200 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 201 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 202 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 203 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 204 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 205 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 206 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 207 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 208 | golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 209 | golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 210 | golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo= 211 | golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= 212 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 213 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 214 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 215 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 216 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 217 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 218 | golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 219 | golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= 220 | golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 221 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 222 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 223 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 224 | golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 225 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 226 | golang.org/x/sys v0.0.0-20191115151921-52ab43148777/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 227 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 228 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 229 | golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 230 | golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 231 | golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 232 | golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 233 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 234 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 235 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 236 | golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 237 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 238 | golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= 239 | golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 240 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 241 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 242 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 243 | golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= 244 | golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= 245 | golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg= 246 | golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 247 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 248 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 249 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 250 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 251 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 252 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 253 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 254 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 255 | golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= 256 | golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= 257 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 258 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 259 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 260 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 261 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 262 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 263 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 264 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 265 | google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 266 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 267 | google.golang.org/genproto v0.0.0-20230711160842-782d3b101e98 h1:Z0hjGZePRE0ZBWotvtrwxFNrNE9CUAGtplaDK5NNI/g= 268 | google.golang.org/genproto v0.0.0-20230711160842-782d3b101e98/go.mod h1:S7mY02OqCJTD0E1OiQy1F72PWFB4bZJ87cAtLPYgDR0= 269 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 h1:BwIjyKYGsK9dMCBOorzRri8MQwmi7mT9rGHsCEinZkA= 270 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= 271 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 272 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 273 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= 274 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 275 | google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 276 | google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= 277 | google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc= 278 | google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= 279 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 280 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 281 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 282 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 283 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 284 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 285 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 286 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 287 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 288 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 289 | google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 290 | google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io= 291 | google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 292 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 293 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= 294 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 295 | gopkg.in/hjson/hjson-go.v3 v3.0.1/go.mod h1:X6zrTSVeImfwfZLfgQdInl9mWjqPqgH90jom9nym/lw= 296 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 297 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 298 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 299 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 300 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 301 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 302 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 303 | gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= 304 | gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= 305 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 306 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 307 | --------------------------------------------------------------------------------