├── .github └── FUNDING.yml ├── .gitignore ├── LICENSE ├── README.md ├── completion ├── kubetail.bash ├── kubetail.fish └── kubetail.zsh ├── kubetail └── kubetail.plugin.zsh /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [johanhaleby] 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | .idea -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kubetail 2 | 3 | Bash script that enables you to aggregate (tail/follow) logs from multiple pods into one stream. 4 | This is the same as running "kubectl logs -f " but for multiple pods. 5 | 6 | ## Installation 7 | 8 | Just download the [kubetail](https://raw.githubusercontent.com/johanhaleby/kubetail/master/kubetail) file (or any of the [releases](https://github.com/johanhaleby/kubetail/releases)) and you're good to go. 9 | 10 | ### Homebrew 11 | 12 | You can also install kubetail using [brew](https://brew.sh/): 13 | 14 | $ brew install johanhaleby/kubetail/kubetail 15 | 16 | It's also possible to install kubetail abbreviated to `kt` by using the `--with-short-names` suffix: 17 | 18 | $ brew install johanhaleby/kubetail/kubetail --with-short-names 19 | 20 | Note that you may need to run `compinit` for zsh to pick-up the changes in competition after having upgraded from the non abbreviated installation. 21 | 22 | Use `brew install --HEAD johanhaleby/kubetail/kubetail` to install the latest (unreleased) version. 23 | 24 | ### ASDF 25 | 26 | You can install kubetail using the [asdf](https://github.com/asdf-vm/asdf) version manager. 27 | 28 | ``` 29 | asdf plugin-add kubetail https://github.com/janpieper/asdf-kubetail.git 30 | asdf install kubetail 31 | ``` 32 | 33 | To find out the available versions to install, you can run the following command: 34 | 35 | ``` 36 | asdf list-all kubetail 37 | ``` 38 | 39 | ### ZSH plugin 40 | 41 | If you're using a ZSH plugin manager, you can install `kubetail` as a plugin. 42 | 43 | #### Antigen 44 | 45 | If you're using [Antigen](https://github.com/zsh-users/antigen): 46 | 47 | 1. Add `antigen bundle johanhaleby/kubetail` to your `.zshrc` where you've listed your other plugins. 48 | 2. Close and reopen your Terminal/iTerm window to **refresh context** and use the plugin. Alternatively, you can run `antigen bundle johanhaleby/kubetail` in a running shell to have `antigen` load the new plugin. 49 | 50 | #### oh-my-zsh 51 | 52 | If you're using [oh-my-zsh](github.com/robbyrussell/oh-my-zsh): 53 | 54 | 1. In the command line, change to _oh-my-zsh_'s custom plugin directory : 55 | 56 | `cd ~/.oh-my-zsh/custom/plugins/` 57 | 58 | 2. Clone the repository into a new `kubetail` directory: 59 | 60 | `git clone https://github.com/johanhaleby/kubetail.git kubetail` 61 | 62 | 3. Edit your `~/.zshrc` and add `kubetail` – same as clone directory – to the list of plugins to enable: 63 | 64 | `plugins=( ... kubetail )` 65 | 66 | 4. Then, restart your terminal application to **refresh context** and use the plugin. Alternatively, you can source your current shell configuration: 67 | 68 | `source ~/.zshrc` 69 | 70 | #### zgen 71 | 72 | If you're using [zgen](https://github.com/tarjoilija/zgen): 73 | 74 | 1. Add `zgen load johanhaleby/kubetail` to your `.zshrc` along with your other `zgen load` commands. 75 | 2. `zgen reset && zgen save` 76 | 77 | ### Completion 78 | 79 | The easiest option is to install kubetail from homebrew to dynamically display the pods names on `$ kubetail `. Alternatively install any of the [completion scripts](completion/) (bash/zsh/fish) manually. For example: 80 | * On Ubuntu, download the [kubetail.bash](https://raw.githubusercontent.com/johanhaleby/kubetail/master/completion/kubetail.bash) script and execute it in your `~/.bash_completion` file `source $HOME/kubetail/completion/kubetail.bash`. 81 | * On Mac with zsh copy the [kubetail.zsh](https://raw.githubusercontent.com/johanhaleby/kubetail/master/completion/kubetail.zsh) script to `/usr/local/share/zsh/site-functions/_kubetail`. 82 | * On Mac with fish copy the [kubetail.fish](https://raw.githubusercontent.com/johanhaleby/kubetail/master/completion/kubetail.fish) script to `~/.config/fish/completions/`. 83 | 84 | Don't forget to restart your terminal afterwards. 85 | 86 | ## Usage 87 | 88 | First find the names of all your pods: 89 | 90 | $ kubectl get pods 91 | 92 | This will return a list looking something like this: 93 | 94 | ```bash 95 | NAME READY STATUS RESTARTS AGE 96 | app1-v1-aba8y 1/1 Running 0 1d 97 | app1-v1-gc4st 1/1 Running 0 1d 98 | app1-v1-m8acl 1/1 Running 0 6d 99 | app1-v1-s20d0 1/1 Running 0 1d 100 | app2-v31-9pbpn 1/1 Running 0 1d 101 | app2-v31-q74wg 1/1 Running 0 1d 102 | my-demo-v5-0fa8o 1/1 Running 0 3h 103 | my-demo-v5-yhren 1/1 Running 0 2h 104 | ``` 105 | 106 | To tail the logs of the two "app2" pods in one go simply do: 107 | 108 | $ kubetail app2 109 | 110 | To tail only a specific container from multiple pods specify the container like this: 111 | 112 | $ kubetail app2 -c container1 113 | 114 | You can repeat `-c` to tail multiple specific containers: 115 | 116 | $ kubetail app2 -c container1 -c container2 117 | 118 | To tail multiple applications at the same time seperate them by comma: 119 | 120 | $ kubetail app1,app2 121 | 122 | For advanced matching you can use regular expressions: 123 | 124 | $ kubetail "^app1|.*my-demo.*" --regex 125 | 126 | To tail logs within a specific namespace, make sure to append the namespace flag *after* you have provided values for containers and applications: 127 | 128 | $ kubetail app2 -c container1 -n namespace1 129 | 130 | Supply `-h` for help and additional options: 131 | 132 | $ kubetail -h 133 | 134 | ## Colors 135 | 136 | By using the `-k` argument you can specify how kubetail makes use of colors (only applicable when tailing multiple pods). 137 | 138 | | Value | Description | 139 | |----------|---------------| 140 | | pod | Only the pod name is colorized but the logged text is using the terminal default color | 141 | | line | The entire line is colorized (default) | 142 | | false | Don't colorize the output at all | 143 | 144 | Example: 145 | 146 | $ kubetail app2 -k false 147 | 148 | If you find that some colors are difficult to see then they can be skipped by supplying the color index either to the `-z` flag or by setting the `KUBETAIL_SKIP_COLORS` environment variable (either choice could be comma seperated). To find the color index you'd like to skip more easily, set the `-i` flag to `true` (`-i true`) or set the `KUBETAIL_SHOW_COLOR_INDEX` environment variable to `true` (`KUBETAIL_SHOW_COLOR_INDEX=true`). This will print the color index as a prefix to the pod name (e.g. `[3:my-pod-12341] some log` where `3` is the index of the color). This is also helpful if you suffer from color blindness since the index will always be printed with the default terminal color. 149 | 150 | ## Filtering / Highlighting etc 151 | 152 | kubetail itself doesn't have filtering or highlighting capabilities built-in. If you're on MacOSX I recommend using [iTerm2](https://www.iterm2.com/) which allows for continuous highlighting of search terms, good scrolling capabilities and multitab arrangements. Another useful feature of iTerm2 is the "timeline" (`cmd` + `shift` + `e`) which lets you display a timeline in your own local timezone next to the logs (that are typically in UTC). 153 | 154 | If you're not using iTerm2 or think that kubetail is lacking in features there's a [fork](https://github.com/aks/kubetail) of kubetail made by [Alan Stebbens](https://github.com/aks) that allows for richer configuration and uses [multitail](https://www.vanheusden.com/multitail/) and [bash-lib](https://github.com/aks/bash-lib). Alan has been kind enough to provide a pull request but my current thinking is that I'd like kubetail to stay simple and small and not use any dependencies. 155 | 156 | ## Environment 157 | 158 | kubetail can take default option values from environment variables matching the option name. 159 | 160 | KUBETAIL_PREVIOUS 161 | KUBETAIL_SINCE 162 | KUBETAIL_NAMESPACE 163 | KUBETAIL_FOLLOW 164 | KUBETAIL_PREFIX 165 | KUBETAIL_LINE_BUFFERED 166 | KUBETAIL_COLORED_OUTPUT 167 | KUBETAIL_TIMESTAMPS 168 | KUBETAIL_JQ_SELECTOR 169 | KUBETAIL_SKIP_COLORS 170 | KUBETAIL_TAIL 171 | KUBETAIL_SHOW_COLOR_INDEX 172 | 173 | ## More 174 | 175 | Pull requests are very welcome! 176 | 177 | See also: http://code.haleby.se/2015/11/13/tail-logs-from-multiple-pods-simultaneously-in-kubernetes/ 178 | 179 | Buy Me A Coffee 180 | -------------------------------------------------------------------------------- /completion/kubetail.bash: -------------------------------------------------------------------------------- 1 | _findnamespace(){ 2 | local next="0" 3 | local namespace="$KUBETAIL_NAMESPACE"; 4 | for wo in "${COMP_WORDS[@]}" 5 | do 6 | if [ "$next" = "0" ]; then 7 | if [ "$wo" = "-n" ] || [ "$wo" = "--namespace" ]; then 8 | next="1" 9 | fi 10 | else 11 | namespace="$wo" 12 | break 13 | fi 14 | done 15 | if [ "$namespace" != "" ]; then 16 | printf "%s" " --namespace $namespace" 17 | else 18 | printf "%s" " --all-namespaces" 19 | fi 20 | } 21 | 22 | _findcontext(){ 23 | local next="0" 24 | local context=""; 25 | for wo in "${COMP_WORDS[@]}" 26 | do 27 | if [ "$next" = "0" ]; then 28 | if [ "$wo" = "-t" ] || [ "$wo" = "--context" ]; then 29 | next="1" 30 | fi 31 | else 32 | context="$wo" 33 | break 34 | fi 35 | done 36 | if [ "$context" != "" ]; then 37 | printf "%s" " --context=$context" 38 | fi 39 | } 40 | 41 | _kubetail() 42 | { 43 | local curr_arg; 44 | curr_arg=${COMP_WORDS[COMP_CWORD]} 45 | prev=${COMP_WORDS[COMP_CWORD-1]} 46 | command=${COMP_WORDS[1]} 47 | case $prev in 48 | -t|--context) 49 | COMPREPLY=( $(compgen -W "$(kubectl config get-contexts -o=name | awk '{print $1}')" -- $curr_arg ) ); 50 | ;; 51 | -n|--namespace) 52 | COMPREPLY=( $(compgen -W "$(kubectl $(_findcontext) get namespaces -o=jsonpath='{range .items[*].metadata.name}{@}{"\n"}{end}' | awk '{print $1}')" -- $curr_arg ) ); 53 | ;; 54 | *) 55 | COMPREPLY=( $(compgen -W "$(kubectl $(_findcontext) get pods $(_findnamespace) -o=jsonpath='{range .items[*].metadata.name}{@}{"\n"}{end}' --no-headers | awk '{print $1}')" -- $curr_arg ) ); 56 | ;; 57 | esac 58 | } 59 | 60 | complete -F _kubetail kubetail kt 61 | -------------------------------------------------------------------------------- /completion/kubetail.fish: -------------------------------------------------------------------------------- 1 | # kubetail 2 | complete -f -c kubetail -a "(kubectl get pods --no-headers | awk '{print \$1}')" 3 | -------------------------------------------------------------------------------- /completion/kubetail.zsh: -------------------------------------------------------------------------------- 1 | #compdef kubetail kt=kubetail 2 | _arguments "1: :($(kubectl get pods --no-headers | awk '{print $1}'))" 3 | -------------------------------------------------------------------------------- /kubetail: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | if [ -z "${KUBECTL_BIN}" ]; then 3 | if hash kubectl 2>/dev/null; then 4 | KUBECTL_BIN='kubectl' 5 | elif hash kubectl.exe 2>/dev/null; then 6 | KUBECTL_BIN='kubectl.exe' 7 | elif hash microk8s 2>/dev/null; then 8 | KUBECTL_BIN='microk8s.kubectl' 9 | fi 10 | fi 11 | 12 | if ! hash "${KUBECTL_BIN}" 2>/dev/null; then 13 | echo >&2 "kubectl is not installed" 14 | exit 1 15 | fi 16 | 17 | readonly PROGNAME=$(basename $0) 18 | 19 | calculate_default_namespace() { 20 | local config_namespace=$(${KUBECTL_BIN} config view --minify --output 'jsonpath={..namespace}') 21 | echo "${KUBETAIL_NAMESPACE:-${config_namespace:-default}}" 22 | } 23 | 24 | # Sets default color ouput to 'false' if output is not a terminal 25 | if [ -n "$(tput colors)" ] && [ "$(tput colors)" -ge 8 ]; then 26 | terminal_aware_default_color=line 27 | else 28 | terminal_aware_default_color=false 29 | fi 30 | 31 | default_previous="${KUBETAIL_PREVIOUS:-false}" 32 | default_since="${KUBETAIL_SINCE:-10s}" 33 | default_namespace=$(calculate_default_namespace) 34 | default_follow="${KUBETAIL_FOLLOW:-true}" 35 | default_prefix="${KUBETAIL_PREFIX:-true}" 36 | default_line_buffered="${KUBETAIL_LINE_BUFFERED:-}" 37 | default_colored_output="${KUBETAIL_COLORED_OUTPUT:-$terminal_aware_default_color}" 38 | default_timestamps="${KUBETAIL_TIMESTAMPS:-}" 39 | default_jq_selector="${KUBETAIL_JQ_SELECTOR:-}" 40 | default_skip_colors="${KUBETAIL_SKIP_COLORS:-7,8}" 41 | default_tail="${KUBETAIL_TAIL:--1}" 42 | default_show_color_index="${KUBETAIL_SHOW_COLOR_INDEX:-false}" 43 | 44 | namespace="${default_namespace}" 45 | follow="${default_follow}" 46 | prefix="${default_prefix}" 47 | line_buffered="${default_line_buffered}" 48 | colored_output="${default_colored_output}" 49 | timestamps="${default_timestamps}" 50 | jq_selector="${default_jq_selector}" 51 | skip_colors="${default_skip_colors}" 52 | tail="${default_tail}" 53 | show_color_index="${default_show_color_index}" 54 | 55 | if [[ ${1} != -* ]] 56 | then 57 | pod="${1}" 58 | fi 59 | containers=() 60 | selector=() 61 | regex='substring' 62 | previous="${default_previous}" 63 | since="${default_since}" 64 | version="1.6.21-SNAPSHOT" 65 | dryrun=false 66 | cluster="" 67 | namespace_arg="-n ${default_namespace}" 68 | 69 | usage="${PROGNAME} [-h] [-c] [-n] [-t] [-l] [-f] [-d] [-P] [-p] [-s] [-b] [-e] [-j] [-k] [-z] [-v] [-r] [-i] -- tail multiple Kubernetes pod logs at the same time 70 | 71 | where: 72 | -h, --help Show this help text. 73 | -c, --container The name of the container to tail in the pod (if multiple containers are defined in the pod). 74 | Defaults to all containers in the pod. Can be used multiple times. 75 | -t, --context The k8s context. ex. int1-context. Relies on ~/.kube/config for the contexts. 76 | -l, --selector Label selector. If used the pod name is ignored. 77 | -n, --namespace The Kubernetes namespace where the pods are located. Defaults to \"${default_namespace}\". 78 | -f, --follow Specify if the logs should be streamed. (true|false) Defaults to ${default_follow}. 79 | -d, --dry-run Print the names of the matched pods and containers, then exit. 80 | -P, --prefix Specify if add the pod name prefix before each line. (true|false) Defaults to ${default_prefix}. 81 | -p, --previous Return logs for the previous instances of the pods, if available. (true|false) Defaults to ${default_previous}. 82 | -s, --since Only return logs newer than a relative duration like 5s, 2m, or 3h. Defaults to ${default_since}. 83 | -b, --line-buffered Controls if lines should be buffered (true|false). Defaults to ${default_line_buffered:-true}. 84 | It can be useful to disable this if you're piping to e.g. sed/awk/grep and $PROGNAME doesn't generate enough data 85 | for sed/awk/grep to output it immediately. 86 | -e, --regex The type of name matching to use (regex|substring). Defaults to ${regex}. 87 | -j, --jq If your output is json - use this jq-selector to parse it. Defaults to \"${default_jq_selector}\". 88 | example: --jq \".logger + \\\" \\\" + .message\" 89 | -k, --colored-output Use colored output (pod|line|false). 90 | pod = only color pod name, line = color entire line, false = don't use any colors. 91 | Defaults to ${default_colored_output}. 92 | -z, --skip-colors Comma-separated list of colors to not use in output. 93 | If you have green foreground on black, this will skip dark grey and some greens: -z 2,8,10 94 | Defaults to: ${default_skip_colors}. 95 | --timestamps Show timestamps for each log line. (true|false) Defaults to ${default_timestamps:-false}. 96 | --tail Lines of recent log file to display. Defaults to ${default_tail}, showing all log lines. 97 | -v, --version Prints the kubetail version. 98 | -r, --cluster The name of the kubeconfig cluster to use. 99 | -i, --show-color-index Show the color index before the pod name prefix that is shown before each log line. 100 | Normally only the pod name is added as a prefix before each line, for example \"[app-5b7ff6cbcd-bjv8n]\", 101 | but if \"show-color-index\" is true then color index is added as well: \"[1:app-5b7ff6cbcd-bjv8n]\". 102 | This is useful if you have color blindness or if you want to know which colors to exclude (see \"--skip-colors\"). 103 | Defaults to ${default_show_color_index}. 104 | 105 | examples: 106 | ${PROGNAME} my-pod-v1 107 | ${PROGNAME} my-pod-v1 -c my-container 108 | ${PROGNAME} my-pod-v1 -t int1-context -c my-container 109 | ${PROGNAME} '(service|consumer|thing)' -e regex 110 | ${PROGNAME} -l service=my-service 111 | ${PROGNAME} --selector service=my-service --since 10m 112 | ${PROGNAME} --tail 1" 113 | 114 | if [ "$#" -ne 0 ]; then 115 | while [ "$#" -gt 0 ] 116 | do 117 | case "$1" in 118 | -h|--help) 119 | echo "$usage" 120 | exit 0 121 | ;; 122 | -v|--version) 123 | echo "$version" 124 | exit 0 125 | ;; 126 | -c|--container) 127 | containers+=("$2") 128 | ;; 129 | -e|--regex) 130 | if [ "$2" = "substring" ]; then 131 | regex="substring" 132 | else 133 | regex="regex" 134 | fi 135 | ;; 136 | -t|--context) 137 | context="$2" 138 | ;; 139 | -r|--cluster) 140 | cluster="--cluster $2" 141 | ;; 142 | -l|--selector) 143 | selector=(--selector "$2") 144 | pod="" 145 | ;; 146 | -d|--dry-run) 147 | dryrun=true 148 | ;; 149 | -p|--previous) 150 | if [ "$2" = "false" ]; then 151 | previous="false" 152 | else 153 | previous="true" 154 | fi 155 | ;; 156 | -s|--since) 157 | if [ -z "$2" ]; then 158 | since="${default_since}" 159 | else 160 | since="$2" 161 | fi 162 | ;; 163 | -n|--namespace) 164 | if [ -z "$2" ]; then 165 | # using namespace from context 166 | : 167 | else 168 | namespace_arg="--namespace $2" 169 | fi 170 | ;; 171 | -f|--follow) 172 | if [ "$2" = "false" ]; then 173 | follow="false" 174 | else 175 | follow="true" 176 | fi 177 | ;; 178 | -P|--prefix) 179 | if [ "$2" = "false" ]; then 180 | prefix="false" 181 | else 182 | prefix="true" 183 | fi 184 | ;; 185 | -b|--line-buffered) 186 | if [ "$2" = "false" ]; then 187 | line_buffered="false" 188 | else 189 | line_buffered="$2" 190 | fi 191 | ;; 192 | -k|--colored-output) 193 | if [ -z "$2" ]; then 194 | colored_output="${default_colored_output}" 195 | else 196 | colored_output="$2" 197 | fi 198 | ;; 199 | -j|--jq) 200 | if [ -z "$2" ]; then 201 | jq_selector="${default_jq_selector}" 202 | else 203 | jq_selector="$2" 204 | fi 205 | ;; 206 | -z|--skip-colors) 207 | if [ -z "$2" ]; then 208 | skip_colors="${default_skip_colors}" 209 | else 210 | skip_colors="$2" 211 | fi 212 | ;; 213 | --timestamps) 214 | if [ "$2" = "false" ]; then 215 | timestamps="$1=$2" 216 | else 217 | timestamps="$1" 218 | fi 219 | ;; 220 | --tail) 221 | if [ -z "$2" ]; then 222 | tail="${default_tail}" 223 | else 224 | tail="$2" 225 | fi 226 | ;; 227 | -i|--show-color-index) 228 | if [ -z "$2" ]; then 229 | show_color_index="${default_show_color_index}" 230 | else 231 | show_color_index="$2" 232 | fi 233 | ;; 234 | --) 235 | break 236 | ;; 237 | -*) 238 | echo "Invalid option '$1'. Use --help to see the valid options" >&2 239 | exit 1 240 | ;; 241 | # an option argument, continue 242 | *) ;; 243 | esac 244 | shift 245 | done 246 | else 247 | echo "$usage" 248 | exit 1 249 | fi 250 | 251 | # Join function that supports a multi-character separator (copied from http://stackoverflow.com/a/23673883/398441) 252 | function join() { 253 | # $1 is sep 254 | # $2... are the elements to join 255 | local sep="$1" 256 | shift 257 | 258 | local F=0 259 | for x in "$@" 260 | do 261 | if [[ F -eq 1 ]] 262 | then 263 | echo -n "$sep" 264 | else 265 | F=1 266 | fi 267 | echo -n "$x" 268 | done 269 | echo 270 | } 271 | 272 | # Check if pod query contains a comma and we've not specified "regex" explicitly, 273 | # if so we convert the pod query string into a regex that matches all pods seperated by the comma 274 | if [[ "${pod}" = *","* ]] && [ ! "${regex}" == 'regex' ]; then 275 | 276 | # Split the supplied query string (in variable pod) by comma into an array named "pods_to_match" 277 | IFS=',' read -r -a pods_to_match <<< "${pod}" 278 | 279 | # Join all pod names into a string with ".*|.*" as delimiter 280 | pod=$(join ".*|.*" "${pods_to_match[@]}") 281 | 282 | # Prepend and initial ".*" and and append the last ".*" 283 | pod=".*${pod}.*" 284 | 285 | # Force the use of regex matching 286 | regex='regex' 287 | fi 288 | 289 | grep_matcher='' 290 | if [ "${regex}" == 'regex' ]; then 291 | echo "Using regex '${pod}' to match pods" 292 | grep_matcher='-E' 293 | fi 294 | 295 | # Get all pods matching the input and put them in an array. If no input then all pods are matched. 296 | matching_pods=(`${KUBECTL_BIN} get pods ${context:+--context=${context}} "${selector[@]}" ${namespace_arg} ${cluster} --field-selector=status.phase=Running --output=jsonpath='{.items[*].metadata.name}' | xargs -n1 | grep --color=never $grep_matcher "${pod}"`) 297 | matching_pods_size=${#matching_pods[@]} 298 | 299 | if [ ${matching_pods_size} -eq 0 ]; then 300 | echo "No pod exists that matches ${pod}" 301 | exit 1 302 | fi 303 | 304 | color_end=$(tput sgr0) 305 | 306 | # Wrap all pod names in the "kubectl logs -f=true/false" command 307 | display_names_preview=() 308 | pod_logs_commands=() 309 | i=0 310 | color_index=0 311 | 312 | function next_col { 313 | potential_col=$(($1+1)) 314 | [[ $skip_colors =~ (^|,)$potential_col($|,) ]] && echo `next_col $potential_col` || echo $potential_col 315 | } 316 | 317 | # Allows for more colors, this is useful if one tails a lot pods 318 | if [ ${colored_output} != "false" ]; then 319 | export TERM=xterm-256color 320 | fi 321 | 322 | # Function that kills all kubectl processes that are started by kubetail in the background 323 | function kill_kubectl_processes { 324 | kill 0 325 | } 326 | 327 | # Invoke the "kill_kubectl_processes" function when the script is stopped (including ctrl+c) 328 | # Note that "INT" is not used because if, for example, kubectl cannot find a container 329 | # (for example when running "kubetail something -c non_matching") 330 | trap kill_kubectl_processes EXIT 331 | 332 | # Putting all needed values in a variable so that multiple requests to Kubernetes api can be avoided, thus making it faster 333 | all_pods_containers=$(echo -e `${KUBECTL_BIN} get pods ${namespace_arg} ${context:+--context=${context}} --output=jsonpath="{range .items[*]}{.metadata.name} {.spec['containers', 'initContainers'][*].name} \n{end}"`) 334 | 335 | 336 | for pod in ${matching_pods[@]}; do 337 | if [ ${#containers[@]} -eq 0 ]; then 338 | pod_containers=($(echo -e "$all_pods_containers" | grep "$pod " | cut -d ' ' -f2- | xargs -n1)) 339 | else 340 | pod_containers=("${containers[@]}") 341 | fi 342 | 343 | for container in ${pod_containers[@]}; do 344 | [ ${matching_pods_size} -eq 1 -a ${#pod_containers[@]} -eq 1 ] && single_stream="true" || single_stream="false" 345 | 346 | if [ ${colored_output} == "false" ] || [ ${single_stream} == "true" ]; then 347 | color_start=$(tput sgr0) 348 | color_index_prefix="" 349 | else 350 | color_index=`next_col $color_index` 351 | color_start=$(tput setaf $color_index) 352 | color_index_prefix=`if [ ${show_color_index} == "true" ]; then echo "${color_index}:"; else echo ""; fi` 353 | fi 354 | 355 | if [ ${#pod_containers[@]} -eq 1 ]; then 356 | display_name="${pod}" 357 | else 358 | display_name="${pod} ${container}" 359 | fi 360 | 361 | if [ ${colored_output} == "false" ]; then 362 | display_names_preview+=("${display_name}") 363 | else 364 | display_names_preview+=("$color_index_prefix${color_start}${display_name}${color_end}") 365 | fi 366 | 367 | if [ ${prefix} == "false" ]; then 368 | prefix_line="" 369 | else 370 | if [ ${colored_output} == "false" ]; then 371 | prefix_line="[${display_name}] " 372 | else 373 | prefix_line="${color_start}[${color_end}${color_index_prefix}${color_start}${display_name}]${color_end} " 374 | fi 375 | fi 376 | 377 | if [ ${colored_output} == "false" ] || [ ${colored_output} == "pod" ]; then 378 | colored_line="${prefix_line}\$REPLY" 379 | else 380 | colored_line="${prefix_line}${color_start}\$REPLY${color_end}" 381 | fi 382 | 383 | kubectl_cmd="${KUBECTL_BIN} ${context:+--context=${context}} logs ${pod} ${container} -f=${follow} --previous=${previous} --since=${since} --tail=${tail} ${namespace_arg} ${cluster}" 384 | colorify_lines_cmd="while read -r; do echo \"$colored_line\" | tail -n +1; done" 385 | if [ "z" == "z$jq_selector" ]; then 386 | logs_commands+=("${kubectl_cmd} ${timestamps} | ${colorify_lines_cmd}"); 387 | else 388 | logs_commands+=("${kubectl_cmd} | jq --unbuffered -r -R --stream '. as \$line | try (fromjson | $jq_selector) catch \$line' | ${colorify_lines_cmd}"); 389 | fi 390 | 391 | # There are only 11 usable colors 392 | i=$(( ($i+1)%13 )) 393 | done 394 | done 395 | 396 | # Preview pod colors 397 | echo "Will tail ${#display_names_preview[@]} logs..." 398 | for preview in "${display_names_preview[@]}"; do 399 | echo "$preview" 400 | done 401 | 402 | if [[ ${dryrun} == true ]]; 403 | then 404 | exit 0 405 | fi 406 | 407 | # Join all log commands into one string separated by " & " 408 | command_to_tail=$(join " & " "${logs_commands[@]}") 409 | 410 | # Aggregate all logs and print to stdout 411 | # Note that tail +1f doesn't work on some Linux distributions so we use this slightly longer alternative 412 | # Note that if --follow=false, then the tail command should also not be followed 413 | tail_follow_command="-f" 414 | if [[ ${follow} == false ]]; 415 | then 416 | tail_follow_command="" 417 | fi 418 | 419 | 420 | command_to_run="tail ${tail_follow_command} -n +1 <( eval "${command_to_tail}" )" 421 | 422 | if [ "${line_buffered}" == "false" ]; then 423 | distro="$(uname)" 424 | if [[ "$distro" == "Darwin" || "$distro" == "FreeBSD" ]]; then 425 | command_to_run="script -q /dev/null ${command_to_run}" 426 | else # Linux etc 427 | command_to_run="script -q -c '${command_to_run}' /dev/null" 428 | fi 429 | fi 430 | 431 | eval "${command_to_run}" -------------------------------------------------------------------------------- /kubetail.plugin.zsh: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed under the Apache License, Version 2.0 (the "License"); 3 | # you may not use this file except in compliance with the License. 4 | # You may obtain a copy of the License at 5 | # 6 | # http://www.apache.org/licenses/LICENSE-2.0 7 | # 8 | # Unless required by applicable law or agreed to in writing, software 9 | # distributed under the License is distributed on an "AS IS" BASIS, 10 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | # See the License for the specific language governing permissions and 12 | # limitations under the License. 13 | 14 | 15 | # Add our plugin's diretory to user's path 16 | PLUGIN_BIN="$(dirname $0)" 17 | export PATH="${PATH}:${PLUGIN_BIN}" 18 | 19 | fpath=($PLUGIN_BIN/completion $fpath) 20 | --------------------------------------------------------------------------------