mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-06-16 03:49:08 -04:00
Bumps [github.com/olekukonko/tablewriter](https://github.com/olekukonko/tablewriter) from 1.1.3 to 1.1.4. - [Release notes](https://github.com/olekukonko/tablewriter/releases) - [Commits](https://github.com/olekukonko/tablewriter/compare/v1.1.3...v1.1.4) --- updated-dependencies: - dependency-name: github.com/olekukonko/tablewriter dependency-version: 1.1.4 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com>
389 lines
9.9 KiB
Go
389 lines
9.9 KiB
Go
package ll
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/olekukonko/ll/lx"
|
|
)
|
|
|
|
// Measure executes one or more functions and logs the duration of each.
|
|
// It returns the total cumulative duration across all functions.
|
|
//
|
|
// Each function in `fns` is run sequentially. If a function is `nil`, it is skipped.
|
|
//
|
|
// Optional labels previously set via `Labels(...)` are applied to the corresponding function
|
|
// by position. If there are fewer labels than functions, missing labels are replaced with
|
|
// default names like "fn_0", "fn_1", etc. Labels are cleared after the call to prevent reuse.
|
|
//
|
|
// Example usage:
|
|
//
|
|
// logger := New("app").Enable()
|
|
//
|
|
// // Optional: add labels for functions
|
|
// logger.Labels("load_users", "process_orders")
|
|
//
|
|
// total := logger.Measure(
|
|
// func() {
|
|
// // simulate work 1
|
|
// time.Sleep(100 * time.Millisecond)
|
|
// },
|
|
// func() {
|
|
// // simulate work 2
|
|
// time.Sleep(200 * time.Millisecond)
|
|
// },
|
|
// func() {
|
|
// // simulate work 3
|
|
// time.Sleep(50 * time.Millisecond)
|
|
// },
|
|
// )
|
|
//
|
|
// // Logs something like:
|
|
// // [load_users] completed duration=100ms
|
|
// // [process_orders] completed duration=200ms
|
|
// // [fn_2] completed duration=50ms
|
|
//
|
|
// Returns the sum of durations of all executed functions.
|
|
func (l *Logger) Measure(fns ...func()) time.Duration {
|
|
if len(fns) == 0 {
|
|
return 0
|
|
}
|
|
|
|
var total time.Duration
|
|
lblPtr := l.labels.Swap(nil)
|
|
var lbls []string
|
|
if lblPtr != nil {
|
|
lbls = *lblPtr
|
|
}
|
|
|
|
for i, fn := range fns {
|
|
if fn == nil {
|
|
continue
|
|
}
|
|
// Use SinceBuilder instead of manual timing
|
|
sb := l.Since() // starts timer internally
|
|
fn()
|
|
duration := sb.Fields(
|
|
"index", i,
|
|
).Info(fmt.Sprintf("[%s] completed", func() string {
|
|
if i < len(lbls) && lbls[i] != "" {
|
|
return lbls[i]
|
|
}
|
|
return fmt.Sprintf("fn_%d", i)
|
|
}()))
|
|
|
|
total += duration
|
|
}
|
|
|
|
return total
|
|
}
|
|
|
|
// Since creates a timer that will log the duration when completed
|
|
// If startTime is provided, uses that as the start time; otherwise uses time.Now()
|
|
//
|
|
// defer logger.Since().Info("request") // Auto-start
|
|
// logger.Since(start).Info("request") // Manual timing
|
|
// logger.Since().If(debug).Debug("timing") // Conditional
|
|
func (l *Logger) Since(startTime ...time.Time) *SinceBuilder {
|
|
start := time.Now()
|
|
if len(startTime) > 0 && !startTime[0].IsZero() {
|
|
start = startTime[0]
|
|
}
|
|
|
|
return &SinceBuilder{
|
|
logger: l,
|
|
start: start,
|
|
condition: true,
|
|
fields: nil, // Lazily initialized
|
|
}
|
|
}
|
|
|
|
// SinceBuilder provides a fluent API for logging timed operations
|
|
// It mirrors FieldBuilder exactly for field operations
|
|
type SinceBuilder struct {
|
|
logger *Logger
|
|
start time.Time
|
|
condition bool
|
|
fields lx.Fields
|
|
}
|
|
|
|
// ---------------------------------------------------------------------
|
|
// Conditional Methods (match conditional.go pattern)
|
|
// ---------------------------------------------------------------------
|
|
|
|
// If adds a condition to this timer - only logs if condition is true
|
|
func (sb *SinceBuilder) If(condition bool) *SinceBuilder {
|
|
sb.condition = sb.condition && condition
|
|
return sb
|
|
}
|
|
|
|
// IfErr adds an error condition - only logs if err != nil
|
|
func (sb *SinceBuilder) IfErr(err error) *SinceBuilder {
|
|
sb.condition = sb.condition && (err != nil)
|
|
return sb
|
|
}
|
|
|
|
// IfAny logs if ANY condition is true
|
|
func (sb *SinceBuilder) IfAny(conditions ...bool) *SinceBuilder {
|
|
if !sb.condition {
|
|
return sb
|
|
}
|
|
|
|
for _, cond := range conditions {
|
|
if cond {
|
|
return sb
|
|
}
|
|
}
|
|
sb.condition = false
|
|
return sb
|
|
}
|
|
|
|
// IfOne logs if ALL conditions are true
|
|
func (sb *SinceBuilder) IfOne(conditions ...bool) *SinceBuilder {
|
|
if !sb.condition {
|
|
return sb
|
|
}
|
|
|
|
for _, cond := range conditions {
|
|
if !cond {
|
|
sb.condition = false
|
|
return sb
|
|
}
|
|
}
|
|
return sb
|
|
}
|
|
|
|
// ---------------------------------------------------------------------
|
|
// Field Methods - EXACT MATCH with FieldBuilder API
|
|
// ---------------------------------------------------------------------
|
|
|
|
// Fields adds key-value pairs as fields (variadic)
|
|
// EXACT match to FieldBuilder.Fields()
|
|
func (sb *SinceBuilder) Fields(pairs ...any) *SinceBuilder {
|
|
if sb.logger.suspend.Load() || !sb.condition {
|
|
return sb
|
|
}
|
|
|
|
// Lazy initialization
|
|
if sb.fields == nil {
|
|
sb.fields = make(lx.Fields, 0, len(pairs)/2)
|
|
}
|
|
|
|
// Process key-value pairs
|
|
for i := 0; i < len(pairs)-1; i += 2 {
|
|
if key, ok := pairs[i].(string); ok {
|
|
sb.fields = append(sb.fields, lx.Field{Key: key, Value: pairs[i+1]})
|
|
} else {
|
|
// Log error for non-string keys (matches Fields behavior)
|
|
sb.fields = append(sb.fields, lx.Field{
|
|
Key: "error",
|
|
Value: fmt.Errorf("missing key '%v'", pairs[i]),
|
|
})
|
|
}
|
|
}
|
|
|
|
// Handle uneven pairs (matches Fields behavior)
|
|
if len(pairs)%2 != 0 {
|
|
sb.fields = append(sb.fields, lx.Field{
|
|
Key: "error",
|
|
Value: fmt.Errorf("missing key '%v'", pairs[len(pairs)-1]),
|
|
})
|
|
}
|
|
|
|
return sb
|
|
}
|
|
|
|
// Field adds fields from a map
|
|
// EXACT match to FieldBuilder.Field()
|
|
func (sb *SinceBuilder) Field(fields map[string]interface{}) *SinceBuilder {
|
|
if sb.logger.suspend.Load() || !sb.condition || len(fields) == 0 {
|
|
return sb
|
|
}
|
|
|
|
// Lazy initialization
|
|
if sb.fields == nil {
|
|
sb.fields = make(lx.Fields, 0, len(fields))
|
|
}
|
|
|
|
// Copy fields from input map (preserves iteration order)
|
|
for k, v := range fields {
|
|
sb.fields = append(sb.fields, lx.Field{Key: k, Value: v})
|
|
}
|
|
|
|
return sb
|
|
}
|
|
|
|
// Err adds one or more errors as a field
|
|
// EXACT match to FieldBuilder.Err()
|
|
func (sb *SinceBuilder) Err(errs ...error) *SinceBuilder {
|
|
if sb.logger.suspend.Load() || !sb.condition {
|
|
return sb
|
|
}
|
|
|
|
// Lazy initialization
|
|
if sb.fields == nil {
|
|
sb.fields = make(lx.Fields, 0, 2)
|
|
}
|
|
|
|
// Collect non-nil errors
|
|
var nonNilErrors []error
|
|
var builder strings.Builder
|
|
count := 0
|
|
|
|
for i, err := range errs {
|
|
if err != nil {
|
|
if i > 0 && count > 0 {
|
|
builder.WriteString("; ")
|
|
}
|
|
builder.WriteString(err.Error())
|
|
nonNilErrors = append(nonNilErrors, err)
|
|
count++
|
|
}
|
|
}
|
|
|
|
if count > 0 {
|
|
if count == 1 {
|
|
sb.fields = append(sb.fields, lx.Field{Key: "error", Value: nonNilErrors[0]})
|
|
} else {
|
|
sb.fields = append(sb.fields, lx.Field{Key: "error", Value: nonNilErrors})
|
|
}
|
|
// Note: Unlike FieldBuilder.Err(), we DON'T log immediately
|
|
// The error will be included in the timing log
|
|
}
|
|
|
|
return sb
|
|
}
|
|
|
|
// Merge adds additional key-value pairs to the fields
|
|
// EXACT match to FieldBuilder.Merge()
|
|
func (sb *SinceBuilder) Merge(pairs ...any) *SinceBuilder {
|
|
if sb.logger.suspend.Load() || !sb.condition {
|
|
return sb
|
|
}
|
|
|
|
// Lazy initialization
|
|
if sb.fields == nil {
|
|
sb.fields = make(lx.Fields, 0, len(pairs)/2)
|
|
}
|
|
|
|
// Process pairs as key-value
|
|
for i := 0; i < len(pairs)-1; i += 2 {
|
|
if key, ok := pairs[i].(string); ok {
|
|
sb.fields = append(sb.fields, lx.Field{Key: key, Value: pairs[i+1]})
|
|
} else {
|
|
sb.fields = append(sb.fields, lx.Field{
|
|
Key: "error",
|
|
Value: fmt.Errorf("non-string key in Merge: %v", pairs[i]),
|
|
})
|
|
}
|
|
}
|
|
|
|
if len(pairs)%2 != 0 {
|
|
sb.fields = append(sb.fields, lx.Field{
|
|
Key: "error",
|
|
Value: fmt.Errorf("uneven key-value pairs in Merge: [%v]", pairs[len(pairs)-1]),
|
|
})
|
|
}
|
|
|
|
return sb
|
|
}
|
|
|
|
// ---------------------------------------------------------------------
|
|
// Logging Methods (match logger pattern)
|
|
// ---------------------------------------------------------------------
|
|
|
|
// Debug logs the duration at Debug level with message
|
|
func (sb *SinceBuilder) Debug(msg string) time.Duration {
|
|
return sb.logAtLevel(lx.LevelDebug, msg)
|
|
}
|
|
|
|
// Info logs the duration at Info level with message
|
|
func (sb *SinceBuilder) Info(msg string) time.Duration {
|
|
return sb.logAtLevel(lx.LevelInfo, msg)
|
|
}
|
|
|
|
// Warn logs the duration at Warn level with message
|
|
func (sb *SinceBuilder) Warn(msg string) time.Duration {
|
|
return sb.logAtLevel(lx.LevelWarn, msg)
|
|
}
|
|
|
|
// Error logs the duration at Error level with message
|
|
func (sb *SinceBuilder) Error(msg string) time.Duration {
|
|
return sb.logAtLevel(lx.LevelError, msg)
|
|
}
|
|
|
|
// Log is an alias for Info (for backward compatibility)
|
|
func (sb *SinceBuilder) Log(msg string) time.Duration {
|
|
return sb.Info(msg)
|
|
}
|
|
|
|
// logAtLevel internal method that handles the actual logging
|
|
func (sb *SinceBuilder) logAtLevel(level lx.LevelType, msg string) time.Duration {
|
|
// Fast path - don't even compute duration if we're not logging
|
|
if !sb.condition || sb.logger.suspend.Load() || !sb.logger.shouldLog(level) {
|
|
return time.Since(sb.start)
|
|
}
|
|
|
|
duration := time.Since(sb.start)
|
|
|
|
// Build final fields in this order:
|
|
// 1. Logger context fields (from logger.context)
|
|
// 2. Builder fields (from sb.fields)
|
|
// 3. Duration fields (always last)
|
|
|
|
// Pre-allocate with exact capacity
|
|
totalFields := 0
|
|
if sb.logger.context != nil {
|
|
totalFields += len(sb.logger.context)
|
|
}
|
|
if sb.fields != nil {
|
|
totalFields += len(sb.fields)
|
|
}
|
|
totalFields += 2 // duration_ms, duration
|
|
|
|
fields := make(lx.Fields, 0, totalFields)
|
|
|
|
// Add logger context fields first (preserves order)
|
|
if sb.logger.context != nil {
|
|
fields = append(fields, sb.logger.context...)
|
|
}
|
|
|
|
// Add builder fields
|
|
if sb.fields != nil {
|
|
fields = append(fields, sb.fields...)
|
|
}
|
|
|
|
// Add duration fields last (so they're visible at the end)
|
|
fields = append(fields,
|
|
lx.Field{Key: "duration_ms", Value: duration.Milliseconds()},
|
|
lx.Field{Key: "duration", Value: duration.String()},
|
|
)
|
|
|
|
sb.logger.log(level, lx.ClassTimed, msg, fields, false)
|
|
return duration
|
|
}
|
|
|
|
// ---------------------------------------------------------------------
|
|
// Utility Methods
|
|
// ---------------------------------------------------------------------
|
|
|
|
// Reset allows reusing the builder with a new start time
|
|
// Zero-allocation - keeps fields slice capacity
|
|
func (sb *SinceBuilder) Reset(startTime ...time.Time) *SinceBuilder {
|
|
sb.start = time.Now()
|
|
if len(startTime) > 0 && !startTime[0].IsZero() {
|
|
sb.start = startTime[0]
|
|
}
|
|
sb.condition = true
|
|
if sb.fields != nil {
|
|
sb.fields = sb.fields[:0] // Keep capacity, zero length
|
|
}
|
|
return sb
|
|
}
|
|
|
|
// Elapsed returns the current duration without logging
|
|
func (sb *SinceBuilder) Elapsed() time.Duration {
|
|
return time.Since(sb.start)
|
|
}
|