diff --git a/CHANGELOG.md b/CHANGELOG.md index 4fe3ebc..9f4e334 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [1.1.0-beta.12-wip] +### Fixed +- **`host.arch` no longer reports the hostname** (#90). The IO resource + detector copy-pasted `Platform.localHostname` into `host.arch`; it now + resolves the real CPU architecture (`amd64`/`arm64`/`arm32`/`x86`/…) + from `Platform.version`, mapped to registry values, and omits the + attribute when it can't be parsed. Fixes downstream consumers that + select per-architecture artifacts (e.g. debug symbols) off the resource. +- The IO detector now keys every attribute from the generated registry + enums (`Host.*`, `Os.*`, `ProcessAttributes.*`) instead of string + literals, so a mistyped key is a compile error — the class of bug that + caused #90. The malformed `host.os.name` is corrected to `os.name`. + +### Removed +- The IO resource detector no longer emits `host.processors`, + `host.locale`, or `process.num_threads` — none are OpenTelemetry + registry attributes. + ## [1.1.0-beta.11] - 2026-07-20 ### Changed - Doc only, README.md platform updates and clarity. diff --git a/lib/src/resource/native_detectors_io.dart b/lib/src/resource/native_detectors_io.dart index 219043f..4ffcbcf 100644 --- a/lib/src/resource/native_detectors_io.dart +++ b/lib/src/resource/native_detectors_io.dart @@ -26,13 +26,16 @@ class ProcessResourceDetector implements ResourceDetector { if (OTelFactory.otelFactory == null) { throw StateError('OTel initialize must be called first.'); } + // Keys come from the generated registry enums (never string literals), + // so a typo is a compile error — the class of bug that put the hostname + // in host.arch (#90). return ResourceCreate.create( - OTelFactory.otelFactory!.attributesFromMap({ - 'process.executable.name': io.Platform.executable, - 'process.command_line': io.Platform.executableArguments.join(' '), - 'process.runtime.name': 'dart', - 'process.runtime.version': io.Platform.version, - 'process.num_threads': io.Platform.numberOfProcessors.toString(), + OTelFactory.otelFactory!.attributesFromMap({ + ProcessAttributes.processExecutableName.key: io.Platform.executable, + ProcessAttributes.processCommandLine.key: + io.Platform.executableArguments.join(' '), + ProcessAttributes.processRuntimeName.key: 'dart', + ProcessAttributes.processRuntimeVersion.key: io.Platform.version, }), ); } @@ -53,29 +56,44 @@ class HostResourceDetector implements ResourceDetector { throw StateError('OTel initialize must be called first.'); } final attributes = { - 'host.name': io.Platform.localHostname, - 'host.arch': io.Platform.localHostname, - 'host.processors': io.Platform.numberOfProcessors, - 'host.os.name': io.Platform.operatingSystem, - 'host.locale': io.Platform.localeName, + Host.hostName.key: io.Platform.localHostname, + if (_hostArch() case final arch?) Host.hostArch.key: arch, + Os.osName.key: io.Platform.operatingSystem, + Os.osVersion.key: io.Platform.operatingSystemVersion, }; - if (io.Platform.isLinux) { - attributes['os.type'] = 'linux'; - } else if (io.Platform.isWindows) { - attributes['os.type'] = 'windows'; - } else if (io.Platform.isMacOS) { - attributes['os.type'] = 'macos'; - } else if (io.Platform.isAndroid) { - attributes['os.type'] = 'android'; - } else if (io.Platform.isIOS) { - attributes['os.type'] = 'ios'; + final osType = switch (io.Platform.operatingSystem) { + 'linux' => 'linux', + 'windows' => 'windows', + 'macos' => 'macos', + 'android' => 'android', + 'ios' => 'ios', + _ => null, + }; + if (osType != null) { + attributes[Os.osType.key] = osType; } - attributes['os.version'] = io.Platform.operatingSystemVersion; - return ResourceCreate.create( OTelFactory.otelFactory!.attributesFromMap(attributes), ); } } + +/// Resolves `host.arch` to a registry value (`amd64`, `arm64`, `arm32`, +/// `x86`, `riscv64`, …) from the runtime's `Platform.version`, whose tail +/// reads `on "_"`. Pure Dart, no `dart:ffi`. Returns `null` when +/// the token can't be parsed, so the attribute is simply omitted. +String? _hostArch() { + final match = + RegExp(r'on "[a-z0-9]+_([a-z0-9]+)"').firstMatch(io.Platform.version); + return switch (match?.group(1)) { + 'arm64' => 'arm64', + 'arm' => 'arm32', + 'x64' => 'amd64', + 'ia32' => 'x86', + 'riscv64' => 'riscv64', + 'riscv32' => 'riscv32', + final other => other, // pass through unknown tokens; null omits it + }; +} diff --git a/test/unit/sdk/resource_detector_test.dart b/test/unit/sdk/resource_detector_test.dart index 3679538..3c5c99c 100644 --- a/test/unit/sdk/resource_detector_test.dart +++ b/test/unit/sdk/resource_detector_test.dart @@ -58,7 +58,6 @@ void main() { expect(attrs.containsKey('process.command_line'), isTrue); expect(attrs.containsKey('process.runtime.name'), isTrue); expect(attrs.containsKey('process.runtime.version'), isTrue); - expect(attrs.containsKey('process.num_threads'), isTrue); }); test('detect() includes process.executable.name', () async { @@ -92,16 +91,6 @@ void main() { expect(version, equals(io.Platform.version)); }); - test('detect() includes process.num_threads as string', () async { - final detector = ProcessResourceDetector(); - final resource = await detector.detect(); - final attrs = resource.attributes.toMap(); - - final numThreads = attrs['process.num_threads']?.value; - expect(numThreads, isA()); - expect(numThreads, equals(io.Platform.numberOfProcessors.toString())); - }); - test('detect() includes process.command_line', () async { final detector = ProcessResourceDetector(); final resource = await detector.detect(); @@ -125,10 +114,28 @@ void main() { expect(attrs, isNotEmpty); expect(attrs.containsKey('host.name'), isTrue); + // host.arch is a registry CPU-arch value, never the hostname (#90). expect(attrs.containsKey('host.arch'), isTrue); - expect(attrs.containsKey('host.processors'), isTrue); - expect(attrs.containsKey('host.os.name'), isTrue); - expect(attrs.containsKey('host.locale'), isTrue); + expect( + const { + 'amd64', + 'arm32', + 'arm64', + 'ia64', + 'ppc32', + 'ppc64', + 'riscv32', + 'riscv64', + 's390x', + 'x86' + }, + contains(attrs['host.arch']?.value), + reason: 'host.arch must be a registry architecture, not the hostname', + ); + expect(attrs['host.arch']?.value, + isNot(equals(io.Platform.localHostname))); + // os.name (registry), not the malformed 'host.os.name' (#90). + expect(attrs.containsKey('os.name'), isTrue); expect(attrs.containsKey('os.version'), isTrue); }); @@ -164,28 +171,17 @@ void main() { } }); - test('detect() includes host.os.name', () async { + test('detect() includes os.name', () async { final detector = HostResourceDetector(); final resource = await detector.detect(); final attrs = resource.attributes.toMap(); - final osName = attrs['host.os.name']?.value; + final osName = attrs['os.name']?.value; expect(osName, isA()); expect((osName as String).isNotEmpty, isTrue); expect(osName, equals(io.Platform.operatingSystem)); }); - test('detect() includes host.processors as integer', () async { - final detector = HostResourceDetector(); - final resource = await detector.detect(); - final attrs = resource.attributes.toMap(); - - final processors = attrs['host.processors']?.value; - expect(processors, isA()); - expect(processors as int, greaterThan(0)); - expect(processors, equals(io.Platform.numberOfProcessors)); - }); - test('detect() includes os.version', () async { final detector = HostResourceDetector(); final resource = await detector.detect(); @@ -196,17 +192,6 @@ void main() { expect((osVersion as String).isNotEmpty, isTrue); expect(osVersion, equals(io.Platform.operatingSystemVersion)); }); - - test('detect() includes host.locale', () async { - final detector = HostResourceDetector(); - final resource = await detector.detect(); - final attrs = resource.attributes.toMap(); - - final locale = attrs['host.locale']?.value; - expect(locale, isA()); - expect((locale as String).isNotEmpty, isTrue); - expect(locale, equals(io.Platform.localeName)); - }); }); // ---------------------------------------------------------------