├── minikube-log-viewer-screenshot.png ├── sa-logviewer.yaml ├── cr-logviewer.yaml ├── Dockerfile ├── crb-logviewer.yaml ├── README.md ├── LICENSE.txt ├── deployment-and-service.yaml ├── index.html └── serve.go /minikube-log-viewer-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivans3/minikube-log-viewer/HEAD/minikube-log-viewer-screenshot.png -------------------------------------------------------------------------------- /sa-logviewer.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: sa-logviewer 5 | namespace: kube-system 6 | -------------------------------------------------------------------------------- /cr-logviewer.yaml: -------------------------------------------------------------------------------- 1 | kind: ClusterRole 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | metadata: 4 | # "namespace" omitted since ClusterRoles are not namespaced 5 | name: cr-logviewer 6 | rules: 7 | - apiGroups: [""] 8 | resources: ["namespaces"] 9 | verbs: ["get", "watch", "list"] 10 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # --- Build stage --- 2 | # 3 | FROM golang:1.22-alpine AS builder 4 | 5 | COPY serve.go /go 6 | RUN go mod init example.com/m/v2 7 | RUN CGO_ENABLED=0 go build -a -installsuffix cgo -o serve 8 | 9 | #--- Final stage --- 10 | 11 | FROM debian:stable 12 | 13 | COPY --from=builder /go/serve /serve 14 | RUN apt-get update && apt-get install -y xtail 15 | COPY index.html / 16 | 17 | ENTRYPOINT /serve 18 | 19 | -------------------------------------------------------------------------------- /crb-logviewer.yaml: -------------------------------------------------------------------------------- 1 | # This cluster role binding allows "sa-logviewer" to read namespace metadata 2 | kind: ClusterRoleBinding 3 | apiVersion: rbac.authorization.k8s.io/v1 4 | metadata: 5 | name: crb-logviewer 6 | namespace: kube-system 7 | subjects: 8 | - kind: ServiceAccount 9 | name: sa-logviewer 10 | namespace: kube-system 11 | roleRef: 12 | kind: ClusterRole #this must be Role or ClusterRole 13 | name: cr-logviewer # this must match the name of the Role or ClusterRole you wish to bind to 14 | apiGroup: rbac.authorization.k8s.io 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # minikube-log-viewer 2 | Lightweight Minikube Log Viewer 3 | 4 | ![minikube-log-viewer-screenshot.png](minikube-log-viewer-screenshot.png) 5 | 6 | Installation 7 | 8 | Available as a Minikube Add-On 9 | 10 | ``` 11 | minikube addons enable logviewer 12 | ``` 13 | 14 | Then, 15 | ``` 16 | minikube service logviewer --url -n kube-system 17 | ``` 18 | 19 | And then visit the URL with your browser. 20 | 21 | Features: 22 | * uses HTTP SSE (no indexer or indexing delay) 23 | * uses xtail as the log collector 24 | * namespace filtering (and you can bookmark a link with a `?namespace=yournamespace` query string to save it) 25 | * search feature 26 | * pause/resume feature 27 | * supports docker(JSON) and containerd log formats 28 | * support for amd64 and arm64 platforms 29 | 30 | 31 | TODO: 32 | * hilight matches in search feature 33 | * "Mark" Button which adds HBar to log stream... 34 | 35 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | License: Unlicense 3 | This is free and unencumbered software released into the public domain. 4 | . 5 | Anyone is free to copy, modify, publish, use, compile, sell, or 6 | distribute this software, either in source code form or as a compiled 7 | binary, for any purpose, commercial or non-commercial, and by any 8 | means. 9 | . 10 | In jurisdictions that recognize copyright laws, the author or authors 11 | of this software dedicate any and all copyright interest in the 12 | software to the public domain. We make this dedication for the benefit 13 | of the public at large and to the detriment of our heirs and 14 | successors. We intend this dedication to be an overt act of 15 | relinquishment in perpetuity of all present and future rights to this 16 | software under copyright law. 17 | . 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 19 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 20 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 21 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 22 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 23 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 24 | OTHER DEALINGS IN THE SOFTWARE. 25 | . 26 | For more information, please refer to 27 | -------------------------------------------------------------------------------- /deployment-and-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: minikube-log-viewer 5 | namespace: kube-system 6 | spec: 7 | ports: 8 | - port: 3000 9 | name: http 10 | nodePort: 32000 11 | selector: 12 | app: minikube-log-viewer 13 | type: NodePort 14 | 15 | --- 16 | apiVersion: apps/v1 17 | kind: Deployment 18 | metadata: 19 | name: minikube-log-viewer 20 | namespace: kube-system 21 | spec: 22 | selector: 23 | matchLabels: #thanks prakaashkpk,sorry i missed the PR... 24 | app: minikube-log-viewer 25 | strategy: 26 | type: Recreate 27 | template: 28 | metadata: 29 | labels: 30 | app: minikube-log-viewer 31 | spec: 32 | serviceAccountName: sa-logviewer 33 | volumes: 34 | - name: logs 35 | hostPath: 36 | path: /var/log/containers 37 | - name: logs-pods 38 | hostPath: 39 | path: /var/log/pods 40 | #for minikube v0.22.2: 41 | - name: logs-containers-mnt-sda1 42 | hostPath: 43 | path: /mnt/sda1/var/lib/docker/containers/ 44 | #for minikube v0.22.3+: 45 | - name: logs-containers 46 | hostPath: 47 | path: /var/lib/docker/containers/ 48 | hostNetwork: true 49 | containers: 50 | - name: logviewer 51 | image: docker.io/ivans3/minikube-log-viewer:latest 52 | imagePullPolicy: Always 53 | volumeMounts: 54 | - name: logs 55 | mountPath: /var/log/containers/ 56 | - name: logs-pods 57 | mountPath: /var/log/pods 58 | - name: logs-containers-mnt-sda1 59 | mountPath: /mnt/sda1/var/lib/docker/containers/ 60 | - name: logs-containers 61 | mountPath: /var/lib/docker/containers/ 62 | 63 | 64 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 55 | 238 |
239 |
Namespace: ISearch: (paused)
240 | 241 | 242 | 243 | -------------------------------------------------------------------------------- /serve.go: -------------------------------------------------------------------------------- 1 | // 2 | // https://gist.github.com/schmohlio/d7bdb255ba61d3f5e51a512a7c0d6a85 3 | package main 4 | 5 | import ( 6 | "fmt" 7 | "log" 8 | "net/http" 9 | "time" 10 | "bufio" 11 | "os/exec" 12 | "regexp" 13 | "io/ioutil" 14 | "strings" 15 | "crypto/x509" 16 | "crypto/tls" 17 | "os" 18 | "html" 19 | "encoding/json" 20 | ) 21 | 22 | // the amount of time to wait when pushing a message to 23 | // a slow client or a client that closed after `range clients` started. 24 | const patience time.Duration = time.Second*1 25 | 26 | // Example SSE server in Golang. 27 | // $ go run sse.go 28 | 29 | //The BackLog 30 | var backLogLength = 1500 31 | var backLog [][]byte = make([][]byte, 0, 2*backLogLength) 32 | //Server-side filtering: 33 | //var backLogFilenameMustContain = "_yournamespace_" //dont put system logs in the backlog 34 | 35 | type Broker struct { 36 | 37 | // Events are pushed to this channel by the main events-gathering routine 38 | Notifier chan []byte 39 | 40 | // New client connections 41 | newClients chan chan []byte 42 | 43 | // Closed client connections 44 | closingClients chan chan []byte 45 | 46 | // Client connections registry 47 | clients map[chan []byte]bool 48 | 49 | } 50 | 51 | func NewServer() (broker *Broker) { 52 | // Instantiate a broker 53 | broker = &Broker{ 54 | Notifier: make(chan []byte, 1), 55 | newClients: make(chan chan []byte), 56 | closingClients: make(chan chan []byte), 57 | clients: make(map[chan []byte]bool), 58 | } 59 | 60 | // Set it running - listening and broadcasting events 61 | go broker.listen() 62 | 63 | return 64 | } 65 | 66 | func fetchAndWriteNamespaces(rw http.ResponseWriter) { 67 | rw.Header().Set("Content-Type", "application/json") 68 | 69 | ca := x509.NewCertPool() 70 | certs, err := ioutil.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/ca.crt") 71 | if err != nil { 72 | log.Print("Error: %v", err) 73 | return; 74 | } 75 | // Append our cert to the pool 76 | if ok := ca.AppendCertsFromPEM(certs); !ok { 77 | log.Print("Error: %v", ok) 78 | return; 79 | } 80 | 81 | // Trust the cert pool in our client 82 | config := &tls.Config{ 83 | RootCAs: ca, 84 | } 85 | tr := &http.Transport{TLSClientConfig: config} 86 | client := &http.Client{Transport: tr} 87 | 88 | url := "https://"+os.Getenv("KUBERNETES_PORT_443_TCP_ADDR")+":"+os.Getenv("KUBERNETES_PORT_443_TCP_PORT")+"/api/v1/namespaces/" 89 | req, _ := http.NewRequest("GET", url, nil) 90 | 91 | //Read token file 92 | token, _ := ioutil.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/token") 93 | 94 | req.Header.Set("Authorization", "Bearer "+string(token)) 95 | resp, err := client.Do(req) 96 | 97 | if err != nil { 98 | log.Print("Error: %v", err) 99 | return 100 | } 101 | 102 | defer resp.Body.Close() 103 | body, err := ioutil.ReadAll(resp.Body) 104 | if err != nil { 105 | log.Print("Error: %v", err) 106 | return 107 | } 108 | 109 | rw.Write(body); 110 | 111 | return 112 | } 113 | 114 | func (broker *Broker) ServeHTTP(rw http.ResponseWriter, req *http.Request) { 115 | 116 | if (req.URL.Path == "/") { 117 | rw.Header().Set("Content-Type", "text/html") 118 | dat, _ := ioutil.ReadFile("/index.html") 119 | //TODO: error handling 120 | rw.Write(dat) 121 | return 122 | } 123 | if (req.URL.Path == "/namespaces") { 124 | fetchAndWriteNamespaces(rw) 125 | //TODO: error handling 126 | return 127 | } 128 | if (req.URL.Path == "/debug") { 129 | rw.Header().Set("Content-Type", "text/plain") 130 | fmt.Fprintf(rw, "length = %d\n", len(backLog)) 131 | 132 | return 133 | } 134 | 135 | // Make sure that the writer supports flushing. 136 | // 137 | flusher, ok := rw.(http.Flusher) 138 | 139 | if !ok { 140 | http.Error(rw, "Streaming unsupported!", http.StatusInternalServerError) 141 | return 142 | } 143 | 144 | rw.Header().Set("Content-Type", "text/event-stream") 145 | rw.Header().Set("Cache-Control", "no-cache") 146 | rw.Header().Set("Connection", "keep-alive") 147 | rw.Header().Set("Access-Control-Allow-Origin", "*") 148 | 149 | // Each connection registers its own message channel with the Broker's connections registry 150 | messageChan := make(chan []byte) 151 | 152 | // Signal the broker that we have a new connection 153 | broker.newClients <- messageChan 154 | 155 | // Remove this client from the map of connected clients 156 | // when this handler exits. 157 | defer func() { 158 | broker.closingClients <- messageChan 159 | }() 160 | 161 | // Listen to connection close and un-register messageChan 162 | notify := rw.(http.CloseNotifier).CloseNotify() 163 | 164 | //dump the backLog 165 | for _, element := range backLog { 166 | fmt.Fprintf(rw, "data: %s\n\n", element) 167 | } 168 | flusher.Flush() 169 | 170 | for { 171 | select { 172 | case <-notify: 173 | return 174 | default: 175 | 176 | // Write to the ResponseWriter 177 | // Server Sent Events compatible 178 | fmt.Fprintf(rw, "data: %s\n\n", <-messageChan) 179 | 180 | // Flush the data immediatly instead of buffering it for later. 181 | flusher.Flush() 182 | } 183 | } 184 | 185 | } 186 | 187 | func (broker *Broker) listen() { 188 | for { 189 | select { 190 | case s := <-broker.newClients: 191 | 192 | // A new client has connected. 193 | // Register their message channel 194 | broker.clients[s] = true 195 | log.Printf("Client added. %d registered clients", len(broker.clients)) 196 | case s := <-broker.closingClients: 197 | 198 | // A client has dettached and we want to 199 | // stop sending them messages. 200 | delete(broker.clients, s) 201 | log.Printf("Removed client. %d registered clients", len(broker.clients)) 202 | case event := <-broker.Notifier: 203 | 204 | // We got a new event from the outside! 205 | // Send event to all connected clients 206 | for clientMessageChan, _ := range broker.clients { 207 | select { 208 | case clientMessageChan <- event: 209 | case <-time.After(patience): 210 | log.Print("Skipping client.") 211 | } 212 | } 213 | } 214 | } 215 | 216 | } 217 | 218 | type DockerJSONLog struct { 219 | Log string `json:"log"` 220 | } 221 | 222 | func main() { 223 | 224 | broker := NewServer() 225 | 226 | cmd := exec.Command("/usr/bin/xtail","/var/log/containers") 227 | 228 | stdout, err := cmd.StdoutPipe() 229 | checkError(err) 230 | err = cmd.Start() 231 | checkError(err) 232 | defer cmd.Wait() // Doesn't block 233 | 234 | scanner := bufio.NewScanner(stdout) 235 | 236 | currentFile := "" 237 | jsonBytes := []byte("") 238 | escapedLogMsg := []byte("") //will include outer double-quotes... 239 | re1 := regexp.MustCompile("^\\*\\*\\* /var/log/containers/(?P.*) \\*\\*\\*$") 240 | //containerd log format: expression /^(?