Skip to content

Commit bd35cc1

Browse files
milos-lkclaude
andauthored
Log destination-caused failures at warn instead of error (#1341)
Failures caused by the user-configured destination (storage 4xx, DNS resolution of user-provided hosts, stream endpoint failures) dominate error-level logs while carrying no operational signal. Classify them via IsDestinationError and log at warn in Controller.OnError and the image sink; internal failures keep error level. Stream endpoint and websocket write failures carry no matchable error type, so they are tagged with MarkDestinationError at the call site, preserving the user-facing message and psrpc code. Backup storage is operator-owned, so backup upload failures flatten their error chains to stay out of the destination classification. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 40af73c commit bd35cc1

5 files changed

Lines changed: 56 additions & 6 deletions

File tree

pkg/errors/errors.go

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2023 LiveKit, Inc.
1+
// Copyright 2026 LiveKit, Inc.
22
//
33
// Licensed under the Apache License, Version 2.0 (the "License");
44
// you may not use this file except in compliance with the License.
@@ -16,9 +16,11 @@ package errors
1616

1717
import (
1818
"errors"
19+
"net"
1920
"strings"
2021

2122
"github.com/livekit/psrpc"
23+
"github.com/livekit/storage"
2224
)
2325

2426
func New(err string) error {
@@ -33,6 +35,46 @@ func As(err error, target any) bool {
3335
return errors.As(err, target)
3436
}
3537

38+
var errDestinationMarker = errors.New("destination error")
39+
40+
type destinationError struct {
41+
err error
42+
}
43+
44+
func (e *destinationError) Error() string {
45+
return e.err.Error()
46+
}
47+
48+
func (e *destinationError) Unwrap() []error {
49+
return []error{e.err, errDestinationMarker}
50+
}
51+
52+
// MarkDestinationError tags err as caused by the user-configured destination,
53+
// preserving its message and unwrap chain.
54+
func MarkDestinationError(err error) error {
55+
if err == nil {
56+
return nil
57+
}
58+
return &destinationError{err: err}
59+
}
60+
61+
// IsDestinationError reports whether a failure was caused by the user-configured
62+
// destination rather than by the egress service: a marked error, a 4xx response
63+
// from storage, or a DNS resolution failure for a user-provided host.
64+
func IsDestinationError(err error) bool {
65+
if errors.Is(err, errDestinationMarker) {
66+
return true
67+
}
68+
69+
var statusErr *storage.ErrorWithStatusCode
70+
if errors.As(err, &statusErr) && statusErr.StatusCode >= 400 && statusErr.StatusCode < 500 {
71+
return true
72+
}
73+
74+
var dnsErr *net.DNSError
75+
return errors.As(err, &dnsErr) && dnsErr.IsNotFound
76+
}
77+
3678
type ErrArray struct {
3779
errs []error
3880
}

pkg/pipeline/controller.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -491,7 +491,7 @@ func (c *Controller) streamFailed(ctx context.Context, stream *config.Stream, st
491491

492492
// fail egress if no outputs remaining
493493
if c.OutputCount.Load() == 0 {
494-
return psrpc.NewError(psrpc.Unavailable, streamErr)
494+
return psrpc.NewError(psrpc.Unavailable, errors.MarkDestinationError(streamErr))
495495
}
496496

497497
logger.Infow("stream failed",
@@ -612,7 +612,11 @@ func (c *Controller) sendEOS() {
612612
}
613613

614614
func (c *Controller) OnError(err error) {
615-
logger.Errorw("controller onError invoked", err)
615+
if errors.IsDestinationError(err) {
616+
logger.Warnw("controller onError invoked", err)
617+
} else {
618+
logger.Errorw("controller onError invoked", err)
619+
}
616620
if errors.Is(err, errors.ErrPipelineFrozen) && c.Debug.EnableProfiling {
617621
c.generateDotFile("error")
618622
c.generatePProf()

pkg/pipeline/sink/image.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,11 @@ func (s *ImageSink) Start() error {
108108
continue
109109
}
110110
if err := s.handleNewImage(update); err != nil {
111-
logger.Errorw("new image handling failed", err)
111+
if errors.IsDestinationError(err) {
112+
logger.Warnw("new image handling failed", err)
113+
} else {
114+
logger.Errorw("new image handling failed", err)
115+
}
112116
failed = true
113117
s.callbacks.OnError(err)
114118
}

pkg/pipeline/sink/uploader/uploader.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ func (u *Uploader) Upload(
177177
return "", 0, psrpc.NewErrorf(psrpc.InvalidArgument,
178178
"primary: %s\nbackup: %s", primaryErr.Error(), backupErr.Error())
179179
}
180-
return "", 0, psrpc.NewError(psrpc.InvalidArgument, backupErr)
180+
return "", 0, psrpc.NewErrorf(psrpc.InvalidArgument, "%s", backupErr.Error())
181181
}
182182

183183
return "", 0, primaryErr

pkg/pipeline/sink/websocket.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ func newWebsocketSink(
9999
if err == io.EOF {
100100
return gst.FlowEOS
101101
}
102-
callbacks.OnError(psrpc.NewError(psrpc.Unavailable, err))
102+
callbacks.OnError(psrpc.NewError(psrpc.Unavailable, errors.MarkDestinationError(err)))
103103
}
104104

105105
return gst.FlowOK

0 commit comments

Comments
 (0)