@zap-js/server 0.0.1 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/lib.rs DELETED
@@ -1,433 +0,0 @@
1
- //! # ZapServer
2
- //!
3
- //! Ultra-fast HTTP server framework with Bun-inspired API
4
- //!
5
- //! ## Features
6
- //! - 🚀 **10-100x faster** than Express.js
7
- //! - 🔥 **Zero-allocation** routing with 9ns static route lookup
8
- //! - ⚡ **SIMD-optimized** HTTP parsing
9
- //! - 🎯 **Type-safe** request/response handling
10
- //! - 🧙‍♂️ **Auto-serialization** for JSON responses
11
- //! - 🔧 **Powerful middleware** system
12
- //! - 📁 **Built-in static** file serving
13
- //! - 🌐 **Modern async** throughout
14
- //!
15
- //! ## Quick Start
16
- //!
17
- //! ```no_run
18
- //! use zap_server::{Zap, Json};
19
- //! use serde_json::json;
20
- //!
21
- //! #[tokio::main]
22
- //! async fn main() -> Result<(), Box<dyn std::error::Error>> {
23
- //! let server = Zap::new()
24
- //! .port(3000)
25
- //! .get("/", || "Hello, World!")
26
- //! .get_async("/api/users/:id", |req| async move {
27
- //! let id = req.param("id").unwrap_or("unknown");
28
- //! Json(json!({ "id": id, "name": "John Doe" })).into()
29
- //! })
30
- //! .post_async("/api/users", |req| async move {
31
- //! // Handle user creation
32
- //! Json(json!({ "status": "created" })).into()
33
- //! });
34
- //!
35
- //! println!("🚀 Server running on http://localhost:3000");
36
- //! Ok(server.listen().await?)
37
- //! }
38
- //! ```
39
- //!
40
- //! ## Advanced Usage
41
- //!
42
- //! ```no_run
43
- //! use zap_server::{Zap, Json, StaticOptions};
44
- //! use serde_json::json;
45
- //! use std::collections::HashMap;
46
- //! use std::time::Duration;
47
- //!
48
- //! #[tokio::main]
49
- //! async fn main() -> Result<(), Box<dyn std::error::Error>> {
50
- //! let server = Zap::new()
51
- //! .port(8080)
52
- //! .hostname("0.0.0.0")
53
- //! .keep_alive_timeout(Duration::from_secs(30))
54
- //! .max_request_body_size(50 * 1024 * 1024) // 50MB
55
- //!
56
- //! // Add middleware
57
- //! .logging()
58
- //! .cors()
59
- //!
60
- //! // API routes
61
- //! .json_get("/api/status", |_req| json!({
62
- //! "status": "ok",
63
- //! "version": "1.0.0"
64
- //! }))
65
- //!
66
- //! // Dynamic routes
67
- //! .get_async("/users/:id", |req| async move {
68
- //! let id = req.param("id").unwrap_or("unknown");
69
- //! Json(json!({
70
- //! "id": id,
71
- //! "name": "John Doe",
72
- //! "email": format!("user{}@example.com", id)
73
- //! })).into()
74
- //! })
75
- //!
76
- //! // Static files
77
- //! .static_files("/assets", "./public")
78
- //!
79
- //! // Health endpoints
80
- //! .health_check("/health")
81
- //! .metrics("/metrics");
82
- //!
83
- //! Ok(server.listen().await?)
84
- //! }
85
- //! ```
86
-
87
- pub mod config;
88
- pub mod connection_pool;
89
- pub mod error;
90
- pub mod handler;
91
- pub mod ipc;
92
- pub mod metrics;
93
- pub mod proxy;
94
- pub mod reliability;
95
- pub mod request;
96
- pub mod request_id;
97
- pub mod response;
98
- pub mod server;
99
- pub mod r#static;
100
- pub mod utils;
101
- pub mod websocket;
102
-
103
- // Re-export main types for convenient use
104
- pub use config::{ServerConfig, ZapConfig};
105
- pub use connection_pool::{ConnectionPool, PoolConfig, PoolStats};
106
- pub use error::{ZapError, ZapResult, ErrorResponse};
107
- pub use handler::{AsyncHandler, BoxedHandler, Handler, SimpleHandler};
108
- pub use ipc::{IpcMessage, IpcRequest, IpcServer, IpcClient, IpcEncoding};
109
- pub use proxy::ProxyHandler;
110
- pub use request::RequestData;
111
- pub use response::{Json, ZapResponse};
112
- pub use server::Zap;
113
- pub use r#static::{ETagStrategy, StaticHandler, StaticOptions, handle_static_files_with_headers};
114
- pub use websocket::{WsConfig, WsHandler, handle_websocket_connection, is_websocket_upgrade};
115
- pub use reliability::{
116
- CircuitBreaker, CircuitBreakerConfig, CircuitBreakerStats, CircuitState,
117
- HealthChecker, HealthCheckResponse, HealthStatus, ComponentHealth,
118
- ResilientIpc, RetryConfig,
119
- };
120
-
121
- // Re-export important types from core crate for convenience
122
- pub use zap_core::{Method, StatusCode};
123
-
124
- // Re-export macros for #[zap::export] syntax
125
- pub use zap_macros::export;
126
-
127
- #[cfg(test)]
128
- mod tests {
129
- use super::*;
130
- use serde_json::json;
131
- use std::collections::HashMap;
132
- use std::time::Duration;
133
-
134
- #[test]
135
- fn test_server_creation() {
136
- let server = Zap::new()
137
- .port(8080)
138
- .hostname("0.0.0.0")
139
- .max_request_body_size(1024 * 1024);
140
-
141
- assert_eq!(server.config().port, 8080);
142
- assert_eq!(server.config().hostname, "0.0.0.0");
143
- assert_eq!(server.config().max_request_body_size, 1024 * 1024);
144
- }
145
-
146
- #[test]
147
- fn test_route_registration() {
148
- let server = Zap::new()
149
- .get("/", || "Hello")
150
- .post_async("/users", |_req| async move {
151
- ZapResponse::Text("Created".to_string())
152
- })
153
- .put_async("/users/:id", |_req| async move {
154
- ZapResponse::Text("Updated".to_string())
155
- });
156
-
157
- // Verify routes were registered
158
- assert_eq!(server.router().len(Method::GET), 1);
159
- assert_eq!(server.router().len(Method::POST), 1);
160
- assert_eq!(server.router().len(Method::PUT), 1);
161
- assert_eq!(server.router().total_routes(), 3);
162
- }
163
-
164
- #[test]
165
- fn test_static_files() {
166
- let server = Zap::new()
167
- .static_files("/assets", "./public")
168
- .static_files_with_options(
169
- "/downloads",
170
- "./downloads",
171
- StaticOptions {
172
- directory_listing: true,
173
- cache_control: Some("no-cache".to_string()),
174
- ..Default::default()
175
- },
176
- );
177
-
178
- assert_eq!(server.static_handlers().len(), 2);
179
- assert_eq!(server.static_handlers()[0].prefix, "/assets");
180
- assert_eq!(server.static_handlers()[1].prefix, "/downloads");
181
- assert!(server.static_handlers()[1].options.directory_listing);
182
- }
183
-
184
- #[test]
185
- fn test_json_response_serialization() {
186
- // Test various JSON responses
187
- let simple_json: ZapResponse = Json(json!({"hello": "world"})).into();
188
- let complex_json: ZapResponse = Json(json!({
189
- "user": {
190
- "id": 123,
191
- "name": "John Doe",
192
- "preferences": {
193
- "theme": "dark",
194
- "language": "en"
195
- }
196
- },
197
- "metadata": {
198
- "created_at": "2024-01-01T00:00:00Z",
199
- "version": 1
200
- }
201
- })).into();
202
-
203
- match simple_json {
204
- ZapResponse::Json(value) => {
205
- assert_eq!(value["hello"], "world");
206
- }
207
- _ => panic!("Expected JSON response"),
208
- }
209
-
210
- match complex_json {
211
- ZapResponse::Json(value) => {
212
- assert_eq!(value["user"]["id"], 123);
213
- assert_eq!(value["user"]["preferences"]["theme"], "dark");
214
- }
215
- _ => panic!("Expected JSON response"),
216
- }
217
- }
218
-
219
- #[test]
220
- fn test_request_data_extraction() {
221
- // Test that RequestData properly extracts all request information
222
- let method = Method::POST;
223
- let path = "/api/users/123?include=profile&format=json".to_string();
224
- let headers = {
225
- let mut h = HashMap::new();
226
- h.insert("Content-Type".to_string(), "application/json".to_string());
227
- h.insert("Authorization".to_string(), "Bearer token123".to_string());
228
- h
229
- };
230
- let params = {
231
- let mut p = HashMap::new();
232
- p.insert("id".to_string(), "123".to_string());
233
- p
234
- };
235
- let query = {
236
- let mut q = HashMap::new();
237
- q.insert("include".to_string(), "profile".to_string());
238
- q.insert("format".to_string(), "json".to_string());
239
- q
240
- };
241
- let cookies = {
242
- let mut c = HashMap::new();
243
- c.insert("session".to_string(), "abc123".to_string());
244
- c
245
- };
246
-
247
- let req_data = RequestData {
248
- method,
249
- path: path.clone(),
250
- path_only: "/api/users/123".to_string(),
251
- version: "HTTP/1.1".to_string(),
252
- headers,
253
- body: b"{\"name\": \"John Doe\"}".to_vec(),
254
- params,
255
- query,
256
- cookies,
257
- };
258
-
259
- assert_eq!(req_data.method, Method::POST);
260
- assert_eq!(req_data.path, path);
261
- assert_eq!(req_data.param("id"), Some("123"));
262
- assert_eq!(req_data.query("include"), Some("profile"));
263
- assert_eq!(req_data.query("format"), Some("json"));
264
- assert_eq!(req_data.header("Content-Type"), Some("application/json"));
265
- assert_eq!(req_data.header("Authorization"), Some("Bearer token123"));
266
- assert_eq!(req_data.cookie("session"), Some("abc123"));
267
- assert_eq!(req_data.body_string().unwrap(), r#"{"name": "John Doe"}"#);
268
- }
269
-
270
- #[test]
271
- fn test_response_types() {
272
- use bytes::Bytes;
273
-
274
- // Test all response types
275
- let text_response = ZapResponse::Text("Hello".to_string());
276
- let html_response = ZapResponse::Html("<h1>Hello</h1>".to_string());
277
- let json_response = ZapResponse::Json(serde_json::json!({"key": "value"}));
278
- let bytes_response = ZapResponse::Bytes(Bytes::from("binary data"));
279
- let redirect_response = ZapResponse::Redirect("/new-location".to_string());
280
- let status_response = ZapResponse::Status(StatusCode::NOT_FOUND);
281
-
282
- // All should be valid response types
283
- assert!(matches!(text_response, ZapResponse::Text(_)));
284
- assert!(matches!(html_response, ZapResponse::Html(_)));
285
- assert!(matches!(json_response, ZapResponse::Json(_)));
286
- assert!(matches!(bytes_response, ZapResponse::Bytes(_)));
287
- assert!(matches!(redirect_response, ZapResponse::Redirect(_)));
288
- assert!(matches!(status_response, ZapResponse::Status(_)));
289
- }
290
-
291
- #[tokio::test]
292
- async fn test_full_api_showcase() {
293
- // Showcase the complete, powerful API
294
- let server = Zap::new()
295
- .port(8080)
296
- .hostname("0.0.0.0")
297
- .keep_alive_timeout(Duration::from_secs(30))
298
- .max_request_body_size(50 * 1024 * 1024) // 50MB
299
-
300
- // Add middleware
301
- .logging()
302
- .cors()
303
-
304
- // Simple routes
305
- .get("/", || "Welcome to Zap!")
306
- .get("/about", || "Ultra-fast Rust HTTP server")
307
-
308
- // Routes with parameters
309
- .get_async("/users/:id", |req| async move {
310
- let id = req.param("id").unwrap_or("unknown");
311
- Json(json!({
312
- "id": id,
313
- "name": "John Doe",
314
- "email": format!("user{}@example.com", id)
315
- })).into()
316
- })
317
-
318
- // JSON API endpoints
319
- .json_get("/api/status", |_req| json!({
320
- "status": "ok",
321
- "version": "1.0.0",
322
- "uptime": "5 minutes"
323
- }))
324
-
325
- .json_post("/api/users", |req| {
326
- // In a real app, you'd parse the request body
327
- json!({
328
- "message": "User created",
329
- "id": 123,
330
- "received_headers": req.headers.len()
331
- })
332
- })
333
-
334
- // File operations
335
- .post_async("/api/upload", |req| async move {
336
- let size = req.body.len();
337
- Json(json!({
338
- "message": "File uploaded",
339
- "size": size,
340
- "filename": "uploaded_file.txt"
341
- })).into()
342
- })
343
-
344
- // Advanced routing with query parameters
345
- .get_async("/search", |req| async move {
346
- let query = req.query("q").unwrap_or("").to_string();
347
- let limit: usize = req.query("limit")
348
- .and_then(|s| s.parse().ok())
349
- .unwrap_or(10);
350
-
351
- Json(json!({
352
- "query": query,
353
- "limit": limit,
354
- "results": ["result1", "result2", "result3"]
355
- })).into()
356
- })
357
-
358
- // Health and metrics
359
- .health_check("/health")
360
- .metrics("/metrics")
361
-
362
- // Static file serving
363
- .static_files("/assets", "./public")
364
- .static_files_with_options("/downloads", "./downloads", StaticOptions {
365
- directory_listing: true,
366
- cache_control: Some("no-cache".to_string()),
367
- headers: {
368
- let mut headers = HashMap::new();
369
- headers.insert("X-Custom-Header".to_string(), "Custom Value".to_string());
370
- headers
371
- },
372
- compress: true,
373
- ..Default::default()
374
- })
375
-
376
- // Error handling routes
377
- .get("/error", || {
378
- // This would normally return an error
379
- "This route works fine"
380
- })
381
-
382
- // All HTTP methods
383
- .get("/api/resource", || "GET resource")
384
- .post_async("/api/resource", |_req| async move {
385
- Json(json!({"message": "Created"})).into()
386
- })
387
- .put_async("/api/resource/:id", |req| async move {
388
- let id = req.param("id").unwrap_or("unknown");
389
- Json(json!({"message": "Updated", "id": id})).into()
390
- })
391
- .delete("/api/resource/:id", || "Deleted");
392
-
393
- // Test configuration
394
- assert_eq!(server.config().port, 8080);
395
- assert_eq!(server.config().hostname, "0.0.0.0");
396
- assert_eq!(server.config().max_request_body_size, 50 * 1024 * 1024);
397
-
398
- // Test route registration
399
- assert!(server.router().total_routes() > 10);
400
- assert!(server.router().len(Method::GET) > 5);
401
- assert!(server.router().len(Method::POST) > 1);
402
-
403
- // Test static handlers
404
- assert_eq!(server.static_handlers().len(), 2);
405
- assert!(server.static_handlers()[1].options.directory_listing);
406
- assert_eq!(server.static_handlers()[1].options.cache_control, Some("no-cache".to_string()));
407
-
408
- println!("🎉 Full API showcase configured with {} routes", server.router().total_routes());
409
- }
410
-
411
- // This would be a real integration test if we could start the server
412
- #[tokio::test]
413
- #[ignore] // Ignored because it would actually start a server
414
- async fn test_real_server_integration() {
415
- let server = Zap::new()
416
- .port(3333)
417
- .get("/", || "Hello from integration test!")
418
- .json_get("/api/test", |_req| serde_json::json!({
419
- "message": "Integration test successful",
420
- "timestamp": chrono::Utc::now()
421
- }));
422
-
423
- // In a real test, we'd start the server and make HTTP requests
424
- // server.listen().await.unwrap();
425
-
426
- // Make HTTP requests to test:
427
- // - GET / should return "Hello from integration test!"
428
- // - GET /api/test should return JSON
429
- // - Invalid routes should return 404
430
-
431
- assert_eq!(server.config().port, 3333);
432
- }
433
- }
package/src/metrics.rs DELETED
@@ -1,264 +0,0 @@
1
- //! Prometheus metrics for ZapJS observability
2
- //!
3
- //! Provides:
4
- //! - HTTP request counters, histograms, gauges
5
- //! - IPC handler metrics
6
- //! - Thread-safe global metrics registry
7
-
8
- use lazy_static::lazy_static;
9
- use prometheus::{
10
- CounterVec, Encoder, Gauge, HistogramOpts, HistogramVec, Opts, Registry,
11
- TextEncoder,
12
- };
13
- use std::sync::Once;
14
-
15
- static INIT: Once = Once::new();
16
-
17
- lazy_static! {
18
- /// Global Prometheus metrics registry
19
- pub static ref REGISTRY: Registry = Registry::new();
20
-
21
- // ========================================================================
22
- // HTTP Metrics
23
- // ========================================================================
24
-
25
- /// Total number of HTTP requests
26
- pub static ref HTTP_REQUESTS_TOTAL: CounterVec = CounterVec::new(
27
- Opts::new("zap_http_requests_total", "Total number of HTTP requests"),
28
- &["method", "path", "status"]
29
- ).expect("metric can be created");
30
-
31
- /// HTTP request duration in seconds
32
- pub static ref HTTP_REQUEST_DURATION_SECONDS: HistogramVec = HistogramVec::new(
33
- HistogramOpts::new(
34
- "zap_http_request_duration_seconds",
35
- "HTTP request duration in seconds"
36
- ).buckets(vec![
37
- 0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0
38
- ]),
39
- &["method", "path"]
40
- ).expect("metric can be created");
41
-
42
- /// Number of HTTP requests currently being processed
43
- pub static ref HTTP_REQUESTS_IN_FLIGHT: Gauge = Gauge::new(
44
- "zap_http_requests_in_flight",
45
- "Number of HTTP requests currently being processed"
46
- ).expect("metric can be created");
47
-
48
- // ========================================================================
49
- // IPC Metrics
50
- // ========================================================================
51
-
52
- /// IPC handler invocation duration in seconds
53
- pub static ref IPC_INVOKE_DURATION_SECONDS: HistogramVec = HistogramVec::new(
54
- HistogramOpts::new(
55
- "zap_ipc_invoke_duration_seconds",
56
- "IPC handler invocation duration in seconds"
57
- ).buckets(vec![
58
- 0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0
59
- ]),
60
- &["handler_id"]
61
- ).expect("metric can be created");
62
-
63
- /// Total number of IPC handler errors
64
- pub static ref IPC_HANDLER_ERRORS_TOTAL: CounterVec = CounterVec::new(
65
- Opts::new("zap_ipc_handler_errors_total", "Total number of IPC handler errors"),
66
- &["handler_id", "error_code"]
67
- ).expect("metric can be created");
68
-
69
- /// Total number of IPC handler invocations
70
- pub static ref IPC_INVOCATIONS_TOTAL: CounterVec = CounterVec::new(
71
- Opts::new("zap_ipc_invocations_total", "Total number of IPC handler invocations"),
72
- &["handler_id"]
73
- ).expect("metric can be created");
74
-
75
- // ========================================================================
76
- // Server Info Metrics
77
- // ========================================================================
78
-
79
- /// Server info gauge (always 1, labels contain version info)
80
- pub static ref SERVER_INFO: CounterVec = CounterVec::new(
81
- Opts::new("zap_server_info", "Server information"),
82
- &["version"]
83
- ).expect("metric can be created");
84
-
85
- /// Server start time (unix timestamp)
86
- pub static ref SERVER_START_TIME: Gauge = Gauge::new(
87
- "zap_server_start_time_seconds",
88
- "Unix timestamp when the server started"
89
- ).expect("metric can be created");
90
- }
91
-
92
- /// Initialize and register all metrics with the global registry
93
- pub fn init_metrics() {
94
- INIT.call_once(|| {
95
- // HTTP metrics
96
- REGISTRY
97
- .register(Box::new(HTTP_REQUESTS_TOTAL.clone()))
98
- .expect("HTTP_REQUESTS_TOTAL can be registered");
99
- REGISTRY
100
- .register(Box::new(HTTP_REQUEST_DURATION_SECONDS.clone()))
101
- .expect("HTTP_REQUEST_DURATION_SECONDS can be registered");
102
- REGISTRY
103
- .register(Box::new(HTTP_REQUESTS_IN_FLIGHT.clone()))
104
- .expect("HTTP_REQUESTS_IN_FLIGHT can be registered");
105
-
106
- // IPC metrics
107
- REGISTRY
108
- .register(Box::new(IPC_INVOKE_DURATION_SECONDS.clone()))
109
- .expect("IPC_INVOKE_DURATION_SECONDS can be registered");
110
- REGISTRY
111
- .register(Box::new(IPC_HANDLER_ERRORS_TOTAL.clone()))
112
- .expect("IPC_HANDLER_ERRORS_TOTAL can be registered");
113
- REGISTRY
114
- .register(Box::new(IPC_INVOCATIONS_TOTAL.clone()))
115
- .expect("IPC_INVOCATIONS_TOTAL can be registered");
116
-
117
- // Server info
118
- REGISTRY
119
- .register(Box::new(SERVER_INFO.clone()))
120
- .expect("SERVER_INFO can be registered");
121
- REGISTRY
122
- .register(Box::new(SERVER_START_TIME.clone()))
123
- .expect("SERVER_START_TIME can be registered");
124
-
125
- // Set server start time
126
- SERVER_START_TIME.set(
127
- std::time::SystemTime::now()
128
- .duration_since(std::time::UNIX_EPOCH)
129
- .unwrap()
130
- .as_secs_f64(),
131
- );
132
-
133
- // Set server info (version from Cargo.toml)
134
- SERVER_INFO
135
- .with_label_values(&[env!("CARGO_PKG_VERSION")])
136
- .inc();
137
-
138
- tracing::debug!("Prometheus metrics initialized");
139
- });
140
- }
141
-
142
- /// Encode metrics in Prometheus text format
143
- pub fn encode_metrics() -> String {
144
- let encoder = TextEncoder::new();
145
- let metric_families = REGISTRY.gather();
146
- let mut buffer = Vec::new();
147
-
148
- if let Err(e) = encoder.encode(&metric_families, &mut buffer) {
149
- tracing::error!("Failed to encode metrics: {}", e);
150
- return format!("# Error encoding metrics: {}\n", e);
151
- }
152
-
153
- String::from_utf8(buffer).unwrap_or_else(|e| format!("# Error converting metrics to UTF-8: {}\n", e))
154
- }
155
-
156
- /// Normalize path for metrics by replacing dynamic segments with placeholders
157
- ///
158
- /// This prevents high cardinality in metrics labels.
159
- /// E.g., `/users/123` -> `/users/:id` if route_pattern is provided,
160
- /// otherwise attempts basic normalization.
161
- pub fn normalize_path(path: &str, route_pattern: Option<&str>) -> String {
162
- // If we have the route pattern, use it directly
163
- if let Some(pattern) = route_pattern {
164
- return pattern.to_string();
165
- }
166
-
167
- // Basic normalization: replace UUIDs and numeric IDs
168
- let mut result = path.to_string();
169
-
170
- // Replace UUIDs
171
- let uuid_regex =
172
- regex_lite::Regex::new(r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}")
173
- .unwrap();
174
- result = uuid_regex.replace_all(&result, ":id").to_string();
175
-
176
- // Replace numeric segments using simple pattern (split and reconstruct)
177
- // We handle /123/ and /123 patterns without look-ahead
178
- let parts: Vec<&str> = result.split('/').collect();
179
- let normalized_parts: Vec<String> = parts
180
- .iter()
181
- .map(|part| {
182
- if part.chars().all(|c| c.is_ascii_digit()) && !part.is_empty() {
183
- ":id".to_string()
184
- } else {
185
- part.to_string()
186
- }
187
- })
188
- .collect();
189
- result = normalized_parts.join("/");
190
-
191
- result
192
- }
193
-
194
- /// Record an HTTP request completion
195
- pub fn record_request(method: &str, path: &str, status: u16, duration_secs: f64) {
196
- let status_str = status.to_string();
197
- HTTP_REQUESTS_TOTAL
198
- .with_label_values(&[method, path, &status_str])
199
- .inc();
200
- HTTP_REQUEST_DURATION_SECONDS
201
- .with_label_values(&[method, path])
202
- .observe(duration_secs);
203
- }
204
-
205
- /// Record an IPC handler invocation
206
- pub fn record_ipc_invoke(handler_id: &str, duration_secs: f64, error_code: Option<&str>) {
207
- IPC_INVOCATIONS_TOTAL
208
- .with_label_values(&[handler_id])
209
- .inc();
210
- IPC_INVOKE_DURATION_SECONDS
211
- .with_label_values(&[handler_id])
212
- .observe(duration_secs);
213
-
214
- if let Some(code) = error_code {
215
- IPC_HANDLER_ERRORS_TOTAL
216
- .with_label_values(&[handler_id, code])
217
- .inc();
218
- }
219
- }
220
-
221
- /// Increment in-flight request counter
222
- pub fn inc_in_flight() {
223
- HTTP_REQUESTS_IN_FLIGHT.inc();
224
- }
225
-
226
- /// Decrement in-flight request counter
227
- pub fn dec_in_flight() {
228
- HTTP_REQUESTS_IN_FLIGHT.dec();
229
- }
230
-
231
- #[cfg(test)]
232
- mod tests {
233
- use super::*;
234
-
235
- #[test]
236
- fn test_normalize_path_with_pattern() {
237
- assert_eq!(
238
- normalize_path("/users/123", Some("/users/:id")),
239
- "/users/:id"
240
- );
241
- }
242
-
243
- #[test]
244
- fn test_normalize_path_numeric() {
245
- let result = normalize_path("/users/123/posts/456", None);
246
- assert!(result.contains(":id"));
247
- }
248
-
249
- #[test]
250
- fn test_normalize_path_uuid() {
251
- let result = normalize_path(
252
- "/users/550e8400-e29b-41d4-a716-446655440000",
253
- None,
254
- );
255
- assert!(result.contains(":id"));
256
- }
257
-
258
- #[test]
259
- fn test_encode_metrics() {
260
- init_metrics();
261
- let output = encode_metrics();
262
- assert!(output.contains("zap_"));
263
- }
264
- }