diff --git a/CHANGELOG.md b/CHANGELOG.md index 842e245ce12..6e3f0334278 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,16 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - The Jaeger exporter now reports dropped attributes for a Span event in the exported log. (#1771) - Adds `k8s.node.name` and `k8s.node.uid` attribute keys to the `semconv` package. (#1789) - Adds `otlpgrpc.WithTimeout` option for configuring timeout to the otlp/gRPC exporter. (#1821) +- Added `WithOSType` resource configuration option to set OS (Operating System) type resource attribute (`os.type`). (#1788) +- Added `WithProcess*` resource configuration options to set Process resource attributes. (#1788) + - `process.pid` + - `process.executable.name` + - `process.executable.path` + - `process.command_args` + - `process.owner` + - `process.runtime.name` + - `process.runtime.version` + - `process.runtime.description` ### Fixed diff --git a/sdk/resource/export_test.go b/sdk/resource/export_test.go new file mode 100644 index 00000000000..87e8853c008 --- /dev/null +++ b/sdk/resource/export_test.go @@ -0,0 +1,31 @@ +// 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 resource // import "go.opentelemetry.io/otel/sdk/resource" + +var ( + SetDefaultOSProviders = setDefaultOSProviders + SetOSProviders = setOSProviders + SetDefaultRuntimeProviders = setDefaultRuntimeProviders + SetRuntimeProviders = setRuntimeProviders + SetDefaultUserProviders = setDefaultUserProviders + SetUserProviders = setUserProviders +) + +var ( + CommandArgs = commandArgs + RuntimeName = runtimeName + RuntimeOS = runtimeOS + RuntimeArch = runtimeArch +) diff --git a/sdk/resource/os.go b/sdk/resource/os.go new file mode 100644 index 00000000000..816d209217a --- /dev/null +++ b/sdk/resource/os.go @@ -0,0 +1,39 @@ +// 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 resource // import "go.opentelemetry.io/otel/sdk/resource" + +import ( + "context" + "strings" + + "go.opentelemetry.io/otel/semconv" +) + +type osTypeDetector struct{} + +// Detect returns a *Resource that describes the operating system type the +// service is running on. +func (osTypeDetector) Detect(ctx context.Context) (*Resource, error) { + osType := runtimeOS() + + return NewWithAttributes( + semconv.OSTypeKey.String(strings.ToLower(osType)), + ), nil +} + +// WithOSType adds an attribute with the operating system type to the configured Resource. +func WithOSType() Option { + return WithDetectors(osTypeDetector{}) +} diff --git a/sdk/resource/os_test.go b/sdk/resource/os_test.go new file mode 100644 index 00000000000..7de00b4f6d6 --- /dev/null +++ b/sdk/resource/os_test.go @@ -0,0 +1,51 @@ +// 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 resource_test + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + + "go.opentelemetry.io/otel/sdk/resource" +) + +func mockRuntimeProviders() { + resource.SetRuntimeProviders( + fakeRuntimeNameProvider, + fakeRuntimeVersionProvider, + func() string { return "LINUX" }, + fakeRuntimeArchProvider, + ) +} + +func TestWithOSType(t *testing.T) { + mockRuntimeProviders() + + ctx := context.Background() + + res, err := resource.New(ctx, + resource.WithoutBuiltin(), + resource.WithOSType(), + ) + + require.NoError(t, err) + require.EqualValues(t, map[string]string{ + "os.type": "linux", + }, toMap(res)) + + restoreProcessAttributesProviders() +} diff --git a/sdk/resource/process.go b/sdk/resource/process.go new file mode 100644 index 00000000000..f15f97ec5ac --- /dev/null +++ b/sdk/resource/process.go @@ -0,0 +1,237 @@ +// 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 resource // import "go.opentelemetry.io/otel/sdk/resource" + +import ( + "context" + "fmt" + "os" + "os/user" + "path/filepath" + "runtime" + + "go.opentelemetry.io/otel/semconv" +) + +type pidProvider func() int +type executablePathProvider func() (string, error) +type commandArgsProvider func() []string +type ownerProvider func() (*user.User, error) +type runtimeNameProvider func() string +type runtimeVersionProvider func() string +type runtimeOSProvider func() string +type runtimeArchProvider func() string + +var ( + defaultPidProvider pidProvider = os.Getpid + defaultExecutablePathProvider executablePathProvider = os.Executable + defaultCommandArgsProvider commandArgsProvider = func() []string { return os.Args } + defaultOwnerProvider ownerProvider = user.Current + defaultRuntimeNameProvider runtimeNameProvider = func() string { return runtime.Compiler } + defaultRuntimeVersionProvider runtimeVersionProvider = runtime.Version + defaultRuntimeOSProvider runtimeOSProvider = func() string { return runtime.GOOS } + defaultRuntimeArchProvider runtimeArchProvider = func() string { return runtime.GOARCH } +) + +var ( + pid = defaultPidProvider + executablePath = defaultExecutablePathProvider + commandArgs = defaultCommandArgsProvider + owner = defaultOwnerProvider + runtimeName = defaultRuntimeNameProvider + runtimeVersion = defaultRuntimeVersionProvider + runtimeOS = defaultRuntimeOSProvider + runtimeArch = defaultRuntimeArchProvider +) + +func setDefaultOSProviders() { + setOSProviders( + defaultPidProvider, + defaultExecutablePathProvider, + defaultCommandArgsProvider, + ) +} + +func setOSProviders( + pidProvider pidProvider, + executablePathProvider executablePathProvider, + commandArgsProvider commandArgsProvider, +) { + pid = pidProvider + executablePath = executablePathProvider + commandArgs = commandArgsProvider +} + +func setDefaultRuntimeProviders() { + setRuntimeProviders( + defaultRuntimeNameProvider, + defaultRuntimeVersionProvider, + defaultRuntimeOSProvider, + defaultRuntimeArchProvider, + ) +} + +func setRuntimeProviders( + runtimeNameProvider runtimeNameProvider, + runtimeVersionProvider runtimeVersionProvider, + runtimeOSProvider runtimeOSProvider, + runtimeArchProvider runtimeArchProvider, +) { + runtimeName = runtimeNameProvider + runtimeVersion = runtimeVersionProvider + runtimeOS = runtimeOSProvider + runtimeArch = runtimeArchProvider +} + +func setDefaultUserProviders() { + setUserProviders(defaultOwnerProvider) +} + +func setUserProviders(ownerProvider ownerProvider) { + owner = ownerProvider +} + +type processPIDDetector struct{} +type processExecutableNameDetector struct{} +type processExecutablePathDetector struct{} +type processCommandArgsDetector struct{} +type processOwnerDetector struct{} +type processRuntimeNameDetector struct{} +type processRuntimeVersionDetector struct{} +type processRuntimeDescriptionDetector struct{} + +// Detect returns a *Resource that describes the process identifier (PID) of the +// executing process. +func (processPIDDetector) Detect(ctx context.Context) (*Resource, error) { + return NewWithAttributes(semconv.ProcessPIDKey.Int(pid())), nil +} + +// Detect returns a *Resource that describes the name of the process executable. +func (processExecutableNameDetector) Detect(ctx context.Context) (*Resource, error) { + executableName := filepath.Base(commandArgs()[0]) + + return NewWithAttributes(semconv.ProcessExecutableNameKey.String(executableName)), nil +} + +// Detect returns a *Resource that describes the full path of the process executable. +func (processExecutablePathDetector) Detect(ctx context.Context) (*Resource, error) { + executablePath, err := executablePath() + if err != nil { + return nil, err + } + + return NewWithAttributes(semconv.ProcessExecutablePathKey.String(executablePath)), nil +} + +// Detect returns a *Resource that describes all the command arguments as received +// by the process. +func (processCommandArgsDetector) Detect(ctx context.Context) (*Resource, error) { + return NewWithAttributes(semconv.ProcessCommandArgsKey.Array(commandArgs())), nil +} + +// Detect returns a *Resource that describes the username of the user that owns the +// process. +func (processOwnerDetector) Detect(ctx context.Context) (*Resource, error) { + owner, err := owner() + if err != nil { + return nil, err + } + + return NewWithAttributes(semconv.ProcessOwnerKey.String(owner.Username)), nil +} + +// Detect returns a *Resource that describes the name of the compiler used to compile +// this process image. +func (processRuntimeNameDetector) Detect(ctx context.Context) (*Resource, error) { + return NewWithAttributes(semconv.ProcessRuntimeNameKey.String(runtimeName())), nil +} + +// Detect returns a *Resource that describes the version of the runtime of this process. +func (processRuntimeVersionDetector) Detect(ctx context.Context) (*Resource, error) { + return NewWithAttributes(semconv.ProcessRuntimeVersionKey.String(runtimeVersion())), nil +} + +// Detect returns a *Resource that describes the runtime of this process. +func (processRuntimeDescriptionDetector) Detect(ctx context.Context) (*Resource, error) { + runtimeDescription := fmt.Sprintf( + "go version %s %s/%s", runtimeVersion(), runtimeOS(), runtimeArch()) + + return NewWithAttributes( + semconv.ProcessRuntimeDescriptionKey.String(runtimeDescription), + ), nil +} + +// WithProcessPID adds an attribute with the process identifier (PID) to the +// configured Resource. +func WithProcessPID() Option { + return WithDetectors(processPIDDetector{}) +} + +// WithProcessExecutableName adds an attribute with the name of the process +// executable to the configured Resource. +func WithProcessExecutableName() Option { + return WithDetectors(processExecutableNameDetector{}) +} + +// WithProcessExecutablePath adds an attribute with the full path to the process +// executable to the configured Resource. +func WithProcessExecutablePath() Option { + return WithDetectors(processExecutablePathDetector{}) +} + +// WithProcessCommandArgs adds an attribute with all the command arguments (including +// the command/executable itself) as received by the process the configured Resource. +func WithProcessCommandArgs() Option { + return WithDetectors(processCommandArgsDetector{}) +} + +// WithProcessOwner adds an attribute with the username of the user that owns the process +// to the configured Resource. +func WithProcessOwner() Option { + return WithDetectors(processOwnerDetector{}) +} + +// WithProcessRuntimeName adds an attribute with the name of the runtime of this +// process to the configured Resource. +func WithProcessRuntimeName() Option { + return WithDetectors(processRuntimeNameDetector{}) +} + +// WithProcessRuntimeVersion adds an attribute with the version of the runtime of +// this process to the configured Resource. +func WithProcessRuntimeVersion() Option { + return WithDetectors(processRuntimeVersionDetector{}) +} + +// WithProcessRuntimeDescription adds an attribute with an additional description +// about the runtime of the process to the configured Resource. +func WithProcessRuntimeDescription() Option { + return WithDetectors(processRuntimeDescriptionDetector{}) +} + +// WithProcess adds all the Process attributes to the configured Resource. +// See individual WithProcess* functions to configure specific attributes. +func WithProcess() Option { + return WithDetectors( + processPIDDetector{}, + processExecutableNameDetector{}, + processExecutablePathDetector{}, + processCommandArgsDetector{}, + processOwnerDetector{}, + processRuntimeNameDetector{}, + processRuntimeVersionDetector{}, + processRuntimeDescriptionDetector{}, + ) +} diff --git a/sdk/resource/process_test.go b/sdk/resource/process_test.go new file mode 100644 index 00000000000..d4692293d3e --- /dev/null +++ b/sdk/resource/process_test.go @@ -0,0 +1,302 @@ +// 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 resource_test + +import ( + "context" + "fmt" + "os" + "os/user" + "runtime" + "testing" + + "github.com/stretchr/testify/require" + + "go.opentelemetry.io/otel/sdk/resource" +) + +var ( + fakePID = 123 + fakeExecutablePath = "/fake/path/mock" + fakeCommandArgs = []string{"mock", "-t", "30"} + fakeOwner = "gopher" + fakeRuntimeName = "gcmock" + fakeRuntimeVersion = "go1.2.3" + fakeRuntimeOS = "linux" + fakeRuntimeArch = "amd64" +) + +var ( + fakeExecutableName = "mock" + fakeRuntimeDescription = "go version go1.2.3 linux/amd64" +) + +var ( + fakePidProvider = func() int { return fakePID } + fakeExecutablePathProvider = func() (string, error) { return fakeExecutablePath, nil } + fakeCommandArgsProvider = func() []string { return fakeCommandArgs } + fakeOwnerProvider = func() (*user.User, error) { return &user.User{Username: fakeOwner}, nil } + fakeRuntimeNameProvider = func() string { return fakeRuntimeName } + fakeRuntimeVersionProvider = func() string { return fakeRuntimeVersion } + fakeRuntimeOSProvider = func() string { return fakeRuntimeOS } + fakeRuntimeArchProvider = func() string { return fakeRuntimeArch } +) + +var ( + fakeExecutablePathProviderWithError = func() (string, error) { + return "", fmt.Errorf("Unable to get process executable") + } + fakeOwnerProviderWithError = func() (*user.User, error) { + return nil, fmt.Errorf("Unable to get process user") + } +) + +func mockProcessAttributesProviders() { + resource.SetOSProviders( + fakePidProvider, + fakeExecutablePathProvider, + fakeCommandArgsProvider, + ) + resource.SetRuntimeProviders( + fakeRuntimeNameProvider, + fakeRuntimeVersionProvider, + fakeRuntimeOSProvider, + fakeRuntimeArchProvider, + ) + resource.SetUserProviders( + fakeOwnerProvider, + ) +} + +func mockProcessAttributesProvidersWithErrors() { + resource.SetOSProviders( + fakePidProvider, + fakeExecutablePathProviderWithError, + fakeCommandArgsProvider, + ) + resource.SetRuntimeProviders( + fakeRuntimeNameProvider, + fakeRuntimeVersionProvider, + fakeRuntimeOSProvider, + fakeRuntimeArchProvider, + ) + resource.SetUserProviders( + fakeOwnerProviderWithError, + ) +} + +func restoreProcessAttributesProviders() { + resource.SetDefaultOSProviders() + resource.SetDefaultRuntimeProviders() + resource.SetDefaultUserProviders() +} + +func TestWithProcessFuncs(t *testing.T) { + mockProcessAttributesProviders() + + t.Run("WithPID", testWithProcessPID) + t.Run("WithExecutableName", testWithProcessExecutableName) + t.Run("WithExecutablePath", testWithProcessExecutablePath) + t.Run("WithCommandArgs", testWithProcessCommandArgs) + t.Run("WithOwner", testWithProcessOwner) + t.Run("WithRuntimeName", testWithProcessRuntimeName) + t.Run("WithRuntimeVersion", testWithProcessRuntimeVersion) + t.Run("WithRuntimeDescription", testWithProcessRuntimeDescription) + t.Run("WithProcess", testWithProcess) + + restoreProcessAttributesProviders() +} + +func TestWithProcessFuncsErrors(t *testing.T) { + mockProcessAttributesProvidersWithErrors() + + t.Run("WithPID", testWithProcessExecutablePathError) + t.Run("WithExecutableName", testWithProcessOwnerError) + + restoreProcessAttributesProviders() +} + +func TestCommandArgs(t *testing.T) { + require.EqualValues(t, os.Args, resource.CommandArgs()) +} + +func TestRuntimeName(t *testing.T) { + require.EqualValues(t, runtime.Compiler, resource.RuntimeName()) +} + +func TestRuntimeOS(t *testing.T) { + require.EqualValues(t, runtime.GOOS, resource.RuntimeOS()) +} + +func TestRuntimeArch(t *testing.T) { + require.EqualValues(t, runtime.GOARCH, resource.RuntimeArch()) +} + +func testWithProcessPID(t *testing.T) { + ctx := context.Background() + + res, err := resource.New(ctx, + resource.WithoutBuiltin(), + resource.WithProcessPID(), + ) + + require.NoError(t, err) + require.EqualValues(t, map[string]string{ + "process.pid": fmt.Sprint(fakePID), + }, toMap(res)) +} + +func testWithProcessExecutableName(t *testing.T) { + ctx := context.Background() + + res, err := resource.New(ctx, + resource.WithoutBuiltin(), + resource.WithProcessExecutableName(), + ) + + require.NoError(t, err) + require.EqualValues(t, map[string]string{ + "process.executable.name": fakeExecutableName, + }, toMap(res)) +} + +func testWithProcessExecutablePath(t *testing.T) { + ctx := context.Background() + + res, err := resource.New(ctx, + resource.WithoutBuiltin(), + resource.WithProcessExecutablePath(), + ) + + require.NoError(t, err) + require.EqualValues(t, map[string]string{ + "process.executable.path": fakeExecutablePath, + }, toMap(res)) +} + +func testWithProcessCommandArgs(t *testing.T) { + ctx := context.Background() + + res, err := resource.New(ctx, + resource.WithoutBuiltin(), + resource.WithProcessCommandArgs(), + ) + + require.NoError(t, err) + require.EqualValues(t, map[string]string{ + "process.command_args": fmt.Sprint(fakeCommandArgs), + }, toMap(res)) +} + +func testWithProcessOwner(t *testing.T) { + ctx := context.Background() + + res, err := resource.New(ctx, + resource.WithoutBuiltin(), + resource.WithProcessOwner(), + ) + + require.NoError(t, err) + require.EqualValues(t, map[string]string{ + "process.owner": fakeOwner, + }, toMap(res)) +} + +func testWithProcessRuntimeName(t *testing.T) { + ctx := context.Background() + + res, err := resource.New(ctx, + resource.WithoutBuiltin(), + resource.WithProcessRuntimeName(), + ) + + require.NoError(t, err) + require.EqualValues(t, map[string]string{ + "process.runtime.name": fakeRuntimeName, + }, toMap(res)) +} + +func testWithProcessRuntimeVersion(t *testing.T) { + ctx := context.Background() + + res, err := resource.New(ctx, + resource.WithoutBuiltin(), + resource.WithProcessRuntimeVersion(), + ) + + require.NoError(t, err) + require.EqualValues(t, map[string]string{ + "process.runtime.version": fakeRuntimeVersion, + }, toMap(res)) +} + +func testWithProcessRuntimeDescription(t *testing.T) { + ctx := context.Background() + + res, err := resource.New(ctx, + resource.WithoutBuiltin(), + resource.WithProcessRuntimeDescription(), + ) + + require.NoError(t, err) + require.EqualValues(t, map[string]string{ + "process.runtime.description": fakeRuntimeDescription, + }, toMap(res)) +} + +func testWithProcess(t *testing.T) { + ctx := context.Background() + + res, err := resource.New(ctx, + resource.WithoutBuiltin(), + resource.WithProcess(), + ) + + require.NoError(t, err) + require.EqualValues(t, map[string]string{ + "process.pid": fmt.Sprint(fakePID), + "process.executable.name": fakeExecutableName, + "process.executable.path": fakeExecutablePath, + "process.command_args": fmt.Sprint(fakeCommandArgs), + "process.owner": fakeOwner, + "process.runtime.name": fakeRuntimeName, + "process.runtime.version": fakeRuntimeVersion, + "process.runtime.description": fakeRuntimeDescription, + }, toMap(res)) +} + +func testWithProcessExecutablePathError(t *testing.T) { + ctx := context.Background() + + res, err := resource.New(ctx, + resource.WithoutBuiltin(), + resource.WithProcessExecutablePath(), + ) + + require.Error(t, err) + require.EqualValues(t, map[string]string{}, toMap(res)) +} + +func testWithProcessOwnerError(t *testing.T) { + ctx := context.Background() + + res, err := resource.New(ctx, + resource.WithoutBuiltin(), + resource.WithProcessOwner(), + ) + + require.Error(t, err) + require.EqualValues(t, map[string]string{}, toMap(res)) +} diff --git a/semconv/resource.go b/semconv/resource.go index 188dacee4f1..69a9b5a6f33 100644 --- a/semconv/resource.go +++ b/semconv/resource.go @@ -116,8 +116,24 @@ const ( // `proc/[pid]/cmdline`. On Windows, can be set to the result of // `GetCommandLineW`. ProcessCommandLineKey = attribute.Key("process.command_line") + // All the command arguments (including the command/executable itself) + // as received by the process. On Linux-based systems (and some other + // Unixoid systems supporting procfs), can be set according to the list + // of null-delimited strings extracted from `proc/[pid]/cmdline`. For + // libc-based executables, this would be the full argv vector passed to + // `main`. + ProcessCommandArgsKey = attribute.Key("process.command_args") // The username of the user that owns the process. ProcessOwnerKey = attribute.Key("process.owner") + // The name of the runtime of this process. For compiled native + // binaries, this SHOULD be the name of the compiler. + ProcessRuntimeNameKey = attribute.Key("process.runtime.name") + // The version of the runtime of this process, as returned by the + // runtime without modification. + ProcessRuntimeVersionKey = attribute.Key("process.runtime.version") + // An additional description about the runtime of the process, for + // example a specific vendor customization of the runtime environment. + ProcessRuntimeDescriptionKey = attribute.Key("process.runtime.description") ) // Semantic conventions for Kubernetes resource attribute keys. @@ -183,6 +199,14 @@ const ( K8SCronJobNameKey = attribute.Key("k8s.cronjob.name") ) +// Semantic conventions for OS resource attribute keys. +const ( + // The operating system type. + OSTypeKey = attribute.Key("os.type") + // Human readable (not intended to be parsed) OS version information. + OSDescriptionKey = attribute.Key("os.description") +) + // Semantic conventions for host resource attribute keys. const ( // A uniquely identifying name for the host: 'hostname', FQDN, or user specified name