Repository: dtan4/k8stail Branch: master Commit: c3afcb05dee3 Files: 19 Total size: 38.1 KB Directory structure: gitextract_b8pp44tf/ ├── .dockerignore ├── .github/ │ └── workflows/ │ ├── release.yaml │ └── test.yaml ├── .gitignore ├── .goreleaser.yml ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── counter.yaml ├── go.mod ├── go.sum ├── logger.go ├── main.go ├── renovate.json ├── tail.go ├── version.go └── watch.go ================================================ FILE CONTENTS ================================================ ================================================ FILE: .dockerignore ================================================ # Created by https://www.gitignore.io/api/go ### Go ### # Compiled Object files, Static and Dynamic libs (Shared Objects) *.o *.a *.so # Folders _obj _test # Architecture specific extensions/prefixes *.[568vq] [568vq].out *.cgo1.go *.cgo2.c _cgo_defun.c _cgo_gotypes.go _cgo_export.* _testmain.go *.exe *.test *.prof # Output of the go coverage tool, specifically when used with LiteIDE *.out # external packages folder vendor/ bin/ dist/ .git .gitignore README.md ================================================ FILE: .github/workflows/release.yaml ================================================ name: Release on: push: tags: - "v*.*.*" jobs: goreleaser: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - name: Setup Go uses: actions/setup-go@v5 with: go-version: 1.20 - name: Run GoReleaser uses: goreleaser/goreleaser-action@v6 with: version: latest args: release --rm-dist env: # To upload Homebrew recipe to dtan4/homebrew-tools, we need a personal token # instead of Action's temporary token GITHUB_TOKEN: ${{ secrets.PERSONAL_GITHUB_TOKEN }} ================================================ FILE: .github/workflows/test.yaml ================================================ name: Test on: push: branches: - master pull_request: jobs: test: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - name: Setup Go uses: actions/setup-go@v5 with: cache: true go-version-file: 'go.mod' - name: Run tests run: make ci-test - name: Send test coverage to Codecov uses: codecov/codecov-action@v5 - name: Run Trivy vulnerability scanner in repo mode uses: aquasecurity/trivy-action@v0.36.0 with: scan-type: "fs" ignore-unfixed: true vuln-type: "os,library" severity: "CRITICAL,HIGH" exit-code: "1" ================================================ FILE: .gitignore ================================================ # Created by https://www.gitignore.io/api/go ### Go ### # Compiled Object files, Static and Dynamic libs (Shared Objects) *.o *.a *.so # Folders _obj _test # Architecture specific extensions/prefixes *.[568vq] [568vq].out *.cgo1.go *.cgo2.c _cgo_defun.c _cgo_gotypes.go _cgo_export.* _testmain.go *.exe *.test *.prof # Output of the go coverage tool, specifically when used with LiteIDE *.out # external packages folder vendor/ /bin/ /dist/ ================================================ FILE: .goreleaser.yml ================================================ # This is an example goreleaser.yaml file with some sane defaults. # Make sure to check the documentation at http://goreleaser.com before: hooks: - make clean - go mod tidy builds: - ldflags: - "-s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}" env: - CGO_ENABLED=0 goos: - darwin - linux - windows goarch: - 386 - amd64 - arm - arm64 archives: - name_template: '{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}' replacements: darwin: Darwin linux: Linux windows: Windows 386: i386 amd64: x86_64 format_overrides: - goos: windows format: zip release: prerelease: auto brews: - tap: owner: dtan4 name: homebrew-tools folder: Formula homepage: https://github.com/dtan4/k8stail description: "`tail -f` experience for Kubernetes Pods" skip_upload: auto # skip if the version is rc (e.g. v1.0.0-rc1) test: | system "#{bin}/k8stail", "-v" checksum: name_template: 'checksums.txt' snapshot: name_template: "{{ .Tag }}-next" changelog: sort: asc filters: exclude: - '^docs:' - '^test:' - Merge pull request - Merge branch ================================================ FILE: CHANGELOG.md ================================================ # [v0.7.0](https://github.com/dtan4/k8stail/releases/tag/v0.7.0) (2022-03-05) - Go 1.17 - Update dependencies # [v0.6.0](https://github.com/dtan4/k8stail/releases/tag/v0.6.0) (2018-07-29) ## Features - Enable external auth provider [#40](https://github.com/dtan4/k8stail/pull/40) ## Others - Use Go 1.10.3 on Travis CI [#42](https://github.com/dtan4/k8stail/pull/42) - Upgrade to client-go 8.0.0 [#39](https://github.com/dtan4/k8stail/pull/39) # [v0.5.2.rc1](https://github.com/dtan4/k8stail/releases/tag/v0.5.2.rc1) (2017-04-27) ## Features - Add debug flag to enable pprof [#31](https://github.com/dtan4/k8stail/pull/31) ## Fixed - Close goroutine immediately if there is no valid object [#30](https://github.com/dtan4/k8stail/pull/30) # [v0.5.1](https://github.com/dtan4/k8stail/releases/tag/v0.5.1) (2017-04-27) ## Fixed - Print selected context correctly [#28](https://github.com/dtan4/k8stail/pull/28) # [v0.5.0](https://github.com/dtan4/k8stail/releases/tag/v0.5.0) (2017-04-25) ## Features - Watch Kubernetes events to detect Pod lifecycle correctly [#26](https://github.com/dtan4/k8stail/pull/26) - Add more short flags [#25](https://github.com/dtan4/k8stail/pull/25) (thanks @atombender) # [v0.4.0](https://github.com/dtan4/k8stail/releases/tag/v0.4.0) (2017-04-11) ## Features - Use default namespace set in kubecfg [#21](https://github.com/dtan4/k8stail/pull/21) - Add `--no-halt` flag [#18](https://github.com/dtan4/k8stail/pull/18) ## Fixed - Detect container recreation [#20](https://github.com/dtan4/k8stail/pull/20) # [v0.3.0](https://github.com/dtan4/k8stail/releases/tag/v0.3.0) (2016-12-12) ## Backward incompatible changes - Deprecate `-flag` style flag, use `--flag` [#11](https://github.com/dtan4/k8stail/pull/11) ## Features - Support context switch by `--context` flag [#13](https://github.com/dtan4/k8stail/pull/13) (Thanks @apstndb) # [v0.2.1](https://github.com/dtan4/k8stail/releases/tag/v0.2.1) (2016-11-16) Rebuilt binaries to be statically-linked. # [v0.2.0](https://github.com/dtan4/k8stail/releases/tag/v0.2.0) (2016-11-16) ## Features - Stream logs of all containers in pod [#5](https://github.com/dtan4/k8stail/pull/5) - Get kubeconfig path from KUBECONFIG [#4](https://github.com/dtan4/k8stail/pull/4) # [v0.1.0](https://github.com/dtan4/k8stail/releases/tag/v0.1.0) (2016-11-15) Initial release. ================================================ FILE: Dockerfile ================================================ FROM golang:1.26 AS builder WORKDIR /go/src/github.com/dtan4/k8stail COPY go.mod go.sum ./ RUN go mod download COPY . . RUN CGO_ENABLED=0 go build -o /k8stail FROM gcr.io/distroless/static:nonroot COPY --from=builder /k8stail /k8stail ENTRYPOINT ["/k8stail"] ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2016 Daisuke Fujita Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Makefile ================================================ NAME := k8stail VERSION := $(shell git tag | sort -V -r | head -n1)-next COMMIT := $(shell git rev-parse HEAD) DATE := $(shell date "+%Y-%m-%dT%H:%M:%S%z") SRCS := $(shell find . -name '*.go' -type f) LDFLAGS := -ldflags="-s -w -X \"main.version=$(VERSION)\" -X \"main.commit=$(COMMIT)\" -X \"main.date=$(DATE)\"" .DEFAULT_GOAL := bin/$(NAME) export GO111MODULE=on bin/$(NAME): $(SRCS) go build $(LDFLAGS) -o bin/$(NAME) .PHONY: ci-test ci-test: go test -coverpkg=./... -coverprofile=coverage.txt -v ./... .PHONY: clean clean: rm -rf bin/* .PHONY: install install: go install $(LDFLAGS) .PHONY: test test: go test -cover -v ================================================ FILE: README.md ================================================ # k8stail [![GitHub Actions](https://github.com/dtan4/k8stail/workflows/Test/badge.svg)](https://github.com/dtan4/k8stail/actions?query=workflow%3ATest+branch%3Amaster) [![codecov](https://codecov.io/gh/dtan4/k8stail/branch/master/graph/badge.svg)](https://codecov.io/gh/dtan4/k8stail) [![GitHub release](https://img.shields.io/github/release/dtan4/k8stail.svg)](https://github.com/dtan4/k8stail/releases) `tail -f` experience for Kubernetes Pods As you know, `kubectl logs` can stream only ONE pod at the same time. `k8stail` enables you to watch __log streams of ALL pods__ in the specified namespace or labels in real time, like `tail -f`. ![example](_images/example.png) ## Table of Contents * [Requirements](#requirements) * [Installation](#installation) + [Using Homebrew (OS X only)](#using-homebrew-os-x-only) + [Precompiled binary](#precompiled-binary) + [From source](#from-source) + [Run in a Docker container](#run-in-a-docker-container) * [Usage](#usage) + [kubeconfig file](#kubeconfig-file) + [Options](#options) * [Development](#development) * [Author](#author) * [License](#license) ## Requirements Kubernetes 1.3 or above ## Installation ### Using Homebrew (OS X only) Formula is available at [dtan4/homebrew-dtan4](https://github.com/dtan4/homebrew-dtan4). ```bash $ brew tap dtan4/dtan4 $ brew install k8stail ``` ### Precompiled binary Precompiled binaries for Windows, OS X, Linux are available at [Releases](https://github.com/dtan4/k8stail/releases). ### From source ```bash $ go get -d github.com/dtan4/k8stail $ cd $GOPATH/src/github.com/dtan4/k8stail $ make deps $ make install ``` ### Run in a Docker container Docker image is no longer provided officially. If you'd like to run k8sec in Docker image, see [`Dockerfile`](Dockerfile) and build image by yourself. ```bash docker build -t k8stail . ``` ## Usage Logs of all pods, all containers in pod in the specified namespace are streaming. When new pod is added, logs of the pod also appears. To stop streaming and exit, press `Ctrl-C`. ```bash $ k8stail --namespace awesome-app Namespace: awesome-app Labels: ---------- Pod awesome-app-web-4212725599-67vd4 has detected Pod awesome-app-web-4212725599-6pduy has detected Pod awesome-app-web-4212725599-lbuny has detected Pod awesome-app-web-4212725599-mh3g1 has detected Pod awesome-app-web-4212725599-pvjsm has detected [awesome-app-web-4212725599-mh3g1][web] | creating base compositions... [awesome-app-web-4212725599-zei9h][web] | (47.1ms) CREATE TABLE "schema_migrations" ("version" character varying NOT NULL) [awesome-app-web-4212725599-zei9h][web] | (45.1ms) CREATE UNIQUE INDEX "unique_schema_migrations" ON "schema_migrations" ("version") [awesome-app-web-4212725599-zei9h][web] | ActiveRecord::SchemaMigration Load (1.8ms) SELECT "schema_migrations".* FROM "schema_migrations" [awesome-app-web-4212725599-zei9h][web] | Migrating to CreatePosts (20160218082522) ``` With `--timestamps` option, log timestamp is printed together. ```bash $ k8stail --namespace awesome-app --timestamps Namespace: awesome-app Labels: ---------- Pod awesome-app-web-4212725599-67vd4 has detected Pod awesome-app-web-4212725599-6pduy has detected Pod awesome-app-web-4212725599-lbuny has detected Pod awesome-app-web-4212725599-mh3g1 has detected Pod awesome-app-web-4212725599-pvjsm has detected [awesome-app-web-4212725599-mh3g1][web] 2016-11-15T10:57:22.178667425Z | creating base compositions... [awesome-app-web-4212725599-zei9h][web] 2016-11-15T10:57:22.309011520Z | (47.1ms) CREATE TABLE "schema_migrations" ("version" character varying NOT NULL) [awesome-app-web-4212725599-zei9h][web] 2016-11-15T10:57:22.309053601Z | (45.1ms) CREATE UNIQUE INDEX "unique_schema_migrations" ON "schema_migrations" ("version") [awesome-app-web-4212725599-zei9h][web] 2016-11-15T10:57:22.463700110Z | ActiveRecord::SchemaMigration Load (1.8ms) SELECT "schema_migrations".* FROM "schema_migrations" [awesome-app-web-4212725599-zei9h][web] 2016-11-15T10:57:22.463743373Z | Migrating to CreatePosts (20160218082522) ``` With `--labels` option, you can filter pods to watch. ```bash $ k8stail --namespace awesome-app --labels name=awesome-app-web Namespace: awesome-app Labels: name=awesome-app-web ---------- Pod awesome-app-web-4212725599-67vd4 has detected Pod awesome-app-web-4212725599-6pduy has detected Pod awesome-app-web-4212725599-lbuny has detected Pod awesome-app-web-4212725599-mh3g1 has detected Pod awesome-app-web-4212725599-pvjsm has detected [awesome-app-web-4212725599-mh3g1][web] | creating base compositions... [awesome-app-web-4212725599-zei9h][web] | (47.1ms) CREATE TABLE "schema_migrations" ("version" character varying NOT NULL) [awesome-app-web-4212725599-zei9h][web] | (45.1ms) CREATE UNIQUE INDEX "unique_schema_migrations" ON "schema_migrations" ("version") [awesome-app-web-4212725599-zei9h][web] | ActiveRecord::SchemaMigration Load (1.8ms) SELECT "schema_migrations".* FROM "schema_migrations" [awesome-app-web-4212725599-zei9h][web] | Migrating to CreatePosts (20160218082522) ``` ### kubeconfig file `k8stail` uses `~/.kube/config` as default. You can specify another path by `KUBECONFIG` environment variable or `--kubeconfig` option. `--kubeconfig` option always overrides `KUBECONFIG` environment variable. ```bash $ KUBECONFIG=/path/to/kubeconfig k8stail # or $ k8stail --kubeconfig=/path/to/kubeconfig ``` ### Options |Option|Description|Required|Default| |---------|-----------|-------|-------| |`--context=CONTEXT`|Kubernetes context||| |`--debug`|Debug mode using pprof (http://localhost:6060)||`false`| |`--kubeconfig=KUBECONFIG`|Path of kubeconfig||`~/.kube/config`| |`--labels=LABELS`|Label filter query (e.g. `app=APP,role=ROLE`)||| |`--namespace=NAMESPACE`|Kubernetes namespace||`default`| |`--timestamps`|Include timestamps on each line||`false`| |`-h`, `-help`|Print command line usage||| |`-v`, `-version`|Print version||| ## Development Go 1.7 or above is required. Clone this repository and build using `make`. ```bash $ go get -d github.com/dtan4/k8stail $ cd $GOPATH/src/github.com/dtan4/k8stail $ make ``` ## Author Daisuke Fujita ([@dtan4](https://github.com/dtan4)) ## License [![MIT License](http://img.shields.io/badge/license-MIT-blue.svg?style=flat)](LICENSE) ================================================ FILE: counter.yaml ================================================ apiVersion: apps/v1 kind: Deployment metadata: name: counter labels: app: counter spec: replicas: 3 selector: matchLabels: app: counter template: metadata: labels: app: counter spec: containers: - name: counter-a image: busybox args: ["/bin/sh", "-c", 'i=0; while true; do echo "$i: $(date)"; i=$((i+1)); sleep 1; done'] - name: counter-b image: busybox args: ["/bin/sh", "-c", 'i=0; while true; do echo "$i: $(date)"; i=$((i+1)); sleep 1; done'] ================================================ FILE: go.mod ================================================ module github.com/dtan4/k8stail go 1.26.3 require ( github.com/fatih/color v1.19.0 github.com/spf13/pflag v1.0.10 k8s.io/api v0.36.1 k8s.io/apimachinery v0.36.1 k8s.io/client-go v0.36.1 ) require ( github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/emicklei/go-restful/v3 v3.13.0 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/swag v0.23.0 // indirect github.com/google/gnostic-models v0.7.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/x448/float16 v0.8.4 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/net v0.49.0 // indirect golang.org/x/oauth2 v0.34.0 // indirect golang.org/x/sys v0.42.0 // indirect golang.org/x/term v0.39.0 // indirect golang.org/x/text v0.33.0 // indirect golang.org/x/time v0.14.0 // indirect google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/klog/v2 v2.140.0 // indirect k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a // indirect k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 // indirect sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect sigs.k8s.io/randfill v1.0.0 // indirect sigs.k8s.io/structured-merge-diff/v6 v6.3.2 // indirect sigs.k8s.io/yaml v1.6.0 // indirect ) ================================================ FILE: go.sum ================================================ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE= github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY= golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww= golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af h1:+5/Sw3GsDNlEmu7TfklWKPdQ0Ykja5VEmq2i817+jbI= google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= k8s.io/api v0.36.1 h1:XbL/EMj8K2aJpJtePmqUyQMsM0D4QI2pvl7YKJ20FTY= k8s.io/api v0.36.1/go.mod h1:KOWo4ey3TINlXjeHVuwB3i+tXXnu+UcwFBHlI/9dvEo= k8s.io/apimachinery v0.36.1 h1:G63Gjx2W+q0YD+72Vo8oY0nDnePVwnuzTmmy5ENrVSA= k8s.io/apimachinery v0.36.1/go.mod h1:ibYOR00vW/I1kzvi5SF0dRuJ52BvKtfvRdOn35GPQ+8= k8s.io/client-go v0.36.1 h1:FN/K8QIT2CEDt+2WB2HnWrUANZ50AP5GII43/SP2JR0= k8s.io/client-go v0.36.1/go.mod h1:s6rAnCtTGYDQnpNjEhSaISV+2O8jwruZ6m3QOYBFbtU= k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc= k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0= k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a h1:xCeOEAOoGYl2jnJoHkC3hkbPJgdATINPMAxaynU2Ovg= k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a/go.mod h1:uGBT7iTA6c6MvqUvSXIaYZo9ukscABYi2btjhvgKGZ0= k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 h1:AZYQSJemyQB5eRxqcPky+/7EdBj0xi3g0ZcxxJ7vbWU= k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= sigs.k8s.io/structured-merge-diff/v6 v6.3.2 h1:kwVWMx5yS1CrnFWA/2QHyRVJ8jM6dBA80uLmm0wJkk8= sigs.k8s.io/structured-merge-diff/v6 v6.3.2/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= ================================================ FILE: logger.go ================================================ package main import ( "fmt" "strings" "sync" "github.com/fatih/color" ) var ( greenBold = color.New(color.FgGreen, color.Bold) yellowBold = color.New(color.FgYellow, color.Bold) redBold = color.New(color.FgRed, color.Bold) boldFunc = color.New(color.Bold).SprintFunc() yellowFunc = color.New(color.FgYellow).SprintFunc() ) // Logger represents logger type Logger struct { m sync.Mutex } // NewLogger returns new Logger object func NewLogger() *Logger { return &Logger{ m: sync.Mutex{}, } } // PrintColorizedLog prints log with the given color func (l *Logger) PrintColorizedLog(c *color.Color, line string) { l.m.Lock() defer l.m.Unlock() c.Println(line) } // PrintHeader prints header func (l *Logger) PrintHeader(context, namespace, labels string) { fmt.Printf("%s %s\n", boldFunc("Context: "), context) fmt.Printf("%s %s\n", boldFunc("Namespace:"), namespace) fmt.Printf("%s %s\n", boldFunc("Labels: "), labels) color.New(color.FgYellow).Println("Press Ctrl-C to exit.") color.New(color.Bold).Println("----------") } // PrintPlainLog prints log with no cosmetics func (l *Logger) PrintPlainLog(line string) { l.m.Lock() defer l.m.Unlock() fmt.Println(line) } // PrintPodDetected prints that Pod was detected func (l *Logger) PrintPodDetected(pod, container string) { l.PrintColorizedLog(greenBold, fmt.Sprintf("Pod:%s Container:%s has been detected", pod, container)) } // PrintPodDeleted prints that Pod was finished func (l *Logger) PrintPodFinished(pod, container string) { l.PrintColorizedLog(yellowBold, fmt.Sprintf("Pod:%s Container:%s has been finished", pod, container)) } // PrintPodDeleted prints that Pod was deleted func (l *Logger) PrintPodDeleted(pod, container string) { l.PrintColorizedLog(redBold, fmt.Sprintf("Pod:%s Container:%s has been deleted", pod, container)) } // PrintPodLog prints Pod log func (l *Logger) PrintPodLog(pod, container, line string, timestamps bool) { l.m.Lock() defer l.m.Unlock() if timestamps { ss := strings.SplitN(line, " ", 2) fmt.Printf("[%s][%s] %s %s %s \n", boldFunc(pod), boldFunc(container), yellowFunc(ss[0]), boldFunc("|"), ss[1]) } else { fmt.Printf("[%s][%s] %s %s\n", boldFunc(pod), boldFunc(container), boldFunc("|"), line) } } ================================================ FILE: main.go ================================================ package main import ( "context" "fmt" "log" "math" "net/http" _ "net/http/pprof" "os" "os/signal" "time" flag "github.com/spf13/pflag" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" _ "k8s.io/client-go/plugin/pkg/client/auth" "k8s.io/client-go/tools/clientcmd" ) const ( debugAddress = ":6060" logSecondsOffset = 10 ) var ( sinceSeconds = int64(math.Ceil(float64(logSecondsOffset) / float64(time.Second))) ) func main() { var ( debug bool kubeContext string kubeconfig string labels string namespace string timestamps bool version bool ) flags := flag.NewFlagSet("k8stail", flag.ExitOnError) flags.Usage = func() { flags.PrintDefaults() } flags.StringVar(&kubeContext, "context", "", "Kubernetes context") flags.BoolVar(&debug, "debug", false, "Debug mode using pprof (http://localhost:6060)") flags.StringVar(&kubeconfig, "kubeconfig", "", "Path of kubeconfig") flags.StringVarP(&labels, "labels", "l", "", "Label filter query") flags.StringVarP(&namespace, "namespace", "n", "", "Kubernetes namespace") flags.BoolVarP(×tamps, "timestamps", "t", false, "Include timestamps on each line") flags.BoolVarP(&version, "version", "v", false, "Print version") if err := flags.Parse(os.Args[1:]); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } if kubeconfig == "" { if os.Getenv("KUBECONFIG") != "" { kubeconfig = os.Getenv("KUBECONFIG") } else { kubeconfig = clientcmd.RecommendedHomeFile } } if version { printVersion() os.Exit(0) } if debug { go func() { log.Println(http.ListenAndServe(debugAddress, nil)) }() } clientConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig( &clientcmd.ClientConfigLoadingRules{ExplicitPath: kubeconfig}, &clientcmd.ConfigOverrides{CurrentContext: kubeContext}) config, err := clientConfig.ClientConfig() if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } clientset, err := kubernetes.NewForConfig(config) if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } rawConfig, err := clientConfig.RawConfig() if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } var currentContext string if kubeContext == "" { currentContext = rawConfig.CurrentContext } else { currentContext = kubeContext } if namespace == "" { if rawConfig.Contexts[currentContext].Namespace == "" { namespace = metav1.NamespaceDefault } else { namespace = rawConfig.Contexts[currentContext].Namespace } } sigCh := make(chan os.Signal, 1) signal.Notify(sigCh, os.Interrupt) logger := NewLogger() logger.PrintHeader(currentContext, namespace, labels) ctx, cancel := context.WithCancel(context.Background()) defer cancel() watcher, err := clientset.CoreV1().Pods(namespace).Watch(ctx, metav1.ListOptions{ LabelSelector: labels, }) if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } added, finished, deleted := Watch(ctx, watcher) tails := NewTailMap() go func() { for target := range added { id := target.GetID() if _, ok := tails.Get(id); ok { continue } tail := NewTail(target.Namespace, target.Pod, target.Container, logger, sinceSeconds, timestamps) tails.Set(id, tail) tail.Start(ctx, clientset) } }() go func() { for target := range finished { id := target.GetID() t, ok := tails.Get(id) if !ok { continue } if t.Finished { continue } t.Finish() tails.Delete(id) } }() go func() { for target := range deleted { id := target.GetID() t, ok := tails.Get(id) if !ok { continue } t.Delete() tails.Delete(id) } }() <-sigCh cancel() } ================================================ FILE: renovate.json ================================================ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": [ "config:recommended", "schedule:weekly" ], "postUpdateOptions": [ "gomodTidy" ], "packageRules": [ { "matchDatasources": ["golang-version"], "rangeStrategy": "bump" }, { "groupName": "all non-major Go dependencies", "groupSlug": "all-minor-patch-gomod", "matchManagers": [ "gomod" ], "matchUpdateTypes": [ "minor", "patch", "pin", "digest" ], "automerge": true, "automergeType": "branch", "matchPackageNames": [ "!go" ] }, { "groupName": "all non-major GitHub Actions dependencies", "groupSlug": "all-minor-patch-github-actions", "matchManagers": [ "github-actions" ], "matchUpdateTypes": [ "minor", "patch", "pin", "digest" ], "automerge": true, "automergeType": "branch" }, { "matchUpdateTypes": [ "minor", "patch", "pin", "digest" ], "automerge": true, "automergeType": "branch" } ] } ================================================ FILE: tail.go ================================================ package main import ( "bufio" "context" "fmt" "os" "sync" v1 "k8s.io/api/core/v1" "k8s.io/client-go/kubernetes" ) type Tail struct { Finished bool closed chan struct{} logger *Logger namespace string pod string container string sinceSeconds int64 timestamps bool } // NewTail creates new Tail object func NewTail(namespace, pod, container string, logger *Logger, sinceSeconds int64, timestamps bool) *Tail { return &Tail{ Finished: false, closed: make(chan struct{}), logger: logger, namespace: namespace, pod: pod, container: container, sinceSeconds: sinceSeconds, timestamps: timestamps, } } // Start starts Pod log streaming func (t *Tail) Start(ctx context.Context, clientset *kubernetes.Clientset) { t.logger.PrintPodDetected(t.pod, t.container) go func() { rs, err := clientset.CoreV1().Pods(t.namespace).GetLogs(t.pod, &v1.PodLogOptions{ Container: t.container, Follow: true, SinceSeconds: &t.sinceSeconds, Timestamps: t.timestamps, }).Stream(ctx) if err != nil { fmt.Fprintln(os.Stderr, err) return } defer rs.Close() go func() { <-t.closed rs.Close() }() sc := bufio.NewScanner(rs) for sc.Scan() { t.logger.PrintPodLog(t.pod, t.container, sc.Text(), t.timestamps) } }() go func() { <-ctx.Done() close(t.closed) }() } // Finish finishes Pod log streaming with Pod completion func (t *Tail) Finish() { t.logger.PrintPodFinished(t.pod, t.container) t.Finished = true } // Delete finishes Pod log streaming with Pod deletion func (t *Tail) Delete() { t.logger.PrintPodDeleted(t.pod, t.container) close(t.closed) } type TailMap struct { mu sync.Mutex data map[string]*Tail } func NewTailMap() *TailMap { return &TailMap{ data: make(map[string]*Tail), } } func (m *TailMap) Get(k string) (*Tail, bool) { m.mu.Lock() defer m.mu.Unlock() d, ok := m.data[k] return d, ok } func (m *TailMap) Set(k string, v *Tail) { m.mu.Lock() defer m.mu.Unlock() m.data[k] = v } func (m *TailMap) Delete(k string) { m.mu.Lock() defer m.mu.Unlock() delete(m.data, k) } ================================================ FILE: version.go ================================================ package main import ( "fmt" ) var ( version string commit string date string ) func printVersion() { fmt.Printf("k8stail version: %s, commit: %s, build at: %s\n", version, commit, date) } ================================================ FILE: watch.go ================================================ package main import ( "context" "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/watch" ) type Target struct { Namespace string Pod string Container string } // NewTarget creates new Target object func NewTarget(namespace, pod, container string) *Target { return &Target{ Namespace: namespace, Pod: pod, Container: container, } } // GetID returns target ID func (t *Target) GetID() string { return t.Namespace + "_" + t.Pod + "_" + t.Container } // Watch starts and listens Kubernetes Pod events func Watch(ctx context.Context, watcher watch.Interface) (chan *Target, chan *Target, chan *Target) { added := make(chan *Target) finished := make(chan *Target) deleted := make(chan *Target) go func() { for { select { case e := <-watcher.ResultChan(): if e.Object == nil { return } pod := e.Object.(*v1.Pod) switch e.Type { case watch.Added: if pod.Status.Phase != v1.PodRunning { continue } for _, container := range pod.Spec.Containers { added <- NewTarget(pod.Namespace, pod.Name, container.Name) } case watch.Modified: switch pod.Status.Phase { case v1.PodRunning: for _, container := range pod.Spec.Containers { added <- NewTarget(pod.Namespace, pod.Name, container.Name) } case v1.PodSucceeded, v1.PodFailed: for _, container := range pod.Spec.Containers { finished <- NewTarget(pod.Namespace, pod.Name, container.Name) } } case watch.Deleted: for _, container := range pod.Spec.Containers { deleted <- NewTarget(pod.Namespace, pod.Name, container.Name) } } case <-ctx.Done(): watcher.Stop() return } } }() return added, finished, deleted }