1
0
mirror of https://github.com/amir20/dozzle.git synced 2025-12-30 17:47:28 +01:00
Files
dozzle/web/logs.go
Amir Raminfar c835f51cc4 Support for JSON logs (#1759)
* WIP for using json all the time

* Updates to render

* adds a new component for json

* Updates styles

* Adds nesting

* Adds field list

* Adds expanding

* Adds new composable for event source

* Creates an add button

* Removes unused code

* Adds and removes fields with defaults

* Fixes jumping when adding new fields

* Returns JSON correctly

* Fixes little bugs

* Fixes js tests

* Adds vscode

* Fixes json buffer error

* Fixes extra line

* Fixes tests

* Fixes tests and adds support for search

* Refactors visible payload keys to a composable

* Fixes typescript errors and refactors

* Fixes visible keys by ComputedRef<Ref>

* Fixes search bugs

* Updates tests

* Fixes go tests

* Fixes scroll view

* Fixes vue tsc errors

* Fixes EOF error

* Fixes build error

* Uses application/ld+json

* Fixes arrays and records

* Marks for json too
2022-08-16 13:53:31 -07:00

216 lines
5.5 KiB
Go

package web
import (
"bufio"
"compress/gzip"
"context"
"encoding/json"
"hash/fnv"
"fmt"
"io"
"net/http"
"runtime"
"strings"
"time"
"github.com/amir20/dozzle/docker"
"github.com/dustin/go-humanize"
log "github.com/sirupsen/logrus"
)
func (h *handler) downloadLogs(w http.ResponseWriter, r *http.Request) {
id := r.URL.Query().Get("id")
container, err := h.client.FindContainer(id)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
now := time.Now()
from := time.Unix(container.Created, 0)
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s-%s.log.gz", container.Name, now.Format("2006-01-02T15-04-05")))
w.Header().Set("Content-Type", "application/gzip")
zw := gzip.NewWriter(w)
defer zw.Close()
zw.Name = fmt.Sprintf("%s-%s.log", container.Name, now.Format("2006-01-02T15-04-05"))
zw.Comment = "Logs generated by Dozzle"
zw.ModTime = now
reader, err := h.client.ContainerLogsBetweenDates(r.Context(), container.ID, from, now)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
io.Copy(zw, reader)
}
func logEventIterator(reader *bufio.Reader) func() (docker.LogEvent, error) {
return func() (docker.LogEvent, error) {
message, readerError := reader.ReadString('\n')
h := fnv.New32a()
h.Write([]byte(message))
logEvent := docker.LogEvent{Id: h.Sum32()}
if index := strings.IndexAny(message, " "); index != -1 {
logId := message[:index]
if timestamp, err := time.Parse(time.RFC3339Nano, logId); err == nil {
logEvent.Timestamp = timestamp.Unix()
message = strings.TrimSuffix(message[index+1:], "\n")
if strings.HasPrefix(message, "{") && strings.HasSuffix(message, "}") {
var data map[string]interface{}
if err := json.Unmarshal([]byte(message), &data); err != nil {
log.Errorf("json unmarshal error while streaming %v", err.Error())
}
logEvent.Data = data
} else {
logEvent.Message = message
}
} else {
logEvent.Message = message
}
} else {
logEvent.Message = message
}
return logEvent, readerError
}
}
func (h *handler) fetchLogsBetweenDates(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/ld+json; charset=UTF-8")
from, _ := time.Parse(time.RFC3339, r.URL.Query().Get("from"))
to, _ := time.Parse(time.RFC3339, r.URL.Query().Get("to"))
id := r.URL.Query().Get("id")
reader, err := h.client.ContainerLogsBetweenDates(r.Context(), id, from, to)
defer reader.Close()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
buffered := bufio.NewReader(reader)
eventIterator := logEventIterator(buffered)
for {
logEvent, readerError := eventIterator()
if readerError != nil {
break
}
if err := json.NewEncoder(w).Encode(logEvent); err != nil {
log.Errorf("json encoding error while streaming %v", err.Error())
}
}
}
func (h *handler) streamLogs(w http.ResponseWriter, r *http.Request) {
id := r.URL.Query().Get("id")
if id == "" {
http.Error(w, "id is required", http.StatusBadRequest)
return
}
f, ok := w.(http.Flusher)
if !ok {
http.Error(w, "Streaming unsupported!", http.StatusInternalServerError)
return
}
container, err := h.client.FindContainer(id)
if err != nil {
http.Error(w, err.Error(), http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-transform")
w.Header().Add("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
w.Header().Set("X-Accel-Buffering", "no")
lastEventId := r.Header.Get("Last-Event-ID")
if len(r.URL.Query().Get("lastEventId")) > 0 {
lastEventId = r.URL.Query().Get("lastEventId")
}
reader, err := h.client.ContainerLogs(r.Context(), container.ID, h.config.TailSize, lastEventId)
if err != nil {
if err == io.EOF {
fmt.Fprintf(w, "event: container-stopped\ndata: end of stream\n\n")
f.Flush()
} else {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
return
}
defer reader.Close()
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
go func() {
for {
select {
case <-r.Context().Done():
return
case <-ticker.C:
fmt.Fprintf(w, ":ping \n\n")
f.Flush()
}
}
}()
buffered := bufio.NewReader(reader)
var readerError error
eventIterator := logEventIterator(buffered)
for {
var logEvent docker.LogEvent
logEvent, readerError = eventIterator()
if buf, err := json.Marshal(logEvent); err != nil {
log.Errorf("json encoding error while streaming %v", err.Error())
} else {
fmt.Fprintf(w, "data: %s\n", buf)
}
if logEvent.Timestamp > 0 {
fmt.Fprintf(w, "id: %d\n", logEvent.Timestamp)
}
fmt.Fprintf(w, "\n")
f.Flush()
if readerError != nil {
break
}
}
log.Debugf("streaming stopped: %v", container.ID)
if readerError == io.EOF {
log.Debugf("container stopped: %v", container.ID)
fmt.Fprintf(w, "event: container-stopped\ndata: end of stream\n\n")
f.Flush()
} else if readerError != context.Canceled {
log.Errorf("unknown error while streaming %v", readerError.Error())
}
log.WithField("routines", runtime.NumGoroutine()).Debug("runtime goroutine stats")
if log.IsLevelEnabled(log.DebugLevel) {
var m runtime.MemStats
runtime.ReadMemStats(&m)
// For info on each, see: https://golang.org/pkg/runtime/#MemStats
log.WithFields(log.Fields{
"allocated": humanize.Bytes(m.Alloc),
"totalAllocated": humanize.Bytes(m.TotalAlloc),
"system": humanize.Bytes(m.Sys),
}).Debug("runtime mem stats")
}
}