Skip to content

Commit 2036eb7

Browse files
committed
chore: implement otlp-bench for resource attribute dictionaries
1 parent 38acd6c commit 2036eb7

File tree

40 files changed

+13668
-0
lines changed

40 files changed

+13668
-0
lines changed

otlp-bench/.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
/internal/otlpbuild/testdata/tmp
2+
/internal/otlpbuild/testdata/dst
3+
otlp-bench-results

otlp-bench/README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# otlp-bench
2+
3+
`otlp-bench` is a tool for comparing different variants for encoding profiling data as OTLP.
4+
5+
For now check [reports/2025-11-27-gh733-resource-attr-dict/README.md]() for more information.

otlp-bench/go.mod

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

otlp-bench/go.sum

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
2+
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
3+
github.com/lmittmann/tint v1.1.2 h1:2CQzrL6rslrsyjqLDwD11bZ5OpLBPU+g3G/r5LSfS8w=
4+
github.com/lmittmann/tint v1.1.2/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE=
5+
github.com/urfave/cli/v3 v3.5.0 h1:qCuFMmdayTF3zmjG8TSsoBzrDqszNrklYg2x3g4MSgw=
6+
github.com/urfave/cli/v3 v3.5.0/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
7+
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
8+
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
package otlpbuild
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"crypto/sha1"
7+
"errors"
8+
"fmt"
9+
"io/fs"
10+
"os"
11+
"os/exec"
12+
"path/filepath"
13+
"regexp"
14+
"sort"
15+
"strings"
16+
)
17+
18+
type Config struct {
19+
// SrcDir is the path to opentelemetry directory, typically inside a copy of
20+
// the opentelemetry-proto repository.
21+
SrcDir string
22+
// TmpDir is the path to a temporary directory that will be used to build a
23+
// version of the OTLP proto files.
24+
TmpDir string
25+
// DstDir is the path to the directory where the built OTLP Go files will be
26+
// stored.
27+
DstDir string
28+
// PackagePrefix is the prefix to use for the Go package names.
29+
PackagePrefix string
30+
}
31+
32+
// Build builds the OTLP Go bindings and uses the base name of the DstDir as a
33+
// namespace to allow importing multiple versions of the same proto files into
34+
// the same program.
35+
func Build(ctx context.Context, c Config) error {
36+
// derive srcDir
37+
srcDir, err := filepath.Abs(filepath.Join(c.TmpDir, "src"))
38+
if err != nil {
39+
return fmt.Errorf("get absolute path: %w", err)
40+
}
41+
42+
// derive namespace directory
43+
namespace := filepath.Base(c.DstDir)
44+
namespaceDir := filepath.Join(srcDir, namespace)
45+
46+
// derive dstDir
47+
dstDir, err := filepath.Abs(c.DstDir)
48+
if err != nil {
49+
return fmt.Errorf("get absolute path: %w", err)
50+
}
51+
52+
// copy srcDir to nameSpaceDir
53+
if err := os.CopyFS(filepath.Join(namespaceDir, "opentelemetry"), os.DirFS(c.SrcDir)); err != nil {
54+
return fmt.Errorf("copy source directory: %w", err)
55+
}
56+
57+
// find proto files
58+
protoFiles, err := findProtoFiles(ctx, namespaceDir)
59+
if err != nil {
60+
return fmt.Errorf("find proto files: %w", err)
61+
}
62+
63+
// rewrite proto files
64+
for _, protoFile := range protoFiles {
65+
if err := rewriteProtoFile(protoFile, namespace, c.PackagePrefix); err != nil {
66+
return fmt.Errorf("rewrite proto file: %w", err)
67+
}
68+
}
69+
70+
// compile proto files
71+
if err := os.MkdirAll(dstDir, 0o755); err != nil {
72+
return fmt.Errorf("create destination directory: %w", err)
73+
}
74+
if err := compileProtoFiles(ctx, c.TmpDir, srcDir, namespace, dstDir, protoFiles); err != nil {
75+
return fmt.Errorf("compile proto files: %w", err)
76+
}
77+
78+
return nil
79+
80+
}
81+
82+
func findProtoFiles(ctx context.Context, protoRootDir string) ([]string, error) {
83+
if _, err := os.Stat(protoRootDir); err != nil {
84+
if errors.Is(err, os.ErrNotExist) {
85+
return nil, nil
86+
}
87+
return nil, fmt.Errorf("find proto files: %w", err)
88+
}
89+
90+
var protoFiles []string
91+
walkErr := filepath.WalkDir(protoRootDir, func(path string, d fs.DirEntry, err error) error {
92+
if err != nil {
93+
return err
94+
}
95+
96+
select {
97+
case <-ctx.Done():
98+
return ctx.Err()
99+
default:
100+
}
101+
102+
if d.IsDir() {
103+
return nil
104+
}
105+
if filepath.Ext(d.Name()) == ".proto" {
106+
protoFiles = append(protoFiles, path)
107+
}
108+
return nil
109+
})
110+
if walkErr != nil {
111+
return nil, fmt.Errorf("find proto files: %w", walkErr)
112+
}
113+
114+
sort.Strings(protoFiles)
115+
return protoFiles, nil
116+
117+
}
118+
119+
func rewriteProtoFile(protoFile, namespace, pkgPrefix string) error {
120+
content, err := os.ReadFile(protoFile)
121+
if err != nil {
122+
return fmt.Errorf("read proto file: %w", err)
123+
}
124+
content = rewriteProtoFileData(content, namespace, pkgPrefix)
125+
if err := os.WriteFile(protoFile, content, 0o644); err != nil {
126+
return fmt.Errorf("write proto file: %w", err)
127+
}
128+
return nil
129+
}
130+
131+
var packageRe = regexp.MustCompile(`(?m)^package\s+(opentelemetry\.proto\.)`)
132+
var importRe = regexp.MustCompile(`(?m)^import\s+"(opentelemetry/proto)`)
133+
var goPackageRe = regexp.MustCompile(`(?m)^option go_package = "go\.opentelemetry\.io/proto/otlp`)
134+
135+
func rewriteProtoFileData(data []byte, namespace, pkgPrefix string) []byte {
136+
pkgNamespace := namespaceHash(namespace)
137+
data = packageRe.ReplaceAll(data, []byte(`package `+pkgNamespace+`.$1`))
138+
data = importRe.ReplaceAll(data, []byte(`import "`+namespace+`/$1`))
139+
data = goPackageRe.ReplaceAll(data, []byte(`option go_package = "`+pkgPrefix+`/`+namespace+"/opentelemetry/proto"))
140+
return data
141+
}
142+
143+
func namespaceHash(namespace string) string {
144+
hash := sha1.Sum([]byte(namespace))
145+
encoded := make([]byte, len(hash)*2)
146+
for i, b := range hash {
147+
encoded[i*2] = 'a' + (b >> 4)
148+
encoded[i*2+1] = 'a' + (b & 0x0f)
149+
}
150+
return string(encoded)
151+
}
152+
153+
func compileProtoFiles(ctx context.Context, tmpDir, protoDir, namespace, dstDir string, protoFiles []string) error {
154+
uid := os.Getuid()
155+
156+
absTmpDir, err := filepath.Abs(tmpDir)
157+
if err != nil {
158+
return fmt.Errorf("get absolute temporary directory: %w", err)
159+
}
160+
161+
tmpDstDir := filepath.Join(absTmpDir, "dst")
162+
if err := os.MkdirAll(tmpDstDir, 0o755); err != nil {
163+
return fmt.Errorf("create destination directory: %w", err)
164+
}
165+
166+
cmdArgs := []string{
167+
"docker",
168+
"run",
169+
"--rm",
170+
"-u", fmt.Sprintf("%d", uid),
171+
"-v", fmt.Sprintf("%s:%s", absTmpDir, absTmpDir),
172+
"-w", absTmpDir,
173+
"otel/build-protobuf:0.9.0",
174+
"--proto_path=" + protoDir,
175+
"--go_opt=paths=source_relative",
176+
"--go_out=" + tmpDstDir,
177+
}
178+
cmdArgs = append(cmdArgs, protoFiles...)
179+
180+
var buf bytes.Buffer
181+
cmd := exec.CommandContext(ctx, cmdArgs[0], cmdArgs[1:]...)
182+
cmd.Dir = absTmpDir
183+
cmd.Stdout = &buf
184+
cmd.Stderr = &buf
185+
if err := cmd.Run(); err != nil {
186+
return fmt.Errorf("%s: %s: %s", strings.Join(cmdArgs, " "), err, buf.String())
187+
}
188+
189+
// copy to dstDir
190+
if err := os.RemoveAll(dstDir); err != nil {
191+
return fmt.Errorf("remove destination directory: %w", err)
192+
}
193+
if err := os.CopyFS(dstDir, os.DirFS(filepath.Join(tmpDstDir, namespace))); err != nil {
194+
return fmt.Errorf("copy tmp dst to final dst directory: %w", err)
195+
}
196+
197+
return nil
198+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
package otlpbuild
2+
3+
import (
4+
"bytes"
5+
"os"
6+
"os/exec"
7+
"path/filepath"
8+
"sync"
9+
"testing"
10+
11+
"github.com/google/go-cmp/cmp"
12+
)
13+
14+
func TestBuild(t *testing.T) {
15+
tmpDir := testSetupTmpDir(t)
16+
var wg sync.WaitGroup
17+
for _, namespace := range []string{"foo", "v1.9.0-bar-baz"} {
18+
wg.Go(func() {
19+
testBuild(t, tmpDir, namespace)
20+
})
21+
}
22+
wg.Wait()
23+
testImport(t)
24+
}
25+
26+
func testSetupTmpDir(t *testing.T) string {
27+
tmpDir := filepath.Join("testdata", "tmp")
28+
if err := os.RemoveAll(tmpDir); err != nil {
29+
t.Fatalf("failed to remove temporary directory: %v", err)
30+
}
31+
return tmpDir
32+
}
33+
34+
func testBuild(t *testing.T, tmpDir, namespace string) {
35+
t.Helper()
36+
config := Config{
37+
SrcDir: filepath.Join("testdata", "src", "opentelemetry"),
38+
TmpDir: tmpDir,
39+
DstDir: filepath.Join("testdata", "dst", namespace),
40+
PackagePrefix: "github.com/open-telemetry/sig-profiling/otlp-bench/internal/otlpbuild/testdata/dst",
41+
}
42+
if err := Build(t.Context(), config); err != nil {
43+
t.Fatalf("failed to build OTLP: %v", err)
44+
}
45+
}
46+
47+
func testImport(t *testing.T) {
48+
t.Helper()
49+
var buf bytes.Buffer
50+
cmd := exec.CommandContext(t.Context(), "go", "test", "-tags", "import_test", "./testdata/import")
51+
cmd.Stdout = &buf
52+
cmd.Stderr = &buf
53+
if err := cmd.Run(); err != nil {
54+
t.Fatalf("failed to test otlpimport: %v\n\n%s\n", err, buf.String())
55+
}
56+
}
57+
58+
func TestRewriteProtoFile(t *testing.T) {
59+
in := bytes.TrimSpace([]byte(`
60+
syntax = "proto3";
61+
62+
package opentelemetry.proto.logs.v1;
63+
64+
import "foo/opentelemetry/proto/common/v1/common.proto";
65+
import "foo/opentelemetry/proto/resource/v1/resource.proto";
66+
67+
option csharp_namespace = "OpenTelemetry.Proto.Logs.V1";
68+
option java_multiple_files = true;
69+
option java_package = "io.opentelemetry.proto.logs.v1";
70+
option java_outer_classname = "LogsProto";
71+
option go_package = "go.opentelemetry.io/proto/otlp/logs/v1";
72+
`))
73+
want := bytes.TrimSpace([]byte(`
74+
syntax = "proto3";
75+
76+
package aloomhlfokdpapnlmjfnannehpdmflmchfnkikdd.opentelemetry.proto.logs.v1;
77+
78+
import "foo/opentelemetry/proto/common/v1/common.proto";
79+
import "foo/opentelemetry/proto/resource/v1/resource.proto";
80+
81+
option csharp_namespace = "OpenTelemetry.Proto.Logs.V1";
82+
option java_multiple_files = true;
83+
option java_package = "io.opentelemetry.proto.logs.v1";
84+
option java_outer_classname = "LogsProto";
85+
option go_package = "github.com/a/b/foo/opentelemetry/proto/logs/v1";
86+
`))
87+
88+
got := rewriteProtoFileData(in, "foo", "github.com/a/b")
89+
if diff := cmp.Diff(string(want), string(got)); diff != "" {
90+
t.Errorf("rewriteProtoFileData mismatch (-want +got):\n%s", diff)
91+
}
92+
93+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
//go:build import_test
2+
3+
package import_test
4+
5+
import (
6+
_ "github.com/open-telemetry/sig-profiling/otlp-bench/internal/otlpbuild/testdata/dst/foo/opentelemetry/proto/profiles/v1development"
7+
_ "github.com/open-telemetry/sig-profiling/otlp-bench/internal/otlpbuild/testdata/dst/v1.9.0-bar-baz/opentelemetry/proto/profiles/v1development"
8+
)
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# OpenTelemetry Collector Proto
2+
3+
This package describes the OpenTelemetry collector protocol.
4+
5+
## Packages
6+
7+
1. `common` package contains the common messages shared between different services.
8+
2. `trace` package contains the Trace Service protos.
9+
3. `metrics` package contains the Metrics Service protos.
10+
4. `logs` package contains the Logs Service protos.

0 commit comments

Comments
 (0)