Skip to content

Commit fbd6cbf

Browse files
committed
wip: otlp-bench
1 parent 38acd6c commit fbd6cbf

File tree

20 files changed

+9484
-0
lines changed

20 files changed

+9484
-0
lines changed

otlp-bench/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
/versions/.opentelemetry-proto-go
2+
/testdata/versions/

otlp-bench/README.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# otlp-bench
2+
3+
`otlp-bench` is a tool for comparing different variants for encoding profiling data as OTLP.
4+
5+
A variant is a combination of a specific version (commit) of the opentelemetry-proto repository as well as code for encoding profiling data as OTLP.
6+
7+
## Managing Versions
8+
9+
### Example Usage
10+
11+
Adding new versions of the OTLP profiling protocol is done via the `version add` command.
12+
13+
```bash
14+
# Add a specific version from upstream
15+
./otlp-bench version add <name> <git-ref>
16+
17+
# Add a version from a fork
18+
./otlp-bench version add <name> <git-ref> <git-remote-url>
19+
```
20+
21+
### Flags
22+
23+
- `--verbose` prints every shell command that the tool launches.
24+
25+
Generated assets are stored in `./versions` using the system `git`, `make`, and `go` binaries, and cloning from the upstream `opentelemetry-proto-go` repository. Each invocation writes metadata to `./versions/versions.json` and initialises/updates `./versions/go.work` so that the generated modules can be consumed immediately.
26+
27+
### How It Works
28+
29+
1. Clones the latest `opentelemetry-proto-go` (build system)
30+
2. Points its submodule at the specified commit/fork
31+
3. Generates Go code with custom module paths
32+
4. Makes versions available via Go workspace

otlp-bench/bench.go

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"os"
7+
8+
"github.com/open-telemetry/sig-profiling/otlp-bench/versions/1.9/slim/otlp/profiles/v1development"
9+
"github.com/urfave/cli/v3"
10+
"google.golang.org/protobuf/proto"
11+
)
12+
13+
// Flag and argument identifiers
14+
const (
15+
benchOutFlag = "out"
16+
benchTruncateFlag = "truncate"
17+
benchFilesArg = "files"
18+
)
19+
20+
// newBenchCommand creates the bench command for benchmarking OTLP profile files
21+
func (a *App) newBenchCommand() *cli.Command {
22+
return &cli.Command{
23+
Name: "bench",
24+
Usage: "Benchmark OTLP encoded profile files",
25+
Description: `Analyzes one or more OTLP encoded profile files and outputs
26+
benchmark metrics to a CSV file.
27+
28+
The command takes one or more OTLP profile files as input and generates
29+
benchmark results in CSV format. Use the --out flag to specify the output
30+
file path (defaults to otlp-bench.csv) and --truncate to overwrite an
31+
existing output file.`,
32+
Flags: []cli.Flag{
33+
&cli.StringFlag{
34+
Name: benchOutFlag,
35+
Aliases: []string{"o"},
36+
Usage: "Output CSV file path",
37+
Value: "otlp-bench.csv",
38+
},
39+
&cli.BoolFlag{
40+
Name: benchTruncateFlag,
41+
Aliases: []string{"t"},
42+
Usage: "Truncate output file if it exists. By default existing records are updated as needed.",
43+
},
44+
},
45+
Arguments: []cli.Argument{
46+
&cli.StringArgs{
47+
Name: benchFilesArg,
48+
UsageText: "<file1> [file2] ...",
49+
Min: 1,
50+
Max: -1, // Unlimited
51+
},
52+
},
53+
Action: a.benchAction,
54+
}
55+
}
56+
57+
// benchAction is the main action handler for the bench command
58+
func (a *App) benchAction(ctx context.Context, cmd *cli.Command) error {
59+
args, err := a.parseBenchArgs(cmd)
60+
if err != nil {
61+
return err
62+
}
63+
64+
a.Log.Info("bench command invoked",
65+
"files", args.Files,
66+
"out", args.Out,
67+
"truncate", args.Truncate,
68+
)
69+
70+
for _, file := range args.Files {
71+
if err := a.benchFile(file); err != nil {
72+
return err
73+
}
74+
}
75+
76+
return nil
77+
}
78+
79+
func (a *App) benchFile(file string) error {
80+
data, err := os.ReadFile(file)
81+
if err != nil {
82+
return fmt.Errorf("read file: %w", err)
83+
}
84+
85+
profilesData := &v1development.ProfilesData{}
86+
if err := proto.Unmarshal(data, profilesData); err != nil {
87+
return fmt.Errorf("unmarshal profiles data: %w", err)
88+
}
89+
90+
fmt.Printf("len(profilesData.Dictionary.StringTable): %v\n", len(profilesData.Dictionary.StringTable))
91+
92+
return nil
93+
}
94+
95+
// benchArgs holds the parsed command arguments and flags
96+
type benchArgs struct {
97+
Files []string
98+
Out string
99+
Truncate bool
100+
}
101+
102+
// parseBenchArgs parses and validates the bench command arguments
103+
func (a *App) parseBenchArgs(cmd *cli.Command) (benchArgs, error) {
104+
args := benchArgs{
105+
Files: cmd.StringArgs(benchFilesArg),
106+
Out: cmd.String(benchOutFlag),
107+
Truncate: cmd.Bool(benchTruncateFlag),
108+
}
109+
110+
if len(args.Files) == 0 {
111+
return benchArgs{}, cli.Exit("at least one input file required", 1)
112+
}
113+
114+
return args, nil
115+
}

otlp-bench/go.mod

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
module github.com/open-telemetry/sig-profiling/otlp-bench
2+
3+
go 1.25.3
4+
5+
require (
6+
github.com/lmittmann/tint v1.1.2 // indirect
7+
github.com/urfave/cli/v3 v3.5.0 // indirect
8+
google.golang.org/protobuf v1.36.10 // indirect
9+
)

otlp-bench/go.sum

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
github.com/lmittmann/tint v1.1.2 h1:2CQzrL6rslrsyjqLDwD11bZ5OpLBPU+g3G/r5LSfS8w=
2+
github.com/lmittmann/tint v1.1.2/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE=
3+
github.com/urfave/cli/v3 v3.5.0 h1:qCuFMmdayTF3zmjG8TSsoBzrDqszNrklYg2x3g4MSgw=
4+
github.com/urfave/cli/v3 v3.5.0/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
5+
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
6+
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=

otlp-bench/main.go

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
package main
2+
3+
import (
4+
"cmp"
5+
"context"
6+
"fmt"
7+
"io"
8+
"log/slog"
9+
"os"
10+
"os/exec"
11+
"path/filepath"
12+
"runtime"
13+
"strings"
14+
15+
"github.com/lmittmann/tint"
16+
"github.com/urfave/cli/v3"
17+
)
18+
19+
func main() {
20+
var app App
21+
app.Run(context.Background(), os.Args)
22+
}
23+
24+
type App struct {
25+
Writer io.Writer
26+
ErrWriter io.Writer
27+
Log *slog.Logger
28+
VersionDir string
29+
ExitErrHandler func(ctx context.Context, c *cli.Command, err error)
30+
}
31+
32+
func (b *App) Run(ctx context.Context, args []string) error {
33+
cmd := b.newRootCommand()
34+
return cmd.Run(ctx, args)
35+
}
36+
37+
func (a *App) newRootCommand() *cli.Command {
38+
// Start app initialization
39+
a.Writer = cmp.Or(a.Writer, io.Writer(os.Stdout))
40+
a.ErrWriter = cmp.Or(a.ErrWriter, io.Writer(os.Stderr))
41+
if a.ExitErrHandler == nil {
42+
a.ExitErrHandler = func(ctx context.Context, c *cli.Command, err error) {
43+
a.Log.Error("command failed", "error", err)
44+
os.Exit(1)
45+
}
46+
}
47+
48+
root := &cli.Command{
49+
Name: "otlp-bench",
50+
Usage: "Manage OTLP profiling benchmarks",
51+
Flags: []cli.Flag{
52+
&cli.BoolFlag{
53+
Name: "verbose",
54+
Usage: "Enable verbose command output",
55+
},
56+
},
57+
Before: a.before,
58+
Writer: a.Writer,
59+
ErrWriter: a.ErrWriter,
60+
ExitErrHandler: a.ExitErrHandler,
61+
Commands: []*cli.Command{
62+
a.newBenchCommand(),
63+
{
64+
Name: "version",
65+
Usage: "Manage OTLP proto versions",
66+
Commands: []*cli.Command{
67+
a.newVersionAddCommand(),
68+
},
69+
},
70+
},
71+
}
72+
return root
73+
}
74+
75+
const (
76+
rootVerboseFlag = "verbose"
77+
)
78+
79+
func (a *App) before(ctx context.Context, cmd *cli.Command) (context.Context, error) {
80+
// Init logger
81+
if a.Log == nil {
82+
handlerOpts := &tint.Options{Level: slog.LevelDebug}
83+
if cmd.Bool(rootVerboseFlag) {
84+
handlerOpts.Level = slog.LevelDebug
85+
}
86+
tint.NewHandler(a.ErrWriter, handlerOpts)
87+
handler := tint.NewHandler(a.Writer, handlerOpts)
88+
logger := slog.New(handler)
89+
a.Log = logger
90+
}
91+
92+
// Resolve versions directory
93+
if a.VersionDir == "" {
94+
a.VersionDir = "versions"
95+
}
96+
var err error
97+
a.VersionDir, err = filepath.Abs(a.VersionDir)
98+
if err != nil {
99+
return ctx, fmt.Errorf("resolve versions dir: %w", err)
100+
}
101+
if !strings.HasPrefix(a.VersionDir, goModRoot()) {
102+
return ctx, fmt.Errorf("versions dir must be a subdirectory of the go mod root: %s is not a subdirectory of %s", a.VersionDir, goModRoot())
103+
}
104+
105+
return ctx, nil
106+
}
107+
108+
func (a *App) exec(ctx context.Context, dir string, name string, args ...string) error {
109+
command := fmt.Sprintf("%s %s", name, strings.Join(args, " "))
110+
a.Log.Info("executing command", "command", command, "dir", dir)
111+
cmd := exec.CommandContext(ctx, name, args...)
112+
cmd.Dir = dir
113+
cmd.Stdout = a.Writer
114+
cmd.Stderr = a.ErrWriter
115+
if err := cmd.Run(); err != nil {
116+
return fmt.Errorf("execute command %s: %w", command, err)
117+
}
118+
return nil
119+
}
120+
121+
func goModRoot() string {
122+
_, file, _, ok := runtime.Caller(1)
123+
if !ok {
124+
return ""
125+
}
126+
return filepath.Dir(file)
127+
}

otlp-bench/main_test.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"os"
6+
"testing"
7+
8+
"github.com/urfave/cli/v3"
9+
)
10+
11+
func TestVersionAdd(t *testing.T) {
12+
for _, removeBefore := range []bool{true, false} {
13+
builder := testAppBuilder{RemoveDirBefore: removeBefore, RemoveDirAfter: !removeBefore}
14+
app := builder.Build(t)
15+
err := app.Run(t.Context(), []string{"", "version", "add", "dict", "bcbee5a324cae805e296be1bf0e4edc8d6da6585", "https://github.com/florianl/opentelemetry-proto.git", "--verbose"})
16+
if err != nil {
17+
t.Fatal(err)
18+
}
19+
}
20+
}
21+
22+
func TestBench(t *testing.T) {
23+
builder := testAppBuilder{}
24+
app := builder.Build(t)
25+
err := app.Run(t.Context(), []string{"", "bench", "testdata/profile.otlp", "--verbose"})
26+
if err != nil {
27+
t.Fatal(err)
28+
}
29+
}
30+
31+
type testAppBuilder struct {
32+
RemoveDirBefore bool
33+
RemoveDirAfter bool
34+
}
35+
36+
func (b testAppBuilder) Build(t *testing.T) *App {
37+
t.Helper()
38+
a := &App{
39+
Writer: t.Output(),
40+
ErrWriter: t.Output(),
41+
VersionDir: "testdata/versions",
42+
ExitErrHandler: func(ctx context.Context, c *cli.Command, err error) {},
43+
}
44+
removeDir := func() {
45+
if err := os.RemoveAll(a.VersionDir); err != nil {
46+
t.Fatal(err)
47+
}
48+
49+
}
50+
if b.RemoveDirBefore {
51+
removeDir()
52+
}
53+
if b.RemoveDirAfter {
54+
t.Cleanup(func() {
55+
if err := os.RemoveAll(a.VersionDir); err != nil {
56+
t.Fatal(err)
57+
}
58+
})
59+
}
60+
return a
61+
}

otlp-bench/testdata/profile.otlp

24.5 KB
Binary file not shown.

0 commit comments

Comments
 (0)