Skip to content

Commit

Permalink
proposal: revised instrumentation API (open-telemetry#284)
Browse files Browse the repository at this point in the history
* PoC of revised instrumentation API

* Update changelog with changes
  • Loading branch information
MrAlias authored Sep 19, 2023
1 parent aeec0d4 commit a4ce6b9
Show file tree
Hide file tree
Showing 6 changed files with 182 additions and 92 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
69 changes: 21 additions & 48 deletions cli/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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")
}
}
142 changes: 142 additions & 0 deletions instrumentation.go
Original file line number Diff line number Diff line change
@@ -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
})
}
11 changes: 8 additions & 3 deletions internal/pkg/instrumentors/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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()
Expand All @@ -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,
Expand Down
21 changes: 8 additions & 13 deletions internal/pkg/opentelemetry/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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 {
Expand Down Expand Up @@ -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)
Expand All @@ -109,14 +100,18 @@ func NewController() (*Controller, error) {
resource.WithAttributes(
semconv.ServiceNameKey.String(serviceName),
semconv.TelemetrySDKLanguageGo,
semconv.TelemetryAutoVersionKey.String(releaseVersion),
semconv.TelemetryAutoVersionKey.String(version),
),
)
if err != nil {
return nil, err
}

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)),
)
Expand Down
28 changes: 0 additions & 28 deletions internal/pkg/opentelemetry/controller_test.go

This file was deleted.

0 comments on commit a4ce6b9

Please sign in to comment.