tool/instrument/instrument.go (194 lines of code) (raw):
// Copyright (c) 2024 Alibaba Group Holding Ltd.
//
// Licensed 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 instrument
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/alibaba/opentelemetry-go-auto-instrumentation/tool/config"
"github.com/alibaba/opentelemetry-go-auto-instrumentation/tool/errc"
"github.com/alibaba/opentelemetry-go-auto-instrumentation/tool/resource"
"github.com/alibaba/opentelemetry-go-auto-instrumentation/tool/util"
"github.com/dave/dst"
)
// -----------------------------------------------------------------------------
// Instrument
//
// The instrument package is used to instrument the source code according to the
// predefined rules. It finds the rules that match the project dependencies and
// applies the rules to the dependencies one by one.
type RuleProcessor struct {
// The package name of the target file
packageName string
// The working directory during compilation
workDir string
// The target file to be instrumented
target *dst.File
// The parser for the target file
parser *util.AstParser
// The compiling arguments for the target file
compileArgs []string
// Randomly generated suffix for the rule, used to avoid name collision
rule2Suffix map[*resource.InstFuncRule]string
// The target function to be instrumented
rawFunc *dst.FuncDecl
// Whether the rule is exact match with target functio, or it's a regexp match
exact bool
// The enter hook function, it should be inserted into the target source file
onEnterHookFunc *dst.FuncDecl
// The exit hook function, it should be inserted into the target source file
onExitHookFunc *dst.FuncDecl
// Variable declarations waiting to be inserted into target source file
varDecls []dst.Decl
// Relocated files
relocated map[string]string
// Optimization candidates for the trampoline function
trampolineJumps []*TJump
// The declaration of the call context, it should be replenished later
callCtxDecl *dst.GenDecl
// The methods of the call context
callCtxMethods []*dst.FuncDecl
}
func newRuleProcessor(args []string, pkgName string) *RuleProcessor {
// Read compilation output directory
var outputDir string
for i, v := range args {
if v == "-o" {
outputDir = filepath.Dir(args[i+1])
break
}
}
util.Assert(outputDir != "", "sanity check")
// Create a new rule processor
rp := &RuleProcessor{
packageName: pkgName,
workDir: outputDir,
target: nil,
compileArgs: args,
rule2Suffix: make(map[*resource.InstFuncRule]string),
relocated: make(map[string]string),
}
return rp
}
func (rp *RuleProcessor) addDecl(decl dst.Decl) {
rp.target.Decls = append(rp.target.Decls, decl)
}
func (rp *RuleProcessor) removeDeclWhen(pred func(dst.Decl) bool) dst.Decl {
for i, decl := range rp.target.Decls {
if pred(decl) {
rp.target.Decls = append(rp.target.Decls[:i], rp.target.Decls[i+1:]...)
return decl
}
}
return nil
}
func (rp *RuleProcessor) setRelocated(name, target string) {
rp.relocated[name] = target
}
func (rp *RuleProcessor) tryRelocated(name string) string {
if target, ok := rp.relocated[name]; ok {
return target
}
return name
}
func (rp *RuleProcessor) addCompileArg(newArg string) {
rp.compileArgs = append(rp.compileArgs, newArg)
}
func haveSameSuffix(s1, s2 string) bool {
minLength := len(s1)
if len(s2) < minLength {
minLength = len(s2)
}
for i := 1; i <= minLength; i++ {
if s1[len(s1)-i] != s2[len(s2)-i] {
return false
}
}
return true
}
func (rp *RuleProcessor) replaceCompileArg(newArg string, pred func(string) bool) error {
variant := ""
for i, arg := range rp.compileArgs {
// Use absolute file path of the compile argument to compare with the
// instrumented file(path), which is also an absolute path
arg, err := filepath.Abs(arg)
if err != nil {
return errc.New(errc.ErrAbsPath, err.Error())
}
if pred(arg) {
rp.compileArgs[i] = newArg
// Relocate the replaced file to new target, any rules targeting the
// replaced file should be updated to target the new file as well
rp.setRelocated(arg, newArg)
return nil
}
if haveSameSuffix(arg, newArg) {
variant = arg
}
}
if variant == "" {
variant = fmt.Sprintf("%v", rp.compileArgs)
}
msg := fmt.Sprintf("expect %s, actual %s", newArg, variant)
return errc.New(errc.ErrInstrument, msg)
}
func (rp *RuleProcessor) saveDebugFile(path string) {
escape := func(s string) string {
dirName := strings.ReplaceAll(s, "/", "_")
dirName = strings.ReplaceAll(dirName, ".", "_")
return dirName
}
dest := filepath.Base(path)
util.Assert(rp.packageName != "", "sanity check")
dest = filepath.Join(escape(rp.packageName), dest)
dest = util.GetInstrumentLogPath(dest)
err := os.MkdirAll(filepath.Dir(dest), os.ModePerm)
if err != nil { // error is tolerable here
util.Log("failed to create debug file directory %s: %v", dest, err)
return
}
err = util.CopyFile(path, dest)
if err != nil { // error is tolerable here
util.Log("failed to save debug file %s: %v", dest, err)
}
}
func (rp *RuleProcessor) applyRules(bundle *resource.RuleBundle) (err error) {
// Apply file instrument rules first
err = rp.applyFileRules(bundle)
if err != nil {
err = errc.Adhere(err, "package", bundle.ImportPath)
return err
}
err = rp.applyStructRules(bundle)
if err != nil {
err = errc.Adhere(err, "package", bundle.ImportPath)
return err
}
err = rp.applyFuncRules(bundle)
if err != nil {
err = errc.Adhere(err, "package", bundle.ImportPath)
return err
}
return nil
}
func matchImportPath(importPath string, args []string) bool {
for _, arg := range args {
if arg == importPath {
return true
}
}
return false
}
func compileRemix(bundle *resource.RuleBundle, args []string) error {
rp := newRuleProcessor(args, bundle.PackageName)
err := rp.applyRules(bundle)
if err != nil {
return err
}
// Strip -complete flag as we may insert some hook points that are not ready
// yet, i.e. they dont have function body
for i, arg := range rp.compileArgs {
if arg == "-complete" {
rp.compileArgs = append(rp.compileArgs[:i], rp.compileArgs[i+1:]...)
break
}
}
// Good, run final compilation after instrumentation
err = util.RunCmd(rp.compileArgs...)
return err
}
func Instrument() error {
// Remove the tool itself from the command line arguments
args := os.Args[2:]
// Is compile command?
if util.IsCompileCommand(strings.Join(args, " ")) {
if config.GetConf().Verbose {
util.Log("RunCmd: %v", args)
}
bundles, err := resource.LoadRuleBundles()
if err != nil {
err = errc.Adhere(err, "cmd", fmt.Sprintf("%v", args))
return err
}
for _, bundle := range bundles {
util.Assert(bundle.IsValid(), "sanity check")
// Is compiling the target package?
if matchImportPath(bundle.ImportPath, args) {
util.Log("Apply bundle %v", bundle)
err = compileRemix(bundle, args)
if err != nil {
err = errc.Adhere(err, "cmd", fmt.Sprintf("%v", args))
err = errc.Adhere(err, "bundle", bundle.String())
return err
}
return nil
}
}
}
// Not a compile command, just run it as is
return util.RunCmd(args...)
}