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 }