diff --git a/cmd/knowledgebot/main.go b/cmd/knowledgebot/main.go index 73e5f60..88092d5 100644 --- a/cmd/knowledgebot/main.go +++ b/cmd/knowledgebot/main.go @@ -2,7 +2,8 @@ package main import ( "context" - "log" + "log/slog" + "os" "os/signal" "syscall" ) @@ -13,6 +14,7 @@ func main() { err := rootCmd.ExecuteContext(ctx) if err != nil { - log.Fatal("FATAL: ", err) + slog.Error(err.Error()) + os.Exit(1) } } diff --git a/cmd/knowledgebot/root.go b/cmd/knowledgebot/root.go index 70d3464..1402654 100644 --- a/cmd/knowledgebot/root.go +++ b/cmd/knowledgebot/root.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "log/slog" "os" "strings" @@ -21,6 +22,7 @@ var ( ) func init() { + rootCmd.PersistentFlags().Var(logLevelFlag("INFO"), "log-level", "set the log level") rootCmd.SetFlagErrorFunc(func(cmd *cobra.Command, err error) error { _ = cmd.Help() return err @@ -32,10 +34,10 @@ func Execute() error { } func preRunEnvVars(cmd *cobra.Command, args []string) error { - return parseFlagsWithEnvVars(cmd.Flags(), "KLB_") + return applyEnvVarsToFlags(cmd.Flags(), "KLB_") } -func parseFlagsWithEnvVars(fs *pflag.FlagSet, envVarPrefix string) error { +func applyEnvVarsToFlags(fs *pflag.FlagSet, envVarPrefix string) error { var err error fs.VisitAll(func(f *pflag.Flag) { @@ -50,3 +52,34 @@ func parseFlagsWithEnvVars(fs *pflag.FlagSet, envVarPrefix string) error { return err } + +type logLevelFlag string + +func (logLevelFlag) Set(s string) error { + var level slog.Level + + switch s { + case "DEBUG": + level = slog.LevelDebug + case "INFO": + level = slog.LevelInfo + case "WARN": + level = slog.LevelWarn + case "ERROR": + level = slog.LevelError + default: + return fmt.Errorf("unsupported log level %q provided. supported log levels are DEBUG, INFO, WARN, ERROR", s) + } + + slog.SetLogLoggerLevel(level) + + return nil +} + +func (f logLevelFlag) String() string { + return string(f) +} + +func (f logLevelFlag) Type() string { + return "LEVEL" +} diff --git a/cmd/knowledgebot/serve.go b/cmd/knowledgebot/serve.go index 3c885b2..cf1aeda 100644 --- a/cmd/knowledgebot/serve.go +++ b/cmd/knowledgebot/serve.go @@ -2,7 +2,7 @@ package main import ( "context" - "log" + "log/slog" "net" "net/http" @@ -101,11 +101,11 @@ func runServer(cmd *cobra.Command, args []string) error { <-ctx.Done() err := srv.Shutdown(ctx) if err != nil { - log.Println("ERROR: failed to shutdown server:", err) + slog.Error("failed to shutdown server: " + err.Error()) } }() - log.Println("listening on", srv.Addr) + slog.Info("listening on " + srv.Addr) err := srv.ListenAndServe() if err == http.ErrServerClosed { diff --git a/internal/importer/crawler/crawler.go b/internal/importer/crawler/crawler.go index 19bb82b..0f72fc2 100644 --- a/internal/importer/crawler/crawler.go +++ b/internal/importer/crawler/crawler.go @@ -5,7 +5,7 @@ import ( "crypto/sha256" "encoding/hex" "fmt" - "log" + "log/slog" "net/url" "regexp" "strings" @@ -32,7 +32,7 @@ type Crawler struct { } func (s *Crawler) Crawl(ctx context.Context, seedURL string) error { - log.Printf("crawling %s with maxDepth=%d, maxPages=%d and urlRegex=%s", seedURL, s.MaxDepth, s.MaxPages, s.URLRegex) + slog.Info("crawling "+seedURL, "maxDepth", s.MaxDepth, "maxPages", s.MaxPages, "urlRegex", s.URLRegex) startTime := time.Now() @@ -84,26 +84,26 @@ func (s *Crawler) crawl(ctx context.Context, seedURL *url.URL, ch chan<- []schem } } - log.Println("visiting", req.URL) + slog.Info("visiting " + req.URL.String()) }) c.OnResponse(func(f *colly.Response) { err := s.processHTML(ctx, f.Request.URL, string(f.Body), ch) if err != nil { - log.Println("WARNING:", err) + slog.Warn(err.Error()) } }) c.OnHTML("a[href]", func(e *colly.HTMLElement) { err := e.Request.Visit(e.Attr("href")) if err != nil { - log.Println("ERROR: failed to visit page:", err) + slog.Debug("failed to visit page: " + err.Error()) } }) err := c.Visit(seedURL.String()) if err != nil { - log.Println("ERROR: failed to crawl page:", err) + slog.Warn("failed to crawl page: " + err.Error()) } c.Wait() @@ -127,7 +127,7 @@ func (s *Crawler) processHTML(ctx context.Context, url *url.URL, html string, ch return fmt.Errorf("split text: %w", err) } - log.Printf("scraped %d chunks from %s", len(chunks), url) + slog.Info(fmt.Sprintf("scraped %d chunks from %s", len(chunks), url)) docs := make([]schema.Document, 0, len(chunks)) @@ -221,7 +221,7 @@ func (s *Crawler) indexDocumentChunks(ctx context.Context, cancel context.Cancel elapsed := time.Since(startTime) - log.Printf("indexed %d chunks of %d document(s) in %s", chunkCount, docCount, elapsed) + slog.Info(fmt.Sprintf("indexed %d chunks of %d document(s) in %s", chunkCount, docCount, elapsed)) return ctx.Err() } diff --git a/internal/qdrantutils/collection.go b/internal/qdrantutils/collection.go index d78e362..7d478b4 100644 --- a/internal/qdrantutils/collection.go +++ b/internal/qdrantutils/collection.go @@ -5,7 +5,7 @@ import ( "context" "encoding/json" "fmt" - "log" + "log/slog" "net/http" "net/url" "time" @@ -37,7 +37,7 @@ func CreateQdrantCollectionIfNotExist(ctx context.Context, qdrantURL, collection } if resp.StatusCode == http.StatusOK { - log.Printf("created qdrant collection %q", collection) + slog.Info("created qdrant collection " + collection) return nil } diff --git a/internal/qna/qnaworkflow.go b/internal/qna/qnaworkflow.go index 1f5266f..5876766 100644 --- a/internal/qna/qnaworkflow.go +++ b/internal/qna/qnaworkflow.go @@ -4,7 +4,7 @@ import ( "context" "errors" "fmt" - "log" + "log/slog" "strings" "github.com/tmc/langchaingo/llms" @@ -82,7 +82,7 @@ func (w *QuestionAnswerWorkflow) Answer(ctx context.Context, question string) (< return nil, err } - log.Println("Requesting LLM answer for prompt:\n ", strings.ReplaceAll(prompt, "\n", "\n ")) + slog.Info("Requesting LLM answer for prompt:" + strings.ReplaceAll("\n"+prompt, "\n", "\n ")) go func() { defer close(ch) @@ -124,13 +124,13 @@ func searchResultsToSourceRefs(docs []schema.Document) []SourceReference { for _, doc := range docs { urlKey, ok := doc.Metadata["url"].(string) if !ok { - log.Println("WARNING: vectordb search result doc does not specify 'url' metadata key") + slog.Warn("vectordb search result doc does not specify 'url' metadata key") continue } title, ok := doc.Metadata["title"].(string) if !ok { - log.Println("WARNING: vectordb search result doc does not specify 'title' metadata key") + slog.Warn("vectordb search result doc does not specify 'title' metadata key") continue } diff --git a/internal/server/qnahandler.go b/internal/server/qnahandler.go index ec1c403..5a9c9d5 100644 --- a/internal/server/qnahandler.go +++ b/internal/server/qnahandler.go @@ -3,7 +3,7 @@ package server import ( "encoding/json" "fmt" - "log" + "log/slog" "net/http" "github.com/mgoltzsche/knowledgebot/internal/qna" @@ -15,7 +15,7 @@ func newQuestionAnswerHandler(ai *qna.QuestionAnswerWorkflow) http.Handler { if question == "" { err := req.ParseForm() if err != nil { - log.Println("WARNING: parse form data:", err) + slog.Warn("parse form data: " + err.Error()) } question = req.Form.Get("q") @@ -40,7 +40,7 @@ func newQuestionAnswerHandler(ai *qna.QuestionAnswerWorkflow) http.Handler { data, err := json.Marshal(chunk) if err != nil { - log.Println("ERROR: failed to marshal chunk:", err) + slog.Error("failed to marshal chunk: " + err.Error()) continue }