yao/trace/jsapi/trace.go
Max c25fbb22d7 Add trace management to context and enhance configuration options
- Introduced a trace manager in the context struct for lazy initialization and management of trace operations.
- Implemented the Trace method in the context to facilitate trace object creation and management within JavaScript.
- Enhanced configuration options for trace drivers, including defaults for local and store drivers, and added path and prefix settings.
- Updated related tests to verify the functionality of the new trace management features.
2025-11-19 12:27:42 +08:00

506 lines
15 KiB
Go

package jsapi
import (
"context"
"fmt"
v8 "github.com/yaoapp/gou/runtime/v8"
"github.com/yaoapp/gou/runtime/v8/bridge"
"github.com/yaoapp/yao/config"
"github.com/yaoapp/yao/trace"
"github.com/yaoapp/yao/trace/types"
"rogchap.com/v8go"
)
func init() {
// Auto-register Trace JavaScript API when package is imported
v8.RegisterFunction("Trace", ExportFunction)
}
// NewTraceObject creates a JavaScript Trace object
func NewTraceObject(v8ctx *v8go.Context, traceID string, manager types.Manager) (*v8go.Value, error) {
jsObject := v8go.NewObjectTemplate(v8ctx.Isolate())
// Set internal field count to 1 to store the __go_id
// Internal fields are not accessible from JavaScript, providing better security
jsObject.SetInternalFieldCount(1)
// Register manager in global bridge registry for efficient Go object retrieval
// The goValueID will be stored in internal field (index 0) after instance creation
// Internal fields are not accessible from JavaScript, providing better security
goValueID := bridge.RegisterGoObject(manager)
// Set primitive fields
jsObject.Set("id", traceID)
// Set release function that will be called when JavaScript object is released
// This function retrieves goValueID from internal field and releases the Go object
jsObject.Set("__release", traceGoRelease(v8ctx.Isolate(), traceID))
// Set methods
jsObject.Set("Add", traceAddMethod(v8ctx.Isolate(), manager))
jsObject.Set("Parallel", traceParallelMethod(v8ctx.Isolate(), manager))
jsObject.Set("Info", traceInfoMethod(v8ctx.Isolate(), manager))
jsObject.Set("Debug", traceDebugMethod(v8ctx.Isolate(), manager))
jsObject.Set("Error", traceErrorMethod(v8ctx.Isolate(), manager))
jsObject.Set("Warn", traceWarnMethod(v8ctx.Isolate(), manager))
jsObject.Set("SetOutput", traceSetOutputMethod(v8ctx.Isolate(), manager))
jsObject.Set("SetMetadata", traceSetMetadataMethod(v8ctx.Isolate(), manager))
jsObject.Set("Complete", traceCompleteMethod(v8ctx.Isolate(), manager))
jsObject.Set("Fail", traceFailMethod(v8ctx.Isolate(), manager))
jsObject.Set("MarkComplete", traceMarkCompleteMethod(v8ctx.Isolate(), manager))
jsObject.Set("CreateSpace", traceCreateSpaceMethod(v8ctx.Isolate(), manager))
jsObject.Set("GetSpace", traceGetSpaceMethod(v8ctx.Isolate(), manager))
jsObject.Set("IsComplete", traceIsCompleteMethod(v8ctx.Isolate(), manager))
// Create instance
instance, err := jsObject.NewInstance(v8ctx)
if err != nil {
// Clean up: release from global registry if instance creation failed
bridge.ReleaseGoObject(goValueID)
return nil, err
}
// Store the goValueID in internal field (index 0)
// This is not accessible from JavaScript, providing better security
obj, err := instance.Value.AsObject()
if err != nil {
bridge.ReleaseGoObject(goValueID)
return nil, err
}
err = obj.SetInternalField(0, goValueID)
if err != nil {
bridge.ReleaseGoObject(goValueID)
return nil, err
}
return instance.Value, nil
}
// TraceNew creates a new Trace instance from JavaScript
// Usage: new Trace(options)
func TraceNew(v8ctx *v8go.Context, options map[string]interface{}) (*v8go.Value, error) {
cfg := config.Conf
// Prepare driver options
var driverOptions []any
var driverType string
// Allow override from options
driver, _ := options["driver"].(string)
if driver == "" {
driver = cfg.Trace.Driver
}
switch driver {
case "store":
driverType = trace.Store
storeID := cfg.Trace.Store
if sid, ok := options["store"].(string); ok && sid != "" {
storeID = sid
}
prefix := cfg.Trace.Prefix
if pfx, ok := options["prefix"].(string); ok {
prefix = pfx
}
driverOptions = []any{storeID, prefix}
case "local", "":
driverType = trace.Local
path := cfg.Trace.Path
if p, ok := options["path"].(string); ok && p != "" {
path = p
}
driverOptions = []any{path}
default:
return nil, fmt.Errorf("unsupported trace driver: %s", driver)
}
// Parse trace options
var traceOpt types.TraceOption
if id, ok := options["id"].(string); ok {
traceOpt.ID = id
}
// Create trace
goCtx := context.Background()
traceID, manager, err := trace.New(goCtx, driverType, &traceOpt, driverOptions...)
if err != nil {
return nil, err
}
return NewTraceObject(v8ctx, traceID, manager)
}
// TraceLoad loads an existing trace from JavaScript
// Usage: Trace.Load(traceID)
func TraceLoad(v8ctx *v8go.Context, traceID string) (*v8go.Value, error) {
manager, err := trace.Load(traceID)
if err != nil {
return nil, err
}
return NewTraceObject(v8ctx, traceID, manager)
}
// traceGoRelease releases the Go object from the global bridge registry
// It retrieves the goValueID from internal field (index 0) and releases the Go object
// It also calls trace.Release to cleanup the trace globally
func traceGoRelease(iso *v8go.Isolate, traceID string) *v8go.FunctionTemplate {
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
// Get the trace object (this)
thisObj, err := info.This().AsObject()
if err == nil && thisObj.InternalFieldCount() > 0 {
// Get goValueID from internal field (index 0)
goValueIDValue := thisObj.GetInternalField(0)
if goValueIDValue != nil && goValueIDValue.IsString() {
goValueID := goValueIDValue.String()
// Release from global bridge registry
bridge.ReleaseGoObject(goValueID)
}
}
// Call global trace.Release to remove from registry and stop background goroutines
trace.Release(traceID)
return v8go.Undefined(info.Context().Isolate())
})
}
// Method templates
func traceAddMethod(iso *v8go.Isolate, manager types.Manager) *v8go.FunctionTemplate {
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
ctx := info.Context()
args := info.Args()
if len(args) < 1 {
return bridge.JsException(ctx, "Add requires at least 1 argument: (input, option?)")
}
// Parse input
input, err := bridge.GoValue(args[0], ctx)
if err != nil {
return bridge.JsException(ctx, fmt.Sprintf("invalid input: %s", err))
}
// Parse option
var option types.TraceNodeOption
if len(args) > 1 && !args[1].IsNullOrUndefined() {
optionObj, err := args[1].AsObject()
if err == nil {
if labelVal, err := optionObj.Get("label"); err == nil && !labelVal.IsNullOrUndefined() {
option.Label = labelVal.String()
}
if iconVal, err := optionObj.Get("icon"); err == nil && !iconVal.IsNullOrUndefined() {
option.Icon = iconVal.String()
}
if descVal, err := optionObj.Get("description"); err == nil && !descVal.IsNullOrUndefined() {
option.Description = descVal.String()
}
}
}
// Call manager.Add
node, err := manager.Add(input, option)
if err != nil {
return bridge.JsException(ctx, err.Error())
}
// Create node JS object
nodeObj, err := NewNodeObject(ctx, node)
if err != nil {
return bridge.JsException(ctx, err.Error())
}
return nodeObj
})
}
func traceParallelMethod(iso *v8go.Isolate, manager types.Manager) *v8go.FunctionTemplate {
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
ctx := info.Context()
args := info.Args()
if len(args) < 1 {
return bridge.JsException(ctx, "Parallel requires an array argument")
}
// Parse array
parallelInputsJS, err := bridge.GoValue(args[0], ctx)
if err != nil {
return bridge.JsException(ctx, fmt.Sprintf("invalid parallel inputs: %s", err))
}
// Convert to []types.TraceParallelInput
parallelInputsArray, ok := parallelInputsJS.([]interface{})
if !ok {
return bridge.JsException(ctx, "Parallel argument must be an array")
}
parallelInputs := make([]types.TraceParallelInput, 0, len(parallelInputsArray))
for _, item := range parallelInputsArray {
itemMap, ok := item.(map[string]interface{})
if !ok {
continue
}
parallelInput := types.TraceParallelInput{}
if input, ok := itemMap["input"]; ok {
parallelInput.Input = input
}
if option, ok := itemMap["option"].(map[string]interface{}); ok {
if label, ok := option["label"].(string); ok {
parallelInput.Option.Label = label
}
if icon, ok := option["icon"].(string); ok {
parallelInput.Option.Icon = icon
}
if desc, ok := option["description"].(string); ok {
parallelInput.Option.Description = desc
}
}
parallelInputs = append(parallelInputs, parallelInput)
}
// Call manager.Parallel
nodes, err := manager.Parallel(parallelInputs)
if err != nil {
return bridge.JsException(ctx, err.Error())
}
// Create array of node objects
result := make([]interface{}, len(nodes))
for i, node := range nodes {
nodeObj, err := NewNodeObject(ctx, node)
if err != nil {
continue
}
result[i] = nodeObj
}
jsVal, err := bridge.JsValue(ctx, result)
if err != nil {
return bridge.JsException(ctx, err.Error())
}
return jsVal
})
}
func traceInfoMethod(iso *v8go.Isolate, manager types.Manager) *v8go.FunctionTemplate {
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
args := info.Args()
if len(args) > 0 {
manager.Info(args[0].String())
}
return info.This().Value
})
}
func traceDebugMethod(iso *v8go.Isolate, manager types.Manager) *v8go.FunctionTemplate {
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
args := info.Args()
if len(args) > 0 {
manager.Debug(args[0].String())
}
return info.This().Value
})
}
func traceErrorMethod(iso *v8go.Isolate, manager types.Manager) *v8go.FunctionTemplate {
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
args := info.Args()
if len(args) > 0 {
manager.Error(args[0].String())
}
return info.This().Value
})
}
func traceWarnMethod(iso *v8go.Isolate, manager types.Manager) *v8go.FunctionTemplate {
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
args := info.Args()
if len(args) > 0 {
manager.Warn(args[0].String())
}
return info.This().Value
})
}
func traceSetOutputMethod(iso *v8go.Isolate, manager types.Manager) *v8go.FunctionTemplate {
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
ctx := info.Context()
args := info.Args()
if len(args) < 1 {
return bridge.JsException(ctx, "SetOutput requires an output argument")
}
output, err := bridge.GoValue(args[0], ctx)
if err != nil {
return bridge.JsException(ctx, err.Error())
}
if err := manager.SetOutput(output); err != nil {
return bridge.JsException(ctx, err.Error())
}
return info.This().Value
})
}
func traceSetMetadataMethod(iso *v8go.Isolate, manager types.Manager) *v8go.FunctionTemplate {
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
ctx := info.Context()
args := info.Args()
if len(args) < 2 {
return bridge.JsException(ctx, "SetMetadata requires 2 arguments: (key, value)")
}
key := args[0].String()
value, err := bridge.GoValue(args[1], ctx)
if err != nil {
return bridge.JsException(ctx, err.Error())
}
if err := manager.SetMetadata(key, value); err != nil {
return bridge.JsException(ctx, err.Error())
}
return info.This().Value
})
}
func traceCompleteMethod(iso *v8go.Isolate, manager types.Manager) *v8go.FunctionTemplate {
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
ctx := info.Context()
args := info.Args()
// Optional output parameter
if len(args) > 0 && !args[0].IsNullOrUndefined() {
output, err := bridge.GoValue(args[0], ctx)
if err != nil {
return bridge.JsException(ctx, err.Error())
}
if err := manager.Complete(output); err != nil {
return bridge.JsException(ctx, err.Error())
}
} else {
if err := manager.Complete(); err != nil {
return bridge.JsException(ctx, err.Error())
}
}
return info.This().Value
})
}
func traceFailMethod(iso *v8go.Isolate, manager types.Manager) *v8go.FunctionTemplate {
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
ctx := info.Context()
args := info.Args()
if len(args) < 1 {
return bridge.JsException(ctx, "Fail requires an error message")
}
errMsg := args[0].String()
if err := manager.Fail(fmt.Errorf("%s", errMsg)); err != nil {
return bridge.JsException(ctx, err.Error())
}
return info.This().Value
})
}
func traceMarkCompleteMethod(iso *v8go.Isolate, manager types.Manager) *v8go.FunctionTemplate {
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
ctx := info.Context()
if err := manager.MarkComplete(); err != nil {
return bridge.JsException(ctx, err.Error())
}
return info.This().Value
})
}
func traceCreateSpaceMethod(iso *v8go.Isolate, manager types.Manager) *v8go.FunctionTemplate {
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
ctx := info.Context()
args := info.Args()
// Parse option
var option types.TraceSpaceOption
if len(args) > 0 && !args[0].IsNullOrUndefined() {
optionObj, err := args[0].AsObject()
if err == nil {
if labelVal, err := optionObj.Get("label"); err == nil && !labelVal.IsNullOrUndefined() {
option.Label = labelVal.String()
}
if iconVal, err := optionObj.Get("icon"); err == nil && !iconVal.IsNullOrUndefined() {
option.Icon = iconVal.String()
}
if descVal, err := optionObj.Get("description"); err == nil && !descVal.IsNullOrUndefined() {
option.Description = descVal.String()
}
}
}
// Call manager.CreateSpace
space, err := manager.CreateSpace(option)
if err != nil {
return bridge.JsException(ctx, err.Error())
}
// Create space JS object
spaceObj, err := NewSpaceObject(ctx, manager, space)
if err != nil {
return bridge.JsException(ctx, err.Error())
}
return spaceObj
})
}
func traceGetSpaceMethod(iso *v8go.Isolate, manager types.Manager) *v8go.FunctionTemplate {
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
ctx := info.Context()
args := info.Args()
if len(args) < 1 {
return bridge.JsException(ctx, "GetSpace requires a space ID")
}
spaceID := args[0].String()
space, err := manager.GetSpace(spaceID)
if err != nil {
return bridge.JsException(ctx, err.Error())
}
if space == nil {
return v8go.Null(iso)
}
// Create space JS object
spaceObj, err := NewSpaceObject(ctx, manager, space)
if err != nil {
return bridge.JsException(ctx, err.Error())
}
return spaceObj
})
}
func traceIsCompleteMethod(iso *v8go.Isolate, manager types.Manager) *v8go.FunctionTemplate {
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
isComplete := manager.IsComplete()
jsVal, _ := v8go.NewValue(iso, isComplete)
return jsVal
})
}