@@ -5,11 +5,14 @@ import (
55 "fmt"
66 "io"
77 "net/http"
8+ "regexp"
89 "strings"
910 "time"
1011
1112 "github.com/amir20/dozzle/internal/auth"
1213 "github.com/amir20/dozzle/internal/container"
14+ container_support "github.com/amir20/dozzle/internal/support/container"
15+ support_web "github.com/amir20/dozzle/internal/support/web"
1316 "github.com/go-chi/chi/v5"
1417 "github.com/rs/zerolog/log"
1518)
@@ -54,15 +57,34 @@ func (h *handler) downloadLogs(w http.ResponseWriter, r *http.Request) {
5457 return
5558 }
5659
57- // Set headers for zip file
58- w .Header ().Set ("Content-Disposition" , fmt .Sprintf ("attachment; filename=container-logs-%s.zip" , nowFmt ))
59- w .Header ().Set ("Content-Type" , "application/zip" )
60+ // Parse filter regex if provided
61+ var regex * regexp.Regexp
62+ var err error
63+ if r .URL .Query ().Has ("filter" ) {
64+ regex , err = support_web .ParseRegex (r .URL .Query ().Get ("filter" ))
65+ if err != nil {
66+ http .Error (w , err .Error (), http .StatusBadRequest )
67+ return
68+ }
69+ }
6070
61- // Create zip writer
62- zw := zip .NewWriter (w )
63- defer zw .Close ()
71+ // Parse level filters if provided
72+ levels := make (map [string ]struct {})
73+ if r .URL .Query ().Has ("levels" ) {
74+ for _ , level := range r .URL .Query ()["levels" ] {
75+ levels [level ] = struct {}{}
76+ }
77+ }
78+
79+ // Validate all containers before starting to write response
80+ type containerInfo struct {
81+ hostId string
82+ host string
83+ id string
84+ containerService * container_support.ContainerService
85+ }
86+ containers := make ([]containerInfo , 0 , len (hostIds ))
6487
65- // Process each container
6688 for _ , hostId := range hostIds {
6789 parts := strings .Split (hostId , "~" )
6890 if len (parts ) != 2 {
@@ -80,29 +102,79 @@ func (h *handler) downloadLogs(w http.ResponseWriter, r *http.Request) {
80102 return
81103 }
82104
105+ containers = append (containers , containerInfo {
106+ hostId : hostId ,
107+ host : host ,
108+ id : id ,
109+ containerService : containerService ,
110+ })
111+ }
112+
113+ // Set headers for zip file
114+ w .Header ().Set ("Content-Disposition" , fmt .Sprintf ("attachment; filename=container-logs-%s.zip" , nowFmt ))
115+ w .Header ().Set ("Content-Type" , "application/zip" )
116+
117+ // Create zip writer
118+ zw := zip .NewWriter (w )
119+ defer zw .Close ()
120+
121+ // Process each container - errors after this point are logged only since response has started
122+ for _ , c := range containers {
83123 // Create new file in zip for this container's logs
84- fileName := fmt .Sprintf ("%s-%s.log" , containerService .Container .Name , nowFmt )
124+ fileName := fmt .Sprintf ("%s-%s.log" , c . containerService .Container .Name , nowFmt )
85125 f , err := zw .Create (fileName )
86126 if err != nil {
87- log .Error ().Err (err ).Msgf ("error creating zip entry for container %s" , id )
88- http .Error (w , fmt .Sprintf ("error creating zip entry: %v" , err ), http .StatusInternalServerError )
127+ log .Error ().Err (err ).Msgf ("error creating zip entry for container %s" , c .id )
89128 return
90129 }
91130
92- // Get container logs
93- reader , err := containerService .RawLogs (r .Context (), time.Time {}, now , stdTypes )
94- if err != nil {
95- log .Error ().Err (err ).Msgf ("error getting logs for container %s" , id )
96- http .Error (w , fmt .Sprintf ("error getting logs for container %s: %v" , id , err ), http .StatusInternalServerError )
97- return
98- }
131+ // Get container logs - use LogsBetweenDates if filtering is needed, otherwise use RawLogs
132+ if regex != nil || len (levels ) > 0 {
133+ // Fetch parsed log events for filtering
134+ events , err := c .containerService .LogsBetweenDates (r .Context (), time.Time {}, now , stdTypes )
135+ if err != nil {
136+ log .Error ().Err (err ).Msgf ("error getting logs for container %s" , c .id )
137+ return
138+ }
99139
100- // Copy logs to zip file
101- _ , err = io .Copy (f , reader )
102- if err != nil {
103- log .Error ().Err (err ).Msgf ("error copying logs for container %s" , id )
104- http .Error (w , fmt .Sprintf ("error copying logs for container %s: %v" , id , err ), http .StatusInternalServerError )
105- return
140+ // Filter and write events
141+ for event := range events {
142+ // Apply regex filter if provided
143+ if regex != nil && ! support_web .Search (regex , event ) {
144+ continue
145+ }
146+
147+ // Apply level filter if provided
148+ if len (levels ) > 0 {
149+ if _ , ok := levels [event .Level ]; ! ok {
150+ continue
151+ }
152+ }
153+
154+ // Format timestamp in UTC
155+ timestamp := time .UnixMilli (event .Timestamp ).UTC ().Format (time .RFC3339Nano )
156+
157+ // Write timestamp followed by message
158+ _ , err = fmt .Fprintf (f , "%s %s\n " , timestamp , event .RawMessage )
159+ if err != nil {
160+ log .Error ().Err (err ).Msgf ("error writing log for container %s" , c .id )
161+ return
162+ }
163+ }
164+ } else {
165+ // No filtering needed, use raw logs for better performance
166+ reader , err := c .containerService .RawLogs (r .Context (), time.Time {}, now , stdTypes )
167+ if err != nil {
168+ log .Error ().Err (err ).Msgf ("error getting logs for container %s" , c .id )
169+ return
170+ }
171+
172+ // Copy logs to zip file
173+ _ , err = io .Copy (f , reader )
174+ if err != nil {
175+ log .Error ().Err (err ).Msgf ("error copying logs for container %s" , c .id )
176+ return
177+ }
106178 }
107179 }
108180}
0 commit comments