Line data Source code
1 : // Copyright The OpenTelemetry Authors
2 : // SPDX-License-Identifier: Apache-2.0
3 :
4 : import 'certificate_utils.dart';
5 :
6 : /// Configuration for the OtlpGrpcSpanExporter.
7 : ///
8 : /// This class configures how the OTLP gRPC span exporter connects to and communicates
9 : /// with the OpenTelemetry collector or backend. It allows customization of connection
10 : /// parameters such as endpoint, security settings, timeouts, and retry behavior.
11 : class OtlpGrpcExporterConfig {
12 : /// The endpoint to which the exporter will send spans, in the format 'host:port'.
13 : final String endpoint;
14 :
15 : /// Custom headers to include in each gRPC request, for authentication or metadata.
16 : final Map<String, String> headers;
17 :
18 : /// Timeout for gRPC operations, after which they'll fail.
19 : final Duration timeout;
20 :
21 : /// Whether to enable gRPC compression for requests.
22 : final bool compression;
23 :
24 : /// Whether to use an insecure connection (true) or TLS (false).
25 : final bool insecure;
26 :
27 : /// Maximum number of retry attempts for failed exports.
28 : final int maxRetries;
29 :
30 : /// Base delay for retry backoff calculation.
31 : final Duration baseDelay;
32 :
33 : /// Maximum delay between retry attempts.
34 : final Duration maxDelay;
35 :
36 : /// Path to the TLS certificate file for secure connections.
37 : final String? certificate;
38 :
39 : /// Path to the client key file for secure connections with client authentication.
40 : final String? clientKey;
41 :
42 : /// Path to the client certificate file for secure connections with client authentication.
43 : final String? clientCertificate;
44 :
45 : /// Creates a new OtlpGrpcExporterConfig with the specified parameters.
46 : ///
47 : /// This configuration controls the connection and behavior settings for the
48 : /// OTLP gRPC exporter.
49 : ///
50 : /// @param endpoint The endpoint to connect to (default: localhost:4317)
51 : /// @param headers Custom headers to include in the requests
52 : /// @param timeout Timeout for gRPC operations
53 : /// @param compression Whether to enable gRPC compression
54 : /// @param insecure Whether to use an insecure connection
55 : /// @param maxRetries Maximum number of retry attempts
56 : /// @param baseDelay Base delay for retry backoff
57 : /// @param maxDelay Maximum delay between retry attempts
58 : /// @param certificate Path to the TLS certificate file
59 : /// @param clientKey Path to the client key file
60 : /// @param clientCertificate Path to the client certificate file
61 7 : OtlpGrpcExporterConfig({
62 : String endpoint = 'localhost:4317',
63 : Map<String, String>? headers,
64 : Duration timeout = const Duration(seconds: 10),
65 : this.compression = false,
66 : this.insecure = false,
67 : int maxRetries = 3,
68 : Duration baseDelay = const Duration(milliseconds: 100),
69 : Duration maxDelay = const Duration(seconds: 1),
70 : this.certificate,
71 : this.clientKey,
72 : this.clientCertificate,
73 7 : }) : endpoint = _validateEndpoint(endpoint),
74 14 : headers = _validateHeaders(headers ?? {}),
75 7 : timeout = _validateTimeout(timeout),
76 7 : maxRetries = _validateRetries(maxRetries),
77 7 : baseDelay = _validateDelay(baseDelay, 'baseDelay'),
78 7 : maxDelay = _validateDelay(maxDelay, 'maxDelay') {
79 14 : if (baseDelay.compareTo(maxDelay) > 0) {
80 3 : throw ArgumentError('maxDelay cannot be less than baseDelay');
81 : }
82 28 : _validateCertificates(certificate, clientKey, clientCertificate);
83 : }
84 :
85 7 : static Map<String, String> _validateHeaders(Map<String, String> headers) {
86 7 : final normalized = <String, String>{};
87 11 : for (final entry in headers.entries) {
88 14 : if (entry.key.isEmpty || entry.value.isEmpty) {
89 3 : throw ArgumentError('Header keys and values cannot be empty');
90 : }
91 12 : normalized[entry.key.toLowerCase()] = entry.value;
92 : }
93 : return normalized;
94 : }
95 :
96 7 : static String _validateEndpoint(String endpoint) {
97 7 : if (endpoint.isEmpty) {
98 2 : throw ArgumentError('Endpoint cannot be empty');
99 : }
100 :
101 : // Handle common localhost variants and validate basic format
102 7 : endpoint = endpoint.trim();
103 :
104 : // First check for invalid formats
105 7 : if (endpoint.contains(' ')) {
106 6 : throw ArgumentError('Endpoint cannot contain spaces: $endpoint');
107 : }
108 :
109 : // Check for specific invalid formats that might parse but are invalid
110 14 : if (endpoint.contains(':port') || endpoint.contains('://port')) {
111 2 : throw ArgumentError('Invalid port specification in endpoint: $endpoint');
112 : }
113 :
114 7 : final lcEndpoint = endpoint.toLowerCase();
115 14 : if (lcEndpoint == 'localhost' || lcEndpoint == '127.0.0.1') {
116 2 : return '$endpoint:4317'; // Add default port if missing
117 : }
118 :
119 : // Handle URL format validation more carefully
120 11 : if (endpoint.startsWith('http://') || endpoint.startsWith('https://')) {
121 : try {
122 6 : final uri = Uri.parse(endpoint);
123 12 : if (uri.host.isEmpty) {
124 2 : throw ArgumentError('Invalid host in endpoint: $endpoint');
125 : }
126 12 : if (uri.port == 0 && !endpoint.contains(':')) {
127 : // No port specified in URL format, add default
128 0 : return '${uri.scheme}://${uri.host}:4317${uri.path}';
129 : }
130 12 : if (uri.port == 0 &&
131 0 : endpoint.contains(':') &&
132 0 : !endpoint.contains('://:')) {
133 : // Port part exists but might be invalid
134 0 : final portStr = endpoint.split(':').last;
135 0 : if (int.tryParse(portStr) == null) {
136 0 : throw ArgumentError(
137 0 : 'Invalid port format in endpoint URL: $endpoint',
138 : );
139 : }
140 : }
141 : return endpoint;
142 : } catch (e) {
143 1 : if (e is ArgumentError) rethrow;
144 0 : throw ArgumentError('Invalid URL format in endpoint: $endpoint');
145 : }
146 : }
147 :
148 : // Try to parse as URI or host:port
149 : try {
150 4 : final parts = endpoint.split(':');
151 12 : if (parts.length == 1 && parts[0].isNotEmpty) {
152 : // Only host provided, add default port
153 4 : return '${parts[0]}:4317';
154 16 : } else if (parts.length == 2 && parts[0].isNotEmpty) {
155 : // Validate port is a number if specified
156 8 : if (parts[1].isEmpty) {
157 4 : throw ArgumentError('Invalid port format in endpoint: $endpoint');
158 : }
159 8 : if (int.tryParse(parts[1]) == null) {
160 2 : throw ArgumentError('Invalid port format in endpoint: $endpoint');
161 : }
162 : // Host and port provided
163 : return endpoint;
164 : }
165 :
166 1 : throw ArgumentError(
167 1 : 'Invalid endpoint format: $endpoint. Expected format: "host:port" or a valid URI',
168 : );
169 : } catch (e) {
170 2 : if (e is ArgumentError) rethrow; // Re-throw our own errors
171 :
172 : // Any other parsing error
173 0 : throw ArgumentError(
174 0 : 'Invalid endpoint format: $endpoint. Expected format: "host:port" or a valid URI',
175 : );
176 : }
177 : }
178 :
179 7 : static Duration _validateTimeout(Duration timeout) {
180 7 : if (timeout < const Duration(milliseconds: 1) ||
181 7 : timeout > const Duration(minutes: 10)) {
182 3 : throw ArgumentError('Timeout must be between 1ms and 10 minutes');
183 : }
184 : return timeout;
185 : }
186 :
187 7 : static int _validateRetries(int retries) {
188 7 : if (retries < 0) {
189 3 : throw ArgumentError('maxRetries cannot be negative');
190 : }
191 : return retries;
192 : }
193 :
194 7 : static Duration _validateDelay(Duration delay, String name) {
195 7 : if (delay < const Duration(milliseconds: 1) ||
196 7 : delay > const Duration(minutes: 5)) {
197 4 : throw ArgumentError('$name must be between 1ms and 5 minutes');
198 : }
199 : return delay;
200 : }
201 :
202 7 : static void _validateCertificates(
203 : String? cert,
204 : String? key,
205 : String? clientCert,
206 : ) {
207 7 : CertificateUtils.validateCertificates(
208 : certificate: cert,
209 : clientKey: key,
210 : clientCertificate: clientCert,
211 : );
212 : }
213 : }
|