Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions test/apps/grpcold/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
module github.com/open-telemetry/opentelemetry-go-compile-instrumentation/test/apps/grpcold

go 1.25.0

require google.golang.org/grpc v1.62.0

require (
github.com/golang/protobuf v1.5.3 // indirect
golang.org/x/net v0.20.0 // indirect
golang.org/x/sys v0.16.0 // indirect
golang.org/x/text v0.14.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80 // indirect
google.golang.org/protobuf v1.32.0 // indirect
)
21 changes: 21 additions & 0 deletions test/apps/grpcold/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80 h1:AjyfHzEPEFp/NpvfN5g+KDla3EMojjhRVZc1i7cj+oM=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80/go.mod h1:PAREbraiVEVGVdTZsVWjSbbTtSyGbAgIIvni8a8CD5s=
google.golang.org/grpc v1.62.0 h1:HQKZ/fa1bXkX1oFOvSjmZEUL8wLSaZTjCcLAlmZRtdk=
google.golang.org/grpc v1.62.0/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
16 changes: 16 additions & 0 deletions test/apps/grpcold/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

// A minimal gRPC client application used to verify that instrumentation
// remains compatible with older gRPC versions during build/setup.
package main

import (
"context"

"google.golang.org/grpc"
)

func main() {
_, _ = grpc.DialContext(context.TODO(), "")
}
24 changes: 24 additions & 0 deletions test/integration/grpc_old_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

//go:build integration

package test

import (
"os"
"path/filepath"
"testing"

"github.com/stretchr/testify/require"

"github.com/open-telemetry/opentelemetry-go-compile-instrumentation/test/testutil"
)

func TestGRPCOld(t *testing.T) {
pwd, err := os.Getwd()
require.NoError(t, err)

// Verifies that we can build a module with old gRPC version
testutil.Build(t, filepath.Join(pwd, "../apps/grpcold"), "go", "build", "-a")
}
17 changes: 9 additions & 8 deletions tool/internal/setup/add.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,14 @@ var requiredImports = map[string]string{
}

func genImportDecl(matched []*rule.InstFuncRule) []dst.Decl {
imports := maps.Clone(requiredImports) // clone required imports to avoid mutating the global constant map
for _, m := range matched {
requiredImports[m.Path] = ast.IdentIgnore
imports[m.Path] = ast.IdentIgnore
}
importDecls := make([]dst.Decl, 0, len(requiredImports))
importDecls := make([]dst.Decl, 0, len(imports))
// Sort the keys to ensure deterministic order
for _, k := range slices.Sorted(maps.Keys(requiredImports)) {
importDecls = append(importDecls, ast.ImportDecl(requiredImports[k], k))
for _, k := range slices.Sorted(maps.Keys(imports)) {
importDecls = append(importDecls, ast.ImportDecl(imports[k], k))
}
return importDecls
}
Expand All @@ -48,13 +49,13 @@ func genVarDecl(matched []*rule.InstFuncRule) []dst.Decl {
}
uniquePath[m.Path] = true
// First variable declaration
// //go:linkname _getstatck%d %s.OtelGetStackImpl
// var _getstatck%d = _otel_debug.Stack
// //go:linkname _getstack%d %s.OtelGetStackImpl
// var _getstack%d = _otel_debug.Stack
value := ast.SelectorExpr(ast.Ident("_otel_debug"), "Stack")
getStackVar := ast.VarDecl(fmt.Sprintf("_getstatck%d", i), value)
getStackVar := ast.VarDecl(fmt.Sprintf("_getstack%d", i), value)
getStackVar.Decs = dst.GenDeclDecorations{
NodeDecs: ast.LineComments(
fmt.Sprintf("//go:linkname _getstatck%d %s.OtelGetStackImpl", i, m.Path)),
fmt.Sprintf("//go:linkname _getstack%d %s.OtelGetStackImpl", i, m.Path)),
}
// Second variable declaration
// //go:linkname _printstack%d %s.OtelPrintStackImpl
Expand Down
5 changes: 2 additions & 3 deletions tool/internal/setup/find.go
Original file line number Diff line number Diff line change
Expand Up @@ -214,16 +214,15 @@ func (sp *SetupPhase) findDeps(ctx context.Context, cmdArgs []string) ([]*Depend
continue
}

if util.IsCompileCommandWithArgs(util.SplitCompileCmds(cmd)) {
args := util.SplitCompileCmds(cmd)
args := util.SplitCompileCmds(cmd)
if util.IsCompileCommandWithArgs(args) {
dep, err1 := findGoSources(sp, args, cgoObjDirs)
if err1 != nil {
return nil, err1
}
deps = append(deps, dep)
sp.Info("Found dependency", "dep", dep)
} else if util.IsCgoCommand(cmd) && currentDir != "" {
args := util.SplitCompileCmds(cmd)
objDir := util.FindFlagValue(args, "-objdir")
util.Assert(objDir != "", "sanity check")
cgoObjDirs[util.NormalizePath(objDir)] = currentDir
Expand Down
18 changes: 6 additions & 12 deletions tool/internal/setup/match.go
Original file line number Diff line number Diff line change
Expand Up @@ -383,17 +383,11 @@ func (sp *SetupPhase) loadRules() ([]rule.InstRule, error) {
return loadDefaultRules()
}

func (sp *SetupPhase) matchDeps(ctx context.Context, deps []*Dependency) ([]*rule.InstRuleSet, error) {
// Construct the set of default allRules by parsing embedded data
allRules, err := sp.loadRules()
if err != nil {
return nil, err
}
sp.Info("Found available rules", "rules", allRules)
if len(allRules) == 0 {
return nil, nil
}

func (sp *SetupPhase) matchDeps(
ctx context.Context,
allRules []rule.InstRule,
deps []*Dependency,
) ([]*rule.InstRuleSet, error) {
// Pre-index rules by target
rulesByTarget := make(map[string][]rule.InstRule)
for _, r := range allRules {
Expand Down Expand Up @@ -422,7 +416,7 @@ func (sp *SetupPhase) matchDeps(ctx context.Context, deps []*Dependency) ([]*rul
})
}

if err = g.Wait(); err != nil {
if err := g.Wait(); err != nil {
return nil, err
}
return matched, nil
Expand Down
167 changes: 123 additions & 44 deletions tool/internal/setup/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,20 @@ import (
"github.com/open-telemetry/opentelemetry-go-compile-instrumentation/tool/ex"
"github.com/open-telemetry/opentelemetry-go-compile-instrumentation/tool/internal/instrument"
"github.com/open-telemetry/opentelemetry-go-compile-instrumentation/tool/internal/pkgload"
"github.com/open-telemetry/opentelemetry-go-compile-instrumentation/tool/internal/rule"
"github.com/open-telemetry/opentelemetry-go-compile-instrumentation/tool/util"
"github.com/urfave/cli/v3"
"golang.org/x/tools/go/packages"
)

// Upper bound for setup graph stabilization passes.
// Each pass may introduce additional instrumentation dependencies,
// requiring dependency discovery and matching to run again.
//
// In practice the dependency graph should stabilize within 3 passes.
// This limit only exists as a safeguard against unexpected infinite loops.
const otelcSetupMaxPasses = 3

type SetupPhase struct {
logger *slog.Logger
ruleConfig string
Expand Down Expand Up @@ -201,6 +210,98 @@ func getPackageDir(pkg *packages.Package) string {
return ""
}

func instrumentedDepsBumped(bumpedDeps []bumpedDep, matched []*rule.InstRuleSet) bool {
for _, bumped := range bumpedDeps {
for _, m := range matched {
// append "/" at the end to avoid false positives like:
// github.com/my/pkg and github.com/my/pkg2
if m.ModulePath == bumped.Req.Mod.Path || strings.HasPrefix(m.ModulePath, bumped.Req.Mod.Path+"/") {
return true
}
}
}
return false
}

func (sp *SetupPhase) runSetupPass(
ctx context.Context,
args []string,
pkgs []*packages.Package,
allRules []rule.InstRule,
originalDeps map[string]bool,
) ([]*rule.InstRuleSet, bool, error) {
// Find all dependencies of the project being build
deps, err := sp.findDeps(ctx, args)
if err != nil {
return nil, false, ex.Wrapf(err, "finding dependencies")
}

// Filter dependencies to only those that were present in the original graph before any modifications.
filteredDeps := make([]*Dependency, 0, len(deps))
if len(originalDeps) == 0 {
for _, dep := range deps {
originalDeps[dep.ImportPath] = true
filteredDeps = append(filteredDeps, dep)
}
} else {
for _, dep := range deps {
if !originalDeps[dep.ImportPath] {
continue
}
filteredDeps = append(filteredDeps, dep)
}
}

// Match the hook code with these dependencies and sync new dependencies to go.mod
matched, err := sp.matchDeps(ctx, allRules, filteredDeps)
if err != nil {
return nil, false, ex.Wrapf(err, "matching dependencies to hook rules")
}

// Generate otelc.runtime.go for all packages
moduleDirs := make(map[string]bool)
for _, pkg := range pkgs {
// file-based builds use synthetic "command-line-arguments" packages
if pkg.Module == nil && pkg.PkgPath != commandLineArgumentsPackage {
sp.Warn("skipping package without module", "package", pkg.PkgPath)
continue
}

pkgDir := getPackageDir(pkg)
if pkgDir == "" {
sp.Warn("skipping package without Go files", "package", pkg.PkgPath)
continue
}

var moduleDir string
if pkg.Module != nil {
moduleDir = pkg.Module.Dir
} else {
if moduleDir, err = pkgload.ResolveModuleDir(ctx, pkgDir); err != nil {
return nil, false, ex.Wrapf(err, "finding module dir for package %s", pkg.PkgPath)
}
}

// Introduce additional hook code by generating otelc.runtime.go
if err = sp.addDeps(matched, pkgDir); err != nil {
return nil, false, ex.Wrapf(err, "adding deps for package at %s", pkgDir)
}
moduleDirs[moduleDir] = true
}

// Sync new dependencies to go.mod
graphChanged := false
for moduleDir := range moduleDirs {
bumpedDeps, syncErr := sp.syncDeps(ctx, matched, moduleDir)
if syncErr != nil {
return nil, false, ex.Wrapf(syncErr, "syncing deps in module dir %s", moduleDir)
}
graphChanged = graphChanged || instrumentedDepsBumped(bumpedDeps, matched)
}

return matched, graphChanged, nil
}

// Setup prepares the environment for further instrumentation.
func Setup(ctx context.Context, cmd *cli.Command) error {
// Since Setup can be invoked in different contexts (i.e, via `otelc setup` or as part of `otelc go build`),
Expand Down Expand Up @@ -237,63 +338,41 @@ func Setup(ctx context.Context, cmd *cli.Command) error {
return err
}

// Find all dependencies of the project being build
deps, err := sp.findDeps(ctx, args)
if err != nil {
return err
}

// Extract the embedded pkg module into local directory
err = sp.extract()
if err != nil {
if err = sp.extract(); err != nil {
return ex.Wrapf(err, "extracting embedded instrumentation pkg")
}

// Match the hook code with these dependencies
matched, err := sp.matchDeps(ctx, deps)
// Construct the set of default allRules by parsing embedded data (or given by user)
allRules, err := sp.loadRules()
if err != nil {
return ex.Wrapf(err, "matching dependencies to hook rules")
return ex.Wrapf(err, "loading instrumentation rules")
}
sp.Info("Found available rules", "rules", allRules)
if len(allRules) == 0 {
return ex.New("no rules found")
}

// Generate otelc.runtime.go for all packages
moduleDirs := make(map[string]bool)
for _, pkg := range pkgs {
// file-based builds use synthetic "command-line-arguments" packages
if pkg.Module == nil && pkg.PkgPath != commandLineArgumentsPackage {
sp.Warn("skipping package without module", "package", pkg.PkgPath)
continue
}

pkgDir := getPackageDir(pkg)
if pkgDir == "" {
sp.Warn("skipping package without Go files", "package", pkg.PkgPath)
continue
}

var moduleDir string
if pkg.Module != nil {
moduleDir = pkg.Module.Dir
} else {
if moduleDir, err = pkgload.ResolveModuleDir(ctx, pkgDir); err != nil {
return ex.Wrapf(err, "finding module dir for package %s", pkg.PkgPath)
}
}
// Run multiple passes of dependency discovery, matching, and syncing until the graph stabilizes or we hit the max pass limit.
var (
matched []*rule.InstRuleSet
graphChanged bool
originalDeps = make(map[string]bool)
)
for i := range otelcSetupMaxPasses {
sp.Debug("starting setup pass", "pass", i+1)

// Introduce additional hook code by generating otelc.runtime.go
if err = sp.addDeps(matched, pkgDir); err != nil {
return ex.Wrapf(err, "adding deps for package at %s", pkgDir)
matched, graphChanged, err = sp.runSetupPass(ctx, args, pkgs, allRules, originalDeps)
if err != nil {
return ex.Wrapf(err, "setup pass %d failed", i+1)
}
moduleDirs[moduleDir] = true
}

// Sync new dependencies to go.mod or vendor/modules.txt
for moduleDir := range moduleDirs {
if err = sp.syncDeps(ctx, matched, moduleDir); err != nil {
return ex.Wrapf(err, "syncing deps in module dir %s", moduleDir)
if !graphChanged {
break
}
}

// Write the matched hook to matched.txt for further instrument phase
// Write the matched hook to matched.json for further instrument phase
return sp.store(matched)
}

Expand Down
Loading
Loading