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
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1 +1,2 @@
|
|||||||
.opencode
|
.opencode
|
||||||
|
/poiesis
|
||||||
|
|||||||
56
AGENTS.md
56
AGENTS.md
@@ -106,6 +106,62 @@ functions.RegisterAsyncFunction[FetchArgs, *FetchResult]("fetch", Fetch)
|
|||||||
- `github.com/evanw/esbuild/pkg/api` - TypeScript transpilation
|
- `github.com/evanw/esbuild/pkg/api` - TypeScript transpilation
|
||||||
- `github.com/fastschema/qjs` - JavaScript execution (CGO-free QuickJS runtime)
|
- `github.com/fastschema/qjs` - JavaScript execution (CGO-free QuickJS runtime)
|
||||||
- `github.com/stretchr/testify/assert` - Test assertions
|
- `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
|
## Code Conventions
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"reichard.io/poiesis/internal/runtime"
|
"reichard.io/poiesis/internal/runtime"
|
||||||
)
|
)
|
||||||
@@ -17,19 +18,22 @@ It also provides a builtin system for exposing Go functions to TypeScript.`,
|
|||||||
Args: cobra.MaximumNArgs(1),
|
Args: cobra.MaximumNArgs(1),
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
rt, err := runtime.New(ctx)
|
logger := logrus.New()
|
||||||
|
rt, err := runtime.New(ctx, runtime.WithLogger(logger))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
logger.WithError(err).Error("Failed to create runtime")
|
||||||
fmt.Fprintf(os.Stderr, "Failed to create runtime: %v\n", err)
|
fmt.Fprintf(os.Stderr, "Failed to create runtime: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
fmt.Fprintln(os.Stderr, "Usage: poiesis execute [file]")
|
fmt.Fprintln(os.Stderr, "Usage: poiesis execute [file]")
|
||||||
cmd.Help()
|
_ = cmd.Help()
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := rt.RunFile(args[0]); err != nil {
|
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)
|
fmt.Fprintf(os.Stderr, "Failed to run file: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
@@ -41,8 +45,10 @@ var typesCmd = &cobra.Command{
|
|||||||
Short: "Print TypeScript type declarations",
|
Short: "Print TypeScript type declarations",
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
rt, err := runtime.New(ctx)
|
logger := logrus.New()
|
||||||
|
rt, err := runtime.New(ctx, runtime.WithLogger(logger))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
logger.WithError(err).Error("Failed to create runtime")
|
||||||
fmt.Fprintf(os.Stderr, "Failed to create runtime: %v\n", err)
|
fmt.Fprintf(os.Stderr, "Failed to create runtime: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|||||||
1
go.mod
1
go.mod
@@ -12,6 +12,7 @@ require (
|
|||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.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/cobra v1.10.2 // indirect
|
||||||
github.com/spf13/pflag v1.0.9 // indirect
|
github.com/spf13/pflag v1.0.9 // indirect
|
||||||
github.com/tetratelabs/wazero v1.9.0 // indirect
|
github.com/tetratelabs/wazero v1.9.0 // indirect
|
||||||
|
|||||||
2
go.sum
2
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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
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/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 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
||||||
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
||||||
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
|
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
package runtime
|
package runtime
|
||||||
|
|
||||||
import "io"
|
import (
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
type RuntimeOption func(*Runtime)
|
type RuntimeOption func(*Runtime)
|
||||||
|
|
||||||
@@ -17,3 +21,9 @@ func WithStderr(stderr io.Writer) RuntimeOption {
|
|||||||
r.opts.Stderr = stderr
|
r.opts.Stderr = stderr
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func WithLogger(logger *logrus.Logger) RuntimeOption {
|
||||||
|
return func(r *Runtime) {
|
||||||
|
r.logger = logger.WithField("service", "runtime")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
|
|
||||||
"github.com/evanw/esbuild/pkg/api"
|
"github.com/evanw/esbuild/pkg/api"
|
||||||
"github.com/fastschema/qjs"
|
"github.com/fastschema/qjs"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
|
||||||
"reichard.io/poiesis/internal/functions"
|
"reichard.io/poiesis/internal/functions"
|
||||||
_ "reichard.io/poiesis/internal/stdlib"
|
_ "reichard.io/poiesis/internal/stdlib"
|
||||||
@@ -18,11 +19,13 @@ type Runtime struct {
|
|||||||
opts qjs.Option
|
opts qjs.Option
|
||||||
funcs map[string]functions.Function
|
funcs map[string]functions.Function
|
||||||
typeDecls map[string]string
|
typeDecls map[string]string
|
||||||
|
logger *logrus.Entry
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(ctx context.Context, opts ...RuntimeOption) (*Runtime, error) {
|
func New(ctx context.Context, opts ...RuntimeOption) (*Runtime, error) {
|
||||||
// Create Runtime
|
// 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 {
|
for _, opt := range opts {
|
||||||
opt(r)
|
opt(r)
|
||||||
}
|
}
|
||||||
@@ -30,12 +33,14 @@ func New(ctx context.Context, opts ...RuntimeOption) (*Runtime, error) {
|
|||||||
// Create QuickJS Context
|
// Create QuickJS Context
|
||||||
rt, err := qjs.New(r.opts)
|
rt, err := qjs.New(r.opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
logger.WithError(err).Error("Failed to create QuickJS context")
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
r.ctx = rt.Context()
|
r.ctx = rt.Context()
|
||||||
|
|
||||||
// Populate Globals
|
// Populate Globals
|
||||||
if err := r.populateGlobals(); err != nil {
|
if err := r.populateGlobals(); err != nil {
|
||||||
|
logger.WithError(err).Error("Failed to populate globals")
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,6 +67,7 @@ func (r *Runtime) populateGlobals() error {
|
|||||||
r.ctx.SetAsyncFunc(name, func(this *qjs.This) {
|
r.ctx.SetAsyncFunc(name, func(this *qjs.This) {
|
||||||
qjsVal, err := callFunc(this, fn)
|
qjsVal, err := callFunc(this, fn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
r.logger.WithError(err).Errorf("Async function %s failed", name)
|
||||||
_ = this.Promise().Reject(this.Context().NewError(err))
|
_ = this.Promise().Reject(this.Context().NewError(err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user