Line data Source code
1 : // Licensed under the Apache License, Version 2.0
2 : // Copyright 2025, Michael Bushe, All rights reserved.
3 :
4 : import 'dart:io' as io;
5 :
6 : import 'package:dartastic_opentelemetry_api/dartastic_opentelemetry_api.dart';
7 :
8 : import '../environment/environment_service.dart';
9 : import 'resource.dart';
10 : import 'web_detector.dart';
11 :
12 : /// Interface for resource detectors that automatically discover resource information.
13 : ///
14 : /// Resource detectors are used to automatically populate resource attributes
15 : /// based on the environment (operating system, platform, etc.).
16 : ///
17 : /// More information:
18 : /// https://opentelemetry.io/docs/specs/otel/resource/sdk/#detecting-resource-information-from-the-environment
19 : abstract class ResourceDetector {
20 : /// Detects resource information from the environment.
21 : ///
22 : /// @return A resource containing the detected attributes
23 : Future<Resource> detect();
24 : }
25 :
26 : /// Detects process-related resource information.
27 : ///
28 : /// This detector populates resource attributes with information about the
29 : /// current process, such as executable name, command line, and runtime information.
30 : ///
31 : /// Semantic conventions:
32 : /// https://opentelemetry.io/docs/specs/semconv/resource/process/
33 : class ProcessResourceDetector implements ResourceDetector {
34 45 : @override
35 : Future<Resource> detect() async {
36 : if (OTelFactory.otelFactory == null) {
37 : throw 'OTel initialize must be called first.';
38 : }
39 135 : return ResourceCreate.create(OTelFactory.otelFactory!.attributesFromMap({
40 45 : 'process.executable.name': io.Platform.executable,
41 90 : 'process.command_line': io.Platform.executableArguments.join(' '),
42 : 'process.runtime.name': 'dart',
43 45 : 'process.runtime.version': io.Platform.version,
44 90 : 'process.num_threads': io.Platform.numberOfProcessors.toString(),
45 : }));
46 : }
47 : }
48 :
49 : /// Detects host-related resource information.
50 : ///
51 : /// This detector populates resource attributes with information about the
52 : /// host machine, such as hostname, architecture, and operating system details.
53 : ///
54 : /// Semantic conventions:
55 : /// https://opentelemetry.io/docs/specs/semconv/resource/host/
56 : class HostResourceDetector implements ResourceDetector {
57 45 : @override
58 : Future<Resource> detect() async {
59 : if (OTelFactory.otelFactory == null) {
60 : throw 'OTel initialize must be called first.';
61 : }
62 45 : final Map<String, Object> attributes = {
63 45 : 'host.name': io.Platform.localHostname,
64 45 : 'host.arch': io.Platform.localHostname,
65 45 : 'host.processors': io.Platform.numberOfProcessors,
66 45 : 'host.os.name': io.Platform.operatingSystem,
67 45 : 'host.locale': io.Platform.localeName,
68 : };
69 :
70 : // Add OS-specific information
71 45 : if (io.Platform.isLinux) {
72 45 : attributes['os.type'] = 'linux';
73 0 : } else if (io.Platform.isWindows) {
74 0 : attributes['os.type'] = 'windows';
75 0 : } else if (io.Platform.isMacOS) {
76 0 : attributes['os.type'] = 'macos';
77 0 : } else if (io.Platform.isAndroid) {
78 0 : attributes['os.type'] = 'android';
79 0 : } else if (io.Platform.isIOS) {
80 0 : attributes['os.type'] = 'ios';
81 : }
82 :
83 90 : attributes['os.version'] = io.Platform.operatingSystemVersion;
84 :
85 45 : return ResourceCreate.create(
86 45 : OTelFactory.otelFactory!.attributesFromMap(attributes));
87 : }
88 : }
89 :
90 : /// Detects resource information from environment variables.
91 : ///
92 : /// This detector looks for the OTEL_RESOURCE_ATTRIBUTES environment variable
93 : /// and parses its contents into resource attributes.
94 : ///
95 : /// More information:
96 : /// https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/#general-sdk-configuration
97 : class EnvVarResourceDetector implements ResourceDetector {
98 : final EnvironmentService _environmentService;
99 :
100 : /// Creates a new EnvVarResourceDetector with the specified environment service.
101 : ///
102 : /// If no environment service is provided, the singleton instance will be used.
103 : ///
104 : /// @param environmentService Optional service for accessing environment variables
105 45 : EnvVarResourceDetector([EnvironmentService? environmentService])
106 45 : : _environmentService = environmentService ?? EnvironmentService.instance;
107 :
108 45 : @override
109 : Future<Resource> detect() async {
110 : if (OTelFactory.otelFactory == null) {
111 : throw 'OTel initialize must be called first.';
112 : }
113 :
114 : //TODO - OTEL_RESOURCE_ATTRIBUTES?
115 : final resourceAttrs =
116 90 : _environmentService.getValue('OTEL_RESOURCE_ATTRIBUTES');
117 0 : if (resourceAttrs == null || resourceAttrs.isEmpty) {
118 45 : return Resource.empty;
119 : }
120 :
121 0 : final attributes = _parseResourceAttributes(resourceAttrs);
122 0 : return ResourceCreate.create(attributes);
123 : }
124 :
125 : /// Parses the OTEL_RESOURCE_ATTRIBUTES environment variable.
126 : ///
127 : /// The format is a comma-separated list of key=value pairs.
128 : /// For example: key1=value1,key2=value2
129 : ///
130 : /// Commas can be escaped with a backslash, and the values can be
131 : /// percent-encoded.
132 : ///
133 : /// @param envValue The value of the OTEL_RESOURCE_ATTRIBUTES environment variable
134 : /// @return Attributes parsed from the environment variable
135 0 : Attributes _parseResourceAttributes(String envValue) {
136 0 : final Map<String, Object> attributes = {};
137 :
138 : // Split on commas, but handle escaped commas
139 0 : final parts = envValue.split(RegExp(r'(?<!\\),'));
140 :
141 0 : for (var part in parts) {
142 : // Remove any leading/trailing whitespace
143 0 : part = part.trim();
144 :
145 : // Split on first equals sign
146 0 : final keyValue = part.split('=');
147 0 : if (keyValue.length != 2) continue;
148 :
149 0 : final key = keyValue[0].trim();
150 0 : var value = keyValue[1].trim();
151 :
152 : // Handle percent-encoded characters
153 0 : value = Uri.decodeComponent(value);
154 :
155 : // Remove escape characters
156 0 : value = value.replaceAll(r'\,', ',');
157 :
158 0 : attributes[key] = value;
159 : }
160 :
161 0 : return OTelFactory.otelFactory!.attributesFromMap(attributes);
162 : }
163 : }
164 :
165 : /// Composite detector that combines multiple resource detectors.
166 : ///
167 : /// This detector runs multiple detectors and merges their results.
168 : /// This is useful for combining resource information from different sources.
169 : ///
170 : /// More information:
171 : /// https://opentelemetry.io/docs/specs/otel/resource/sdk/#resource-creation
172 : class CompositeResourceDetector implements ResourceDetector {
173 : final List<ResourceDetector> _detectors;
174 :
175 : /// Creates a new CompositeResourceDetector with the specified detectors.
176 : ///
177 : /// @param detectors The list of detectors to run
178 45 : CompositeResourceDetector(this._detectors);
179 :
180 45 : @override
181 : Future<Resource> detect() async {
182 : if (OTelFactory.otelFactory == null) {
183 : throw 'OTel initialize must be called first.';
184 : }
185 45 : Resource result = Resource.empty;
186 :
187 90 : for (final detector in _detectors) {
188 : try {
189 45 : final resource = await detector.detect();
190 45 : result = result.merge(resource);
191 : } catch (e) {
192 : // Log error but continue with other detectors
193 3 : if (OTelLog.isError()) OTelLog.error('Error in resource detector: $e');
194 : }
195 : }
196 :
197 : return result;
198 : }
199 : }
200 :
201 : /// Factory for creating platform-appropriate resource detectors.
202 : ///
203 : /// This factory creates a composite detector with the appropriate
204 : /// detectors for the current platform (web or native).
205 : class PlatformResourceDetector {
206 : /// Creates a composite detector with platform-appropriate detectors.
207 : ///
208 : /// @return A ResourceDetector that combines all appropriate detectors
209 45 : static ResourceDetector create() {
210 45 : final detectors = <ResourceDetector>[
211 45 : EnvVarResourceDetector(),
212 : ];
213 :
214 : // For non-web platforms (native)
215 : if (!const bool.fromEnvironment('dart.library.js_interop')) {
216 : try {
217 90 : detectors.addAll([
218 45 : ProcessResourceDetector(),
219 45 : HostResourceDetector(),
220 : ]);
221 : } catch (e) {
222 0 : if (OTelLog.isError()) {
223 0 : OTelLog.error('Error adding native detectors: $e');
224 : }
225 : }
226 : }
227 : // For web platforms
228 : else {
229 : try {
230 0 : detectors.add(WebResourceDetector());
231 : } catch (e) {
232 0 : if (OTelLog.isError()) OTelLog.error('Error adding web detector: $e');
233 : }
234 : }
235 :
236 45 : return CompositeResourceDetector(detectors);
237 : }
238 : }
|