diff --git a/examples/otel/cmd/main.go b/examples/otel/cmd/main.go index 8e56a1e..d15c939 100644 --- a/examples/otel/cmd/main.go +++ b/examples/otel/cmd/main.go @@ -98,13 +98,14 @@ func main() { // //autometrics:doc --slo "API" --latency-target 99 --latency-ms 5 func indexHandler(w http.ResponseWriter, r *http.Request) error { - defer autometrics.Instrument(autometrics.PreInstrument(autometrics.NewContext( + r.Context() = autometrics.PreInstrument(autometrics.NewContext( r.Context(), autometrics.WithConcurrentCalls(true), autometrics.WithCallerName(true), autometrics.WithSloName("API"), autometrics.WithAlertLatency(5000000*time.Nanosecond, 99), - )), nil) //autometrics:defer + )) //autometrics:shadow-ctx + defer autometrics.Instrument(r.Context(), nil) //autometrics:defer msSleep := rand.Intn(200) time.Sleep(time.Duration(msSleep) * time.Millisecond) @@ -148,13 +149,14 @@ var handlerError = errors.New("failed to handle request") // //autometrics:doc --slo "API" --success-target 90 func randomErrorHandler(w http.ResponseWriter, r *http.Request) (err error) { - defer autometrics.Instrument(autometrics.PreInstrument(autometrics.NewContext( + r.Context() = autometrics.PreInstrument(autometrics.NewContext( r.Context(), autometrics.WithConcurrentCalls(true), autometrics.WithCallerName(true), autometrics.WithSloName("API"), autometrics.WithAlertSuccess(90), - )), &err) //autometrics:defer + )) //autometrics:shadow-ctx + defer autometrics.Instrument(r.Context(), &err) //autometrics:defer isOk := rand.Intn(10) == 0 diff --git a/examples/web/cmd/main.go b/examples/web/cmd/main.go index f420007..8a3868b 100644 --- a/examples/web/cmd/main.go +++ b/examples/web/cmd/main.go @@ -98,22 +98,23 @@ func main() { // // autometrics:doc-end Generated documentation by Autometrics. // -// [Request Rate]: http://localhost:9090/graph?g0.expr=%23+Rate+of+calls+to+the+%60indexHandler%60+function+per+second%2C+averaged+over+5+minute+windows%0A%0Asum+by+%28function%2C+module%2C+version%2C+commit%29+%28rate%28function_calls_count%7Bfunction%3D%22indexHandler%22%7D%5B5m%5D%29+%2A+on+%28instance%2C+job%29+group_left%28version%2C+commit%29+last_over_time%28build_info%5B1s%5D%29%29&g0.tab=0 -// [Error Ratio]: http://localhost:9090/graph?g0.expr=%23+Percentage+of+calls+to+the+%60indexHandler%60+function+that+return+errors%2C+averaged+over+5+minute+windows%0A%0A%28sum+by+%28function%2C+module%2C+version%2C+commit%29+%28rate%28function_calls_count%7Bfunction%3D%22indexHandler%22%2Cresult%3D%22error%22%7D%5B5m%5D%29+%2A+on+%28instance%2C+job%29+group_left%28version%2C+commit%29+last_over_time%28build_info%5B1s%5D%29%29%29+%2F+%28sum+by+%28function%2C+module%2C+version%2C+commit%29+%28rate%28function_calls_count%7Bfunction%3D%22indexHandler%22%7D%5B5m%5D%29+%2A+on+%28instance%2C+job%29+group_left%28version%2C+commit%29+last_over_time%28build_info%5B1s%5D%29%29%29&g0.tab=0 -// [Latency (95th and 99th percentiles)]: http://localhost:9090/graph?g0.expr=%23+95th+and+99th+percentile+latencies+%28in+seconds%29+for+the+%60indexHandler%60+function%0A%0Alabel_replace%28histogram_quantile%280.99%2C+sum+by+%28le%2C+function%2C+module%2C+version%2C+commit%29+%28rate%28function_calls_duration_bucket%7Bfunction%3D%22indexHandler%22%7D%5B5m%5D%29+%2A+on+%28instance%2C+job%29+group_left%28version%2C+commit%29+last_over_time%28build_info%5B1s%5D%29%29%29%2C+%22percentile_latency%22%2C+%2299%22%2C+%22%22%2C+%22%22%29+or+label_replace%28histogram_quantile%280.95%2C+sum+by+%28le%2C+function%2C+module%2C+version%2C+commit%29+%28rate%28function_calls_duration_bucket%7Bfunction%3D%22indexHandler%22%7D%5B5m%5D%29+%2A+on+%28instance%2C+job%29+group_left%28version%2C+commit%29+last_over_time%28build_info%5B1s%5D%29%29%29%2C%22percentile_latency%22%2C+%2295%22%2C+%22%22%2C+%22%22%29&g0.tab=0 -// [Concurrent Calls]: http://localhost:9090/graph?g0.expr=%23+Concurrent+calls+to+the+%60indexHandler%60+function%0A%0Asum+by+%28function%2C+module%2C+version%2C+commit%29+%28function_calls_concurrent%7Bfunction%3D%22indexHandler%22%7D+%2A+on+%28instance%2C+job%29+group_left%28version%2C+commit%29+last_over_time%28build_info%5B1s%5D%29%29&g0.tab=0 -// [Request Rate Callee]: http://localhost:9090/graph?g0.expr=%23+Rate+of+function+calls+emanating+from+%60indexHandler%60+function+per+second%2C+averaged+over+5+minute+windows%0A%0Asum+by+%28function%2C+module%2C+version%2C+commit%29+%28rate%28function_calls_count%7Bcaller%3D%22main.indexHandler%22%7D%5B5m%5D%29+%2A+on+%28instance%2C+job%29+group_left%28version%2C+commit%29+last_over_time%28build_info%5B1s%5D%29%29&g0.tab=0 -// [Error Ratio Callee]: http://localhost:9090/graph?g0.expr=%23+Percentage+of+function+emanating+from+%60indexHandler%60+function+that+return+errors%2C+averaged+over+5+minute+windows%0A%0A%28sum+by+%28function%2C+module%2C+version%2C+commit%29+%28rate%28function_calls_count%7Bcaller%3D%22main.indexHandler%22%2Cresult%3D%22error%22%7D%5B5m%5D%29+%2A+on+%28instance%2C+job%29+group_left%28version%2C+commit%29+last_over_time%28build_info%5B1s%5D%29%29%29+%2F+%28sum+by+%28function%2C+module%2C+version%2C+commit%29+%28rate%28function_calls_count%7Bcaller%3D%22main.indexHandler%22%7D%5B5m%5D%29+%2A+on+%28instance%2C+job%29+group_left%28version%2C+commit%29+last_over_time%28build_info%5B1s%5D%29%29%29&g0.tab=0 +// [Request Rate]: http://localhost:9090/graph?g0.expr=%23+Rate+of+calls+to+the+%60indexHandler%60+function+per+second%2C+averaged+over+5+minute+windows%0A%0Asum+by+%28function%2C+module%2C+service_name%2C+version%2C+commit%29+%28rate%28function_calls_total%7Bfunction%3D%22indexHandler%22%7D%5B5m%5D%29+%2A+on+%28instance%2C+job%29+group_left%28version%2C+commit%29+last_over_time%28build_info%5B1s%5D%29%29&g0.tab=0 +// [Error Ratio]: http://localhost:9090/graph?g0.expr=%23+Percentage+of+calls+to+the+%60indexHandler%60+function+that+return+errors%2C+averaged+over+5+minute+windows%0A%0A%28sum+by+%28function%2C+module%2C+service_name%2C+version%2C+commit%29+%28rate%28function_calls_total%7Bfunction%3D%22indexHandler%22%2Cresult%3D%22error%22%7D%5B5m%5D%29+%2A+on+%28instance%2C+job%29+group_left%28version%2C+commit%29+last_over_time%28build_info%5B1s%5D%29%29%29+%2F+%28sum+by+%28function%2C+module%2C+service_name%2C+version%2C+commit%29+%28rate%28function_calls_total%7Bfunction%3D%22indexHandler%22%7D%5B5m%5D%29+%2A+on+%28instance%2C+job%29+group_left%28version%2C+commit%29+last_over_time%28build_info%5B1s%5D%29%29%29&g0.tab=0 +// [Latency (95th and 99th percentiles)]: http://localhost:9090/graph?g0.expr=%23+95th+and+99th+percentile+latencies+%28in+seconds%29+for+the+%60indexHandler%60+function%0A%0Alabel_replace%28histogram_quantile%280.99%2C+sum+by+%28le%2C+function%2C+module%2C+service_name%2C+version%2C+commit%29+%28rate%28function_calls_duration_seconds_bucket%7Bfunction%3D%22indexHandler%22%7D%5B5m%5D%29+%2A+on+%28instance%2C+job%29+group_left%28version%2C+commit%29+last_over_time%28build_info%5B1s%5D%29%29%29%2C+%22percentile_latency%22%2C+%2299%22%2C+%22%22%2C+%22%22%29+or+label_replace%28histogram_quantile%280.95%2C+sum+by+%28le%2C+function%2C+module%2C+service_name%2C+version%2C+commit%29+%28rate%28function_calls_duration_seconds_bucket%7Bfunction%3D%22indexHandler%22%7D%5B5m%5D%29+%2A+on+%28instance%2C+job%29+group_left%28version%2C+commit%29+last_over_time%28build_info%5B1s%5D%29%29%29%2C%22percentile_latency%22%2C+%2295%22%2C+%22%22%2C+%22%22%29&g0.tab=0 +// [Concurrent Calls]: http://localhost:9090/graph?g0.expr=%23+Concurrent+calls+to+the+%60indexHandler%60+function%0A%0Asum+by+%28function%2C+module%2C+service_name%2C+version%2C+commit%29+%28function_calls_concurrent%7Bfunction%3D%22indexHandler%22%7D+%2A+on+%28instance%2C+job%29+group_left%28version%2C+commit%29+last_over_time%28build_info%5B1s%5D%29%29&g0.tab=0 +// [Request Rate Callee]: http://localhost:9090/graph?g0.expr=%23+Rate+of+function+calls+emanating+from+%60indexHandler%60+function+per+second%2C+averaged+over+5+minute+windows%0A%0Asum+by+%28function%2C+module%2C+service_name%2C+version%2C+commit%29+%28rate%28function_calls_total%7Bcaller_function%3D%22indexHandler%22%7D%5B5m%5D%29+%2A+on+%28instance%2C+job%29+group_left%28version%2C+commit%29+last_over_time%28build_info%5B1s%5D%29%29&g0.tab=0 +// [Error Ratio Callee]: http://localhost:9090/graph?g0.expr=%23+Percentage+of+function+emanating+from+%60indexHandler%60+function+that+return+errors%2C+averaged+over+5+minute+windows%0A%0A%28sum+by+%28function%2C+module%2C+service_name%2C+version%2C+commit%29+%28rate%28function_calls_total%7Bcaller_function%3D%22indexHandler%22%2Cresult%3D%22error%22%7D%5B5m%5D%29+%2A+on+%28instance%2C+job%29+group_left%28version%2C+commit%29+last_over_time%28build_info%5B1s%5D%29%29%29+%2F+%28sum+by+%28function%2C+module%2C+service_name%2C+version%2C+commit%29+%28rate%28function_calls_total%7Bcaller_function%3D%22indexHandler%22%7D%5B5m%5D%29+%2A+on+%28instance%2C+job%29+group_left%28version%2C+commit%29+last_over_time%28build_info%5B1s%5D%29%29%29&g0.tab=0 // //autometrics:inst --slo "API" --latency-target 99 --latency-ms 5 func indexHandler(w http.ResponseWriter, r *http.Request) error { - defer autometrics.Instrument(autometrics.PreInstrument(autometrics.NewContext( + r.Context() = autometrics.PreInstrument(autometrics.NewContext( r.Context(), autometrics.WithConcurrentCalls(true), autometrics.WithCallerName(true), autometrics.WithSloName("API"), autometrics.WithAlertLatency(5000000*time.Nanosecond, 99), - )), nil) //autometrics:defer + )) //autometrics:shadow-ctx + defer autometrics.Instrument(r.Context(), nil) //autometrics:defer msSleep := rand.Intn(200) time.Sleep(time.Duration(msSleep) * time.Millisecond) diff --git a/internal/generate/context.go b/internal/generate/context.go new file mode 100644 index 0000000..c3b9dd0 --- /dev/null +++ b/internal/generate/context.go @@ -0,0 +1,363 @@ +package generate // import "github.com/autometrics-dev/autometrics-go/internal/generate" + +import ( + "fmt" + "go/token" + "log" + "reflect" + "strings" + + "golang.org/x/exp/slices" + + internal "github.com/autometrics-dev/autometrics-go/internal/autometrics" + am "github.com/autometrics-dev/autometrics-go/pkg/autometrics" + + "github.com/dave/dst" + "github.com/dave/dst/decorator" +) + +const ( + vanillaContext = "context" + gin = "github.com/gin-gonic/gin" + buffalo = "github.com/gobuffalo/buffalo" + echoV4 = "github.com/labstack/echo/v4" + netHttp = "net/http" + amDefaultContextName = "amCtx" + contextDecoration = "//autometrics:shadow-ctx" +) + +// injectContextStatement add all the necessary information into context to produce the correct context shadowing statement. +func injectContextStatement(ctx *internal.GeneratorContext, funcDeclaration *dst.FuncDecl) (identName string, err error) { + err = detectContext(ctx, funcDeclaration) + if err != nil { + return "", fmt.Errorf("failed to get context for tracing: %w", err) + } + + autometricsContextStatement, err := buildAutometricsContextStatement(ctx) + if err != nil { + return "", fmt.Errorf("failed to build the defer statement for instrumentation: %w", err) + } + + funcDeclaration.Body.List = append([]dst.Stmt{&autometricsContextStatement}, funcDeclaration.Body.List...) + + // The hard cast is okay here because we just built ourselves the statement. + return autometricsContextStatement.Lhs[0].(*dst.Ident).Name, nil +} + +// removeContextStatement removes, if detected, a previously injected context shadowing statement. +func removeContextStatement(ctx *internal.GeneratorContext, funcDeclaration *dst.FuncDecl) error { + for index, statement := range funcDeclaration.Body.List { + if assignStatement, ok := statement.(*dst.AssignStmt); ok { + decorations := assignStatement.Decorations().End + if slices.Contains(decorations.All(), contextDecoration) { + funcDeclaration.Body.List = append(funcDeclaration.Body.List[:index], funcDeclaration.Body.List[index+1:]...) + return nil + } + } + } + return nil +} + +// findContextStatement finds a previously injected context shadowing statement. +// It also updates the passed context so that the ContextVariableName is accurate. +func findContextStatement(ctx *internal.GeneratorContext, funcDeclaration *dst.FuncDecl) (index int, found bool) { + for index, statement := range funcDeclaration.Body.List { + if assignStatement, ok := statement.(*dst.AssignStmt); ok { + decorations := assignStatement.Decorations().End + if slices.Contains(decorations.All(), contextDecoration) { + ctx.RuntimeCtx.ContextVariableName = assignStatement.Lhs[0].(*dst.Ident).Name + return index, true + } + } + } + return 0, false +} + +// buildAutometricsContextStatement builds the AST node for the context shadowing instrumentation statement to be inserted. +func buildAutometricsContextStatement(ctx *internal.GeneratorContext) (dst.AssignStmt, error) { + preInstrumentArg, contextShadowName, err := buildAutometricsContextNode(ctx) + if err != nil { + return dst.AssignStmt{}, fmt.Errorf("could not generate the runtime context value: %w", err) + } + + // Crude detection of "should the assignment be `:=` or `=`" + assignmentToken := token.ASSIGN + if contextShadowName == amDefaultContextName { + assignmentToken = token.DEFINE + } + + // TODO: deal with cases where the context variable name does not exist. + statement := dst.AssignStmt{ + Lhs: []dst.Expr{ + dst.NewIdent(contextShadowName), + }, + Tok: assignmentToken, + Rhs: []dst.Expr{ + &dst.CallExpr{ + Fun: dst.NewIdent(fmt.Sprintf("%vPreInstrument", autometricsNamespacePrefix(ctx))), + Args: []dst.Expr{ + preInstrumentArg, + }, + }, + }, + } + + statement.Decs.Before = dst.NewLine + statement.Decs.End = []string{contextDecoration} + statement.Decs.After = dst.NewLine + + return statement, nil +} + +// buildAutometricsContextNode creates an AST node representing the runtime context to inject in the instrumented code. +// +// This AST node is later used to create the defer statement responsible for instrumenting the code. +func buildAutometricsContextNode(agc *internal.GeneratorContext) (newContextCall *dst.CallExpr, contextShadowName string, err error) { + // Using https://github.com/dave/dst/issues/73 workaround + + var options []string + + if agc.RuntimeCtx.TraceIDGetter != "" { + options = append(options, fmt.Sprintf("%vWithTraceID(%v)", autometricsNamespacePrefix(agc), agc.RuntimeCtx.TraceIDGetter)) + } + if agc.RuntimeCtx.SpanIDGetter != "" { + options = append(options, fmt.Sprintf("%vWithSpanID(%v)", autometricsNamespacePrefix(agc), agc.RuntimeCtx.SpanIDGetter)) + } + + options = append(options, + fmt.Sprintf("%vWithConcurrentCalls(%#v)", autometricsNamespacePrefix(agc), agc.RuntimeCtx.TrackConcurrentCalls), + fmt.Sprintf("%vWithCallerName(%#v)", autometricsNamespacePrefix(agc), agc.RuntimeCtx.TrackCallerName), + ) + + if agc.RuntimeCtx.AlertConf != nil { + options = append(options, fmt.Sprintf("%vWithSloName(%#v)", + autometricsNamespacePrefix(agc), + agc.RuntimeCtx.AlertConf.ServiceName, + )) + if agc.RuntimeCtx.AlertConf.Latency != nil { + options = append(options, fmt.Sprintf("%vWithAlertLatency(%#v * time.Nanosecond, %#v)", + autometricsNamespacePrefix(agc), + agc.RuntimeCtx.AlertConf.Latency.Target, + agc.RuntimeCtx.AlertConf.Latency.Objective, + )) + } + if agc.RuntimeCtx.AlertConf.Success != nil { + options = append(options, fmt.Sprintf("%vWithAlertSuccess(%#v)", + autometricsNamespacePrefix(agc), + agc.RuntimeCtx.AlertConf.Success.Objective)) + } + } + + contextShadowName = agc.RuntimeCtx.ContextVariableName + if contextShadowName == "nil" { + contextShadowName = amDefaultContextName + } + + var buf strings.Builder + _, err = fmt.Fprintf( + &buf, + ` +package main + +var dummy = %vNewContext( + %s, +`, + autometricsNamespacePrefix(agc), + agc.RuntimeCtx.ContextVariableName, + ) + if err != nil { + return nil, contextShadowName, fmt.Errorf("could not write string builder to build dummy source code: %w", err) + } + + for _, o := range options { + _, err = fmt.Fprintf(&buf, "\t%s,\n", o) + if err != nil { + return nil, contextShadowName, fmt.Errorf("could not write string builder to build dummy source code: %w", err) + } + } + + _, err = fmt.Fprint(&buf, ")\n") + if err != nil { + return nil, contextShadowName, fmt.Errorf("could not write string builder to build dummy source code: %w", err) + } + + sourceCode := buf.String() + sourceAst, err := decorator.Parse(sourceCode) + if err != nil { + return nil, contextShadowName, fmt.Errorf( + "could not parse dummy code\n%s\n: %w", + sourceCode, + err, + ) + } + + genDeclNode, ok := sourceAst.Decls[0].(*dst.GenDecl) + if !ok { + return nil, contextShadowName, fmt.Errorf("unexpected node in the dummy code (expected dst.GenDecl): %w", err) + } + + specNode, ok := genDeclNode.Specs[0].(*dst.ValueSpec) + if !ok { + return nil, contextShadowName, fmt.Errorf("unexpected node in the dummy code (expected dst.ValueSpec): %w", err) + } + + callExpr, ok := specNode.Values[0].(*dst.CallExpr) + if !ok { + return nil, contextShadowName, fmt.Errorf("unexpected node in the dummy code (expected dst.CallExpr): %w", err) + } + + return callExpr, contextShadowName, nil +} + +// detectContextIdentImpl is a Context detection logic helper for arguments whose type is an identifier +// +// The function returns true when it found enough information to ask for iteration to stop. +func detectContextIdentImpl(ctx *internal.GeneratorContext, argName string, ident *dst.Ident) (bool, error) { + typeName := ident.Name + // If argType is just a dst.Ident when parsing, that means + // it is a single identifier ('Context', _not_ 'context.Context'). + // Therefore we can solely check imports that got imported as '.' + for alias, canonical := range ctx.ImportsMap { + if alias != "." { + continue + } + + if canonical == vanillaContext && typeName == "Context" { + ctx.RuntimeCtx.ContextVariableName = argName + ctx.RuntimeCtx.SpanIDGetter = "" + ctx.RuntimeCtx.TraceIDGetter = "" + return true, nil + } + + if canonical == netHttp && typeName == "Request" { + if argName == "_" { + log.Println("Warning: an unnamed net/http.Request has been detected. To make Autometrics reuse its context for tracing purposes, please name it, and run 'go generate' again") + ctx.RuntimeCtx.ContextVariableName = "nil" + } else { + ctx.RuntimeCtx.ContextVariableName = fmt.Sprintf("%s.Context()", argName) + } + ctx.RuntimeCtx.SpanIDGetter = "" + ctx.RuntimeCtx.TraceIDGetter = "" + return true, nil + } + + if canonical == gin && typeName == "Context" { + ctx.RuntimeCtx.SpanIDGetter = fmt.Sprintf("%s.DecodeString(%s.GetString(%#v))", ctx.FuncCtx.ImplImportName, argName, am.MiddlewareSpanIDKey) + ctx.RuntimeCtx.TraceIDGetter = fmt.Sprintf("%s.DecodeString(%s.GetString(%#v))", ctx.FuncCtx.ImplImportName, argName, am.MiddlewareTraceIDKey) + return true, nil + } + + // Buffalo context embeds a context.Context so it can work like vanilla + if canonical == buffalo && typeName == "Context" { + ctx.RuntimeCtx.ContextVariableName = argName + ctx.RuntimeCtx.SpanIDGetter = "" + ctx.RuntimeCtx.TraceIDGetter = "" + return true, nil + } + + if canonical == echoV4 && typeName == "Context" { + ctx.RuntimeCtx.SpanIDGetter = fmt.Sprintf("%s.DecodeString(%s.Get(%#v))", ctx.FuncCtx.ImplImportName, argName, am.MiddlewareSpanIDKey) + ctx.RuntimeCtx.TraceIDGetter = fmt.Sprintf("%s.DecodeString(%s.Get(%#v))", ctx.FuncCtx.ImplImportName, argName, am.MiddlewareTraceIDKey) + return true, nil + } + } + + return false, nil +} + +// detectContextIdentImpl is a Context detection logic helper for arguments whose type is a selector expression. +// +// The function returns true when it found enough information to ask for iteration to stop. +func detectContextSelectorImpl(ctx *internal.GeneratorContext, argName string, selector *dst.SelectorExpr) (bool, error) { + typeName := selector.Sel.Name + if parent, p_ok := selector.X.(*dst.Ident); p_ok { + parentName := parent.Name + for alias, canonical := range ctx.ImportsMap { + if canonical == vanillaContext && parentName == alias && typeName == "Context" { + ctx.RuntimeCtx.ContextVariableName = argName + ctx.RuntimeCtx.SpanIDGetter = "" + ctx.RuntimeCtx.TraceIDGetter = "" + return true, nil + } + + if canonical == netHttp && parentName == alias && typeName == "Request" { + ctx.RuntimeCtx.ContextVariableName = fmt.Sprintf("%s.Context()", argName) + if argName == "_" { + log.Println("Warning: an unnamed net/http.Request has been detected. To make Autometrics reuse its context for tracing purposes, please name it, and run 'go generate' again") + ctx.RuntimeCtx.ContextVariableName = "nil" + } else { + ctx.RuntimeCtx.ContextVariableName = fmt.Sprintf("%s.Context()", argName) + } + ctx.RuntimeCtx.SpanIDGetter = "" + ctx.RuntimeCtx.TraceIDGetter = "" + return true, nil + } + + if canonical == gin && parentName == alias && typeName == "Context" { + ctx.RuntimeCtx.SpanIDGetter = fmt.Sprintf("%s.DecodeString(%s.GetString(%#v))", ctx.FuncCtx.ImplImportName, argName, am.MiddlewareSpanIDKey) + ctx.RuntimeCtx.TraceIDGetter = fmt.Sprintf("%s.DecodeString(%s.GetString(%#v))", ctx.FuncCtx.ImplImportName, argName, am.MiddlewareTraceIDKey) + return true, nil + } + + // Buffalo context embeds a context.Context so it can work like vanilla + if canonical == buffalo && parentName == alias && typeName == "Context" { + ctx.RuntimeCtx.ContextVariableName = argName + ctx.RuntimeCtx.SpanIDGetter = "" + ctx.RuntimeCtx.TraceIDGetter = "" + return true, nil + } + + if canonical == echoV4 && typeName == "Context" && (parentName == alias || parentName == "echo") { + ctx.RuntimeCtx.SpanIDGetter = fmt.Sprintf("%s.DecodeString(%s.Get(%#v))", ctx.FuncCtx.ImplImportName, argName, am.MiddlewareSpanIDKey) + ctx.RuntimeCtx.TraceIDGetter = fmt.Sprintf("%s.DecodeString(%s.Get(%#v))", ctx.FuncCtx.ImplImportName, argName, am.MiddlewareTraceIDKey) + return true, nil + } + } + } else { + // TODO: log that autometrics cannot detect multi-nested contexts instead of errorring + // continue + return true, fmt.Errorf("expecting parent to be an identifier, got %s instead", reflect.TypeOf(selector.X).String()) + } + return false, nil +} + +// detectContext modifies a RuntimeCtxInfo to inject context when detected in the function signature. +func detectContext(ctx *internal.GeneratorContext, funcDeclaration *dst.FuncDecl) error { + arguments := funcDeclaration.Type.Params.List + for _, argGroup := range arguments { + if len(argGroup.Names) > 1 { + continue + } + argName := argGroup.Names[0].Name + if argGroup.Type == nil { + continue + } + + if argType, ok := argGroup.Type.(*dst.Ident); ok { + if found, err := detectContextIdentImpl(ctx, argName, argType); found { + return err + } + } else if argType, ok := argGroup.Type.(*dst.SelectorExpr); ok { + if found, err := detectContextSelectorImpl(ctx, argName, argType); found { + return err + } + } else if argType, ok := argGroup.Type.(*dst.StarExpr); ok { + if ident, ok := argType.X.(*dst.Ident); ok { + if found, err := detectContextIdentImpl(ctx, argName, ident); found { + return err + } + } else if selector, ok := argType.X.(*dst.SelectorExpr); ok { + if found, err := detectContextSelectorImpl(ctx, argName, selector); found { + return err + } + } else { + return fmt.Errorf("expecting the type being pointed to to be an identifier, got %s instead", reflect.TypeOf(argType.X).String()) + } + } else { + return fmt.Errorf("expecting the type of argGroup to be an identifier, got %s instead", reflect.TypeOf(argGroup.Type).String()) + } + } + + ctx.RuntimeCtx.ContextVariableName = "nil" + return nil +} diff --git a/internal/generate/defer.go b/internal/generate/defer.go index 1559f82..0be3302 100644 --- a/internal/generate/defer.go +++ b/internal/generate/defer.go @@ -2,34 +2,28 @@ package generate // import "github.com/autometrics-dev/autometrics-go/internal/g import ( "fmt" - "log" - "reflect" - "strings" "golang.org/x/exp/slices" internal "github.com/autometrics-dev/autometrics-go/internal/autometrics" - am "github.com/autometrics-dev/autometrics-go/pkg/autometrics" "github.com/dave/dst" - "github.com/dave/dst/decorator" ) const ( - vanillaContext = "context" - gin = "github.com/gin-gonic/gin" - buffalo = "github.com/gobuffalo/buffalo" - echoV4 = "github.com/labstack/echo/v4" - netHttp = "net/http" + deferDecoration = "//autometrics:defer" ) // injectDeferStatement add all the necessary information into context to produce the correct defer instrumentation statement. +// +// injectDeferStatement is _always_ meant to be called after injectContextStatement, so the injection will always try to happen after the +// statemtent marked with the shadowing marker func injectDeferStatement(ctx *internal.GeneratorContext, funcDeclaration *dst.FuncDecl) error { - err := detectContext(ctx, funcDeclaration) - if err != nil { - return fmt.Errorf("failed to get context for tracing: %w", err) + contextAssignmentIndex, found := findContextStatement(ctx, funcDeclaration) + if !found { + return fmt.Errorf("failed to get context: the %v statement is missing", contextDecoration) } - firstStatement := funcDeclaration.Body.List[0] + variable, err := errorReturnValueName(funcDeclaration) if err != nil { return fmt.Errorf("failed to get error return value name: %w", err) @@ -46,31 +40,38 @@ func injectDeferStatement(ctx *internal.GeneratorContext, funcDeclaration *dst.F return fmt.Errorf("failed to build the defer statement for instrumentation: %w", err) } - if deferStatement, ok := firstStatement.(*dst.DeferStmt); ok { - decorations := deferStatement.Decorations().End + funcDeclaration.Body.List = insertStatements(funcDeclaration.Body.List, contextAssignmentIndex+1, []dst.Stmt{&autometricsDeferStatement}) + return nil +} - if slices.Contains(decorations.All(), "//autometrics:defer") { - funcDeclaration.Body.List[0] = &autometricsDeferStatement - } else { - funcDeclaration.Body.List = append([]dst.Stmt{&autometricsDeferStatement}, funcDeclaration.Body.List...) - } - } else { - funcDeclaration.Body.List = append([]dst.Stmt{&autometricsDeferStatement}, funcDeclaration.Body.List...) +func insertStatements(inputArray []dst.Stmt, index int, values []dst.Stmt) []dst.Stmt { + if len(inputArray) == index { // nil or empty slice or after last element + return append(inputArray, values...) } - return nil + + beginning := inputArray[:index] + // Maybe the deep copy is not necessary, wasn't able to + // specify the semantics properly here. + end := make([]dst.Stmt, len(inputArray[index:])) + copy(end, inputArray[index:]) + + inputArray = append(beginning, values...) + inputArray = append(inputArray, end...) + + return inputArray } // removeDeferStatement removes, if detected, a previously injected defer statement. func removeDeferStatement(ctx *internal.GeneratorContext, funcDeclaration *dst.FuncDecl) error { - firstStatement := funcDeclaration.Body.List[0] - - if deferStatement, ok := firstStatement.(*dst.DeferStmt); ok { - decorations := deferStatement.Decorations().End - if slices.Contains(decorations.All(), "//autometrics:defer") { - funcDeclaration.Body.List = funcDeclaration.Body.List[1:] + for index, statement := range funcDeclaration.Body.List { + if deferStatement, ok := statement.(*dst.DeferStmt); ok { + decorations := deferStatement.Decorations().End + if slices.Contains(decorations.All(), deferDecoration) { + funcDeclaration.Body.List = append(funcDeclaration.Body.List[:index], funcDeclaration.Body.List[index+1:]...) + return nil + } } } - return nil } @@ -99,104 +100,9 @@ func errorReturnValueName(funcNode *dst.FuncDecl) (string, error) { return "", nil } -// buildAutometricsContextNode creates an AST node representing the runtime context to inject in the instrumented code. -// -// This AST node is later used to create the defer statement responsible for instrumenting the code. -func buildAutometricsContextNode(agc *internal.GeneratorContext) (*dst.CallExpr, error) { - // Using https://github.com/dave/dst/issues/73 workaround - - var options []string - - if agc.RuntimeCtx.TraceIDGetter != "" { - options = append(options, fmt.Sprintf("%vWithTraceID(%v)", autometricsNamespacePrefix(agc), agc.RuntimeCtx.TraceIDGetter)) - } - if agc.RuntimeCtx.SpanIDGetter != "" { - options = append(options, fmt.Sprintf("%vWithSpanID(%v)", autometricsNamespacePrefix(agc), agc.RuntimeCtx.SpanIDGetter)) - } - - options = append(options, - fmt.Sprintf("%vWithConcurrentCalls(%#v)", autometricsNamespacePrefix(agc), agc.RuntimeCtx.TrackConcurrentCalls), - fmt.Sprintf("%vWithCallerName(%#v)", autometricsNamespacePrefix(agc), agc.RuntimeCtx.TrackCallerName), - ) - - if agc.RuntimeCtx.AlertConf != nil { - options = append(options, fmt.Sprintf("%vWithSloName(%#v)", - autometricsNamespacePrefix(agc), - agc.RuntimeCtx.AlertConf.ServiceName, - )) - if agc.RuntimeCtx.AlertConf.Latency != nil { - options = append(options, fmt.Sprintf("%vWithAlertLatency(%#v * time.Nanosecond, %#v)", - autometricsNamespacePrefix(agc), - agc.RuntimeCtx.AlertConf.Latency.Target, - agc.RuntimeCtx.AlertConf.Latency.Objective, - )) - } - if agc.RuntimeCtx.AlertConf.Success != nil { - options = append(options, fmt.Sprintf("%vWithAlertSuccess(%#v)", - autometricsNamespacePrefix(agc), - agc.RuntimeCtx.AlertConf.Success.Objective)) - } - } - - var buf strings.Builder - _, err := fmt.Fprintf( - &buf, - ` -package main - -var dummy = %vNewContext( - %s, -`, - autometricsNamespacePrefix(agc), - agc.RuntimeCtx.ContextVariableName, - ) - if err != nil { - return nil, fmt.Errorf("could not write string builder to build dummy source code: %w", err) - } - - for _, o := range options { - _, err = fmt.Fprintf(&buf, "\t%s,\n", o) - if err != nil { - return nil, fmt.Errorf("could not write string builder to build dummy source code: %w", err) - } - } - - _, err = fmt.Fprint(&buf, ")\n") - if err != nil { - return nil, fmt.Errorf("could not write string builder to build dummy source code: %w", err) - } - - sourceCode := buf.String() - sourceAst, err := decorator.Parse(sourceCode) - if err != nil { - return nil, fmt.Errorf( - "could not parse dummy code\n%s\n: %w", - sourceCode, - err, - ) - } - - genDeclNode, ok := sourceAst.Decls[0].(*dst.GenDecl) - if !ok { - return nil, fmt.Errorf("unexpected node in the dummy code (expected dst.GenDecl): %w", err) - } - - specNode, ok := genDeclNode.Specs[0].(*dst.ValueSpec) - if !ok { - return nil, fmt.Errorf("unexpected node in the dummy code (expected dst.ValueSpec): %w", err) - } - - callExpr, ok := specNode.Values[0].(*dst.CallExpr) - if !ok { - return nil, fmt.Errorf("unexpected node in the dummy code (expected dst.CallExpr): %w", err) - } - - return callExpr, nil -} - // buildAutometricsDeferStatement builds the AST node for the defer instrumentation statement to be inserted. -func buildAutometricsDeferStatement(ctx *internal.GeneratorContext, secondVar string) (dst.DeferStmt, error) { - preInstrumentArg, err := buildAutometricsContextNode(ctx) +func buildAutometricsDeferStatement(ctx *internal.GeneratorContext, errorPointerVariable string) (dst.DeferStmt, error) { + _, contextName, err := buildAutometricsContextNode(ctx) if err != nil { return dst.DeferStmt{}, fmt.Errorf("could not generate the runtime context value: %w", err) } @@ -204,19 +110,14 @@ func buildAutometricsDeferStatement(ctx *internal.GeneratorContext, secondVar st Call: &dst.CallExpr{ Fun: dst.NewIdent(fmt.Sprintf("%vInstrument", autometricsNamespacePrefix(ctx))), Args: []dst.Expr{ - &dst.CallExpr{ - Fun: dst.NewIdent(fmt.Sprintf("%vPreInstrument", autometricsNamespacePrefix(ctx))), - Args: []dst.Expr{ - preInstrumentArg, - }, - }, - dst.NewIdent(secondVar), + dst.NewIdent(contextName), + dst.NewIdent(errorPointerVariable), }, }, } statement.Decs.Before = dst.NewLine - statement.Decs.End = []string{"//autometrics:defer"} + statement.Decs.End = []string{deferDecoration} statement.Decs.After = dst.EmptyLine return statement, nil @@ -229,156 +130,3 @@ func autometricsNamespacePrefix(ctx *internal.GeneratorContext) string { return fmt.Sprintf("%v.", ctx.FuncCtx.ImplImportName) } } - -// detectContextIdentImpl is a Context detection logic helper for arguments whose type is an identifier -// -// The function returns true when it found enough information to ask for iteration to stop. -func detectContextIdentImpl(ctx *internal.GeneratorContext, argName string, ident *dst.Ident) (bool, error) { - typeName := ident.Name - // If argType is just a dst.Ident when parsing, that means - // it is a single identifier ('Context', _not_ 'context.Context'). - // Therefore we can solely check imports that got imported as '.' - for alias, canonical := range ctx.ImportsMap { - if alias != "." { - continue - } - - if canonical == vanillaContext && typeName == "Context" { - ctx.RuntimeCtx.ContextVariableName = argName - ctx.RuntimeCtx.SpanIDGetter = "" - ctx.RuntimeCtx.TraceIDGetter = "" - return true, nil - } - - if canonical == netHttp && typeName == "Request" { - if argName == "_" { - log.Println("Warning: an unnamed net/http.Request has been detected. To make Autometrics reuse its context for tracing purposes, please name it, and run 'go generate' again") - ctx.RuntimeCtx.ContextVariableName = "nil" - } else { - ctx.RuntimeCtx.ContextVariableName = fmt.Sprintf("%s.Context()", argName) - } - ctx.RuntimeCtx.SpanIDGetter = "" - ctx.RuntimeCtx.TraceIDGetter = "" - return true, nil - } - - if canonical == gin && typeName == "Context" { - ctx.RuntimeCtx.SpanIDGetter = fmt.Sprintf("%s.DecodeString(%s.GetString(%#v))", ctx.FuncCtx.ImplImportName, argName, am.MiddlewareSpanIDKey) - ctx.RuntimeCtx.TraceIDGetter = fmt.Sprintf("%s.DecodeString(%s.GetString(%#v))", ctx.FuncCtx.ImplImportName, argName, am.MiddlewareTraceIDKey) - return true, nil - } - - // Buffalo context embeds a context.Context so it can work like vanilla - if canonical == buffalo && typeName == "Context" { - ctx.RuntimeCtx.ContextVariableName = argName - ctx.RuntimeCtx.SpanIDGetter = "" - ctx.RuntimeCtx.TraceIDGetter = "" - return true, nil - } - - if canonical == echoV4 && typeName == "Context" { - ctx.RuntimeCtx.SpanIDGetter = fmt.Sprintf("%s.DecodeString(%s.Get(%#v))", ctx.FuncCtx.ImplImportName, argName, am.MiddlewareSpanIDKey) - ctx.RuntimeCtx.TraceIDGetter = fmt.Sprintf("%s.DecodeString(%s.Get(%#v))", ctx.FuncCtx.ImplImportName, argName, am.MiddlewareTraceIDKey) - return true, nil - } - } - - return false, nil -} - -// detectContextIdentImpl is a Context detection logic helper for arguments whose type is a selector expression. -// -// The function returns true when it found enough information to ask for iteration to stop. -func detectContextSelectorImpl(ctx *internal.GeneratorContext, argName string, selector *dst.SelectorExpr) (bool, error) { - typeName := selector.Sel.Name - if parent, p_ok := selector.X.(*dst.Ident); p_ok { - parentName := parent.Name - for alias, canonical := range ctx.ImportsMap { - if canonical == vanillaContext && parentName == alias && typeName == "Context" { - ctx.RuntimeCtx.ContextVariableName = argName - ctx.RuntimeCtx.SpanIDGetter = "" - ctx.RuntimeCtx.TraceIDGetter = "" - return true, nil - } - - if canonical == netHttp && parentName == alias && typeName == "Request" { - ctx.RuntimeCtx.ContextVariableName = fmt.Sprintf("%s.Context()", argName) - if argName == "_" { - log.Println("Warning: an unnamed net/http.Request has been detected. To make Autometrics reuse its context for tracing purposes, please name it, and run 'go generate' again") - ctx.RuntimeCtx.ContextVariableName = "nil" - } else { - ctx.RuntimeCtx.ContextVariableName = fmt.Sprintf("%s.Context()", argName) - } - ctx.RuntimeCtx.SpanIDGetter = "" - ctx.RuntimeCtx.TraceIDGetter = "" - return true, nil - } - - if canonical == gin && parentName == alias && typeName == "Context" { - ctx.RuntimeCtx.SpanIDGetter = fmt.Sprintf("%s.DecodeString(%s.GetString(%#v))", ctx.FuncCtx.ImplImportName, argName, am.MiddlewareSpanIDKey) - ctx.RuntimeCtx.TraceIDGetter = fmt.Sprintf("%s.DecodeString(%s.GetString(%#v))", ctx.FuncCtx.ImplImportName, argName, am.MiddlewareTraceIDKey) - return true, nil - } - - // Buffalo context embeds a context.Context so it can work like vanilla - if canonical == buffalo && parentName == alias && typeName == "Context" { - ctx.RuntimeCtx.ContextVariableName = argName - ctx.RuntimeCtx.SpanIDGetter = "" - ctx.RuntimeCtx.TraceIDGetter = "" - return true, nil - } - - if canonical == echoV4 && typeName == "Context" && (parentName == alias || parentName == "echo") { - ctx.RuntimeCtx.SpanIDGetter = fmt.Sprintf("%s.DecodeString(%s.Get(%#v))", ctx.FuncCtx.ImplImportName, argName, am.MiddlewareSpanIDKey) - ctx.RuntimeCtx.TraceIDGetter = fmt.Sprintf("%s.DecodeString(%s.Get(%#v))", ctx.FuncCtx.ImplImportName, argName, am.MiddlewareTraceIDKey) - return true, nil - } - } - } else { - // TODO: log that autometrics cannot detect multi-nested contexts instead of errorring - // continue - return true, fmt.Errorf("expecting parent to be an identifier, got %s instead", reflect.TypeOf(selector.X).String()) - } - return false, nil -} - -// detectContext modifies a RuntimeCtxInfo to inject context when detected in the function signature. -func detectContext(ctx *internal.GeneratorContext, funcDeclaration *dst.FuncDecl) error { - arguments := funcDeclaration.Type.Params.List - for _, argGroup := range arguments { - if len(argGroup.Names) > 1 { - continue - } - argName := argGroup.Names[0].Name - if argGroup.Type == nil { - continue - } - - if argType, ok := argGroup.Type.(*dst.Ident); ok { - if found, err := detectContextIdentImpl(ctx, argName, argType); found { - return err - } - } else if argType, ok := argGroup.Type.(*dst.SelectorExpr); ok { - if found, err := detectContextSelectorImpl(ctx, argName, argType); found { - return err - } - } else if argType, ok := argGroup.Type.(*dst.StarExpr); ok { - if ident, ok := argType.X.(*dst.Ident); ok { - if found, err := detectContextIdentImpl(ctx, argName, ident); found { - return err - } - } else if selector, ok := argType.X.(*dst.SelectorExpr); ok { - if found, err := detectContextSelectorImpl(ctx, argName, selector); found { - return err - } - } else { - return fmt.Errorf("expecting the type being pointed to to be an identifier, got %s instead", reflect.TypeOf(argType.X).String()) - } - } else { - return fmt.Errorf("expecting the type of argGroup to be an identifier, got %s instead", reflect.TypeOf(argGroup.Type).String()) - } - } - - ctx.RuntimeCtx.ContextVariableName = "nil" - return nil -} diff --git a/internal/generate/defer_test.go b/internal/generate/defer_test.go index 4f8ee6c..6f7d1fa 100644 --- a/internal/generate/defer_test.go +++ b/internal/generate/defer_test.go @@ -42,13 +42,14 @@ func main(thisIsAContext context.Context) { "//\n" + "//autometrics:inst --no-doc --slo \"Service Test\" --success-target 99\n" + "func main(thisIsAContext context.Context) {\n" + - "\tdefer prom.Instrument(prom.PreInstrument(prom.NewContext(\n" + + "\tthisIsAContext = prom.PreInstrument(prom.NewContext(\n" + "\t\tthisIsAContext,\n" + "\t\tprom.WithConcurrentCalls(true),\n" + "\t\tprom.WithCallerName(true),\n" + "\t\tprom.WithSloName(\"Service Test\"),\n" + "\t\tprom.WithAlertSuccess(99),\n" + - "\t)), nil) //autometrics:defer\n" + + "\t)) //autometrics:shadow-ctx\n" + + "\tdefer prom.Instrument(thisIsAContext, nil) //autometrics:defer\n" + "\n" + " fmt.Println(hello) // line comment 3\n" + "}\n" @@ -99,13 +100,14 @@ func main(thisIsAContext vanilla.Context) { "//\n" + "//autometrics:inst --no-doc --slo \"Service Test\" --success-target 99\n" + "func main(thisIsAContext vanilla.Context) {\n" + - "\tdefer prom.Instrument(prom.PreInstrument(prom.NewContext(\n" + + "\tthisIsAContext = prom.PreInstrument(prom.NewContext(\n" + "\t\tthisIsAContext,\n" + "\t\tprom.WithConcurrentCalls(true),\n" + "\t\tprom.WithCallerName(true),\n" + "\t\tprom.WithSloName(\"Service Test\"),\n" + "\t\tprom.WithAlertSuccess(99),\n" + - "\t)), nil) //autometrics:defer\n" + + "\t)) //autometrics:shadow-ctx\n" + + "\tdefer prom.Instrument(thisIsAContext, nil) //autometrics:defer\n" + "\n" + " fmt.Println(hello) // line comment 3\n" + "}\n" @@ -156,13 +158,14 @@ func main(thisIsAContext Context) { "//\n" + "//autometrics:inst --no-doc --slo \"Service Test\" --success-target 99\n" + "func main(thisIsAContext Context) {\n" + - "\tdefer prom.Instrument(prom.PreInstrument(prom.NewContext(\n" + + "\tthisIsAContext = prom.PreInstrument(prom.NewContext(\n" + "\t\tthisIsAContext,\n" + "\t\tprom.WithConcurrentCalls(true),\n" + "\t\tprom.WithCallerName(true),\n" + "\t\tprom.WithSloName(\"Service Test\"),\n" + "\t\tprom.WithAlertSuccess(99),\n" + - "\t)), nil) //autometrics:defer\n" + + "\t)) //autometrics:shadow-ctx\n" + + "\tdefer prom.Instrument(thisIsAContext, nil) //autometrics:defer\n" + "\n" + " fmt.Println(hello) // line comment 3\n" + "}\n" @@ -213,13 +216,14 @@ func main(w http.ResponseWriter, req *http.Request) { "//\n" + "//autometrics:inst --no-doc --slo \"Service Test\" --success-target 99\n" + "func main(w http.ResponseWriter, req *http.Request) {\n" + - "\tdefer prom.Instrument(prom.PreInstrument(prom.NewContext(\n" + + "\treq.Context() = prom.PreInstrument(prom.NewContext(\n" + "\t\treq.Context(),\n" + "\t\tprom.WithConcurrentCalls(true),\n" + "\t\tprom.WithCallerName(true),\n" + "\t\tprom.WithSloName(\"Service Test\"),\n" + "\t\tprom.WithAlertSuccess(99),\n" + - "\t)), nil) //autometrics:defer\n" + + "\t)) //autometrics:shadow-ctx\n" + + "\tdefer prom.Instrument(req.Context(), nil) //autometrics:defer\n" + "\n" + " fmt.Println(hello) // line comment 3\n" + "}\n" @@ -270,13 +274,14 @@ func main(w vanilla.ResponseWriter, req *vanilla.Request) { "//\n" + "//autometrics:inst --no-doc --slo \"Service Test\" --success-target 99\n" + "func main(w vanilla.ResponseWriter, req *vanilla.Request) {\n" + - "\tdefer prom.Instrument(prom.PreInstrument(prom.NewContext(\n" + + "\treq.Context() = prom.PreInstrument(prom.NewContext(\n" + "\t\treq.Context(),\n" + "\t\tprom.WithConcurrentCalls(true),\n" + "\t\tprom.WithCallerName(true),\n" + "\t\tprom.WithSloName(\"Service Test\"),\n" + "\t\tprom.WithAlertSuccess(99),\n" + - "\t)), nil) //autometrics:defer\n" + + "\t)) //autometrics:shadow-ctx\n" + + "\tdefer prom.Instrument(req.Context(), nil) //autometrics:defer\n" + "\n" + " fmt.Println(hello) // line comment 3\n" + "}\n" @@ -327,13 +332,14 @@ func main(w ResponseWriter, req *Request) { "//\n" + "//autometrics:inst --no-doc --slo \"Service Test\" --success-target 99\n" + "func main(w ResponseWriter, req *Request) {\n" + - "\tdefer prom.Instrument(prom.PreInstrument(prom.NewContext(\n" + + "\treq.Context() = prom.PreInstrument(prom.NewContext(\n" + "\t\treq.Context(),\n" + "\t\tprom.WithConcurrentCalls(true),\n" + "\t\tprom.WithCallerName(true),\n" + "\t\tprom.WithSloName(\"Service Test\"),\n" + "\t\tprom.WithAlertSuccess(99),\n" + - "\t)), nil) //autometrics:defer\n" + + "\t)) //autometrics:shadow-ctx\n" + + "\tdefer prom.Instrument(req.Context(), nil) //autometrics:defer\n" + "\n" + " fmt.Println(hello) // line comment 3\n" + "}\n" @@ -384,13 +390,14 @@ func main(thisIsAContext buffalo.Context) { "//\n" + "//autometrics:inst --no-doc --slo \"Service Test\" --success-target 99\n" + "func main(thisIsAContext buffalo.Context) {\n" + - "\tdefer prom.Instrument(prom.PreInstrument(prom.NewContext(\n" + + "\tthisIsAContext = prom.PreInstrument(prom.NewContext(\n" + "\t\tthisIsAContext,\n" + "\t\tprom.WithConcurrentCalls(true),\n" + "\t\tprom.WithCallerName(true),\n" + "\t\tprom.WithSloName(\"Service Test\"),\n" + "\t\tprom.WithAlertSuccess(99),\n" + - "\t)), nil) //autometrics:defer\n" + + "\t)) //autometrics:shadow-ctx\n" + + "\tdefer prom.Instrument(thisIsAContext, nil) //autometrics:defer\n" + "\n" + " fmt.Println(hello) // line comment 3\n" + "}\n" @@ -441,13 +448,14 @@ func main(thisIsAContext vanilla.Context) { "//\n" + "//autometrics:inst --no-doc --slo \"Service Test\" --success-target 99\n" + "func main(thisIsAContext vanilla.Context) {\n" + - "\tdefer prom.Instrument(prom.PreInstrument(prom.NewContext(\n" + + "\tthisIsAContext = prom.PreInstrument(prom.NewContext(\n" + "\t\tthisIsAContext,\n" + "\t\tprom.WithConcurrentCalls(true),\n" + "\t\tprom.WithCallerName(true),\n" + "\t\tprom.WithSloName(\"Service Test\"),\n" + "\t\tprom.WithAlertSuccess(99),\n" + - "\t)), nil) //autometrics:defer\n" + + "\t)) //autometrics:shadow-ctx\n" + + "\tdefer prom.Instrument(thisIsAContext, nil) //autometrics:defer\n" + "\n" + " fmt.Println(hello) // line comment 3\n" + "}\n" @@ -498,13 +506,14 @@ func main(thisIsAContext Context) { "//\n" + "//autometrics:inst --no-doc --slo \"Service Test\" --success-target 99\n" + "func main(thisIsAContext Context) {\n" + - "\tdefer prom.Instrument(prom.PreInstrument(prom.NewContext(\n" + + "\tthisIsAContext = prom.PreInstrument(prom.NewContext(\n" + "\t\tthisIsAContext,\n" + "\t\tprom.WithConcurrentCalls(true),\n" + "\t\tprom.WithCallerName(true),\n" + "\t\tprom.WithSloName(\"Service Test\"),\n" + "\t\tprom.WithAlertSuccess(99),\n" + - "\t)), nil) //autometrics:defer\n" + + "\t)) //autometrics:shadow-ctx\n" + + "\tdefer prom.Instrument(thisIsAContext, nil) //autometrics:defer\n" + "\n" + " fmt.Println(hello) // line comment 3\n" + "}\n" @@ -555,7 +564,7 @@ func main(thisIsAContext echo.Context) { "//\n" + "//autometrics:inst --no-doc --slo \"Service Test\" --success-target 99\n" + "func main(thisIsAContext echo.Context) {\n" + - "\tdefer prom.Instrument(prom.PreInstrument(prom.NewContext(\n" + + "\tamCtx := prom.PreInstrument(prom.NewContext(\n" + "\t\tnil,\n" + "\t\tprom.WithTraceID(prom.DecodeString(thisIsAContext.Get(\"autometricsTraceID\"))),\n" + "\t\tprom.WithSpanID(prom.DecodeString(thisIsAContext.Get(\"autometricsSpanID\"))),\n" + @@ -563,7 +572,8 @@ func main(thisIsAContext echo.Context) { "\t\tprom.WithCallerName(true),\n" + "\t\tprom.WithSloName(\"Service Test\"),\n" + "\t\tprom.WithAlertSuccess(99),\n" + - "\t)), nil) //autometrics:defer\n" + + "\t)) //autometrics:shadow-ctx\n" + + "\tdefer prom.Instrument(amCtx, nil) //autometrics:defer\n" + "\n" + " fmt.Println(hello) // line comment 3\n" + "}\n" @@ -614,7 +624,7 @@ func main(thisIsAContext vanilla.Context) { "//\n" + "//autometrics:inst --no-doc --slo \"Service Test\" --success-target 99\n" + "func main(thisIsAContext vanilla.Context) {\n" + - "\tdefer prom.Instrument(prom.PreInstrument(prom.NewContext(\n" + + "\tamCtx := prom.PreInstrument(prom.NewContext(\n" + "\t\tnil,\n" + "\t\tprom.WithTraceID(prom.DecodeString(thisIsAContext.Get(\"autometricsTraceID\"))),\n" + "\t\tprom.WithSpanID(prom.DecodeString(thisIsAContext.Get(\"autometricsSpanID\"))),\n" + @@ -622,7 +632,8 @@ func main(thisIsAContext vanilla.Context) { "\t\tprom.WithCallerName(true),\n" + "\t\tprom.WithSloName(\"Service Test\"),\n" + "\t\tprom.WithAlertSuccess(99),\n" + - "\t)), nil) //autometrics:defer\n" + + "\t)) //autometrics:shadow-ctx\n" + + "\tdefer prom.Instrument(amCtx, nil) //autometrics:defer\n" + "\n" + " fmt.Println(hello) // line comment 3\n" + "}\n" @@ -673,7 +684,7 @@ func main(thisIsAContext Context) { "//\n" + "//autometrics:inst --no-doc --slo \"Service Test\" --success-target 99\n" + "func main(thisIsAContext Context) {\n" + - "\tdefer prom.Instrument(prom.PreInstrument(prom.NewContext(\n" + + "\tamCtx := prom.PreInstrument(prom.NewContext(\n" + "\t\tnil,\n" + "\t\tprom.WithTraceID(prom.DecodeString(thisIsAContext.Get(\"autometricsTraceID\"))),\n" + "\t\tprom.WithSpanID(prom.DecodeString(thisIsAContext.Get(\"autometricsSpanID\"))),\n" + @@ -681,7 +692,8 @@ func main(thisIsAContext Context) { "\t\tprom.WithCallerName(true),\n" + "\t\tprom.WithSloName(\"Service Test\"),\n" + "\t\tprom.WithAlertSuccess(99),\n" + - "\t)), nil) //autometrics:defer\n" + + "\t)) //autometrics:shadow-ctx\n" + + "\tdefer prom.Instrument(amCtx, nil) //autometrics:defer\n" + "\n" + " fmt.Println(hello) // line comment 3\n" + "}\n" @@ -732,7 +744,7 @@ func main(thisIsAContext *gin.Context) { "//\n" + "//autometrics:inst --no-doc --slo \"Service Test\" --success-target 99\n" + "func main(thisIsAContext *gin.Context) {\n" + - "\tdefer prom.Instrument(prom.PreInstrument(prom.NewContext(\n" + + "\tamCtx := prom.PreInstrument(prom.NewContext(\n" + "\t\tnil,\n" + "\t\tprom.WithTraceID(prom.DecodeString(thisIsAContext.GetString(\"autometricsTraceID\"))),\n" + "\t\tprom.WithSpanID(prom.DecodeString(thisIsAContext.GetString(\"autometricsSpanID\"))),\n" + @@ -740,7 +752,8 @@ func main(thisIsAContext *gin.Context) { "\t\tprom.WithCallerName(true),\n" + "\t\tprom.WithSloName(\"Service Test\"),\n" + "\t\tprom.WithAlertSuccess(99),\n" + - "\t)), nil) //autometrics:defer\n" + + "\t)) //autometrics:shadow-ctx\n" + + "\tdefer prom.Instrument(amCtx, nil) //autometrics:defer\n" + "\n" + " fmt.Println(hello) // line comment 3\n" + "}\n" @@ -791,7 +804,7 @@ func main(thisIsAContext *vanilla.Context) { "//\n" + "//autometrics:inst --no-doc --slo \"Service Test\" --success-target 99\n" + "func main(thisIsAContext *vanilla.Context) {\n" + - "\tdefer prom.Instrument(prom.PreInstrument(prom.NewContext(\n" + + "\tamCtx := prom.PreInstrument(prom.NewContext(\n" + "\t\tnil,\n" + "\t\tprom.WithTraceID(prom.DecodeString(thisIsAContext.GetString(\"autometricsTraceID\"))),\n" + "\t\tprom.WithSpanID(prom.DecodeString(thisIsAContext.GetString(\"autometricsSpanID\"))),\n" + @@ -799,7 +812,8 @@ func main(thisIsAContext *vanilla.Context) { "\t\tprom.WithCallerName(true),\n" + "\t\tprom.WithSloName(\"Service Test\"),\n" + "\t\tprom.WithAlertSuccess(99),\n" + - "\t)), nil) //autometrics:defer\n" + + "\t)) //autometrics:shadow-ctx\n" + + "\tdefer prom.Instrument(amCtx, nil) //autometrics:defer\n" + "\n" + " fmt.Println(hello) // line comment 3\n" + "}\n" @@ -850,7 +864,7 @@ func main(thisIsAContext *Context) { "//\n" + "//autometrics:inst --no-doc --slo \"Service Test\" --success-target 99\n" + "func main(thisIsAContext *Context) {\n" + - "\tdefer prom.Instrument(prom.PreInstrument(prom.NewContext(\n" + + "\tamCtx := prom.PreInstrument(prom.NewContext(\n" + "\t\tnil,\n" + "\t\tprom.WithTraceID(prom.DecodeString(thisIsAContext.GetString(\"autometricsTraceID\"))),\n" + "\t\tprom.WithSpanID(prom.DecodeString(thisIsAContext.GetString(\"autometricsSpanID\"))),\n" + @@ -858,7 +872,8 @@ func main(thisIsAContext *Context) { "\t\tprom.WithCallerName(true),\n" + "\t\tprom.WithSloName(\"Service Test\"),\n" + "\t\tprom.WithAlertSuccess(99),\n" + - "\t)), nil) //autometrics:defer\n" + + "\t)) //autometrics:shadow-ctx\n" + + "\tdefer prom.Instrument(amCtx, nil) //autometrics:defer\n" + "\n" + " fmt.Println(hello) // line comment 3\n" + "}\n" diff --git a/internal/generate/generate.go b/internal/generate/generate.go index 5f61d06..c565daa 100644 --- a/internal/generate/generate.go +++ b/internal/generate/generate.go @@ -154,6 +154,13 @@ func walkFuncDeclaration(ctx *internal.GeneratorContext, funcDeclaration *dst.Fu funcDeclaration.Name.Name, err) } + err = removeContextStatement(ctx, funcDeclaration) + if err != nil { + return fmt.Errorf( + "error removing an older autometrics context statement in %v: %w", + funcDeclaration.Name.Name, + err) + } // Early exit if we wanted to remove everything if ctx.RemoveEverything { @@ -192,8 +199,14 @@ func walkFuncDeclaration(ctx *internal.GeneratorContext, funcDeclaration *dst.Fu funcDeclaration.Decorations().Start.Replace(docComments...) } + // context statement + _, err := injectContextStatement(ctx, funcDeclaration) + if err != nil { + return fmt.Errorf("failed to inject context statement: %w", err) + } + // defer statement - err := injectDeferStatement(ctx, funcDeclaration) + err = injectDeferStatement(ctx, funcDeclaration) if err != nil { return fmt.Errorf("failed to inject defer statement: %w", err) } diff --git a/internal/generate/generate_test.go b/internal/generate/generate_test.go index 431c88c..d95a602 100644 --- a/internal/generate/generate_test.go +++ b/internal/generate/generate_test.go @@ -69,13 +69,14 @@ import ( // //autometrics:inst --slo "Service Test" --success-target 99 func main() { - defer prom.Instrument(prom.PreInstrument(prom.NewContext( + amCtx := prom.PreInstrument(prom.NewContext( nil, prom.WithConcurrentCalls(true), prom.WithCallerName(true), prom.WithSloName("Service Test"), prom.WithAlertSuccess(99), - )), nil) //autometrics:defer + )) //autometrics:shadow-ctx + defer prom.Instrument(amCtx, nil) //autometrics:defer fmt.Println(hello) // line comment 3 } @@ -148,13 +149,14 @@ import ( // //autometrics:inst --slo "Service Test" --success-target 99 func main() { - defer prom.Instrument(prom.PreInstrument(prom.NewContext( + amCtx := prom.PreInstrument(prom.NewContext( nil, prom.WithConcurrentCalls(true), prom.WithCallerName(true), prom.WithSloName("Service Test"), prom.WithAlertSuccess(99), - )), nil) //autometrics:defer + )) //autometrics:shadow-ctx + defer prom.Instrument(amCtx, nil) //autometrics:defer fmt.Println(hello) // line comment 3 } @@ -199,13 +201,14 @@ import ( // //autometrics:inst --no-doc --slo "Service Test" --success-target 99 func main() { - defer prom.Instrument(prom.PreInstrument(prom.NewContext( + amCtx := prom.PreInstrument(prom.NewContext( nil, prom.WithConcurrentCalls(true), prom.WithCallerName(true), prom.WithSloName("Service Test"), prom.WithAlertSuccess(99), - )), nil) //autometrics:defer + )) //autometrics:shadow-ctx + defer prom.Instrument(amCtx, nil) //autometrics:defer fmt.Println(hello) // line comment 3 } @@ -271,11 +274,12 @@ import ( // [Request Rate Callee]: http://localhost:9090/graph?g0.expr=%23+Rate+of+function+calls+emanating+from+%60main%60+function+per+second%2C+averaged+over+5+minute+windows%0A%0Asum+by+%28function%2C+module%2C+service_name%2C+version%2C+commit%29+%28rate%28function_calls_total%7Bcaller_function%3D%22main%22%7D%5B5m%5D%29+%2A+on+%28instance%2C+job%29+group_left%28version%2C+commit%29+last_over_time%28build_info%5B1s%5D%29%29&g0.tab=0 // [Error Ratio Callee]: http://localhost:9090/graph?g0.expr=%23+Percentage+of+function+emanating+from+%60main%60+function+that+return+errors%2C+averaged+over+5+minute+windows%0A%0A%28sum+by+%28function%2C+module%2C+service_name%2C+version%2C+commit%29+%28rate%28function_calls_total%7Bcaller_function%3D%22main%22%2Cresult%3D%22error%22%7D%5B5m%5D%29+%2A+on+%28instance%2C+job%29+group_left%28version%2C+commit%29+last_over_time%28build_info%5B1s%5D%29%29%29+%2F+%28sum+by+%28function%2C+module%2C+service_name%2C+version%2C+commit%29+%28rate%28function_calls_total%7Bcaller_function%3D%22main%22%7D%5B5m%5D%29+%2A+on+%28instance%2C+job%29+group_left%28version%2C+commit%29+last_over_time%28build_info%5B1s%5D%29%29%29&g0.tab=0 func main() { - defer prom.Instrument(prom.PreInstrument(prom.NewContext( + amCtx := prom.PreInstrument(prom.NewContext( nil, prom.WithConcurrentCalls(true), prom.WithCallerName(true), - )), nil) //autometrics:defer + )) //autometrics:shadow-ctx + defer prom.Instrument(amCtx, nil) //autometrics:defer fmt.Println(hello) // line comment 3 } @@ -315,11 +319,12 @@ import ( ) func main() { - defer prom.Instrument(prom.PreInstrument(prom.NewContext( + amCtx := prom.PreInstrument(prom.NewContext( nil, prom.WithConcurrentCalls(true), prom.WithCallerName(true), - )), nil) //autometrics:defer + )) //autometrics:shadow-ctx + defer prom.Instrument(amCtx, nil) //autometrics:defer fmt.Println(hello) // line comment 3 } @@ -361,13 +366,14 @@ import ( //autometrics:inst --no-doc --slo "Service Test" --success-target 99 func main() { - defer prom.Instrument(prom.PreInstrument(prom.NewContext( + amCtx := prom.PreInstrument(prom.NewContext( nil, prom.WithConcurrentCalls(true), prom.WithCallerName(true), prom.WithSloName("Service Test"), prom.WithAlertSuccess(99), - )), nil) //autometrics:defer + )) //autometrics:shadow-ctx + defer prom.Instrument(amCtx, nil) //autometrics:defer fmt.Println(hello) // line comment 3 } @@ -525,13 +531,14 @@ import ( // //autometrics:inst --slo "API" --latency-target 99.9 --latency-ms 500 func main() { - defer prom.Instrument(prom.PreInstrument(prom.NewContext( + amCtx := prom.PreInstrument(prom.NewContext( nil, prom.WithConcurrentCalls(true), prom.WithCallerName(true), prom.WithSloName("API"), prom.WithAlertLatency(500000000*time.Nanosecond, 99.9), - )), nil) //autometrics:defer + )) //autometrics:shadow-ctx + defer prom.Instrument(amCtx, nil) //autometrics:defer fmt.Println(hello) // line comment 3 } @@ -573,13 +580,14 @@ import "github.com/autometrics-dev/autometrics-go/prometheus/autometrics" // //autometrics:inst --no-doc --slo "API" --latency-target 99.9 --latency-ms 500 func main() { - defer autometrics.Instrument(autometrics.PreInstrument(autometrics.NewContext( + amCtx := autometrics.PreInstrument(autometrics.NewContext( nil, autometrics.WithConcurrentCalls(true), autometrics.WithCallerName(true), autometrics.WithSloName("API"), autometrics.WithAlertLatency(500000000*time.Nanosecond, 99.9), - )), nil) //autometrics:defer + )) //autometrics:shadow-ctx + defer autometrics.Instrument(amCtx, nil) //autometrics:defer fmt.Println(hello) // line comment 3 } @@ -630,13 +638,14 @@ import ( // //autometrics:inst --no-doc --slo "API" --latency-target 99.9 --latency-ms 500 func main() { - defer autometrics.Instrument(autometrics.PreInstrument(autometrics.NewContext( + amCtx := autometrics.PreInstrument(autometrics.NewContext( nil, autometrics.WithConcurrentCalls(true), autometrics.WithCallerName(true), autometrics.WithSloName("API"), autometrics.WithAlertLatency(500000000*time.Nanosecond, 99.9), - )), nil) //autometrics:defer + )) //autometrics:shadow-ctx + defer autometrics.Instrument(amCtx, nil) //autometrics:defer fmt.Println(hello) // line comment 3 } @@ -687,13 +696,14 @@ import "github.com/autometrics-dev/autometrics-go/prometheus/autometrics" // This comment is associated with the main function. //autometrics:inst --no-doc --slo "API" --latency-target 99.9 --latency-ms 500 func main() { - defer autometrics.Instrument(autometrics.PreInstrument(autometrics.NewContext( + amCtx := autometrics.PreInstrument(autometrics.NewContext( nil, autometrics.WithConcurrentCalls(true), autometrics.WithCallerName(true), autometrics.WithSloName("API"), autometrics.WithAlertLatency(500000000*time.Nanosecond, 99.9), - )), nil) //autometrics:defer + )) //autometrics:shadow-ctx + defer autometrics.Instrument(amCtx, nil) //autometrics:defer fmt.Println(hello) // line comment 3 } @@ -751,13 +761,14 @@ import ( // //autometrics:inst --slo "API" --latency-target 99.9 --latency-ms 500 func main() { - defer prom.Instrument(prom.PreInstrument(prom.NewContext( + amCtx := prom.PreInstrument(prom.NewContext( nil, prom.WithConcurrentCalls(true), prom.WithCallerName(true), prom.WithSloName("API"), prom.WithAlertLatency(500000000*time.Nanosecond, 99.9), - )), nil) //autometrics:defer + )) //autometrics:shadow-ctx + defer prom.Instrument(amCtx, nil) //autometrics:defer fmt.Println(hello) // line comment 3 } @@ -1388,7 +1399,7 @@ func implementContextCodeGenTest(t *testing.T, contextToSerialize internal.Runti }, } - node, err := buildAutometricsContextNode(&sourceContext) + node, _, err := buildAutometricsContextNode(&sourceContext) if err != nil { t.Fatalf("error building the context node: %s", err) } @@ -1526,13 +1537,14 @@ import _ "github.com/autometrics-dev/autometrics-go/prometheus/autometrics" // //autometrics:inst --no-doc --slo "API" --latency-target 99.9 --latency-ms 500 func main() { - defer Instrument(PreInstrument(NewContext( + amCtx := PreInstrument(NewContext( nil, WithConcurrentCalls(true), WithCallerName(true), WithSloName("API"), WithAlertLatency(500000000*time.Nanosecond, 99.9), - )), nil) //autometrics:defer + )) //autometrics:shadow-ctx + defer Instrument(amCtx, nil) //autometrics:defer fmt.Println(hello) // line comment 3 }