From f308970531d8eac111439fc1cb9fb6e9432b5909 Mon Sep 17 00:00:00 2001 From: Evan Reichard Date: Thu, 29 Jan 2026 20:28:19 -0500 Subject: [PATCH] chore(runtime): Add logrus logging framework with structured logging Add logrus as the primary logging framework with dependency injection pattern. All errors now use WithError() for context, and structured logging uses camelCase field names. Tag runtime service with service field for better log organization. - Add logrus dependency - Update Runtime to accept logger via dependency injection - Add WithLogger() option for logger configuration - Log errors with WithError() for context - Log async function failures with service context - Document logging practices in AGENTS.md --- .gitignore | 1 + AGENTS.md | 56 +++++++++++++++++++++++++++++++++++++ cmd/poiesis/main.go | 14 +++++++--- go.mod | 1 + go.sum | 2 ++ internal/runtime/options.go | 12 +++++++- internal/runtime/runtime.go | 8 +++++- 7 files changed, 88 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index 59c239b..73d9665 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ .opencode +/poiesis diff --git a/AGENTS.md b/AGENTS.md index d7dfd25..1231393 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -106,6 +106,62 @@ functions.RegisterAsyncFunction[FetchArgs, *FetchResult]("fetch", Fetch) - `github.com/evanw/esbuild/pkg/api` - TypeScript transpilation - `github.com/fastschema/qjs` - JavaScript execution (CGO-free QuickJS runtime) - `github.com/stretchr/testify/assert` - Test assertions +- `github.com/sirupsen/logrus` - Logging framework + +## Logging Practices + +### Primary Logger + +Use `github.com/sirupsen/logrus` as the primary logging framework throughout the codebase. + +### Error Handling + +- Use `WithError(err)` when logging errors to include the error context +- Every error should eventually be logged somewhere appropriate in the call chain +- Don't log on every error condition - find the appropriate upstream logging location +- Use structured logging with context fields when relevant for debugging + +### Structured Logging + +- Use `WithField(key, value)` for contextual information +- Use `WithFields(fields)` when adding multiple fields +- Field names must be in camelCase (e.g., `WithField("filePath", path)`) +- Only use WithField(s) when the field is relevant and helpful for debugging issues +- Tag services with their name: `logger.WithField("service", "runtime")` + +### Example Usage + +```go +import "github.com/sirupsen/logrus" + +func New(ctx context.Context) (*Runtime, error) { + logger := logrus.New() + r := &Runtime{logger: logger.WithField("service", "runtime")} + + rt, err := qjs.New(r.opts) + if err != nil { + logger.WithError(err).Error("Failed to create QuickJS context") + return nil, err + } + + return r, nil +} + +func (r *Runtime) ExecuteFile(filePath string) error { + data, err := os.ReadFile(filePath) + if err != nil { + return fmt.Errorf("read file %s: %w", filePath, err) + } + + result, err := r.ctx.Eval("code.ts", qjs.Code(string(data))) + if err != nil { + r.logger.WithField("filePath", filePath).WithError(err).Error("Execution failed") + return err + } + + return nil +} +``` ## Code Conventions diff --git a/cmd/poiesis/main.go b/cmd/poiesis/main.go index af71dd6..74981d6 100644 --- a/cmd/poiesis/main.go +++ b/cmd/poiesis/main.go @@ -5,6 +5,7 @@ import ( "fmt" "os" + "github.com/sirupsen/logrus" "github.com/spf13/cobra" "reichard.io/poiesis/internal/runtime" ) @@ -13,23 +14,26 @@ var runCmd = &cobra.Command{ Use: "execute [file]", Short: "Transpile and execute TypeScript code", Long: `Poiesis transpiles TypeScript to JavaScript using esbuild and executes it with QuickJS. -It also provides a builtin system for exposing Go functions to TypeScript.`, + It also provides a builtin system for exposing Go functions to TypeScript.`, Args: cobra.MaximumNArgs(1), Run: func(cmd *cobra.Command, args []string) { ctx := context.Background() - rt, err := runtime.New(ctx) + logger := logrus.New() + rt, err := runtime.New(ctx, runtime.WithLogger(logger)) if err != nil { + logger.WithError(err).Error("Failed to create runtime") fmt.Fprintf(os.Stderr, "Failed to create runtime: %v\n", err) os.Exit(1) } if len(args) == 0 { fmt.Fprintln(os.Stderr, "Usage: poiesis execute [file]") - cmd.Help() + _ = cmd.Help() os.Exit(1) } if err := rt.RunFile(args[0]); err != nil { + logger.WithError(err).Error("Failed to run file") fmt.Fprintf(os.Stderr, "Failed to run file: %v\n", err) os.Exit(1) } @@ -41,8 +45,10 @@ var typesCmd = &cobra.Command{ Short: "Print TypeScript type declarations", Run: func(cmd *cobra.Command, args []string) { ctx := context.Background() - rt, err := runtime.New(ctx) + logger := logrus.New() + rt, err := runtime.New(ctx, runtime.WithLogger(logger)) if err != nil { + logger.WithError(err).Error("Failed to create runtime") fmt.Fprintf(os.Stderr, "Failed to create runtime: %v\n", err) os.Exit(1) } diff --git a/go.mod b/go.mod index 6117202..5bb09ff 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/sirupsen/logrus v1.9.4 // indirect github.com/spf13/cobra v1.10.2 // indirect github.com/spf13/pflag v1.0.9 // indirect github.com/tetratelabs/wazero v1.9.0 // indirect diff --git a/go.sum b/go.sum index 9ff65dd..c820898 100644 --- a/go.sum +++ b/go.sum @@ -10,6 +10,8 @@ github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLf github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= +github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= diff --git a/internal/runtime/options.go b/internal/runtime/options.go index 9213669..a0a8b64 100644 --- a/internal/runtime/options.go +++ b/internal/runtime/options.go @@ -1,6 +1,10 @@ package runtime -import "io" +import ( + "io" + + "github.com/sirupsen/logrus" +) type RuntimeOption func(*Runtime) @@ -17,3 +21,9 @@ func WithStderr(stderr io.Writer) RuntimeOption { r.opts.Stderr = stderr } } + +func WithLogger(logger *logrus.Logger) RuntimeOption { + return func(r *Runtime) { + r.logger = logger.WithField("service", "runtime") + } +} diff --git a/internal/runtime/runtime.go b/internal/runtime/runtime.go index e068fb6..478c89a 100644 --- a/internal/runtime/runtime.go +++ b/internal/runtime/runtime.go @@ -8,6 +8,7 @@ import ( "github.com/evanw/esbuild/pkg/api" "github.com/fastschema/qjs" + "github.com/sirupsen/logrus" "reichard.io/poiesis/internal/functions" _ "reichard.io/poiesis/internal/stdlib" @@ -18,11 +19,13 @@ type Runtime struct { opts qjs.Option funcs map[string]functions.Function typeDecls map[string]string + logger *logrus.Entry } func New(ctx context.Context, opts ...RuntimeOption) (*Runtime, error) { // Create Runtime - r := &Runtime{opts: qjs.Option{Context: ctx}} + logger := logrus.New() + r := &Runtime{opts: qjs.Option{Context: ctx}, logger: logger.WithField("service", "runtime")} for _, opt := range opts { opt(r) } @@ -30,12 +33,14 @@ func New(ctx context.Context, opts ...RuntimeOption) (*Runtime, error) { // Create QuickJS Context rt, err := qjs.New(r.opts) if err != nil { + logger.WithError(err).Error("Failed to create QuickJS context") return nil, err } r.ctx = rt.Context() // Populate Globals if err := r.populateGlobals(); err != nil { + logger.WithError(err).Error("Failed to populate globals") return nil, err } @@ -62,6 +67,7 @@ func (r *Runtime) populateGlobals() error { r.ctx.SetAsyncFunc(name, func(this *qjs.This) { qjsVal, err := callFunc(this, fn) if err != nil { + r.logger.WithError(err).Errorf("Async function %s failed", name) _ = this.Promise().Reject(this.Context().NewError(err)) return }