Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
64 changes: 41 additions & 23 deletions lib/src/resource/native_detectors_io.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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(<String, Object>{
ProcessAttributes.processExecutableName.key: io.Platform.executable,
ProcessAttributes.processCommandLine.key:
io.Platform.executableArguments.join(' '),
ProcessAttributes.processRuntimeName.key: 'dart',
ProcessAttributes.processRuntimeVersion.key: io.Platform.version,
}),
);
}
Expand All @@ -53,29 +56,44 @@ class HostResourceDetector implements ResourceDetector {
throw StateError('OTel initialize must be called first.');
}
final attributes = <String, Object>{
'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 "<os>_<arch>"`. 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
};
}
61 changes: 23 additions & 38 deletions test/unit/sdk/resource_detector_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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<String>());
expect(numThreads, equals(io.Platform.numberOfProcessors.toString()));
});

test('detect() includes process.command_line', () async {
final detector = ProcessResourceDetector();
final resource = await detector.detect();
Expand All @@ -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);
});

Expand Down Expand Up @@ -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<String>());
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<int>());
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();
Expand All @@ -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<String>());
expect((locale as String).isNotEmpty, isTrue);
expect(locale, equals(io.Platform.localeName));
});
});

// ---------------------------------------------------------------
Expand Down
Loading