Line data Source code
1 : // Licensed under the Apache License, Version 2.0
2 : // Copyright 2025, Michael Bushe, All rights reserved.
3 :
4 : import '../certificate_utils.dart';
5 :
6 : /// Configuration for the OpenTelemetry span exporter that exports spans using OTLP over HTTP/protobuf
7 : class OtlpHttpExporterConfig {
8 : /// The endpoint to export spans to (e.g., 'http://localhost:4318/v1/traces')
9 : /// Default: 'http://localhost:4318'
10 : final String endpoint;
11 :
12 : /// Additional HTTP headers to include in the export requests
13 : final Map<String, String> headers;
14 :
15 : /// The timeout for export HTTP requests
16 : /// Default: 10 seconds
17 : final Duration timeout;
18 :
19 : /// Whether to use gzip compression for the HTTP body
20 : /// Default: false
21 : final bool compression;
22 :
23 : /// Maximum number of retries for failed export requests
24 : /// Default: 3
25 : final int maxRetries;
26 :
27 : /// Base delay for exponential backoff when retrying
28 : /// Default: 100 milliseconds
29 : final Duration baseDelay;
30 :
31 : /// Maximum delay for exponential backoff when retrying
32 : /// Default: 1 second
33 : final Duration maxDelay;
34 :
35 : /// Path to the TLS certificate file for secure connections.
36 : final String? certificate;
37 :
38 : /// Path to the client key file for secure connections with client authentication.
39 : final String? clientKey;
40 :
41 : /// Path to the client certificate file for secure connections with client authentication.
42 : final String? clientCertificate;
43 :
44 : /// Creates a new configuration for the OTLP HTTP span exporter
45 : ///
46 : /// The endpoint must be a valid URL and will default to http://localhost:4318
47 : /// if not specified. The path '/v1/traces' will be appended if not already present.
48 72 : OtlpHttpExporterConfig({
49 : String endpoint = 'http://localhost:4318',
50 : Map<String, String>? headers,
51 : Duration timeout = const Duration(seconds: 10),
52 : this.compression = false,
53 : int maxRetries = 3,
54 : Duration baseDelay = const Duration(milliseconds: 100),
55 : Duration maxDelay = const Duration(seconds: 1),
56 : this.certificate,
57 : this.clientKey,
58 : this.clientCertificate,
59 72 : }) : endpoint = _validateEndpoint(endpoint),
60 72 : headers = _validateHeaders(headers ?? {}),
61 72 : timeout = _validateTimeout(timeout),
62 72 : maxRetries = _validateRetries(maxRetries),
63 72 : baseDelay = _validateDelay(baseDelay, 'baseDelay'),
64 72 : maxDelay = _validateDelay(maxDelay, 'maxDelay') {
65 144 : if (baseDelay.compareTo(maxDelay) > 0) {
66 0 : throw ArgumentError('maxDelay cannot be less than baseDelay');
67 : }
68 288 : _validateCertificates(certificate, clientKey, clientCertificate);
69 : }
70 :
71 72 : static Map<String, String> _validateHeaders(Map<String, String> headers) {
72 72 : final normalized = <String, String>{};
73 72 : for (final entry in headers.entries) {
74 0 : if (entry.key.isEmpty || entry.value.isEmpty) {
75 0 : throw ArgumentError('Header keys and values cannot be empty');
76 : }
77 0 : normalized[entry.key.toLowerCase()] = entry.value;
78 : }
79 : return normalized;
80 : }
81 :
82 72 : static String _validateEndpoint(String endpoint) {
83 72 : if (endpoint.isEmpty) {
84 0 : throw ArgumentError('Endpoint cannot be empty');
85 : }
86 :
87 : // Handle common localhost variants and validate basic format
88 72 : endpoint = endpoint.trim();
89 :
90 : // First check for invalid formats
91 72 : if (endpoint.contains(' ')) {
92 0 : throw ArgumentError('Endpoint cannot contain spaces: $endpoint');
93 : }
94 :
95 : // Ensure endpoint starts with http:// or https://
96 72 : final lcEndpoint = endpoint.toLowerCase();
97 72 : if (!lcEndpoint.startsWith('http://') &&
98 0 : !lcEndpoint.startsWith('https://')) {
99 0 : endpoint = 'http://$endpoint';
100 : }
101 :
102 : // Default port for OTLP/HTTP is 4318
103 72 : if (lcEndpoint == 'http://localhost' ||
104 72 : lcEndpoint == 'http://127.0.0.1' ||
105 72 : lcEndpoint == 'https://localhost' ||
106 72 : lcEndpoint == 'https://127.0.0.1') {
107 0 : return '$endpoint:4318';
108 : }
109 :
110 : // Handle URL format validation
111 : try {
112 72 : final uri = Uri.parse(endpoint);
113 144 : if (uri.host.isEmpty) {
114 0 : throw ArgumentError('Invalid host in endpoint: $endpoint');
115 : }
116 :
117 : // If there's no port and no explicit path, ensure we have the correct default port
118 144 : if (uri.port == 0 && !endpoint.contains(':') && uri.path.isEmpty) {
119 0 : return '${uri.scheme}://${uri.host}:4318';
120 : }
121 :
122 : return endpoint;
123 : } catch (e) {
124 0 : if (e is ArgumentError) rethrow;
125 0 : throw ArgumentError('Invalid URL format in endpoint: $endpoint');
126 : }
127 : }
128 :
129 72 : static Duration _validateTimeout(Duration timeout) {
130 72 : if (timeout < const Duration(milliseconds: 1) ||
131 72 : timeout > const Duration(minutes: 10)) {
132 0 : throw ArgumentError('Timeout must be between 1ms and 10 minutes');
133 : }
134 : return timeout;
135 : }
136 :
137 72 : static int _validateRetries(int retries) {
138 72 : if (retries < 0) {
139 0 : throw ArgumentError('maxRetries cannot be negative');
140 : }
141 : return retries;
142 : }
143 :
144 72 : static Duration _validateDelay(Duration delay, String name) {
145 72 : if (delay < const Duration(milliseconds: 1) ||
146 72 : delay > const Duration(minutes: 5)) {
147 0 : throw ArgumentError('$name must be between 1ms and 5 minutes');
148 : }
149 : return delay;
150 : }
151 :
152 72 : static void _validateCertificates(
153 : String? cert, String? key, String? clientCert) {
154 72 : CertificateUtils.validateCertificates(
155 : certificate: cert,
156 : clientKey: key,
157 : clientCertificate: clientCert,
158 : );
159 : }
160 : }
|