detached d95b77e

refactor: Improve Docker event stream resilience and logging

- Enhance container event listener with advanced EOF and error handling
- Add configurable reconnection strategies for different error types
- Implement more granular logging for event stream connections
- Reduce log noise with intelligent logging thresholds
- Add Podman socket compatibility in Docker client initialization
- Improve debug logging for Docker client and event stream operations
commit
d95b77e02d0a460390f424640b1c1dd19daaaeb0
author
bamen · · 2025-03-10 16:52 UTC
parent
9fac95c
signature
unsigned
2 files · +77 -26
M pkg/docker/client.go +8 -1
@@ -6,6 +6,7 @@ import (
66 "os"
77 "time"
88
9+ "github.com/charmbracelet/log"
910 "github.com/docker/docker/api/types"
1011 "github.com/docker/docker/api/types/container"
1112 "github.com/docker/docker/client"
@@ -30,13 +31,19 @@ func InitializeClient(config *Config) error {
3031 return err
3132 }
3233
34+ // For Podman compatibility, we need to ensure we're using the right connection approach
35+ host := "unix://" + config.Sock
36+
37+ // Create the Docker client with appropriate options
3338 cli, err := client.NewClientWithOpts(
3439 client.WithAPIVersionNegotiation(),
35- client.WithHost("unix://"+config.Sock),
40+ client.WithHost(host),
3641 )
3742 if err != nil {
3843 return fmt.Errorf("error initializing Docker client: %s", err)
3944 }
45+
46+ log.Debug("Docker client initialized", "socket", config.Sock, "podman_enabled", config.PodmanEnable)
4047 dockerCli = cli
4148 currentConfig = config
4249 return nil
@@ -6,6 +6,7 @@ import (
6 "os"6 "os"
7 "time"7 "time"
8 8
9+ "github.com/charmbracelet/log"
9 "github.com/docker/docker/api/types"10 "github.com/docker/docker/api/types"
10 "github.com/docker/docker/api/types/container"11 "github.com/docker/docker/api/types/container"
11 "github.com/docker/docker/client"12 "github.com/docker/docker/client"
@@ -30,13 +31,19 @@ func InitializeClient(config *Config) error {
30 return err31 return err
31 }32 }
32 33
34+ // For Podman compatibility, we need to ensure we're using the right connection approach
35+ host := "unix://" + config.Sock
36+
37+ // Create the Docker client with appropriate options
33 cli, err := client.NewClientWithOpts(38 cli, err := client.NewClientWithOpts(
34 client.WithAPIVersionNegotiation(),39 client.WithAPIVersionNegotiation(),
35- client.WithHost("unix://"+config.Sock),40+ client.WithHost(host),
36 )41 )
37 if err != nil {42 if err != nil {
38 return fmt.Errorf("error initializing Docker client: %s", err)43 return fmt.Errorf("error initializing Docker client: %s", err)
39 }44 }
45+
46+ log.Debug("Docker client initialized", "socket", config.Sock, "podman_enabled", config.PodmanEnable)
40 dockerCli = cli47 dockerCli = cli
41 currentConfig = config48 currentConfig = config
42 return nil49 return nil
M pkg/docker/containers.go +69 -25
@@ -760,6 +760,13 @@ func ListenForContainerEvents(networkFilter string, callback func(string, string
760760 consecutiveFailures := 0
761761 maxConsecutiveFailures := 10
762762
763+ // Track EOFs for reduced logging
764+ eofCount := 0
765+ lastEOFLogTime := time.Now().Add(-10 * time.Minute) // Initialize to past time
766+
767+ // Fixed delay for EOFs to prevent rapid reconnection
768+ eofReconnectDelay := 5 * time.Second
769+
763770 for {
764771 // Check if context was canceled (application is shutting down)
765772 if ctx.Err() != nil {
@@ -767,18 +774,24 @@ func ListenForContainerEvents(networkFilter string, callback func(string, string
767774 }
768775
769776 // Add a timeout context for the Events call
770- // This helps prevent long-running connections that might become stale
771- eventCtx, eventCancel := context.WithTimeout(ctx, 5*time.Minute)
777+ eventCtx, eventCancel := context.WithCancel(ctx)
772778
773779 // Set up filters for the events we're interested in
774780 filters := makeEventFilters(networkFilter)
775781
776- // Start listening for events with timeout context
782+ log.Debug("connecting to container event stream", "network_filter", networkFilter)
783+
784+ // Start listening for events
777785 messages, errs := dockerCli.Events(eventCtx, events.ListOptions{
778786 Filters: filters,
779787 })
780788
781- log.Info("Started container event listener", "network_filter", networkFilter)
789+ // Only log the first connection and every 10th reconnection for EOF
790+ if eofCount == 0 {
791+ log.Info("Started container event listener", "network_filter", networkFilter)
792+ } else {
793+ log.Debug("Reconnected container event listener", "network_filter", networkFilter, "reconnect_count", eofCount)
794+ }
782795
783796 // Process events in an inner loop
784797 listenSuccess := false
@@ -789,7 +802,24 @@ func ListenForContainerEvents(networkFilter string, callback func(string, string
789802 case err := <-errs:
790803 if err != nil && ctx.Err() == nil {
791804 // Only log if this isn't due to context cancellation
792- log.Error("Error in container event stream", "error", err)
805+ if err.Error() == "EOF" {
806+ // Increment EOF counter
807+ eofCount++
808+
809+ // Log EOF errors less frequently to reduce spam
810+ now := time.Now()
811+ if eofCount == 1 || eofCount%10 == 0 || now.Sub(lastEOFLogTime) > time.Minute {
812+ log.Debug("Docker event stream closed with EOF - will reconnect",
813+ "count", eofCount,
814+ "next_reconnect_delay", eofReconnectDelay)
815+ lastEOFLogTime = now
816+ }
817+ } else {
818+ // Non-EOF errors are still important, log them as errors
819+ log.Error("Error in container event stream", "error", err)
820+ // Reset EOF counter for non-EOF errors
821+ eofCount = 0
822+ }
793823
794824 // Cancel the event context to clean up resources
795825 eventCancel()
@@ -798,28 +828,26 @@ func ListenForContainerEvents(networkFilter string, callback func(string, string
798828 consecutiveFailures++
799829
800830 // If we have too many consecutive failures, log a warning
801- if consecutiveFailures >= maxConsecutiveFailures {
831+ if consecutiveFailures >= maxConsecutiveFailures && consecutiveFailures%maxConsecutiveFailures == 0 {
802832 log.Warn("Multiple consecutive failures in container event stream",
803833 "count", consecutiveFailures,
804834 "will_continue_trying", true)
805835 }
806836
807- // Calculate backoff delay with exponential increase
808- if listenSuccess {
809- // If we had successful events before failure, reset the delay
810- currentDelay = initialDelay
811- } else {
812- // Exponential backoff: increase the delay each time, up to max
813- currentDelay = time.Duration(float64(currentDelay) * 1.5)
814- if currentDelay > maxDelay {
815- currentDelay = maxDelay
837+ // Calculate backoff delay with exponential increase for non-EOF errors
838+ if err.Error() != "EOF" {
839+ if listenSuccess {
840+ // If we had successful events before failure, reset the delay
841+ currentDelay = initialDelay
842+ } else {
843+ // Exponential backoff: increase the delay each time, up to max
844+ currentDelay = time.Duration(float64(currentDelay) * 1.5)
845+ if currentDelay > maxDelay {
846+ currentDelay = maxDelay
847+ }
816848 }
817849 }
818850
819- log.Debug("Will attempt to reconnect to container event stream",
820- "delay", currentDelay,
821- "consecutive_failures", consecutiveFailures)
822-
823851 // Break out of the inner event loop to reconnect
824852 break eventStreamLoop
825853 }
@@ -834,7 +862,10 @@ func ListenForContainerEvents(networkFilter string, callback func(string, string
834862 listenSuccess = true
835863 // Reset failure counter on successful event
836864 consecutiveFailures = 0
837- log.Info("Successfully receiving container events")
865+ // Only log first successful connection or after errors
866+ if eofCount <= 1 {
867+ log.Info("Successfully receiving container events")
868+ }
838869 }
839870
840871 // Update last event time
@@ -858,7 +889,7 @@ func ListenForContainerEvents(networkFilter string, callback func(string, string
858889 switch event.Action {
859890 case "start":
860891 // Container started - need to get its IP and update any proxy routes
861- log.Debug("Container started event",
892+ log.Debug("container started event",
862893 "container_id", containerID,
863894 "container_name", containerName)
864895
@@ -876,14 +907,14 @@ func ListenForContainerEvents(networkFilter string, callback func(string, string
876907
877908 case "die", "kill", "destroy":
878909 // Container stopped - might need to mark routes as inactive
879- log.Debug("Container stopped event",
910+ log.Debug("container stopped event",
880911 "container_id", containerID,
881912 "container_name", containerName,
882913 "action", event.Action)
883914
884915 case "rename":
885916 // Container renamed - update related proxy routes
886- log.Debug("Container renamed event",
917+ log.Debug("container renamed event",
887918 "container_id", containerID,
888919 "container_name", containerName,
889920 "new_name", event.Actor.Attributes["name"])
@@ -916,10 +947,23 @@ func ListenForContainerEvents(networkFilter string, callback func(string, string
916947 // Clean up the event context
917948 eventCancel()
918949
950+ // Use different reconnection delays for different error types
951+ var reconnectDelay time.Duration
952+ if eofCount > 0 {
953+ // Use fixed delay for EOF errors to prevent rapid reconnection cycles
954+ reconnectDelay = eofReconnectDelay
955+ } else {
956+ // Use exponential backoff for other errors
957+ reconnectDelay = currentDelay
958+ }
959+
919960 // Wait for the calculated delay before reconnecting
920961 select {
921- case <-time.After(currentDelay):
922- log.Info("Attempting to reconnect to container event stream")
962+ case <-time.After(reconnectDelay):
963+ // Only log reconnection attempts for non-EOF errors or occasional EOF errors
964+ if eofCount == 0 || eofCount%10 == 0 {
965+ log.Debug("Attempting to reconnect to container event stream", "delay", reconnectDelay)
966+ }
923967 case <-ctx.Done():
924968 return
925969 }
@@ -760,6 +760,13 @@ func ListenForContainerEvents(networkFilter string, callback func(string, string
760 consecutiveFailures := 0760 consecutiveFailures := 0
761 maxConsecutiveFailures := 10761 maxConsecutiveFailures := 10
762 762
763+ // Track EOFs for reduced logging
764+ eofCount := 0
765+ lastEOFLogTime := time.Now().Add(-10 * time.Minute) // Initialize to past time
766+
767+ // Fixed delay for EOFs to prevent rapid reconnection
768+ eofReconnectDelay := 5 * time.Second
769+
763 for {770 for {
764 // Check if context was canceled (application is shutting down)771 // Check if context was canceled (application is shutting down)
765 if ctx.Err() != nil {772 if ctx.Err() != nil {
@@ -767,18 +774,24 @@ func ListenForContainerEvents(networkFilter string, callback func(string, string
767 }774 }
768 775
769 // Add a timeout context for the Events call776 // Add a timeout context for the Events call
770- // This helps prevent long-running connections that might become stale777+ eventCtx, eventCancel := context.WithCancel(ctx)
771- eventCtx, eventCancel := context.WithTimeout(ctx, 5*time.Minute)
772 778
773 // Set up filters for the events we're interested in779 // Set up filters for the events we're interested in
774 filters := makeEventFilters(networkFilter)780 filters := makeEventFilters(networkFilter)
775 781
776- // Start listening for events with timeout context782+ log.Debug("connecting to container event stream", "network_filter", networkFilter)
783+
784+ // Start listening for events
777 messages, errs := dockerCli.Events(eventCtx, events.ListOptions{785 messages, errs := dockerCli.Events(eventCtx, events.ListOptions{
778 Filters: filters,786 Filters: filters,
779 })787 })
780 788
781- log.Info("Started container event listener", "network_filter", networkFilter)789+ // Only log the first connection and every 10th reconnection for EOF
790+ if eofCount == 0 {
791+ log.Info("Started container event listener", "network_filter", networkFilter)
792+ } else {
793+ log.Debug("Reconnected container event listener", "network_filter", networkFilter, "reconnect_count", eofCount)
794+ }
782 795
783 // Process events in an inner loop796 // Process events in an inner loop
784 listenSuccess := false797 listenSuccess := false
@@ -789,7 +802,24 @@ func ListenForContainerEvents(networkFilter string, callback func(string, string
789 case err := <-errs:802 case err := <-errs:
790 if err != nil && ctx.Err() == nil {803 if err != nil && ctx.Err() == nil {
791 // Only log if this isn't due to context cancellation804 // Only log if this isn't due to context cancellation
792- log.Error("Error in container event stream", "error", err)805+ if err.Error() == "EOF" {
806+ // Increment EOF counter
807+ eofCount++
808+
809+ // Log EOF errors less frequently to reduce spam
810+ now := time.Now()
811+ if eofCount == 1 || eofCount%10 == 0 || now.Sub(lastEOFLogTime) > time.Minute {
812+ log.Debug("Docker event stream closed with EOF - will reconnect",
813+ "count", eofCount,
814+ "next_reconnect_delay", eofReconnectDelay)
815+ lastEOFLogTime = now
816+ }
817+ } else {
818+ // Non-EOF errors are still important, log them as errors
819+ log.Error("Error in container event stream", "error", err)
820+ // Reset EOF counter for non-EOF errors
821+ eofCount = 0
822+ }
793 823
794 // Cancel the event context to clean up resources824 // Cancel the event context to clean up resources
795 eventCancel()825 eventCancel()
@@ -798,28 +828,26 @@ func ListenForContainerEvents(networkFilter string, callback func(string, string
798 consecutiveFailures++828 consecutiveFailures++
799 829
800 // If we have too many consecutive failures, log a warning830 // If we have too many consecutive failures, log a warning
801- if consecutiveFailures >= maxConsecutiveFailures {831+ if consecutiveFailures >= maxConsecutiveFailures && consecutiveFailures%maxConsecutiveFailures == 0 {
802 log.Warn("Multiple consecutive failures in container event stream",832 log.Warn("Multiple consecutive failures in container event stream",
803 "count", consecutiveFailures,833 "count", consecutiveFailures,
804 "will_continue_trying", true)834 "will_continue_trying", true)
805 }835 }
806 836
807- // Calculate backoff delay with exponential increase837+ // Calculate backoff delay with exponential increase for non-EOF errors
808- if listenSuccess {838+ if err.Error() != "EOF" {
809- // If we had successful events before failure, reset the delay839+ if listenSuccess {
810- currentDelay = initialDelay840+ // If we had successful events before failure, reset the delay
811- } else {841+ currentDelay = initialDelay
812- // Exponential backoff: increase the delay each time, up to max842+ } else {
813- currentDelay = time.Duration(float64(currentDelay) * 1.5)843+ // Exponential backoff: increase the delay each time, up to max
814- if currentDelay > maxDelay {844+ currentDelay = time.Duration(float64(currentDelay) * 1.5)
815- currentDelay = maxDelay845+ if currentDelay > maxDelay {
846+ currentDelay = maxDelay
847+ }
816 }848 }
817 }849 }
818 850
819- log.Debug("Will attempt to reconnect to container event stream",
820- "delay", currentDelay,
821- "consecutive_failures", consecutiveFailures)
822-
823 // Break out of the inner event loop to reconnect851 // Break out of the inner event loop to reconnect
824 break eventStreamLoop852 break eventStreamLoop
825 }853 }
@@ -834,7 +862,10 @@ func ListenForContainerEvents(networkFilter string, callback func(string, string
834 listenSuccess = true862 listenSuccess = true
835 // Reset failure counter on successful event863 // Reset failure counter on successful event
836 consecutiveFailures = 0864 consecutiveFailures = 0
837- log.Info("Successfully receiving container events")865+ // Only log first successful connection or after errors
866+ if eofCount <= 1 {
867+ log.Info("Successfully receiving container events")
868+ }
838 }869 }
839 870
840 // Update last event time871 // Update last event time
@@ -858,7 +889,7 @@ func ListenForContainerEvents(networkFilter string, callback func(string, string
858 switch event.Action {889 switch event.Action {
859 case "start":890 case "start":
860 // Container started - need to get its IP and update any proxy routes891 // Container started - need to get its IP and update any proxy routes
861- log.Debug("Container started event",892+ log.Debug("container started event",
862 "container_id", containerID,893 "container_id", containerID,
863 "container_name", containerName)894 "container_name", containerName)
864 895
@@ -876,14 +907,14 @@ func ListenForContainerEvents(networkFilter string, callback func(string, string
876 907
877 case "die", "kill", "destroy":908 case "die", "kill", "destroy":
878 // Container stopped - might need to mark routes as inactive909 // Container stopped - might need to mark routes as inactive
879- log.Debug("Container stopped event",910+ log.Debug("container stopped event",
880 "container_id", containerID,911 "container_id", containerID,
881 "container_name", containerName,912 "container_name", containerName,
882 "action", event.Action)913 "action", event.Action)
883 914
884 case "rename":915 case "rename":
885 // Container renamed - update related proxy routes916 // Container renamed - update related proxy routes
886- log.Debug("Container renamed event",917+ log.Debug("container renamed event",
887 "container_id", containerID,918 "container_id", containerID,
888 "container_name", containerName,919 "container_name", containerName,
889 "new_name", event.Actor.Attributes["name"])920 "new_name", event.Actor.Attributes["name"])
@@ -916,10 +947,23 @@ func ListenForContainerEvents(networkFilter string, callback func(string, string
916 // Clean up the event context947 // Clean up the event context
917 eventCancel()948 eventCancel()
918 949
950+ // Use different reconnection delays for different error types
951+ var reconnectDelay time.Duration
952+ if eofCount > 0 {
953+ // Use fixed delay for EOF errors to prevent rapid reconnection cycles
954+ reconnectDelay = eofReconnectDelay
955+ } else {
956+ // Use exponential backoff for other errors
957+ reconnectDelay = currentDelay
958+ }
959+
919 // Wait for the calculated delay before reconnecting960 // Wait for the calculated delay before reconnecting
920 select {961 select {
921- case <-time.After(currentDelay):962+ case <-time.After(reconnectDelay):
922- log.Info("Attempting to reconnect to container event stream")963+ // Only log reconnection attempts for non-EOF errors or occasional EOF errors
964+ if eofCount == 0 || eofCount%10 == 0 {
965+ log.Debug("Attempting to reconnect to container event stream", "delay", reconnectDelay)
966+ }
923 case <-ctx.Done():967 case <-ctx.Done():
924 return968 return
925 }969 }

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close