From a4ce6b9837ee98194f40d06d7860eb0cfef80a08 Mon Sep 17 00:00:00 2001 From: Tyler Yahn Date: Tue, 19 Sep 2023 07:47:49 -0700 Subject: [PATCH] proposal: revised instrumentation API (#284) * PoC of revised instrumentation API * Update changelog with changes --- CHANGELOG.md | 3 + cli/main.go | 69 +++------ instrumentation.go | 142 ++++++++++++++++++ internal/pkg/instrumentors/runner.go | 11 +- internal/pkg/opentelemetry/controller.go | 21 +-- internal/pkg/opentelemetry/controller_test.go | 28 ---- 6 files changed, 182 insertions(+), 92 deletions(-) create mode 100644 instrumentation.go delete mode 100644 internal/pkg/opentelemetry/controller_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index b6749fee1..f7088ecb3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,6 +47,9 @@ OpenTelemetry Go Automatic Instrumentation adheres to [Semantic Versioning](http - Add database/sql instrumentation ([#240](https://github.com/open-telemetry/opentelemetry-go-instrumentation/pull/240)) - Support Go 1.21. ([#264](https://github.com/open-telemetry/opentelemetry-go-instrumentation/pull/264)) +- Add `Instrumentation` to `go.opentelemetry.io/auto` to manage and run the auto-instrumentation provided by the project. ([#284](https://github.com/open-telemetry/opentelemetry-go-instrumentation/pull/284)) + - Use the `NewInstrumentation` to create a `Instrumentation` with the desired configuration by passing zero or more `InstrumentationOption`s. + - Use `WithTarget` when creating an `Instrumentation` to specify its target binary. ### Changed diff --git a/cli/main.go b/cli/main.go index c94c31297..f5396113f 100644 --- a/cli/main.go +++ b/cli/main.go @@ -15,16 +15,15 @@ package main import ( + "context" "fmt" "os" "os/signal" "syscall" + "go.opentelemetry.io/auto" "go.opentelemetry.io/auto/internal/pkg/errors" - "go.opentelemetry.io/auto/internal/pkg/instrumentors" "go.opentelemetry.io/auto/internal/pkg/log" - "go.opentelemetry.io/auto/internal/pkg/opentelemetry" - "go.opentelemetry.io/auto/internal/pkg/process" ) func main() { @@ -34,57 +33,31 @@ func main() { os.Exit(1) } - log.Logger.V(0).Info("starting Go OpenTelemetry Agent ...") - target := process.ParseTargetArgs() - if err = target.Validate(); err != nil { - log.Logger.Error(err, "invalid target args") - return - } - - processAnalyzer := process.NewAnalyzer() - otelController, err := opentelemetry.NewController() - if err != nil { - log.Logger.Error(err, "unable to create OpenTelemetry controller") - return - } - - instManager, err := instrumentors.NewManager(otelController) + log.Logger.V(0).Info("building OpenTelemetry Go instrumentation ...") + inst, err := auto.NewInstrumentation() if err != nil { - log.Logger.Error(err, "error creating instrumetors manager") + log.Logger.Error(err, "failed to create instrumentation") return } - stopper := make(chan os.Signal, 1) - signal.Notify(stopper, os.Interrupt, syscall.SIGTERM) - go func() { - <-stopper - log.Logger.V(0).Info("Got SIGTERM, cleaning up..") - processAnalyzer.Close() - instManager.Close() + // Trap Ctrl+C and SIGTERM and call cancel on the context. + ctx, cancel := context.WithCancel(context.Background()) + ch := make(chan os.Signal, 1) + signal.Notify(ch, os.Interrupt, syscall.SIGTERM) + defer func() { + signal.Stop(ch) + cancel() }() - - pid, err := processAnalyzer.DiscoverProcessID(target) - if err != nil { - if err != errors.ErrInterrupted { - log.Logger.Error(err, "error while discovering process id") + go func() { + select { + case <-ch: + cancel() + case <-ctx.Done(): } - return - } - - targetDetails, err := processAnalyzer.Analyze(pid, instManager.GetRelevantFuncs()) - if err != nil { - log.Logger.Error(err, "error while analyzing target process") - return - } - log.Logger.V(0).Info("target process analysis completed", "pid", targetDetails.PID, - "go_version", targetDetails.GoVersion, "dependencies", targetDetails.Libraries, - "total_functions_found", len(targetDetails.Functions)) - - instManager.FilterUnusedInstrumentors(targetDetails) + }() - log.Logger.V(0).Info("invoking instrumentors") - err = instManager.Run(targetDetails) - if err != nil && err != errors.ErrInterrupted { - log.Logger.Error(err, "error while running instrumentors") + log.Logger.V(0).Info("starting instrumentors...") + if err = inst.Run(ctx); err != nil && err != errors.ErrInterrupted { + log.Logger.Error(err, "instrumentation crashed") } } diff --git a/instrumentation.go b/instrumentation.go new file mode 100644 index 000000000..299458fd2 --- /dev/null +++ b/instrumentation.go @@ -0,0 +1,142 @@ +// Copyright The OpenTelemetry Authors +// +// 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 auto + +import ( + "context" + "os" + + "go.opentelemetry.io/auto/internal/pkg/instrumentors" + "go.opentelemetry.io/auto/internal/pkg/log" + "go.opentelemetry.io/auto/internal/pkg/opentelemetry" + "go.opentelemetry.io/auto/internal/pkg/process" +) + +// envTargetExeKey is the key for the environment variable value pointing to the +// target binary to instrument. +const envTargetExeKey = "OTEL_GO_AUTO_TARGET_EXE" + +// Instrumentation manages and controls all OpenTelemetry Go +// auto-instrumentation. +type Instrumentation struct { + target *process.TargetDetails + analyzer *process.Analyzer + manager *instrumentors.Manager +} + +// NewInstrumentation returns a new [Instrumentation] configured with the +// provided opts. +func NewInstrumentation(opts ...InstrumentationOption) (*Instrumentation, error) { + c := newInstConfig(opts) + if err := c.validate(); err != nil { + return nil, err + } + + pa := process.NewAnalyzer() + pid, err := pa.DiscoverProcessID(c.target) + if err != nil { + return nil, err + } + + ctrl, err := opentelemetry.NewController(Version()) + if err != nil { + return nil, err + } + + mngr, err := instrumentors.NewManager(ctrl) + if err != nil { + return nil, err + } + + td, err := pa.Analyze(pid, mngr.GetRelevantFuncs()) + if err != nil { + mngr.Close() + return nil, err + } + log.Logger.V(0).Info( + "target process analysis completed", + "pid", td.PID, + "go_version", td.GoVersion, + "dependencies", td.Libraries, + "total_functions_found", len(td.Functions), + ) + mngr.FilterUnusedInstrumentors(td) + + return &Instrumentation{ + target: td, + analyzer: pa, + manager: mngr, + }, nil +} + +// Run starts the instrumentation. +func (i *Instrumentation) Run(ctx context.Context) error { + return i.manager.Run(ctx, i.target) +} + +// Close closes the Instrumentation, cleaning up all used resources. +func (i *Instrumentation) Close() error { + i.analyzer.Close() + i.manager.Close() + return nil +} + +// InstrumentationOption applies a configuration option to [Instrumentation]. +type InstrumentationOption interface { + apply(instConfig) instConfig +} + +type instConfig struct { + target *process.TargetArgs +} + +func newInstConfig(opts []InstrumentationOption) instConfig { + var c instConfig + for _, opt := range opts { + c = opt.apply(c) + } + c = c.applyEnv() + return c +} + +func (c instConfig) applyEnv() instConfig { + if v, ok := os.LookupEnv(envTargetExeKey); ok { + c.target = &process.TargetArgs{ExePath: v} + } + return c +} + +func (c instConfig) validate() error { + return c.target.Validate() +} + +type fnOpt func(instConfig) instConfig + +func (o fnOpt) apply(c instConfig) instConfig { return o(c) } + +// WithTarget returns an [InstrumentationOption] defining the target binary for +// [Instrumentation] that is being executed at the provided path. +// +// If multiple of these options are provided to an [Instrumentation], the last +// one will be used. +// +// If OTEL_GO_AUTO_TARGET_EXE is defined it will take precedence over any value +// passed here. +func WithTarget(path string) InstrumentationOption { + return fnOpt(func(c instConfig) instConfig { + c.target = &process.TargetArgs{ExePath: path} + return c + }) +} diff --git a/internal/pkg/instrumentors/runner.go b/internal/pkg/instrumentors/runner.go index 12da4506d..b0007ea9c 100644 --- a/internal/pkg/instrumentors/runner.go +++ b/internal/pkg/instrumentors/runner.go @@ -15,19 +15,20 @@ package instrumentors import ( + "context" "fmt" "github.com/cilium/ebpf/link" "github.com/cilium/ebpf/rlimit" "go.opentelemetry.io/auto/internal/pkg/inject" - "go.opentelemetry.io/auto/internal/pkg/instrumentors/context" + iCtx "go.opentelemetry.io/auto/internal/pkg/instrumentors/context" "go.opentelemetry.io/auto/internal/pkg/log" "go.opentelemetry.io/auto/internal/pkg/process" ) // Run runs the event processing loop for all managed Instrumentors. -func (m *Manager) Run(target *process.TargetDetails) error { +func (m *Manager) Run(ctx context.Context, target *process.TargetDetails) error { if len(m.instrumentors) == 0 { log.Logger.V(0).Info("there are no available instrumentations for target process") return nil @@ -44,6 +45,10 @@ func (m *Manager) Run(target *process.TargetDetails) error { for { select { + case <-ctx.Done(): + m.Close() + m.cleanup() + return ctx.Err() case <-m.done: log.Logger.V(0).Info("shutting down all instrumentors due to signal") m.cleanup() @@ -69,7 +74,7 @@ func (m *Manager) load(target *process.TargetDetails) error { if err != nil { return err } - ctx := &context.InstrumentorContext{ + ctx := &iCtx.InstrumentorContext{ TargetDetails: target, Executable: exe, Injector: injector, diff --git a/internal/pkg/opentelemetry/controller.go b/internal/pkg/opentelemetry/controller.go index 67e4702f4..3f6ce517e 100644 --- a/internal/pkg/opentelemetry/controller.go +++ b/internal/pkg/opentelemetry/controller.go @@ -25,7 +25,6 @@ import ( "golang.org/x/sys/unix" "google.golang.org/grpc" - "go.opentelemetry.io/auto" "go.opentelemetry.io/auto/internal/pkg/instrumentors/events" "go.opentelemetry.io/auto/internal/pkg/log" "go.opentelemetry.io/otel/exporters/otlp/otlptrace" @@ -40,16 +39,8 @@ const ( otelServiceNameEnvVar = "OTEL_SERVICE_NAME" ) -var ( - // Controller-local reference to the auto-instrumentation release version. - releaseVersion = auto.Version() - // Start of this auto-instrumentation's exporter User-Agent header, e.g. ""OTel-Go-Auto-Instrumentation/1.2.3". - baseUserAgent = fmt.Sprintf("OTel-Go-Auto-Instrumentation/%s", releaseVersion) - // Information about the runtime environment for inclusion in User-Agent, e.g. "go/1.18.2 (linux/amd64)". - runtimeInfo = fmt.Sprintf("%s (%s/%s)", strings.Replace(runtime.Version(), "go", "go/", 1), runtime.GOOS, runtime.GOARCH) - // Combined User-Agent identifying this auto-instrumentation and its runtime environment, see RFC7231 for format considerations. - autoinstUserAgent = fmt.Sprintf("%s %s", baseUserAgent, runtimeInfo) -) +// Information about the runtime environment for inclusion in User-Agent, e.g. "go/1.18.2 (linux/amd64)". +var runtimeInfo = fmt.Sprintf("%s (%s/%s)", strings.Replace(runtime.Version(), "go", "go/", 1), runtime.GOOS, runtime.GOARCH) // Controller handles OpenTelemetry telemetry generation for events. type Controller struct { @@ -98,7 +89,7 @@ func (c *Controller) convertTime(t int64) time.Time { } // NewController returns a new initialized [Controller]. -func NewController() (*Controller, error) { +func NewController(version string) (*Controller, error) { serviceName, exists := os.LookupEnv(otelServiceNameEnvVar) if !exists { return nil, fmt.Errorf("%s env var must be set", otelServiceNameEnvVar) @@ -109,7 +100,7 @@ func NewController() (*Controller, error) { resource.WithAttributes( semconv.ServiceNameKey.String(serviceName), semconv.TelemetrySDKLanguageGo, - semconv.TelemetryAutoVersionKey.String(releaseVersion), + semconv.TelemetryAutoVersionKey.String(version), ), ) if err != nil { @@ -117,6 +108,10 @@ func NewController() (*Controller, error) { } log.Logger.V(0).Info("Establishing connection to OTLP receiver ...") + // Controller-local reference to the auto-instrumentation release version. + // Start of this auto-instrumentation's exporter User-Agent header, e.g. ""OTel-Go-Auto-Instrumentation/1.2.3". + baseUserAgent := fmt.Sprintf("OTel-Go-Auto-Instrumentation/%s", version) + autoinstUserAgent := fmt.Sprintf("%s %s", baseUserAgent, runtimeInfo) otlpTraceClient := otlptracegrpc.NewClient( otlptracegrpc.WithDialOption(grpc.WithUserAgent(autoinstUserAgent)), ) diff --git a/internal/pkg/opentelemetry/controller_test.go b/internal/pkg/opentelemetry/controller_test.go deleted file mode 100644 index f6bef3538..000000000 --- a/internal/pkg/opentelemetry/controller_test.go +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright The OpenTelemetry Authors -// -// 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 opentelemetry - -import ( - "fmt" - "testing" - - "github.com/stretchr/testify/assert" - - "go.opentelemetry.io/auto" -) - -func TestUserAgent(t *testing.T) { - assert.Contains(t, autoinstUserAgent, fmt.Sprintf("OTel-Go-Auto-Instrumentation/%s", auto.Version())) -}