diff --git a/.goreleaser.yml b/.goreleaser.yml index 8710d56..ae128e9 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -7,6 +7,10 @@ before: builds: - main: ./cmd/nrseg + ldflags: + - -s -w + - -X nrseg.Version={{.Version}} + - -X nrseg.Revision={{.ShortCommit}} env: - CGO_ENABLED=0 archives: diff --git a/README.md b/README.md index 5cd35db..99a2db9 100644 --- a/README.md +++ b/README.md @@ -5,5 +5,159 @@ nrseg [![test](https://github.com/budougumi0617/nrseg/workflows/test/badge.svg)](https://github.com/budougumi0617/nrseg/actions?query=workflow%3Atest) [![reviewdog](https://github.com/budougumi0617/nrseg/workflows/reviewdog/badge.svg)](https://github.com/budougumi0617/nrseg/actions?query=workflow%3Areviewdog) +## Background +https://docs.newrelic.com/docs/agents/go-agent/instrumentation/instrument-go-segments +NewRelic is excellent o11y service, but if we use Newrelic in Go app, we need to insert `segment` into every function/method to measure the time taken by functions and code blocks. +For example, we can use with `newrelic.FromContext` and `defer` statement. + +```go +func SampleFunc(ctx context.Context) { + defer newrelic.FromContext(ctx).StartSegment("sample_func").End() + // do anything... +} +``` + +If there is your application in production already, you must add a segment into any function/method. It is a very time-consuming and tedious task. + +## Description +`nrseg` is cli tool for insert segment into all function/method in specified directory. + +Before code is below, +```go +package input + +import ( + "context" + "fmt" + "net/http" +) + +type S struct{} + +func (s *S) SampleMethod(ctx context.Context) { + fmt.Println("Hello, playground") + fmt.Println("end function") +} + +func SampleFunc(ctx context.Context) { + fmt.Println("Hello, playground") + fmt.Println("end function") +} + +func SampleHandler(w http.ResponseWriter, req *http.Request) { + fmt.Fprintf(w, "Hello, %q", req.URL.Path) +} + +// nrseg:ignore you can be ignored if you want to not insert segment. +func IgnoreHandler(w http.ResponseWriter, req *http.Request) { + fmt.Fprintf(w, "Hello, %q", req.URL.Path) +} +``` + +After execute `nrseg`, modified code is below. + +```go +package input + +import ( + "context" + "fmt" + "net/http" + + "github.com/newrelic/go-agent/v3/newrelic" +) + +type S struct{} + +func (s *S) SampleMethod(ctx context.Context) { + defer newrelic.FromContext(ctx).StartSegment("sample_method").End() + fmt.Println("Hello, playground") + fmt.Println("end function") +} + +func SampleFunc(ctx context.Context) { + defer newrelic.FromContext(ctx).StartSegment("sample_func").End() + fmt.Println("Hello, playground") + fmt.Println("end function") +} + +func SampleHandler(w http.ResponseWriter, req *http.Request) { + defer newrelic.FromContext(req.Context()).StartSegment("sample_handler").End() + fmt.Fprintf(w, "Hello, %q", req.URL.Path) +} + +// nrseg:ignore you can be ignored if you want to not insert segment. +func IgnoreHandler(w http.ResponseWriter, req *http.Request) { + fmt.Fprintf(w, "Hello, %q", req.URL.Path) +} +``` + +### Features +- [x] Insert `Function segments` into the function with following arguments. + - The function/method signature has `context.Context`. + - `defer newrelic.FromContext(ctx).StartSegment("func_name").End()` + - The function/method signature has `*http.Request`. + - `defer newrelic.FromContext(req.Context()).StartSegment("func_name").End()` +- [x] Support any variable name of `context.Context`/`*http.Request`. +- [x] Use function/method name to segment name. +- [x] This processing is recursively repeated. +- [x] Able to ignore function/method by `nrseg:ignore` comment. +- [x] Ignore specified directories with cli option `-i`/`-ignore`. +- [ ] Remove all `Function segments`. + +## Synopsis +``` +$ nrseg -i testuitl ./ +``` + +## Options + +``` +$ nrseg -h +Insert function segments into any function/method for Newrelic APM. + +Usage of nrseg: + -i string + ignore directory names. ex: foo,bar,baz + (testdata directory is always ignored.) + -ignore string + ignore directory names. ex: foo,bar,baz + (testdata directory is always ignored.) + -v print version information and quit. + -version + print version information and quit. +exit status 1 + +``` + +## Installation + +``` +$ go install github.com/budougumi0617/nrseg/cmd/nrseg +``` + +Built binaries are available on gihub releases. https://github.com/budougumi0617/nrseg/releases + +### MacOS +If you want to install on MacOS, you can use Homebrew. +``` +brew install budougumi0617/tap/nrseg +``` + +## Contribution +1. Fork ([https://github.com/budougumi0617/nrseg/fork](https://github.com/budougumi0617/nrseg/fork)) +2. Create a feature branch +3. Commit your changes +4. Rebase your local changes against the master branch +5. Run test suite with the `go test ./...` command and confirm that it passes +6. Run `gofmt -s` +7. Create new Pull Request + +## License + +[MIT](https://github.com/budougumi0617/nrseg/blob/master/LICENSE) + +## Author +[budougumi0617](https://github.com/budougumi0617) diff --git a/nrseg.go b/nrseg.go index bb246d2..aa55d89 100644 --- a/nrseg.go +++ b/nrseg.go @@ -19,7 +19,8 @@ var ( ) var ( - version = "dev" + Version = "devel" + Revision = "unset" ) type nrseg struct { @@ -32,6 +33,14 @@ func fill(args []string, outStream, errStream io.Writer) (*nrseg, error) { cn := args[0] flags := flag.NewFlagSet(cn, flag.ContinueOnError) flags.SetOutput(errStream) + flags.Usage = func() { + fmt.Fprintf( + flag.CommandLine.Output(), + "Insert function segments into any function/method for Newrelic APM.\n\nUsage of %s:\n", + os.Args[0], + ) + flags.PrintDefaults() + } var v bool vdesc := "print version information and quit." @@ -39,7 +48,7 @@ func fill(args []string, outStream, errStream io.Writer) (*nrseg, error) { flags.BoolVar(&v, "v", false, vdesc) var ignoreDirs string - idesc := "ignore directory names. ex: foo,bar,baz" + idesc := "ignore directory names. ex: foo,bar,baz\n(testdata directory is always ignored.)" flags.StringVar(&ignoreDirs, "ignore", "", idesc) flags.StringVar(&ignoreDirs, "i", "", idesc) @@ -47,7 +56,7 @@ func fill(args []string, outStream, errStream io.Writer) (*nrseg, error) { return nil, err } if v { - fmt.Fprintf(errStream, "%s version %s\n", cn, version) + fmt.Fprintf(errStream, "%s version %q, revison %q\n", cn, Version, Revision) return nil, ErrShowVersion } @@ -59,7 +68,7 @@ func fill(args []string, outStream, errStream io.Writer) (*nrseg, error) { dir := "./" nargs := flags.Args() if len(nargs) > 1 { - msg := "execution path must be only one or no-set(current dirctory)." + msg := "execution path must be only one or no-set(current directory)." return nil, fmt.Errorf(msg) } if len(nargs) == 1 { @@ -87,7 +96,6 @@ func (n *nrseg) skipDir(p string) bool { func (n *nrseg) run() error { return filepath.Walk(n.in, func(path string, info os.FileInfo, err error) error { - fmt.Fprintf(n.outStream, "walk %q\n", path) if info.IsDir() && n.skipDir(path) { return filepath.SkipDir } @@ -102,32 +110,24 @@ func (n *nrseg) run() error { return nil } - fmt.Fprintf(n.outStream, "start %q\n", path) f, err := os.OpenFile(path, os.O_RDWR, 0664) if err != nil { - fmt.Fprintf(n.errStream, "cannot open %q: %v\n", path, err) return err } defer f.Close() org, err := ioutil.ReadAll(f) if err != nil { - fmt.Fprintf(n.errStream, "cannot read %q: %v\n", path, err) return err } got, err := Process(path, org) if err != nil { - fmt.Fprintf(n.errStream, "process failed %q: %v\n", path, err) return err } - fmt.Fprintf(n.outStream, "got %q\n", got) if !bytes.Equal(org, got) { - fmt.Fprintf(n.outStream, "update!! %q\n", path) if len(n.dist) != 0 && n.in != n.dist { - fmt.Fprintf(n.outStream, "update!! %q\n", n.dist) return n.writeOtherPath(n.in, n.dist, path, got) } if _, err := f.WriteAt(got, 0); err != nil { - fmt.Fprintf(n.errStream, "file update failed %q: %v\n", path, err) return err } }