├── 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 | 
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 |
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 /^(?