@zap-js/server 0.0.2 → 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/index.js +18 -14
- package/package.json +4 -9
- package/src/bin/zap.rs +0 -154
- package/src/config.rs +0 -253
- package/src/connection_pool.rs +0 -404
- package/src/error.rs +0 -380
- package/src/handler.rs +0 -89
- package/src/ipc.js +0 -10
- package/src/ipc.rs +0 -499
- package/src/lib.rs +0 -433
- package/src/metrics.rs +0 -264
- package/src/proxy.rs +0 -436
- package/src/reliability.rs +0 -917
- package/src/request.rs +0 -60
- package/src/request_id.rs +0 -97
- package/src/response.rs +0 -182
- package/src/rpc.js +0 -14
- package/src/server.rs +0 -597
- package/src/static.rs +0 -572
- package/src/types.js +0 -21
- package/src/utils.rs +0 -18
- package/src/websocket.rs +0 -429
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
|
-
}
|