From e9d0bbbb9ca7e7821334bc91ecf95e0fb596d1aa Mon Sep 17 00:00:00 2001 From: Georges Haidar Date: Tue, 27 Feb 2024 14:47:35 +0000 Subject: [PATCH] feat: add OpenTelemetry support (#16) This change adds support for tracing templating operations and function calls using an OpenTelemetry tracer that is provided as an option to the easytemplate engine. --- engine.go | 74 +++++++++++++++++++++++++++++++++++++++++++++------ go.mod | 4 ++- go.sum | 16 +++++------ templating.go | 45 ++++++++++++++++++++++++++++--- 4 files changed, 118 insertions(+), 21 deletions(-) diff --git a/engine.go b/engine.go index a1629ab..cbbf914 100644 --- a/engine.go +++ b/engine.go @@ -5,6 +5,7 @@ package easytemplate import ( + "context" "errors" "fmt" "io/fs" @@ -15,6 +16,10 @@ import ( "github.com/speakeasy-api/easytemplate/internal/template" "github.com/speakeasy-api/easytemplate/internal/utils" "github.com/speakeasy-api/easytemplate/internal/vm" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/trace" + "go.opentelemetry.io/otel/trace/noop" ) var ( @@ -33,7 +38,8 @@ var ( // CallContext is the context that is passed to go functions when called from js. type CallContext struct { goja.FunctionCall - VM *vm.VM + VM *vm.VM + Ctx context.Context //nolint:containedctx // runtime context is necessarily stored in a struct as it jumps from Go to JS. } // Opt is a function that configures the Engine. @@ -93,6 +99,13 @@ func WithJSFiles(files map[string]string) Opt { } } +// WithTracer attaches an OpenTelemetry tracer to the engine and enables tracing support. +func WithTracer(t trace.Tracer) Opt { + return func(e *Engine) { + e.tracer = t + } +} + // WithDebug enables debug mode for the engine, which will log additional information when errors occur. func WithDebug() Opt { return func(e *Engine) { @@ -110,6 +123,8 @@ type Engine struct { ran bool jsFuncs map[string]func(call CallContext) goja.Value jsFiles map[string]string + + tracer trace.Tracer } // New creates a new Engine with the provided options. @@ -148,12 +163,21 @@ func New(opts ...Opt) *Engine { opt(e) } + if e.tracer == nil { + e.tracer = noop.NewTracerProvider().Tracer("easytemplate") + } + return e } // RunScript runs the provided script file, with the provided data, starting the template engine and templating any templates triggered from the script. func (e *Engine) RunScript(scriptFile string, data any) error { - vm, err := e.init(data) + return e.RunScriptWithContext(context.Background(), scriptFile, data) +} + +// RunScriptWithContext runs the provided script file, with the provided data, starting the template engine and templating any templates triggered from the script. +func (e *Engine) RunScriptWithContext(ctx context.Context, scriptFile string, data any) error { + vm, err := e.init(ctx, data) if err != nil { return err } @@ -172,7 +196,12 @@ func (e *Engine) RunScript(scriptFile string, data any) error { // RunMethod enables calls to global template methods from easytemplate. func (e *Engine) RunMethod(scriptFile string, data any, fnName string, args ...any) (goja.Value, error) { - vm, err := e.init(data) + return e.RunMethodWithContext(context.Background(), scriptFile, data, fnName, args...) +} + +// RunMethodWithContext enables calls to global template methods from easytemplate. +func (e *Engine) RunMethodWithContext(ctx context.Context, scriptFile string, data any, fnName string, args ...any) (goja.Value, error) { + vm, err := e.init(ctx, data) if err != nil { return nil, err } @@ -205,7 +234,12 @@ func (e *Engine) RunMethod(scriptFile string, data any, fnName string, args ...a // RunTemplate runs the provided template file, with the provided data, starting the template engine and templating the provided template to a file. func (e *Engine) RunTemplate(templateFile string, outFile string, data any) error { - vm, err := e.init(data) + return e.RunTemplateWithContext(context.Background(), templateFile, outFile, data) +} + +// RunTemplateWithContext runs the provided template file, with the provided data, starting the template engine and templating the provided template to a file. +func (e *Engine) RunTemplateWithContext(ctx context.Context, templateFile string, outFile string, data any) error { + vm, err := e.init(ctx, data) if err != nil { return err } @@ -215,7 +249,12 @@ func (e *Engine) RunTemplate(templateFile string, outFile string, data any) erro // RunTemplateString runs the provided template file, with the provided data, starting the template engine and templating the provided template, returning the rendered result. func (e *Engine) RunTemplateString(templateFile string, data any) (string, error) { - vm, err := e.init(data) + return e.RunTemplateStringWithContext(context.Background(), templateFile, data) +} + +// RunTemplateStringWithContext runs the provided template file, with the provided data, starting the template engine and templating the provided template, returning the rendered result. +func (e *Engine) RunTemplateStringWithContext(ctx context.Context, templateFile string, data any) (string, error) { + vm, err := e.init(ctx, data) if err != nil { return "", err } @@ -225,7 +264,12 @@ func (e *Engine) RunTemplateString(templateFile string, data any) (string, error // RunTemplateStringInput runs the provided input template string, with the provided data, starting the template engine and templating the provided template, returning the rendered result. func (e *Engine) RunTemplateStringInput(name, template string, data any) (string, error) { - vm, err := e.init(data) + return e.RunTemplateStringInputWithContext(context.Background(), name, template, data) +} + +// RunTemplateStringInputWithContext runs the provided input template string, with the provided data, starting the template engine and templating the provided template, returning the rendered result. +func (e *Engine) RunTemplateStringInputWithContext(ctx context.Context, name, template string, data any) (string, error) { + vm, err := e.init(ctx, data) if err != nil { return "", err } @@ -234,7 +278,7 @@ func (e *Engine) RunTemplateStringInput(name, template string, data any) (string } //nolint:funlen -func (e *Engine) init(data any) (*vm.VM, error) { +func (e *Engine) init(ctx context.Context, data any) (*vm.VM, error) { if e.ran { return nil, ErrAlreadyRan } @@ -258,6 +302,7 @@ func (e *Engine) init(data any) (*vm.VM, error) { return fn(CallContext{ FunctionCall: call, VM: v, + Ctx: ctx, }) } }(fn) @@ -270,7 +315,20 @@ func (e *Engine) init(data any) (*vm.VM, error) { // This need to have the vm passed in so that the functions can be called e.templator.TmplFuncs["templateFile"] = func(v *vm.VM) func(string, string, any) (string, error) { return func(templateFile, outFile string, data any) (string, error) { - err := e.templator.TemplateFile(v, templateFile, outFile, data) + var err error + _, span := e.tracer.Start(ctx, "templateFile", trace.WithAttributes( + attribute.String("templateFile", templateFile), + attribute.String("outFile", outFile), + )) + defer func() { + span.RecordError(err) + if err != nil { + span.SetStatus(codes.Error, err.Error()) + } + span.End() + }() + + err = e.templator.TemplateFile(v, templateFile, outFile, data) if err != nil { return "", err } diff --git a/go.mod b/go.mod index 5974603..0573f79 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,9 @@ require ( github.com/evanw/esbuild v0.19.11 github.com/go-sourcemap/sourcemap v2.1.3+incompatible github.com/golang/mock v1.6.0 - github.com/stretchr/testify v1.8.1 + github.com/stretchr/testify v1.8.4 + go.opentelemetry.io/otel v1.24.0 + go.opentelemetry.io/otel/trace v1.24.0 ) require ( diff --git a/go.sum b/go.sum index 5d5902a..7e032f9 100644 --- a/go.sum +++ b/go.sum @@ -2,7 +2,6 @@ github.com/chzyer/logex v1.2.0/go.mod h1:9+9sk7u7pGNWYMkh0hdiL++6OeibzJccyQU4p4M github.com/chzyer/readline v1.5.0/go.mod h1:x22KAscuvRqlLoK9CsoYsmxoXZMMFVyOl86cAH8qUic= github.com/chzyer/test v0.0.0-20210722231415-061457976a23/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= @@ -22,6 +21,7 @@ github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyL github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/pprof v0.0.0-20230207041349-798e818bf904/go.mod h1:uglQLonpP8qtYCYyzA+8c/9qtqgA3qsXGYqCPKARAFg= github.com/google/pprof v0.0.0-20240117000934-35fc243c5815 h1:WzfWbQz/Ze8v6l++GGbGNFZnUShVpP/0xffCPLL+ax8= github.com/google/pprof v0.0.0-20240117000934-35fc243c5815/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= @@ -38,15 +38,14 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo= +go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo= +go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI= +go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= @@ -95,6 +94,5 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/templating.go b/templating.go index 2634320..676faed 100644 --- a/templating.go +++ b/templating.go @@ -2,12 +2,28 @@ package easytemplate import ( "github.com/dop251/goja" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/trace" ) func (e *Engine) templateFileJS(call CallContext) goja.Value { + templateFile := call.Argument(0).String() + outFile := call.Argument(1).String() inputData := call.Argument(2).Export() //nolint:gomnd - if err := e.templator.TemplateFile(call.VM, call.Argument(0).String(), call.Argument(1).String(), inputData); err != nil { + ctx := call.Ctx + _, span := e.tracer.Start(ctx, "js:templateFile", trace.WithAttributes( + attribute.String("templateFile", templateFile), + attribute.String("outFile", outFile), + )) + defer span.End() + + if err := e.templator.TemplateFile(call.VM, templateFile, outFile, inputData); err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + span.End() + panic(call.VM.NewGoError(err)) } @@ -15,10 +31,21 @@ func (e *Engine) templateFileJS(call CallContext) goja.Value { } func (e *Engine) templateStringJS(call CallContext) goja.Value { + templateFile := call.Argument(0).String() inputData := call.Argument(1).Export() - output, err := e.templator.TemplateString(call.VM, call.Argument(0).String(), inputData) + ctx := call.Ctx + _, span := e.tracer.Start(ctx, "js:templateString", trace.WithAttributes( + attribute.String("templateFile", templateFile), + )) + defer span.End() + + output, err := e.templator.TemplateString(call.VM, templateFile, inputData) if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + span.End() + panic(call.VM.NewGoError(err)) } @@ -26,10 +53,22 @@ func (e *Engine) templateStringJS(call CallContext) goja.Value { } func (e *Engine) templateStringInputJS(call CallContext) goja.Value { + name := call.Argument(0).String() + input := call.Argument(1).String() inputData := call.Argument(2).Export() //nolint:gomnd - output, err := e.templator.TemplateStringInput(call.VM, call.Argument(0).String(), call.Argument(1).String(), inputData) + ctx := call.Ctx + _, span := e.tracer.Start(ctx, "js:templateStringInput", trace.WithAttributes( + attribute.String("name", name), + )) + defer span.End() + + output, err := e.templator.TemplateStringInput(call.VM, name, input, inputData) if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + span.End() + panic(call.VM.NewGoError(err)) }