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