@@ -8,68 +8,113 @@ import (
88 "github.com/rs/zerolog"
99)
1010
11- // LogProgressFunc is a function that can be called to add to the progress
12- type LogProgressFunc func (addProgress int )
13-
14- type LogProgressConfig struct {
15- Message string
16- Total int
17- Sampler zerolog.Sampler
11+ // LogProgressFunc is a function that can be called to add to the progress.
12+ // The function can be called concurrently. addProgress is the amount to add to the progress.
13+ // It is any integer number type, but all negative values are ignored.
14+ type LogProgressFunc [T int | uint | int32 | uint32 | uint64 | int64 ] func (addProgress T )
15+
16+ type LogProgressConfig [T int | uint | int32 | uint32 | uint64 | int64 ] struct {
17+ // message is part of the messages that will be logged.
18+ // The full template is: `%s progress %d/%d (%.1f%%) total time %s`.
19+ message string
20+ // total is the total value of progress expected.
21+ // When total is added to LogProgressFunc the progress is considered to be 100%.
22+ total T
23+ // noDataLogDuration. If the last log line was more than this duration ago and a new data point is added, a new log line is logged.
24+ // No line is logged if no data is received. The minimum resolution for noDataLogDuration is 1 millisecond.
25+ noDataLogDuration time.Duration
26+ // ticks is the number of increments to log at. If total is > 0 there will be at least 2 ticks. One at 0 and one at total.
27+ // If you want to log at every 10% set ticks to 11 (one is at 0%).
28+ // If the number of ticks is more than total, it will be set to total + 1.
29+ ticks uint64
1830}
1931
20- func DefaultLogProgressConfig (
32+ // DefaultLogProgressConfig returns a LogProgressConfig with default values.
33+ // The default values will log every 10% and will log an additional line if new data is received
34+ // after no data has been received for 1 minute.
35+ func DefaultLogProgressConfig [T int | uint | int32 | uint32 | uint64 | int64 ](
2136 message string ,
22- total int ,
23- ) LogProgressConfig {
24- nth := uint32 (total / 10 ) // sample every 10% by default
25- if nth == 0 {
26- nth = 1
27- }
28-
29- sampler := newProgressLogsSampler (nth , 60 * time .Second )
30- return NewLogProgressConfig (
37+ total T ,
38+ ) LogProgressConfig [T ] {
39+ return NewLogProgressConfig [T ](
3140 message ,
3241 total ,
33- sampler ,
42+ 60 * time .Second ,
43+ 10 ,
3444 )
3545}
3646
37- func NewLogProgressConfig (
47+ // NewLogProgressConfig creates and returns a new LogProgressConfig with the specified message, total, duration, and ticks.
48+ // The duration is rounded to the nearest millisecond.
49+ // The number of ticks is the number of increments to log at. Logging at 0% is always done.
50+ // If you want to log at 10% increments, set ticks to 10.
51+ func NewLogProgressConfig [T int | uint | int32 | uint32 | uint64 | int64 ](
3852 message string ,
39- total int ,
40- sampler zerolog.Sampler ) LogProgressConfig {
41- return LogProgressConfig {
42- Message : message ,
43- Total : total ,
44- Sampler : sampler ,
53+ total T ,
54+ noDataLogDuration time.Duration ,
55+ ticks uint64 ,
56+ ) LogProgressConfig [T ] {
57+ // sanitize total
58+ if total < 0 {
59+ total = 0
4560 }
4661
47- }
62+ // add the tick at 0%
63+ ticks = ticks + 1
4864
49- type LogProgressOption func (config * LogProgressConfig )
65+ // sanitize ticks
66+ // number of ticks should be at most total + 1
67+ if uint64 (total + 1 ) < ticks {
68+ ticks = uint64 (total + 1 )
69+ }
70+
71+ // sanitize noDataLogDuration
72+ if noDataLogDuration < time .Millisecond {
73+ noDataLogDuration = time .Millisecond
74+ }
75+
76+ return LogProgressConfig [T ]{
77+ message : message ,
78+ total : total ,
79+ noDataLogDuration : noDataLogDuration ,
80+ ticks : ticks ,
81+ }
5082
51- // LogProgress takes a total and return function such that when called adds the given
52- // number to the progress and logs the progress every 10% or every 60 seconds whichever
53- // comes first.
83+ }
84+
85+ // LogProgress takes a LogProgressConfig and return function such that when called adds the given
86+ // number to the progress and logs the progress in defined increments or there is a time gap between progress
87+ // updates.
5488// The returned function can be called concurrently.
5589// An eta is also logged, but it assumes that the progress is linear.
56- func LogProgress (
90+ func LogProgress [ T int | uint | int32 | uint32 | uint64 | int64 ] (
5791 log zerolog.Logger ,
58- config LogProgressConfig ,
59- ) LogProgressFunc {
60- sampler := log .Sample (config .Sampler )
61-
62- start := time .Now ()
63- currentIndex := uint64 (0 )
64- return func (add int ) {
65- current := atomic .AddUint64 (& currentIndex , uint64 (add ))
92+ config LogProgressConfig [T ],
93+ ) LogProgressFunc [T ] {
94+
95+ start := time .Now ().UnixMilli ()
96+ var lastDataTime atomic.Int64
97+ lastDataTime .Store (start )
98+ var currentIndex atomic.Uint64
99+
100+ // mutex to protect logProgress from concurrent calls
101+ // mutex is technically only needed for when the underlying io.Writer for the provider zerolog.Logger
102+ // is not thread safe. However we lock conservatively because we intend to call logProgress infrequently in normal
103+ // usage anyway.
104+ var mux sync.Mutex
105+
106+ total := uint64 (config .total )
107+ logProgress := func (current uint64 ) {
108+ mux .Lock ()
109+ defer mux .Unlock ()
110+
111+ elapsed := time .Since (time .UnixMilli (start ))
112+ elapsedString := elapsed .Round (1 * time .Second ).String ()
66113
67114 percentage := float64 (100 )
68- if config .Total > 0 {
69- percentage = (float64 (current ) / float64 (config .Total )) * 100.
115+ if config .total > 0 {
116+ percentage = (float64 (current ) / float64 (config .total )) * 100.
70117 }
71- elapsed := time .Since (start )
72- elapsedString := elapsed .Round (1 * time .Second ).String ()
73118
74119 etaString := "unknown"
75120 if percentage > 0 {
@@ -78,72 +123,76 @@ func LogProgress(
78123 eta = 0
79124 }
80125 etaString = eta .Round (1 * time .Second ).String ()
81-
82126 }
83127
84- if current != uint64 ( config . Total ) {
85- sampler .Info ().Msgf ("%s progress %d/%d (%.1f%%) elapsed: %s, eta %s" , config .Message , current , config .Total , percentage , elapsedString , etaString )
128+ if current < total {
129+ log .Info ().Msgf ("%s progress %d/%d (%.1f%%) elapsed: %s, eta %s" , config .message , current , config .total , percentage , elapsedString , etaString )
86130 } else {
87- log .Info ().Msgf ("%s progress %d/%d (%.1f%%) total time %s" , config .Message , current , config .Total , percentage , elapsedString )
131+ log .Info ().Msgf ("%s progress %d/%d (%.1f%%) total time %s" , config .message , current , config .total , percentage , elapsedString )
88132 }
89133 }
90- }
91134
92- type TimedSampler struct {
93- start time.Time
94- Duration time.Duration
95- mu sync.Mutex
96- }
135+ // log 0% progress
136+ logProgress (0 )
97137
98- var _ zerolog.Sampler = (* TimedSampler )(nil )
99-
100- func NewTimedSampler (duration time.Duration ) * TimedSampler {
101- return & TimedSampler {
102- start : time .Now (),
103- Duration : duration ,
104- mu : sync.Mutex {},
138+ // sanitize inputs and calculate increment
139+ ticksIncludingZero := config .ticks
140+ if ticksIncludingZero < 2 {
141+ ticksIncludingZero = 2
105142 }
106- }
143+ ticks := ticksIncludingZero - 1
107144
108- func (s * TimedSampler ) Sample (_ zerolog.Level ) bool {
109- s .mu .Lock ()
110- defer s .mu .Unlock ()
111-
112- if time .Since (s .start ) > s .Duration {
113- s .start = time .Now ()
114- return true
145+ increment := total / ticks
146+ if increment == 0 {
147+ increment = 1
115148 }
116- return false
117- }
118149
119- func (s * TimedSampler ) Reset () {
120- s .mu .Lock ()
121- defer s .mu .Unlock ()
150+ // increment doesn't necessarily divide config.total
151+ // Because we want 100% to mean 100% we need to deduct this overflow from the current value
152+ // before checking if it is a multiple of the increment.
153+ incrementsOverflow := total % increment
154+ noLogDurationMillis := config .noDataLogDuration .Milliseconds ()
122155
123- s .start = time .Now ()
124- }
125-
126- type progressLogsSampler struct {
127- basicSampler * zerolog.BasicSampler
128- timedSampler * TimedSampler
129- }
156+ return func (add T ) {
157+ if total == 0 {
158+ return
159+ }
160+ if add < 0 {
161+ return
162+ }
163+ diff := uint64 (add )
164+ now := time .Now ().UnixMilli ()
165+
166+ // it can technically happen that current > total. In this case we continue to log
167+ // the progress using the calculated increments
168+ current := currentIndex .Add (diff )
169+ lastTime := lastDataTime .Swap (now )
170+
171+ // if the diff went over one or more increments, log the progress for each increment
172+ fromTick := uint64 (0 )
173+ if current - diff >= incrementsOverflow {
174+ fromTick = (current - diff - incrementsOverflow ) / increment
175+ }
176+ toTick := uint64 (0 )
177+ if current >= incrementsOverflow {
178+ toTick = (current - incrementsOverflow ) / increment
179+ }
130180
131- var _ zerolog.Sampler = (* progressLogsSampler )(nil )
181+ if fromTick == toTick && now - lastTime > noLogDurationMillis {
182+ // no data for a while, log whatever we are at now
183+ logProgress (current )
184+ return
185+ }
132186
133- // newProgressLogsSampler returns a sampler that samples every nth log
134- // and also samples a log if the last log was more than duration ago
135- func newProgressLogsSampler (nth uint32 , duration time.Duration ) zerolog.Sampler {
136- return & progressLogsSampler {
137- basicSampler : & zerolog.BasicSampler {N : nth },
138- timedSampler : NewTimedSampler (duration ),
139- }
140- }
187+ if toTick <= fromTick {
188+ return
189+ }
141190
142- func (s * progressLogsSampler ) Sample (lvl zerolog.Level ) bool {
143- sample := s .basicSampler .Sample (lvl )
144- if sample {
145- s .timedSampler .Reset ()
146- return true
191+ for t := fromTick ; t < toTick ; t ++ {
192+ // (t+1) because we want to log the progress for the increment reached
193+ // not the increment past
194+ current := increment * (t + 1 ) + incrementsOverflow
195+ logProgress (current )
196+ }
147197 }
148- return s .timedSampler .Sample (lvl )
149198}
0 commit comments