This is an automated email from the ASF dual-hosted git repository.
liuhan pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/skywalking-go.git
The following commit(s) were added to refs/heads/main by this push:
new f14250f Support inject the skywalking-go into project through agent
(#58)
f14250f is described below
commit f14250f3b6da338098d396e2c29935d7f7708a22
Author: mrproliu <[email protected]>
AuthorDate: Fri Jun 9 06:42:43 2023 +0000
Support inject the skywalking-go into project through agent (#58)
---
CHANGES.md | 1 +
docs/en/setup/gobuild.md | 39 ++-
tools/go-agent/cmd/helper.go | 34 ++-
tools/go-agent/cmd/injector.go | 327 +++++++++++++++++++++
tools/go-agent/cmd/{helper.go => injector_test.go} | 33 +--
tools/go-agent/cmd/main.go | 11 +
.../cmd/{helper.go => testdata/entry/test.go} | 24 --
.../cmd/{helper.go => testdata/imports/test.go} | 26 +-
.../cmd/{helper.go => testdata/noimports/test.go} | 26 +-
9 files changed, 421 insertions(+), 100 deletions(-)
diff --git a/CHANGES.md b/CHANGES.md
index ea7b236..d95cbac 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -6,6 +6,7 @@ Release Notes.
------------------
#### Features
* Enhance the plugin rewrite ability to support `switch` and `if/else` in the
plugin codes.
+* Support inject the skywalking-go into project through agent.
#### Plugins
* Support [go-redis](https://github.com/redis/go-redis) v9 redis client
framework.
diff --git a/docs/en/setup/gobuild.md b/docs/en/setup/gobuild.md
index 1145705..53d3e96 100644
--- a/docs/en/setup/gobuild.md
+++ b/docs/en/setup/gobuild.md
@@ -2,7 +2,29 @@
When you want to integrate the Agent using the original go build command, you
need to follow these steps.
-## Install SkyWalking Go
+## 1. Download Agent
+
+Download the Agent from the [official
website](https://skywalking.apache.org/downloads/#GoAgent).
+
+## 2. Install SkyWalking Go
+
+SkyWalking Go offers two ways for integration into your project.
+
+### 2.1 Agent Injector
+
+Agent injector is recommended when you only want to include SkyWalking Go
agent in the compiling pipeline or shell.
+
+Please execute the following command, which would automatically import
SkyWalking Go into your project.
+
+```shell
+/path/to/agent -inject /path/to/your/project [-all]
+```
+
+* `/path/to/agent` is the path to the agent which your downloaded.
+* `/path/to/your/project` is the home path to your project.
+* `-all` is the parameter for injecting all submodules in your project.
+
+### 2.2 Code Dependency
Use `go get` to import the `skywalking-go` program.
@@ -16,14 +38,12 @@ Also, import the module to your `main` package:
import _ "github.com/apache/skywalking-go"
```
-## Download Agent
-
-Download the Agent from the [official
website](https://skywalking.apache.org/downloads/#GoAgent).
-
-**NOTICE**: Please ensure that the version of the Agent you downloaded is
consistent with the version installed via `go get` in the previous section,
+**NOTICE**: Please ensure that the version of the Agent you downloaded is
consistent with the version installed via `go get` in the previous section,
to prevent errors such as missing package references during compilation.
-Next, add the following parameters in `go build`:
+## 3. Build with SkyWalking Go Agent
+
+Add the following parameters in `go build`:
```shell
-toolexec="/path/to/go-agent" -a
@@ -37,4 +57,7 @@ If you want to customize the configuration information for
the current service,
```shell
-toolexec="/path/to/go-agent -config /path/to/config.yaml" -a
-```
\ No newline at end of file
+```
+
+# Binary Output
+The binary would be weaved and instrumented by SkyWalking Go.
diff --git a/tools/go-agent/cmd/helper.go b/tools/go-agent/cmd/helper.go
index f3cccff..045e38e 100644
--- a/tools/go-agent/cmd/helper.go
+++ b/tools/go-agent/cmd/helper.go
@@ -22,21 +22,45 @@ import (
"os"
)
+var version string
+
type EnhancementToolFlags struct {
- Help bool `swflag:"-h"`
- Debug string `swflag:"-debug"`
- Config string `swflag:"-config"`
+ Help bool `swflag:"-h"`
+ Debug string `swflag:"-debug"`
+ Config string `swflag:"-config"`
+ Inject string `swflag:"-inject"`
+ AllProjects bool `swflag:"-all"`
+ Version bool `swflag:"-version"`
}
func PrintUsageWithExit() {
fmt.Printf(`Usage: go {build,install} -a [-work] -toolexec "%s"
PACKAGE...
-The Go-agent-enhance tool is designed for automatic enhancement of Golang
programs,
-providing the capability for Distribute Tracing.
+The Go-agent-enhance tool is designed for automatic enhancement of Golang
programs, or inject the agent code into the project.
Options:
-h
Print the usage message.
+ -inject
+ Inject the agent code into the project, the
value is the path of the project or single file.
+ -all
+ Inject the agent code into all the project in
the current directory.
+ -debug
+ Hepling to debug the enhance process, the value
is the path of the debug file.
+ -config
+ The file path of the agent config file.
+ -version
+ Print current agent version.
`, os.Args[0])
os.Exit(1)
}
+
+func PrintVersion() {
+ res := version
+ if res == "" {
+ res = "unknown"
+ } else {
+ res = fmt.Sprintf("v%s", res)
+ }
+ fmt.Printf("skywalking-go agent version: %s\n", res)
+}
diff --git a/tools/go-agent/cmd/injector.go b/tools/go-agent/cmd/injector.go
new file mode 100644
index 0000000..15525f2
--- /dev/null
+++ b/tools/go-agent/cmd/injector.go
@@ -0,0 +1,327 @@
+// Licensed to Apache Software Foundation (ASF) under one or more contributor
+// license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright
+// ownership. Apache Software Foundation (ASF) licenses this file to you under
+// the Apache License, Version 2.0 (the "License"); you may
+// not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package main
+
+import (
+ "fmt"
+ "go/parser"
+ "go/token"
+ "io/fs"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strings"
+
+ "github.com/apache/skywalking-go/tools/go-agent/tools"
+
+ "github.com/dave/dst"
+ "github.com/dave/dst/decorator"
+)
+
+var projectBaseImportPath = "github.com/apache/skywalking-go"
+var goModFileName = "go.mod"
+
+var swImportFileName = "skywalking_inject.go"
+var swImportFileContent = fmt.Sprintf(`// Code generated by
skywalking-go-agent. DO NOT EDIT.
+
+package main
+
+import _ "%s"`, projectBaseImportPath)
+
+type projectInjector struct {
+}
+
+func InjectProject(flags *EnhancementToolFlags) error {
+ stat, err := os.Stat(flags.Inject)
+ if err != nil {
+ return err
+ }
+ if version == "" {
+ return fmt.Errorf("version is empty, please use the release
version of skywalking-go")
+ }
+ injector := &projectInjector{}
+ if stat.IsDir() {
+ return injector.injectDir(flags.Inject, flags.AllProjects)
+ }
+ return injector.injectFile(flags.Inject)
+}
+
+func (i *projectInjector) injectDir(path string, allProjects bool) error {
+ if !i.findGoModFileInDir(path) {
+ return fmt.Errorf("cannot fing go.mod file in %s, plase make
sure that your inject path is a project directory", path)
+ }
+ // find all projects and main directory
+ projects, err := i.findProjects(path, allProjects)
+ if err != nil {
+ return err
+ }
+ // filter validated projects
+ validatedProjects := make([]*projectWithMainDirectory, 0)
+ for _, project := range projects {
+ if project.isValid() {
+ validatedProjects = append(validatedProjects, project)
+ }
+ }
+ fmt.Printf("total %d validate projects found\n", len(validatedProjects))
+ // inject library
+ for _, project := range validatedProjects {
+ if err := i.injectLibraryInRoot(project.ProjectPath); err !=
nil {
+ return err
+ }
+ for _, mainDir := range project.MainPackageDirs {
+ contains, err := i.alreadyContainsLibraryImport(mainDir)
+ if err != nil {
+ return err
+ }
+ if contains {
+ fmt.Printf("main package %s already contains
imports, skip\n", mainDir)
+ continue
+ }
+
+ // append a new file to the main package
+ if err := i.appendNewImportFile(mainDir); err != nil {
+ return fmt.Errorf("append new import file
failed in %s, %v", mainDir, err)
+ }
+ fmt.Printf("append new import file success in %s\n",
mainDir)
+ }
+ }
+ return nil
+}
+
+func (i *projectInjector) injectFile(path string) error {
+ if !strings.HasSuffix(path, ".go") {
+ return fmt.Errorf("only support inject go file, %s is not a go
file", path)
+ }
+ dir := filepath.Dir(path)
+ if !i.findGoModFileInDir(dir) {
+ return fmt.Errorf("cannot fing go.mod file in %s", dir)
+ }
+ // inject library
+ if err := i.injectLibraryInRoot(dir); err != nil {
+ return err
+ }
+ // if only inject to a file, then just add import into the file
+ return i.injectImportInFile(path)
+}
+
+func (i *projectInjector) findGoModFileInDir(dir string) bool {
+ path := filepath.Join(dir, goModFileName)
+ stat, err := os.Stat(path)
+ if err != nil {
+ return false
+ }
+ return stat != nil
+}
+
+func (i *projectInjector) injectLibraryInRoot(dir string) error {
+ fmt.Printf("injecting skywalking-go@v%s depenedency into %s\n",
version, dir)
+ command := exec.Command("go", "get",
"github.com/apache/skywalking-go@v"+version)
+ command.Dir = dir
+ command.Stdin = os.Stdin
+ command.Stdout = os.Stdout
+ command.Stderr = os.Stderr
+
+ err := command.Run()
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+func (i *projectInjector) injectImportInFile(path string) error {
+ filename := filepath.Base(path)
+ content, err := os.ReadFile(path)
+ if err != nil {
+ return err
+ }
+ f, err := decorator.ParseFile(nil, filename, content,
parser.ParseComments)
+ if err != nil {
+ return fmt.Errorf("parse file %s failed, %v", path, err)
+ }
+ if i.addingProjectImportInFileAndRewrite(f) {
+ fmt.Printf("already existing library import in %s, skip\n",
path)
+ }
+ fileContent, err := tools.GenerateDSTFileContent(f, nil)
+ if err != nil {
+ return fmt.Errorf("generate file content failed, %v", err)
+ }
+ err = os.WriteFile(path, []byte(fileContent), 0o600)
+ if err != nil {
+ return fmt.Errorf("rewrite the file %s failed, %v", path, err)
+ }
+ fmt.Printf("adding skywalking-go import into the file: %s", path)
+ return nil
+}
+
+func (i *projectInjector) addingProjectImportInFileAndRewrite(f *dst.File)
bool {
+ var latestImportDel *dst.GenDecl
+ var existingImport bool
+ for _, decl := range f.Decls {
+ if gen, ok := decl.(*dst.GenDecl); ok && gen != nil && gen.Tok
== token.IMPORT {
+ latestImportDel = gen
+ if !existingImport && i.containsImport(gen) {
+ existingImport = true
+ }
+ }
+ }
+ if existingImport {
+ return true
+ }
+ if latestImportDel == nil {
+ latestImportDel = &dst.GenDecl{
+ Tok: token.IMPORT,
+ Specs: []dst.Spec{},
+ }
+ f.Decls = append([]dst.Decl{latestImportDel}, f.Decls...)
+ }
+ latestImportDel.Specs = append(latestImportDel.Specs, &dst.ImportSpec{
+ Name: dst.NewIdent("_"),
+ Path: &dst.BasicLit{
+ Kind: token.STRING,
+ Value: fmt.Sprintf("%q", projectBaseImportPath),
+ },
+ })
+ return false
+}
+
+func (i *projectInjector) findProjects(currentDir string, all bool)
([]*projectWithMainDirectory, error) {
+ result := make([]*projectWithMainDirectory, 0)
+ stack := make([]*projectWithMainDirectory, 0)
+ currentStackPrefix := ""
+ err := filepath.WalkDir(currentDir, func(path string, d fs.DirEntry,
err error) error {
+ if !d.IsDir() {
+ return nil
+ }
+ if strings.HasPrefix(filepath.Base(path), ".") {
+ return filepath.SkipDir
+ }
+ if currentStackPrefix != "" && !strings.HasPrefix(path,
currentStackPrefix) {
+ stack = stack[:len(stack)-1]
+ currentStackPrefix = stack[len(stack)-1].ProjectPath
+ }
+ if f, e := os.Stat(filepath.Join(path, goModFileName)); e ==
nil && f != nil {
+ if len(stack) > 0 && !all {
+ return filepath.SkipDir
+ }
+ info := &projectWithMainDirectory{
+ ProjectPath: path,
+ }
+ result = append(result, info)
+ stack = append(stack, info)
+ currentStackPrefix = path
+ }
+ if mainPackage, e :=
i.containsMainPackageInCurrentDirectory(path); e != nil {
+ return err
+ } else if mainPackage {
+ currentModule := stack[len(stack)-1]
+ currentModule.MainPackageDirs =
append(currentModule.MainPackageDirs, path)
+ }
+
+ return nil
+ })
+ if err != nil {
+ return nil, err
+ }
+ return result, nil
+}
+
+func (i *projectInjector) containsMainPackageInCurrentDirectory(dir string)
(bool, error) {
+ readDir, err := os.ReadDir(dir)
+ if err != nil {
+ return false, fmt.Errorf("read dir %s failed, %v", dir, err)
+ }
+ for _, file := range readDir {
+ if file.IsDir() || !strings.HasSuffix(file.Name(), ".go") {
+ continue
+ }
+
+ parseFile, err := parser.ParseFile(token.NewFileSet(),
filepath.Join(dir, file.Name()), nil, parser.PackageClauseOnly)
+ if err != nil {
+ return false, err
+ }
+ if parseFile.Name.Name == "main" {
+ return true, nil
+ }
+
+ // only needs to check the first .go file, other files should
be same
+ return false, nil
+ }
+ return false, nil
+}
+
+func (i *projectInjector) alreadyContainsLibraryImport(dir string) (bool,
error) {
+ readDir, err := os.ReadDir(dir)
+ if err != nil {
+ return false, fmt.Errorf("reding directory %s failure, %v",
dir, err)
+ }
+ for _, f := range readDir {
+ if f.IsDir() {
+ continue
+ }
+ if !strings.HasSuffix(f.Name(), ".go") {
+ continue
+ }
+ file, err := os.ReadFile(filepath.Join(dir, f.Name()))
+ if err != nil {
+ return false, fmt.Errorf("read file %s failed, %v",
f.Name(), err)
+ }
+
+ dstFile, err := decorator.ParseFile(nil, f.Name(), file,
parser.ImportsOnly)
+ if err != nil {
+ return false, fmt.Errorf("parsing file %s failed, %v",
f.Name(), err)
+ }
+
+ var existingImport = false
+ for _, decl := range dstFile.Decls {
+ if gen, ok := decl.(*dst.GenDecl); ok && gen != nil &&
gen.Tok == token.IMPORT &&
+ !existingImport && i.containsImport(gen) {
+ existingImport = true
+ }
+ }
+ if existingImport {
+ return true, nil
+ }
+ }
+
+ return false, nil
+}
+
+func (i *projectInjector) containsImport(imp *dst.GenDecl) bool {
+ for _, spec := range imp.Specs {
+ if i, ok := spec.(*dst.ImportSpec); !ok || i == nil {
+ continue
+ } else if i.Path != nil && i.Path.Value == fmt.Sprintf("%q",
projectBaseImportPath) {
+ return true
+ }
+ }
+ return false
+}
+
+func (i *projectInjector) appendNewImportFile(dir string) error {
+ importFilePath := filepath.Join(dir, swImportFileName)
+ return os.WriteFile(importFilePath, []byte(swImportFileContent), 0o600)
+}
+
+type projectWithMainDirectory struct {
+ ProjectPath string
+ MainPackageDirs []string
+}
+
+func (p *projectWithMainDirectory) isValid() bool {
+ return p.ProjectPath != "" && len(p.MainPackageDirs) > 0
+}
diff --git a/tools/go-agent/cmd/helper.go b/tools/go-agent/cmd/injector_test.go
similarity index 53%
copy from tools/go-agent/cmd/helper.go
copy to tools/go-agent/cmd/injector_test.go
index f3cccff..e83f066 100644
--- a/tools/go-agent/cmd/helper.go
+++ b/tools/go-agent/cmd/injector_test.go
@@ -18,25 +18,24 @@
package main
import (
- "fmt"
- "os"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
)
-type EnhancementToolFlags struct {
- Help bool `swflag:"-h"`
- Debug string `swflag:"-debug"`
- Config string `swflag:"-config"`
+func TestContainsLibraryImport(t *testing.T) {
+ injector := &projectInjector{}
+ libraryImport, err :=
injector.alreadyContainsLibraryImport("./testdata/noimports")
+ assert.Nil(t, err, "should not return an error")
+ assert.False(t, libraryImport, "should not contain library import")
+ libraryImport, err =
injector.alreadyContainsLibraryImport("./testdata/imports")
+ assert.Nil(t, err, "should not return an error")
+ assert.True(t, libraryImport, "should contain library import")
}
-func PrintUsageWithExit() {
- fmt.Printf(`Usage: go {build,install} -a [-work] -toolexec "%s"
PACKAGE...
-
-The Go-agent-enhance tool is designed for automatic enhancement of Golang
programs,
-providing the capability for Distribute Tracing.
-
-Options:
- -h
- Print the usage message.
-`, os.Args[0])
- os.Exit(1)
+func TestContainsMainPackage(t *testing.T) {
+ injector := &projectInjector{}
+ mainPackage, err :=
injector.containsMainPackageInCurrentDirectory("./testdata/entry")
+ assert.Nil(t, err, "should not return an error")
+ assert.True(t, mainPackage, "should contain main package")
}
diff --git a/tools/go-agent/cmd/main.go b/tools/go-agent/cmd/main.go
index 467e263..46f319e 100644
--- a/tools/go-agent/cmd/main.go
+++ b/tools/go-agent/cmd/main.go
@@ -39,6 +39,17 @@ func main() {
if firstNonOptionIndex, err = tools.ParseFlags(toolFlags, args); err !=
nil || toolFlags.Help {
PrintUsageWithExit()
}
+ if toolFlags.Inject != "" {
+ if err1 := InjectProject(toolFlags); err1 != nil {
+ log.Fatal(err1)
+ }
+ return
+ } else if toolFlags.Version {
+ PrintVersion()
+ return
+ } else if firstNonOptionIndex < 0 {
+ PrintUsageWithExit()
+ }
if toolFlags.Debug != "" {
stat, err1 := os.Stat(toolFlags.Debug)
diff --git a/tools/go-agent/cmd/helper.go
b/tools/go-agent/cmd/testdata/entry/test.go
similarity index 63%
copy from tools/go-agent/cmd/helper.go
copy to tools/go-agent/cmd/testdata/entry/test.go
index f3cccff..2881d22 100644
--- a/tools/go-agent/cmd/helper.go
+++ b/tools/go-agent/cmd/testdata/entry/test.go
@@ -16,27 +16,3 @@
// under the License.
package main
-
-import (
- "fmt"
- "os"
-)
-
-type EnhancementToolFlags struct {
- Help bool `swflag:"-h"`
- Debug string `swflag:"-debug"`
- Config string `swflag:"-config"`
-}
-
-func PrintUsageWithExit() {
- fmt.Printf(`Usage: go {build,install} -a [-work] -toolexec "%s"
PACKAGE...
-
-The Go-agent-enhance tool is designed for automatic enhancement of Golang
programs,
-providing the capability for Distribute Tracing.
-
-Options:
- -h
- Print the usage message.
-`, os.Args[0])
- os.Exit(1)
-}
diff --git a/tools/go-agent/cmd/helper.go
b/tools/go-agent/cmd/testdata/imports/test.go
similarity index 63%
copy from tools/go-agent/cmd/helper.go
copy to tools/go-agent/cmd/testdata/imports/test.go
index f3cccff..642971c 100644
--- a/tools/go-agent/cmd/helper.go
+++ b/tools/go-agent/cmd/testdata/imports/test.go
@@ -15,28 +15,12 @@
// specific language governing permissions and limitations
// under the License.
-package main
+package imports
import (
- "fmt"
- "os"
+ _ "fmt"
)
-type EnhancementToolFlags struct {
- Help bool `swflag:"-h"`
- Debug string `swflag:"-debug"`
- Config string `swflag:"-config"`
-}
-
-func PrintUsageWithExit() {
- fmt.Printf(`Usage: go {build,install} -a [-work] -toolexec "%s"
PACKAGE...
-
-The Go-agent-enhance tool is designed for automatic enhancement of Golang
programs,
-providing the capability for Distribute Tracing.
-
-Options:
- -h
- Print the usage message.
-`, os.Args[0])
- os.Exit(1)
-}
+import (
+ _ "github.com/apache/skywalking-go"
+)
diff --git a/tools/go-agent/cmd/helper.go
b/tools/go-agent/cmd/testdata/noimports/test.go
similarity index 62%
copy from tools/go-agent/cmd/helper.go
copy to tools/go-agent/cmd/testdata/noimports/test.go
index f3cccff..42b2460 100644
--- a/tools/go-agent/cmd/helper.go
+++ b/tools/go-agent/cmd/testdata/noimports/test.go
@@ -15,28 +15,4 @@
// specific language governing permissions and limitations
// under the License.
-package main
-
-import (
- "fmt"
- "os"
-)
-
-type EnhancementToolFlags struct {
- Help bool `swflag:"-h"`
- Debug string `swflag:"-debug"`
- Config string `swflag:"-config"`
-}
-
-func PrintUsageWithExit() {
- fmt.Printf(`Usage: go {build,install} -a [-work] -toolexec "%s"
PACKAGE...
-
-The Go-agent-enhance tool is designed for automatic enhancement of Golang
programs,
-providing the capability for Distribute Tracing.
-
-Options:
- -h
- Print the usage message.
-`, os.Args[0])
- os.Exit(1)
-}
+package noimports