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

Reply via email to