diff --git a/cmd/autometrics/main.go b/cmd/autometrics/main.go index 1fac70f..2149982 100644 --- a/cmd/autometrics/main.go +++ b/cmd/autometrics/main.go @@ -23,6 +23,7 @@ type args struct { PrometheusUrl string `arg:"--prom_url,env:AM_PROMETHEUS_URL" placeholder:"PROMETHEUS_URL" default:"http://localhost:9090" help:"Base URL of the Prometheus instance to generate links to."` UseOtel bool `arg:"--otel" default:"false" help:"Use OpenTelemetry client library to instrument code instead of default Prometheus."` AllowCustomLatencies bool `arg:"--custom-latency" default:"false" help:"Allow non-default latencies to be used in latency-based SLOs."` + DisableDocGeneration bool `arg:"--no-doc,env:AM_NO_DOCGEN" default:"false" help:"Disable documentation links generation for all instrumented functions. Has the same effect as --no-doc in the //autometrics:inst directive."` } func (args) Version() string { @@ -63,7 +64,12 @@ func main() { implementation = autometrics.OTEL } - ctx, err := internal.NewGeneratorContext(implementation, args.PrometheusUrl, args.AllowCustomLatencies) + ctx, err := internal.NewGeneratorContext( + implementation, + args.PrometheusUrl, + args.AllowCustomLatencies, + args.DisableDocGeneration, + ) if err != nil { log.Fatalf("error initialising autometrics context: %s", err) } diff --git a/internal/autometrics/ctx.go b/internal/autometrics/ctx.go index 79c504d..3ac6e3d 100644 --- a/internal/autometrics/ctx.go +++ b/internal/autometrics/ctx.go @@ -7,12 +7,27 @@ import ( "github.com/autometrics-dev/autometrics-go/pkg/autometrics" ) +// GeneratorContext contains the complete command-line and environment context from the generator invocation. +// +// This context contains all the information necessary to properly process the `autometrics` directives over +// each instrumented function in the file. type GeneratorContext struct { - RuntimeCtx autometrics.Context - FuncCtx GeneratorFunctionContext - Implementation autometrics.Implementation + // RuntimeCtx holds the runtime context to build from in the generated code. + RuntimeCtx autometrics.Context + // FuncCtx holds the function specific information for the detected autometrics directive. + // + // Notably, it contains all the data relative to the parsing of the arguments in the directive. + FuncCtx GeneratorFunctionContext + // Implementation is the metrics library we expect to use in the instrumented code. + Implementation autometrics.Implementation + // DocumentationGenerator is the generator to use to generate comments. DocumentationGenerator AutometricsLinkCommentGenerator - AllowCustomLatencies bool + // Allow the autometrics directive to have latency targets outside the default buckets. + AllowCustomLatencies bool + // Flag to disable/remove the documentation links when calling the generator. + // + // This can be set in the command for the generator or through the environment. + DisableDocGeneration bool } type GeneratorFunctionContext struct { @@ -20,6 +35,7 @@ type GeneratorFunctionContext struct { FunctionName string ModuleName string ImplImportName string + DisableDocGeneration bool } func (c *GeneratorContext) ResetFuncCtx() { @@ -32,10 +48,11 @@ func (c *GeneratorContext) SetCommentIdx(i int) { c.FuncCtx.CommentIndex = i } -func NewGeneratorContext(implementation autometrics.Implementation, prometheusUrl string, allowCustomLatencies bool) (GeneratorContext, error) { +func NewGeneratorContext(implementation autometrics.Implementation, prometheusUrl string, allowCustomLatencies, disableDocGeneration bool) (GeneratorContext, error) { ctx := GeneratorContext{ Implementation: implementation, AllowCustomLatencies: allowCustomLatencies, + DisableDocGeneration: disableDocGeneration, RuntimeCtx: autometrics.NewContext(), FuncCtx: GeneratorFunctionContext{}, } diff --git a/internal/generate/generate.go b/internal/generate/generate.go index 3f548fb..7b2d453 100644 --- a/internal/generate/generate.go +++ b/internal/generate/generate.go @@ -23,13 +23,14 @@ const ( SuccessObjArgument = "--success-target" LatencyMsArgument = "--latency-ms" LatencyObjArgument = "--latency-target" + NoDocArgument = "--no-doc" AmPromPackage = "\"github.com/autometrics-dev/autometrics-go/pkg/autometrics/prometheus\"" AmOtelPackage = "\"github.com/autometrics-dev/autometrics-go/pkg/autometrics/otel\"" ) // TransformFile takes a file path and generates the documentation -// for the `//autometrics:doc` functions. +// for the `//autometrics:inst` functions. // // It also replaces the file in place. func TransformFile(ctx internal.GeneratorContext, path, moduleName string) error { @@ -65,7 +66,7 @@ func TransformFile(ctx internal.GeneratorContext, path, moduleName string) error } // GenerateDocumentationAndInstrumentation takes the raw source code from a file and generates -// the documentation for the `//autometrics:doc` functions. +// the documentation for the `//autometrics:inst` functions. // // It returns the new source code with augmented documentation. func GenerateDocumentationAndInstrumentation(ctx internal.GeneratorContext, sourceCode, moduleName string) (string, error) { @@ -185,8 +186,12 @@ func GenerateDocumentationAndInstrumentation(ctx internal.GeneratorContext, sour listIndex := ctx.FuncCtx.CommentIndex if listIndex >= 0 { // Insert comments - autometricsComment := generateAutometricsComment(ctx) - funcDeclaration.Decorations().Start.Replace(insertComments(docComments, listIndex, autometricsComment)...) + if !ctx.DisableDocGeneration && !ctx.FuncCtx.DisableDocGeneration { + autometricsComment := generateAutometricsComment(ctx) + funcDeclaration.Decorations().Start.Replace(insertComments(docComments, listIndex, autometricsComment)...) + } else { + funcDeclaration.Decorations().Start.Replace(docComments...) + } // defer statement firstStatement := funcDeclaration.Body.List[0] @@ -351,6 +356,9 @@ func buildAutometricsDeferStatement(ctx internal.GeneratorContext, secondVar str func parseAutometricsFnContext(ctx *internal.GeneratorContext, commentGroup []string) error { for i, comment := range commentGroup { if args, found := cutPrefix(comment, "//autometrics:"); found { + if !strings.Contains(comment, "autometrics:doc") && !strings.Contains(comment, "autometrics:inst") { + return fmt.Errorf("invalid directive comment '%s': only '//autometrics:doc' and '//autometrics:inst' are allowed.", comment) + } ctx.FuncCtx.CommentIndex = i ctx.RuntimeCtx = autometrics.NewContext() @@ -475,6 +483,9 @@ func parseAutometricsFnContext(ctx *internal.GeneratorContext, commentGroup []st } // Advance past the "value" tokenIndex = tokenIndex + 1 + case token == NoDocArgument: + ctx.FuncCtx.DisableDocGeneration = true + tokenIndex = tokenIndex + 1 default: // Advance past the "value" tokenIndex = tokenIndex + 1 diff --git a/internal/generate/generate_test.go b/internal/generate/generate_test.go index 5936c29..744715e 100644 --- a/internal/generate/generate_test.go +++ b/internal/generate/generate_test.go @@ -28,7 +28,7 @@ import ( // This comment is associated with the main function. // -//autometrics:doc --slo "Service Test" --success-target 99 +//autometrics:inst --slo "Service Test" --success-target 99 func main() { fmt.Println(hello) // line comment 3 } @@ -69,7 +69,7 @@ func main() { "// [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+version%2C+commit%29+%28rate%28function_calls_count%7Bcaller%3D%22main.main%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\n" + "// [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+version%2C+commit%29+%28rate%28function_calls_count%7Bcaller%3D%22main.main%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.main%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\n" + "//\n" + - "//autometrics:doc --slo \"Service Test\" --success-target 99\n" + + "//autometrics:inst --slo \"Service Test\" --success-target 99\n" + "func main() {\n" + "\tdefer prom.Instrument(prom.PreInstrument(prom.NewContext(\n" + "\t\tprom.WithConcurrentCalls(true),\n" + @@ -81,7 +81,7 @@ func main() { " fmt.Println(hello) // line comment 3\n" + "}\n" - ctx, err := internal.NewGeneratorContext(autometrics.PROMETHEUS, DefaultPrometheusInstanceUrl, false) + ctx, err := internal.NewGeneratorContext(autometrics.PROMETHEUS, DefaultPrometheusInstanceUrl, false, false) if err != nil { t.Fatalf("error creating the generation context: %s", err) } @@ -114,7 +114,7 @@ import ( // // autometrics:doc-end DO NOT EDIT // -//autometrics:doc --slo "API" --latency-target 99.9 --latency-ms 500 +//autometrics:inst --slo "API" --latency-target 99.9 --latency-ms 500 func main() { fmt.Println(hello) // line comment 3 } @@ -155,7 +155,7 @@ func main() { "// [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+version%2C+commit%29+%28rate%28function_calls_count%7Bcaller%3D%22main.main%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\n" + "// [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+version%2C+commit%29+%28rate%28function_calls_count%7Bcaller%3D%22main.main%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.main%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\n" + "//\n" + - "//autometrics:doc --slo \"API\" --latency-target 99.9 --latency-ms 500\n" + + "//autometrics:inst --slo \"API\" --latency-target 99.9 --latency-ms 500\n" + "func main() {\n" + "\tdefer prom.Instrument(prom.PreInstrument(prom.NewContext(\n" + "\t\tprom.WithConcurrentCalls(true),\n" + @@ -167,7 +167,131 @@ func main() { " fmt.Println(hello) // line comment 3\n" + "}\n" - ctx, err := internal.NewGeneratorContext(autometrics.PROMETHEUS, DefaultPrometheusInstanceUrl, false) + ctx, err := internal.NewGeneratorContext(autometrics.PROMETHEUS, DefaultPrometheusInstanceUrl, false, false) + if err != nil { + t.Fatalf("error creating the generation context: %s", err) + } + + actual, err := GenerateDocumentationAndInstrumentation(ctx, sourceCode, "main") + if err != nil { + t.Fatalf("error generating the documentation: %s", err) + } + + assert.Equal(t, want, actual, "The generated source code is not as expected.") +} + +// TestCommentDelete calls GenerateDocumentationAndInstrumentation on a +// decorated function that already has a comment, making sure that the autometrics +// directive deletes the comment section about autometrics, from the `--no-doc` argument +// on the directive +func TestCommentDelete(t *testing.T) { + sourceCode := `// This is the package comment. +package main + +import ( + "github.com/autometrics-dev/autometrics-go/pkg/autometrics" + prom "github.com/autometrics-dev/autometrics-go/pkg/autometrics/prometheus" +) + +// This comment is associated with the main function. +// +// autometrics:doc-start +// +// Obviously not a good comment +// +// autometrics:doc-end DO NOT EDIT +// +//autometrics:inst --no-doc --slo "API" --latency-target 99.9 --latency-ms 500 +func main() { + fmt.Println(hello) // line comment 3 +} +` + + want := "// This is the package comment.\n" + + "package main\n" + + "\n" + + "import (\n" + + "\t\"github.com/autometrics-dev/autometrics-go/pkg/autometrics\"\n" + + "\tprom \"github.com/autometrics-dev/autometrics-go/pkg/autometrics/prometheus\"\n" + + ")\n" + + "\n" + + "// This comment is associated with the main function.\n" + + "//\n" + + "//autometrics:inst --no-doc --slo \"API\" --latency-target 99.9 --latency-ms 500\n" + + "func main() {\n" + + "\tdefer prom.Instrument(prom.PreInstrument(prom.NewContext(\n" + + "\t\tprom.WithConcurrentCalls(true),\n" + + "\t\tprom.WithCallerName(true),\n" + + "\t\tprom.WithSloName(\"API\"),\n" + + "\t\tprom.WithAlertLatency(500000000*time.Nanosecond, 99.9),\n" + + "\t)), nil) //autometrics:defer\n" + + "\n" + + " fmt.Println(hello) // line comment 3\n" + + "}\n" + + ctx, err := internal.NewGeneratorContext(autometrics.PROMETHEUS, DefaultPrometheusInstanceUrl, false, false) + if err != nil { + t.Fatalf("error creating the generation context: %s", err) + } + + actual, err := GenerateDocumentationAndInstrumentation(ctx, sourceCode, "main") + if err != nil { + t.Fatalf("error generating the documentation: %s", err) + } + + assert.Equal(t, want, actual, "The generated source code is not as expected.") +} + +// TestCommentDeleteGlobalFlag calls GenerateDocumentationAndInstrumentation on a +// decorated function that already has a comment, making sure that the autometrics +// directive deletes the comment section about autometrics, from the global flag, +// emulated in the GeneratorContext constructor +func TestCommentDeleteGlobalFlag(t *testing.T) { + sourceCode := `// This is the package comment. +package main + +import ( + "github.com/autometrics-dev/autometrics-go/pkg/autometrics" + prom "github.com/autometrics-dev/autometrics-go/pkg/autometrics/prometheus" +) + +// This comment is associated with the main function. +// +// autometrics:doc-start +// +// Obviously not a good comment +// +// autometrics:doc-end DO NOT EDIT +// +//autometrics:inst --slo "API" --latency-target 99.9 --latency-ms 500 +func main() { + fmt.Println(hello) // line comment 3 +} +` + + want := "// This is the package comment.\n" + + "package main\n" + + "\n" + + "import (\n" + + "\t\"github.com/autometrics-dev/autometrics-go/pkg/autometrics\"\n" + + "\tprom \"github.com/autometrics-dev/autometrics-go/pkg/autometrics/prometheus\"\n" + + ")\n" + + "\n" + + "// This comment is associated with the main function.\n" + + "//\n" + + "//autometrics:inst --slo \"API\" --latency-target 99.9 --latency-ms 500\n" + + "func main() {\n" + + "\tdefer prom.Instrument(prom.PreInstrument(prom.NewContext(\n" + + "\t\tprom.WithConcurrentCalls(true),\n" + + "\t\tprom.WithCallerName(true),\n" + + "\t\tprom.WithSloName(\"API\"),\n" + + "\t\tprom.WithAlertLatency(500000000*time.Nanosecond, 99.9),\n" + + "\t)), nil) //autometrics:defer\n" + + "\n" + + " fmt.Println(hello) // line comment 3\n" + + "}\n" + + ctx, err := internal.NewGeneratorContext(autometrics.PROMETHEUS, DefaultPrometheusInstanceUrl, false, true) if err != nil { t.Fatalf("error creating the generation context: %s", err) } @@ -191,12 +315,12 @@ import ( // This comment is associated with the main function. // -//autometrics:doc --slo "Service Test" --success-target 12394 +//autometrics:inst --slo "Service Test" --success-target 12394 func main() { fmt.Println(hello) // line comment 3 } ` - ctx, err := internal.NewGeneratorContext(autometrics.PROMETHEUS, DefaultPrometheusInstanceUrl, false) + ctx, err := internal.NewGeneratorContext(autometrics.PROMETHEUS, DefaultPrometheusInstanceUrl, false, false) if err != nil { t.Fatalf("error creating the generation context: %s", err) } @@ -214,12 +338,12 @@ import ( // This comment is associated with the main function. // -//autometrics:doc --slo "Service Test" --success-target -49 +//autometrics:inst --slo "Service Test" --success-target -49 func main() { fmt.Println(hello) // line comment 3 } ` - ctx, err = internal.NewGeneratorContext(autometrics.PROMETHEUS, DefaultPrometheusInstanceUrl, false) + ctx, err = internal.NewGeneratorContext(autometrics.PROMETHEUS, DefaultPrometheusInstanceUrl, false, false) if err != nil { t.Fatalf("error creating the generation context: %s", err) } @@ -237,12 +361,12 @@ import ( // This comment is associated with the main function. // -//autometrics:doc --success-target 90 +//autometrics:inst --success-target 90 func main() { fmt.Println(hello) // line comment 3 } ` - ctx, err = internal.NewGeneratorContext(autometrics.PROMETHEUS, DefaultPrometheusInstanceUrl, false) + ctx, err = internal.NewGeneratorContext(autometrics.PROMETHEUS, DefaultPrometheusInstanceUrl, false, false) if err != nil { t.Fatalf("error creating the generation context: %s", err) } @@ -260,12 +384,12 @@ import ( // This comment is associated with the main function. // -//autometrics:doc --latency-ms 5000 --latency-target 99.9 +//autometrics:inst --latency-ms 5000 --latency-target 99.9 func main() { fmt.Println(hello) // line comment 3 } ` - ctx, err = internal.NewGeneratorContext(autometrics.PROMETHEUS, DefaultPrometheusInstanceUrl, false) + ctx, err = internal.NewGeneratorContext(autometrics.PROMETHEUS, DefaultPrometheusInstanceUrl, false, false) if err != nil { t.Fatalf("error creating the generation context: %s", err) } @@ -283,12 +407,12 @@ import ( // This comment is associated with the main function. // -//autometrics:doc --slo "API" --latency-target 90 +//autometrics:inst --slo "API" --latency-target 90 func main() { fmt.Println(hello) // line comment 3 } ` - ctx, err = internal.NewGeneratorContext(autometrics.PROMETHEUS, DefaultPrometheusInstanceUrl, false) + ctx, err = internal.NewGeneratorContext(autometrics.PROMETHEUS, DefaultPrometheusInstanceUrl, false, false) if err != nil { t.Fatalf("error creating the generation context: %s", err) } @@ -306,12 +430,12 @@ import ( // This comment is associated with the main function. // -//autometrics:doc --slo "API" --latency-ms 1000 +//autometrics:inst --slo "API" --latency-ms 1000 func main() { fmt.Println(hello) // line comment 3 } ` - ctx, err = internal.NewGeneratorContext(autometrics.PROMETHEUS, DefaultPrometheusInstanceUrl, false) + ctx, err = internal.NewGeneratorContext(autometrics.PROMETHEUS, DefaultPrometheusInstanceUrl, false, false) if err != nil { t.Fatalf("error creating the generation context: %s", err) } @@ -329,12 +453,12 @@ import ( // This comment is associated with the main function. // -//autometrics:doc --slo "API" --latency-ms -5000 --latency-target 99.9 +//autometrics:inst --slo "API" --latency-ms -5000 --latency-target 99.9 func main() { fmt.Println(hello) // line comment 3 } ` - ctx, err = internal.NewGeneratorContext(autometrics.PROMETHEUS, DefaultPrometheusInstanceUrl, false) + ctx, err = internal.NewGeneratorContext(autometrics.PROMETHEUS, DefaultPrometheusInstanceUrl, false, false) if err != nil { t.Fatalf("error creating the generation context: %s", err) } @@ -352,12 +476,12 @@ import ( // This comment is associated with the main function. // -//autometrics:doc --slo "API" --latency-ms 5000 --latency-target 49999 +//autometrics:inst --slo "API" --latency-ms 5000 --latency-target 49999 func main() { fmt.Println(hello) // line comment 3 } ` - ctx, err = internal.NewGeneratorContext(autometrics.PROMETHEUS, DefaultPrometheusInstanceUrl, false) + ctx, err = internal.NewGeneratorContext(autometrics.PROMETHEUS, DefaultPrometheusInstanceUrl, false, false) if err != nil { t.Fatalf("error creating the generation context: %s", err) } @@ -375,12 +499,12 @@ import ( // This comment is associated with the main function. // -//autometrics:doc --slo "API" --latency-ms 5000 --latency-target -123 +//autometrics:inst --slo "API" --latency-ms 5000 --latency-target -123 func main() { fmt.Println(hello) // line comment 3 } ` - ctx, err = internal.NewGeneratorContext(autometrics.PROMETHEUS, DefaultPrometheusInstanceUrl, false) + ctx, err = internal.NewGeneratorContext(autometrics.PROMETHEUS, DefaultPrometheusInstanceUrl, false, false) if err != nil { t.Fatalf("error creating the generation context: %s", err) }