From 994c7038afd267bb83a0f65f0dc6a885dfbb822c Mon Sep 17 00:00:00 2001 From: Aniruddh Krovvidi Date: Sat, 10 Oct 2026 22:58:22 -0700 Subject: [PATCH] fix(gallery): log failed gallery operations Signed-off-by: Aniruddh Krovvidi (cherry picked from commit 63a1bd9b99da7a23b298fc16e0375a903ecf9480) --- .../galleryop/op_error_logging_test.go | 60 +++++++++++++++++++ core/services/galleryop/service.go | 4 +- 2 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 core/services/galleryop/op_error_logging_test.go diff --git a/core/services/galleryop/op_error_logging_test.go b/core/services/galleryop/op_error_logging_test.go new file mode 100644 index 000000000..98d0ea49f --- /dev/null +++ b/core/services/galleryop/op_error_logging_test.go @@ -0,0 +1,60 @@ +package galleryop_test + +import ( + "bytes" + "context" + "errors" + "log/slog" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/mudler/LocalAI/core/config" + "github.com/mudler/LocalAI/core/gallery" + "github.com/mudler/LocalAI/core/services/galleryop" + "github.com/mudler/xlog" +) + +type failingModelManager struct{ err error } + +func (m *failingModelManager) InstallModel(context.Context, *galleryop.ManagementOp[gallery.GalleryModel, gallery.ModelConfig], galleryop.ProgressCallback) error { + return m.err +} + +func (m *failingModelManager) DeleteModel(string) error { return m.err } + +// A failed operation used to exist only in the job status. The UI's delete is +// fire-and-forget, so nothing read that status, and the server log had no +// trace of it either: a model that would not delete simply came back on the +// next rescan (#12562). +var _ = Describe("gallery operation failures", func() { + It("are logged, not only recorded in the job status", func() { + captured := &bytes.Buffer{} + xlog.SetLogger(xlog.NewLoggerWithHandler(slog.NewTextHandler(captured, &slog.HandlerOptions{Level: slog.LevelError}), xlog.LogLevelError)) + DeferCleanup(func() { xlog.SetLogger(xlog.NewLogger(xlog.LogLevel("info"), "text")) }) + + svc := galleryop.NewGalleryService(&config.ApplicationConfig{}, nil) + svc.SetModelManager(&failingModelManager{err: errors.New("failed to verify path /escape.bin: path is outside of trusted root")}) + ctx, cancel := context.WithCancel(context.Background()) + DeferCleanup(cancel) + Expect(svc.Start(ctx, nil, nil)).To(Succeed()) + + svc.EnqueueModelOp(galleryop.ManagementOp[gallery.GalleryModel, gallery.ModelConfig]{ + ID: "job-doomed", + GalleryElementName: "doomed", + Delete: true, + }) + + Eventually(func() error { + if st := svc.GetStatus("job-doomed"); st != nil && st.Processed { + return st.Error + } + return nil + }, "2s", "10ms").Should(MatchError(ContainSubstring("outside of trusted root"))) + Expect(captured.String()).To(And( + ContainSubstring("Gallery operation failed"), + ContainSubstring("op_id=job-doomed"), + ContainSubstring("outside of trusted root"), + )) + }) +}) diff --git a/core/services/galleryop/service.go b/core/services/galleryop/service.go index 424244ff9..af9718569 100644 --- a/core/services/galleryop/service.go +++ b/core/services/galleryop/service.go @@ -597,10 +597,12 @@ func (g *GalleryService) Start(c context.Context, cl *config.ModelConfigLoader, var updateError func(id string, e error) if !g.appConfig.OpaqueErrors { updateError = func(id string, e error) { + xlog.Error("Gallery operation failed", "op_id", id, "error", e) g.UpdateStatus(id, &OpStatus{Error: e, Processed: true, Message: "error: " + e.Error()}) } } else { - updateError = func(id string, _ error) { + updateError = func(id string, e error) { + xlog.Error("Gallery operation failed", "op_id", id, "error", e) g.UpdateStatus(id, &OpStatus{Error: fmt.Errorf("an error occurred"), Processed: true}) } }