mirror of
https://github.com/amir20/dozzle.git
synced 2025-12-21 13:23:07 +01:00
* Adds support for multiple hosts * Adds UI for drop down * Adds support for TLS and remove SSH * Changes dropdown to only show up with >1 hosts * Fixes js tests * Fixes go tests * Fixes download link * Updates readme * Removes unused imports * Fixes spaces
181 lines
4.5 KiB
Go
181 lines
4.5 KiB
Go
package web
|
|
|
|
import (
|
|
"bufio"
|
|
"compress/gzip"
|
|
"context"
|
|
"encoding/json"
|
|
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"runtime"
|
|
|
|
"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.clientFromRequest(r).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.clientFromRequest(r).ContainerLogsBetweenDates(r.Context(), container.ID, from, now)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
io.Copy(zw, reader)
|
|
}
|
|
|
|
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.clientFromRequest(r).ContainerLogsBetweenDates(r.Context(), id, from, to)
|
|
defer reader.Close()
|
|
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
buffered := bufio.NewReader(reader)
|
|
iterator := docker.NewEventIterator(buffered)
|
|
|
|
for {
|
|
logEvent, readerError := iterator.Next()
|
|
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.clientFromRequest(r).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.clientFromRequest(r).ContainerLogs(r.Context(), container.ID, 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)
|
|
iterator := docker.NewEventIterator(buffered)
|
|
|
|
for {
|
|
|
|
logEvent, err := iterator.Next()
|
|
if err != nil {
|
|
break
|
|
}
|
|
|
|
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()
|
|
}
|
|
|
|
log.Debugf("streaming stopped: %v", container.ID)
|
|
|
|
if iterator.LastError() == 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 iterator.LastError() != context.Canceled {
|
|
log.Errorf("unknown error while streaming %v", iterator.LastError().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")
|
|
}
|
|
}
|