From 5dadbcb53d894471f80ce0c0f54c1a8a338033b7 Mon Sep 17 00:00:00 2001 From: Jonas Hess Date: Thu, 31 Oct 2024 20:00:43 +0100 Subject: [PATCH] feat: improve auto throughput & logging --- README.md | 3 ++ app_http_handlers.go | 17 +++++----- app_llm.go | 11 +++--- go.mod | 3 +- go.sum | 3 ++ jobs.go | 34 +++++++++++++------ main.go | 81 +++++++++++++++++++++++++++++++++----------- paperless.go | 13 ++++--- 8 files changed, 113 insertions(+), 52 deletions(-) diff --git a/README.md b/README.md index ba2ec68..c829818 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,7 @@ services: OLLAMA_HOST: 'http://host.docker.internal:11434' # If using Ollama VISION_LLM_PROVIDER: 'ollama' # Optional, for OCR VISION_LLM_MODEL: 'minicpm-v' # Optional, for OCR + LOG_LEVEL: 'info' # Optional or 'debug', 'warn', 'error' volumes: - ./prompts:/app/prompts # Mount the prompts directory ports: @@ -122,6 +123,7 @@ If you prefer to run the application manually: -e LLM_LANGUAGE='English' \ -e VISION_LLM_PROVIDER='ollama' \ -e VISION_LLM_MODEL='minicpm-v' \ + -e LOG_LEVEL='info' \ -v $(pwd)/prompts:/app/prompts \ # Mount the prompts directory -p 8080:8080 \ paperless-gpt @@ -142,6 +144,7 @@ If you prefer to run the application manually: | `OLLAMA_HOST` | The URL of the Ollama server (e.g., `http://host.docker.internal:11434`). Useful if using Ollama. Default is `http://127.0.0.1:11434`. | No | | `VISION_LLM_PROVIDER` | The vision LLM provider to use for OCR (`openai` or `ollama`). | No | | `VISION_LLM_MODEL` | The model name to use for OCR (e.g., `minicpm-v`). | No | +| `LOG_LEVEL` | The log level for the application (`info`, `debug`, `warn`, `error`). Default is `info`. | No | **Note:** When using Ollama, ensure that the Ollama server is running and accessible from the paperless-gpt container. diff --git a/app_http_handlers.go b/app_http_handlers.go index c7ab6af..c9c243f 100644 --- a/app_http_handlers.go +++ b/app_http_handlers.go @@ -2,7 +2,6 @@ package main import ( "fmt" - "log" "net/http" "os" "strconv" @@ -59,7 +58,7 @@ func updatePromptsHandler(c *gin.Context) { titleTemplate = t err = os.WriteFile("prompts/title_prompt.tmpl", []byte(req.TitleTemplate), 0644) if err != nil { - log.Printf("Failed to write title_prompt.tmpl: %v", err) + log.Errorf("Failed to write title_prompt.tmpl: %v", err) } } @@ -73,7 +72,7 @@ func updatePromptsHandler(c *gin.Context) { tagTemplate = t err = os.WriteFile("prompts/tag_prompt.tmpl", []byte(req.TagTemplate), 0644) if err != nil { - log.Printf("Failed to write tag_prompt.tmpl: %v", err) + log.Errorf("Failed to write tag_prompt.tmpl: %v", err) } } @@ -87,7 +86,7 @@ func (app *App) getAllTagsHandler(c *gin.Context) { tags, err := app.Client.GetAllTags(ctx) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Error fetching tags: %v", err)}) - log.Printf("Error fetching tags: %v", err) + log.Errorf("Error fetching tags: %v", err) return } @@ -101,7 +100,7 @@ func (app *App) documentsHandler(c *gin.Context) { documents, err := app.Client.GetDocumentsByTags(ctx, []string{manualTag}) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Error fetching documents: %v", err)}) - log.Printf("Error fetching documents: %v", err) + log.Errorf("Error fetching documents: %v", err) return } @@ -115,14 +114,14 @@ func (app *App) generateSuggestionsHandler(c *gin.Context) { var suggestionRequest GenerateSuggestionsRequest if err := c.ShouldBindJSON(&suggestionRequest); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("Invalid request payload: %v", err)}) - log.Printf("Invalid request payload: %v", err) + log.Errorf("Invalid request payload: %v", err) return } results, err := app.generateDocumentSuggestions(ctx, suggestionRequest) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Error processing documents: %v", err)}) - log.Printf("Error processing documents: %v", err) + log.Errorf("Error processing documents: %v", err) return } @@ -135,14 +134,14 @@ func (app *App) updateDocumentsHandler(c *gin.Context) { var documents []DocumentSuggestion if err := c.ShouldBindJSON(&documents); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("Invalid request payload: %v", err)}) - log.Printf("Invalid request payload: %v", err) + log.Errorf("Invalid request payload: %v", err) return } err := app.Client.UpdateDocuments(ctx, documents) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Error updating documents: %v", err)}) - log.Printf("Error updating documents: %v", err) + log.Errorf("Error updating documents: %v", err) return } diff --git a/app_llm.go b/app_llm.go index 15fb79d..b7228f6 100644 --- a/app_llm.go +++ b/app_llm.go @@ -4,7 +4,6 @@ import ( "bytes" "context" "fmt" - "log" "strings" "sync" @@ -30,7 +29,7 @@ func (app *App) getSuggestedTags(ctx context.Context, content string, suggestedT } prompt := promptBuffer.String() - log.Printf("Tag suggestion prompt: %s", prompt) + log.Debugf("Tag suggestion prompt: %s", prompt) completion, err := app.LLM.GenerateContent(ctx, []llms.MessageContent{ { @@ -119,7 +118,7 @@ func (app *App) getSuggestedTitle(ctx context.Context, content string) (string, prompt := promptBuffer.String() - log.Printf("Title suggestion prompt: %s", prompt) + log.Debugf("Title suggestion prompt: %s", prompt) completion, err := app.LLM.GenerateContent(ctx, []llms.MessageContent{ { @@ -183,7 +182,7 @@ func (app *App) generateDocumentSuggestions(ctx context.Context, suggestionReque mu.Lock() errorsList = append(errorsList, fmt.Errorf("Document %d: %v", documentID, err)) mu.Unlock() - log.Printf("Error processing document %d: %v", documentID, err) + log.Errorf("Error processing document %d: %v", documentID, err) return } } @@ -194,7 +193,7 @@ func (app *App) generateDocumentSuggestions(ctx context.Context, suggestionReque mu.Lock() errorsList = append(errorsList, fmt.Errorf("Document %d: %v", documentID, err)) mu.Unlock() - log.Printf("Error generating tags for document %d: %v", documentID, err) + log.Errorf("Error generating tags for document %d: %v", documentID, err) return } } @@ -206,6 +205,7 @@ func (app *App) generateDocumentSuggestions(ctx context.Context, suggestionReque } // Titles if suggestionRequest.GenerateTitles { + log.Printf("Suggested title for document %d: %s", documentID, suggestedTitle) suggestion.SuggestedTitle = suggestedTitle } else { suggestion.SuggestedTitle = doc.Title @@ -213,6 +213,7 @@ func (app *App) generateDocumentSuggestions(ctx context.Context, suggestionReque // Tags if suggestionRequest.GenerateTags { + log.Printf("Suggested tags for document %d: %v", documentID, suggestedTags) suggestion.SuggestedTags = suggestedTags } else { suggestion.SuggestedTags = removeTagFromList(doc.Tags, manualTag) diff --git a/go.mod b/go.mod index 6825cd4..babd910 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,8 @@ require ( github.com/Masterminds/sprig/v3 v3.2.3 github.com/gen2brain/go-fitz v1.24.14 github.com/gin-gonic/gin v1.10.0 + github.com/google/uuid v1.6.0 + github.com/sirupsen/logrus v1.9.3 github.com/stretchr/testify v1.9.0 github.com/tmc/langchaingo v0.1.12 golang.org/x/sync v0.7.0 @@ -29,7 +31,6 @@ require ( github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.20.0 // indirect github.com/goccy/go-json v0.10.2 // indirect - github.com/google/uuid v1.6.0 // indirect github.com/huandu/xstrings v1.3.3 // indirect github.com/imdario/mergo v0.3.13 // indirect github.com/json-iterator/go v1.1.12 // indirect diff --git a/go.sum b/go.sum index d4dfaab..76d584c 100644 --- a/go.sum +++ b/go.sum @@ -77,6 +77,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -123,6 +125,7 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/jobs.go b/jobs.go index a2635a4..bc58b82 100644 --- a/jobs.go +++ b/jobs.go @@ -3,7 +3,6 @@ package main import ( "context" "fmt" - "log" "os" "sort" "strings" @@ -11,6 +10,7 @@ import ( "time" "github.com/google/uuid" + "github.com/sirupsen/logrus" ) // Job represents an OCR job @@ -31,13 +31,25 @@ type JobStore struct { } var ( + logger = logrus.New() + jobStore = &JobStore{ jobs: make(map[string]*Job), } jobQueue = make(chan *Job, 100) // Buffered channel with capacity of 100 jobs - logger = log.New(os.Stdout, "OCR_JOB: ", log.LstdFlags) ) +func init() { + + // Initialize logger + logger.SetOutput(os.Stdout) + logger.SetFormatter(&logrus.TextFormatter{ + FullTimestamp: true, + }) + logger.SetLevel(logrus.InfoLevel) + logger.WithField("prefix", "OCR_JOB") +} + func generateJobID() string { return uuid.New().String() } @@ -47,7 +59,7 @@ func (store *JobStore) addJob(job *Job) { defer store.Unlock() job.PagesDone = 0 // Initialize PagesDone to 0 store.jobs[job.ID] = job - logger.Printf("Job added: %v", job) + logger.Infof("Job added: %v", job) } func (store *JobStore) getJob(jobID string) (*Job, bool) { @@ -82,7 +94,7 @@ func (store *JobStore) updateJobStatus(jobID, status, result string) { job.Result = result } job.UpdatedAt = time.Now() - logger.Printf("Job status updated: %v", job) + logger.Infof("Job status updated: %v", job) } } @@ -92,16 +104,16 @@ func (store *JobStore) updatePagesDone(jobID string, pagesDone int) { if job, exists := store.jobs[jobID]; exists { job.PagesDone = pagesDone job.UpdatedAt = time.Now() - logger.Printf("Job pages done updated: %v", job) + logger.Infof("Job pages done updated: %v", job) } } func startWorkerPool(app *App, numWorkers int) { for i := 0; i < numWorkers; i++ { go func(workerID int) { - logger.Printf("Worker %d started", workerID) + logger.Infof("Worker %d started", workerID) for job := range jobQueue { - logger.Printf("Worker %d processing job: %s", workerID, job.ID) + logger.Infof("Worker %d processing job: %s", workerID, job.ID) processJob(app, job) } }(i) @@ -116,7 +128,7 @@ func processJob(app *App, job *Job) { // Download images of the document imagePaths, err := app.Client.DownloadDocumentAsImages(ctx, job.DocumentID) if err != nil { - logger.Printf("Error downloading document images for job %s: %v", job.ID, err) + logger.Infof("Error downloading document images for job %s: %v", job.ID, err) jobStore.updateJobStatus(job.ID, "failed", fmt.Sprintf("Error downloading document images: %v", err)) return } @@ -125,14 +137,14 @@ func processJob(app *App, job *Job) { for i, imagePath := range imagePaths { imageContent, err := os.ReadFile(imagePath) if err != nil { - logger.Printf("Error reading image file for job %s: %v", job.ID, err) + logger.Errorf("Error reading image file for job %s: %v", job.ID, err) jobStore.updateJobStatus(job.ID, "failed", fmt.Sprintf("Error reading image file: %v", err)) return } ocrText, err := app.doOCRViaLLM(ctx, imageContent) if err != nil { - logger.Printf("Error performing OCR for job %s: %v", job.ID, err) + logger.Errorf("Error performing OCR for job %s: %v", job.ID, err) jobStore.updateJobStatus(job.ID, "failed", fmt.Sprintf("Error performing OCR: %v", err)) return } @@ -146,5 +158,5 @@ func processJob(app *App, job *Job) { // Update job status and result jobStore.updateJobStatus(job.ID, "completed", fullOcrText) - logger.Printf("Job completed: %s", job.ID) + logger.Infof("Job completed: %s", job.ID) } diff --git a/main.go b/main.go index 234b5a5..5c8d3ef 100644 --- a/main.go +++ b/main.go @@ -3,7 +3,6 @@ package main import ( "context" "fmt" - "log" "net/http" "os" "path/filepath" @@ -14,6 +13,7 @@ import ( "github.com/Masterminds/sprig/v3" "github.com/gin-gonic/gin" + "github.com/sirupsen/logrus" "github.com/tmc/langchaingo/llms" "github.com/tmc/langchaingo/llms/ollama" "github.com/tmc/langchaingo/llms/openai" @@ -21,6 +21,11 @@ import ( // Global Variables and Constants var ( + + // Logger + log = logrus.New() + + // Environment Variables paperlessBaseURL = os.Getenv("PAPERLESS_BASE_URL") paperlessAPIToken = os.Getenv("PAPERLESS_API_TOKEN") openaiAPIKey = os.Getenv("OPENAI_API_KEY") @@ -30,6 +35,7 @@ var ( llmModel = os.Getenv("LLM_MODEL") visionLlmProvider = os.Getenv("VISION_LLM_PROVIDER") visionLlmModel = os.Getenv("VISION_LLM_MODEL") + logLevel = strings.ToLower(os.Getenv("LOG_LEVEL")) // Templates titleTemplate *template.Template @@ -75,6 +81,9 @@ func main() { // Validate Environment Variables validateEnvVars() + // Initialize logrus logger + initLogger() + // Initialize PaperlessClient client := NewPaperlessClient(paperlessBaseURL, paperlessAPIToken) @@ -102,25 +111,28 @@ func main() { // Start background process for auto-tagging go func() { - - minBackoffDuration := time.Second + minBackoffDuration := 10 * time.Second maxBackoffDuration := time.Hour pollingInterval := 10 * time.Second backoffDuration := minBackoffDuration for { - if err := app.processAutoTagDocuments(); err != nil { - log.Printf("Error in processAutoTagDocuments: %v", err) + processedCount, err := app.processAutoTagDocuments() + if err != nil { + log.Errorf("Error in processAutoTagDocuments: %v", err) time.Sleep(backoffDuration) backoffDuration *= 2 // Exponential backoff if backoffDuration > maxBackoffDuration { - log.Printf("Repeated errors in processAutoTagDocuments detected. Setting backoff to %v", maxBackoffDuration) + log.Warnf("Repeated errors in processAutoTagDocuments detected. Setting backoff to %v", maxBackoffDuration) backoffDuration = maxBackoffDuration } } else { backoffDuration = minBackoffDuration } - time.Sleep(pollingInterval) + + if processedCount == 0 { + time.Sleep(pollingInterval) + } } }() @@ -168,12 +180,34 @@ func main() { numWorkers := 1 // Number of workers to start startWorkerPool(app, numWorkers) - log.Println("Server started on port :8080") + log.Infoln("Server started on port :8080") if err := router.Run(":8080"); err != nil { log.Fatalf("Failed to run server: %v", err) } } +func initLogger() { + switch logLevel { + case "debug": + log.SetLevel(logrus.DebugLevel) + case "info": + log.SetLevel(logrus.InfoLevel) + case "warn": + log.SetLevel(logrus.WarnLevel) + case "error": + log.SetLevel(logrus.ErrorLevel) + default: + log.SetLevel(logrus.InfoLevel) + if logLevel != "" { + log.Fatalf("Invalid log level: '%s'.", logLevel) + } + } + + log.SetFormatter(&logrus.TextFormatter{ + FullTimestamp: true, + }) +} + func isOcrEnabled() bool { return visionLlmModel != "" && visionLlmProvider != "" } @@ -192,28 +226,37 @@ func validateEnvVars() { log.Fatal("Please set the LLM_PROVIDER environment variable.") } + if visionLlmProvider != "" && visionLlmProvider != "openai" && visionLlmProvider != "ollama" { + log.Fatal("Please set the LLM_PROVIDER environment variable to 'openai' or 'ollama'.") + } + if llmModel == "" { log.Fatal("Please set the LLM_MODEL environment variable.") } - if llmProvider == "openai" && openaiAPIKey == "" { + if (llmProvider == "openai" || visionLlmProvider == "openai") && openaiAPIKey == "" { log.Fatal("Please set the OPENAI_API_KEY environment variable for OpenAI provider.") } } // processAutoTagDocuments handles the background auto-tagging of documents -func (app *App) processAutoTagDocuments() error { +func (app *App) processAutoTagDocuments() (int, error) { ctx := context.Background() documents, err := app.Client.GetDocumentsByTags(ctx, []string{autoTag}) if err != nil { - return fmt.Errorf("error fetching documents with autoTag: %w", err) + return 0, fmt.Errorf("error fetching documents with autoTag: %w", err) } if len(documents) == 0 { - return nil // No documents to process + log.Debugf("No documents with tag %s found", autoTag) + return 0, nil // No documents to process } + log.Debugf("Found at least %d remaining documents with tag %s", len(documents), autoTag) + + documents = documents[:1] // Process only one document at a time + suggestionRequest := GenerateSuggestionsRequest{ Documents: documents, GenerateTitles: true, @@ -222,15 +265,15 @@ func (app *App) processAutoTagDocuments() error { suggestions, err := app.generateDocumentSuggestions(ctx, suggestionRequest) if err != nil { - return fmt.Errorf("error generating suggestions: %w", err) + return 0, fmt.Errorf("error generating suggestions: %w", err) } err = app.Client.UpdateDocuments(ctx, suggestions) if err != nil { - return fmt.Errorf("error updating documents: %w", err) + return 0, fmt.Errorf("error updating documents: %w", err) } - return nil + return len(documents), nil } // removeTagFromList removes a specific tag from a list of tags @@ -268,7 +311,7 @@ func loadTemplates() { titleTemplatePath := filepath.Join(promptsDir, "title_prompt.tmpl") titleTemplateContent, err := os.ReadFile(titleTemplatePath) if err != nil { - log.Printf("Could not read %s, using default template: %v", titleTemplatePath, err) + log.Errorf("Could not read %s, using default template: %v", titleTemplatePath, err) titleTemplateContent = []byte(defaultTitleTemplate) if err := os.WriteFile(titleTemplatePath, titleTemplateContent, os.ModePerm); err != nil { log.Fatalf("Failed to write default title template to disk: %v", err) @@ -283,7 +326,7 @@ func loadTemplates() { tagTemplatePath := filepath.Join(promptsDir, "tag_prompt.tmpl") tagTemplateContent, err := os.ReadFile(tagTemplatePath) if err != nil { - log.Printf("Could not read %s, using default template: %v", tagTemplatePath, err) + log.Errorf("Could not read %s, using default template: %v", tagTemplatePath, err) tagTemplateContent = []byte(defaultTagTemplate) if err := os.WriteFile(tagTemplatePath, tagTemplateContent, os.ModePerm); err != nil { log.Fatalf("Failed to write default tag template to disk: %v", err) @@ -298,7 +341,7 @@ func loadTemplates() { ocrTemplatePath := filepath.Join(promptsDir, "ocr_prompt.tmpl") ocrTemplateContent, err := os.ReadFile(ocrTemplatePath) if err != nil { - log.Printf("Could not read %s, using default template: %v", ocrTemplatePath, err) + log.Errorf("Could not read %s, using default template: %v", ocrTemplatePath, err) ocrTemplateContent = []byte(defaultOcrPrompt) if err := os.WriteFile(ocrTemplatePath, ocrTemplateContent, os.ModePerm); err != nil { log.Fatalf("Failed to write default OCR template to disk: %v", err) @@ -355,7 +398,7 @@ func createVisionLLM() (llms.Model, error) { ollama.WithServerURL(host), ) default: - log.Printf("No Vision LLM provider created: %s", visionLlmProvider) + log.Infoln("Vision LLM not enabled") return nil, nil } } diff --git a/paperless.go b/paperless.go index b477324..18ccf52 100644 --- a/paperless.go +++ b/paperless.go @@ -7,7 +7,6 @@ import ( "fmt" "image/jpeg" "io" - "log" "net/http" "os" "path/filepath" @@ -223,7 +222,7 @@ func (c *PaperlessClient) UpdateDocuments(ctx context.Context, documents []Docum // Fetch all available tags availableTags, err := c.GetAllTags(ctx) if err != nil { - log.Printf("Error fetching available tags: %v", err) + log.Errorf("Error fetching available tags: %v", err) return err } @@ -249,7 +248,7 @@ func (c *PaperlessClient) UpdateDocuments(ctx context.Context, documents []Docum } newTags = append(newTags, tagID) } else { - log.Printf("Tag '%s' does not exist in paperless-ngx, skipping.", tagName) + log.Warnf("Tag '%s' does not exist in paperless-ngx, skipping.", tagName) } } @@ -262,7 +261,7 @@ func (c *PaperlessClient) UpdateDocuments(ctx context.Context, documents []Docum if suggestedTitle != "" { updatedFields["title"] = suggestedTitle } else { - log.Printf("No valid title found for document %d, skipping.", documentID) + log.Warnf("No valid title found for document %d, skipping.", documentID) } // Suggested Content @@ -274,7 +273,7 @@ func (c *PaperlessClient) UpdateDocuments(ctx context.Context, documents []Docum // Marshal updated fields to JSON jsonData, err := json.Marshal(updatedFields) if err != nil { - log.Printf("Error marshalling JSON for document %d: %v", documentID, err) + log.Errorf("Error marshalling JSON for document %d: %v", documentID, err) return err } @@ -282,14 +281,14 @@ func (c *PaperlessClient) UpdateDocuments(ctx context.Context, documents []Docum path := fmt.Sprintf("api/documents/%d/", documentID) resp, err := c.Do(ctx, "PATCH", path, bytes.NewBuffer(jsonData)) if err != nil { - log.Printf("Error updating document %d: %v", documentID, err) + log.Errorf("Error updating document %d: %v", documentID, err) return err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { bodyBytes, _ := io.ReadAll(resp.Body) - log.Printf("Error updating document %d: %d, %s", documentID, resp.StatusCode, string(bodyBytes)) + log.Errorf("Error updating document %d: %d, %s", documentID, resp.StatusCode, string(bodyBytes)) return fmt.Errorf("error updating document %d: %d, %s", documentID, resp.StatusCode, string(bodyBytes)) }