create-feltdb 0.5.6 → 0.5.7

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.
@@ -3,8 +3,12 @@ pub mod application_contract;
3
3
  pub mod artifacts;
4
4
  pub mod audit;
5
5
  pub mod auth;
6
+ pub mod authenticated_principal;
6
7
  pub mod authorization;
7
8
  pub mod backup;
9
+ pub mod certification_harness;
10
+ pub mod delegation_token;
11
+ pub mod durable_operations;
8
12
  pub mod key_management;
9
13
  pub mod causal;
10
14
  pub mod certification;
@@ -15,6 +19,8 @@ pub mod content;
15
19
  pub mod identity;
16
20
  pub mod key_provider;
17
21
  pub mod leases;
22
+ pub mod managed_diagnostics;
23
+ pub mod membership_policy;
18
24
  pub mod metrics;
19
25
  pub mod portable_bundle;
20
26
  pub mod principals;
@@ -22,6 +28,9 @@ pub mod providers;
22
28
  pub mod releases;
23
29
  pub mod request_telemetry;
24
30
  pub mod sessions;
31
+ pub mod snapshot_cursor;
32
+ pub mod tenant_policies;
25
33
  pub mod tenancy;
26
34
  pub mod transaction_idempotency;
35
+ pub mod transaction_recovery;
27
36
  pub mod versions;
@@ -9,7 +9,7 @@ use std::{
9
9
  atomic::{AtomicU64, Ordering},
10
10
  Arc,
11
11
  },
12
- time::Duration,
12
+ time::{Duration, Instant},
13
13
  };
14
14
 
15
15
  use axum::{
@@ -64,6 +64,7 @@ use feltdb_server::{
64
64
  artifacts::{ArtifactKind, ArtifactLifecycle, ArtifactMetadata, ArtifactStore, ProducerRef},
65
65
  audit::{AuditEvent, AuditLog},
66
66
  auth::{KeyStore, Principal},
67
+ authenticated_principal::AuthenticatedPrincipal,
67
68
  authorization::{authorize, AuthorizationRequest, Subject},
68
69
  backup::{
69
70
  create_bundle, create_bundle_with_virtual, restore_bundle, select_bundle_at,
@@ -142,6 +143,19 @@ struct RuntimeResponse {
142
143
  features: Value,
143
144
  }
144
145
 
146
+ #[derive(Serialize)]
147
+ struct ManagedDiagnosticsResponse {
148
+ engine_version: String,
149
+ server_commit: String,
150
+ core_protocol_version: String,
151
+ authorization_contract: u32,
152
+ storage_format: u32,
153
+ promoted_revision: String,
154
+ environment: String,
155
+ recovery: String,
156
+ uptime: String,
157
+ }
158
+
145
159
  #[derive(Serialize)]
146
160
  struct RecordResponse {
147
161
  id: String,
@@ -5657,6 +5671,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
5657
5671
  let provider_store = ProviderStore::load(config.data.with_extension("providers.json"))?;
5658
5672
  let readiness_probe = config.data.with_extension("readiness");
5659
5673
  let state = AppState {
5674
+ started_at: Instant::now(),
5660
5675
  ids: Arc::new(AtomicU64::new(db.sequence()?)),
5661
5676
  db,
5662
5677
  namespace: Arc::from(config.namespace.clone()),
@@ -5749,6 +5764,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
5749
5764
  axum::routing::post(construct_application_contract),
5750
5765
  )
5751
5766
  .route("/v1/identity/me", get(identity_me))
5767
+ .route("/v1/diagnostics", get(managed_diagnostics))
5752
5768
  .route("/v1/identity/sessions", get(identity_sessions))
5753
5769
  .route(
5754
5770
  "/v1/identity/sessions/{id}/revoke",
@@ -6493,7 +6509,12 @@ async fn authenticate(
6493
6509
  let human_contract = principal.subject_type == "human"
6494
6510
  && path.starts_with("/v1/applications/")
6495
6511
  && path.split('/').any(|segment| segment == "contract");
6496
- if !path.starts_with("/api/") && !human_contract && !decision.allowed {
6512
+ let authenticated_diagnostics = path == "/v1/diagnostics";
6513
+ if !path.starts_with("/api/")
6514
+ && !human_contract
6515
+ && !authenticated_diagnostics
6516
+ && !decision.allowed
6517
+ {
6497
6518
  state.metrics.authentication_failure();
6498
6519
  audit(&state, &principal.key_id, required, path, "denied", 403);
6499
6520
  return ApiError(StatusCode::FORBIDDEN, format!("missing scope: {required}"))
@@ -6506,8 +6527,18 @@ async fn authenticate(
6506
6527
  } else {
6507
6528
  state.metrics.state_mutation();
6508
6529
  }
6509
- request.extensions_mut().insert(principal);
6510
- let principal = request
6530
+
6531
+ let authenticated = AuthenticatedPrincipal::from_api_key(
6532
+ principal.key_id.clone(),
6533
+ principal.key_id.clone(),
6534
+ state.namespace.to_string(),
6535
+ principal.scopes.clone(),
6536
+ );
6537
+
6538
+ request.extensions_mut().insert(principal.clone());
6539
+ request.extensions_mut().insert(authenticated.clone());
6540
+
6541
+ let principal_id = request
6511
6542
  .extensions()
6512
6543
  .get::<Principal>()
6513
6544
  .expect("principal inserted")
@@ -6518,7 +6549,7 @@ async fn authenticate(
6518
6549
  let status = response.status().as_u16();
6519
6550
  audit(
6520
6551
  &state,
6521
- &principal,
6552
+ &principal_id,
6522
6553
  required,
6523
6554
  &target,
6524
6555
  if status < 400 { "allowed" } else { "failed" },
@@ -6594,6 +6625,32 @@ async fn health(State(state): State<AppState>) -> Json<HealthResponse<'static>>
6594
6625
  })
6595
6626
  }
6596
6627
 
6628
+ async fn managed_diagnostics(
6629
+ State(state): State<AppState>,
6630
+ ) -> Json<ManagedDiagnosticsResponse> {
6631
+ let recovery = match state.cluster.proposal().map(|value| value.phase) {
6632
+ Some(ProposalPhase::Preparing | ProposalPhase::Prepared) => "recovering",
6633
+ Some(ProposalPhase::Aborted) => "attention-required",
6634
+ _ if !state.lease_clock.is_healthy() => "degraded",
6635
+ _ => "healthy",
6636
+ };
6637
+ let uptime_seconds = state.started_at.elapsed().as_secs();
6638
+
6639
+ Json(ManagedDiagnosticsResponse {
6640
+ engine_version: env!("CARGO_PKG_VERSION").into(),
6641
+ server_commit: option_env!("FELTDB_GIT_COMMIT").unwrap_or("unknown").into(),
6642
+ core_protocol_version: PROTOCOL_VERSION.into(),
6643
+ authorization_contract: 2,
6644
+ storage_format: 1,
6645
+ promoted_revision: std::env::var("FELTDB_PROMOTED_REVISION")
6646
+ .unwrap_or_else(|_| "unknown".into()),
6647
+ environment: std::env::var("FELTDB_ENVIRONMENT")
6648
+ .unwrap_or_else(|_| "development".into()),
6649
+ recovery: recovery.into(),
6650
+ uptime: format!("{uptime_seconds}s"),
6651
+ })
6652
+ }
6653
+
6597
6654
  async fn readiness(State(state): State<AppState>) -> Result<Json<Value>, ApiError> {
6598
6655
  if let Some(parent) = state.readiness_probe.parent() {
6599
6656
  std::fs::create_dir_all(parent)
@@ -0,0 +1,413 @@
1
+ use serde::{Deserialize, Serialize};
2
+ use std::collections::HashMap;
3
+
4
+ /// Engine version for FeltDB.
5
+ pub const ENGINE_VERSION: &str = "0.2.0";
6
+
7
+ /// API protocol version for compatibility tracking.
8
+ pub const PROTOCOL_VERSION: u32 = 1;
9
+
10
+ /// Storage format identifier.
11
+ pub const STORAGE_FORMAT: &str = "feltdb-v2";
12
+
13
+ /// Environment type.
14
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
15
+ #[serde(rename_all = "lowercase")]
16
+ pub enum Environment {
17
+ Dev,
18
+ Staging,
19
+ Prod,
20
+ }
21
+
22
+ /// Health status for subsystems.
23
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
24
+ #[serde(rename_all = "lowercase")]
25
+ pub enum HealthStatus {
26
+ Healthy,
27
+ Degraded,
28
+ Unhealthy,
29
+ }
30
+
31
+ impl HealthStatus {
32
+ pub fn is_healthy(&self) -> bool {
33
+ *self == HealthStatus::Healthy
34
+ }
35
+ }
36
+
37
+ /// Recovery status and health.
38
+ #[derive(Debug, Clone, Serialize, Deserialize)]
39
+ pub struct RecoveryStatus {
40
+ /// Overall recovery system health.
41
+ pub status: HealthStatus,
42
+
43
+ /// Transaction recovery health.
44
+ pub transaction_recovery: HealthStatus,
45
+
46
+ /// Durable operation recovery health.
47
+ pub durable_operations: HealthStatus,
48
+
49
+ /// Last recovery check timestamp (unix seconds).
50
+ pub last_check_at: u64,
51
+
52
+ /// Details about any issues (empty if healthy).
53
+ pub issues: Vec<String>,
54
+
55
+ /// Metrics: count of operations awaiting recovery.
56
+ pub pending_recovery_count: usize,
57
+
58
+ /// Metrics: count of failed recovery attempts (recent).
59
+ pub failed_recovery_attempts: usize,
60
+ }
61
+
62
+ impl RecoveryStatus {
63
+ pub fn is_healthy(&self) -> bool {
64
+ self.status.is_healthy()
65
+ }
66
+ }
67
+
68
+ /// Diagnostic information about server health and status.
69
+ #[derive(Debug, Clone, Serialize, Deserialize)]
70
+ pub struct DiagnosticsReport {
71
+ /// FeltDB engine version.
72
+ pub engine_version: String,
73
+
74
+ /// Git commit hash of this deployment.
75
+ pub server_commit: String,
76
+
77
+ /// API protocol version.
78
+ pub protocol_version: u32,
79
+
80
+ /// Storage format identifier.
81
+ pub storage_format: String,
82
+
83
+ /// Deployment environment.
84
+ pub environment: Environment,
85
+
86
+ /// Overall server health status.
87
+ pub status: HealthStatus,
88
+
89
+ /// Recovery system status and health.
90
+ pub recovery: RecoveryStatus,
91
+
92
+ /// Uptime in seconds since server start.
93
+ pub uptime_seconds: u64,
94
+
95
+ /// Timestamp of this report (unix seconds).
96
+ pub reported_at: u64,
97
+
98
+ /// Optional detailed metrics by subsystem.
99
+ pub subsystems: HashMap<String, SubsystemMetrics>,
100
+ }
101
+
102
+ /// Metrics for a single subsystem.
103
+ #[derive(Debug, Clone, Serialize, Deserialize)]
104
+ pub struct SubsystemMetrics {
105
+ pub name: String,
106
+ pub status: HealthStatus,
107
+ pub request_count: u64,
108
+ pub error_count: u64,
109
+ pub avg_latency_ms: f64,
110
+ pub details: HashMap<String, serde_json::Value>,
111
+ }
112
+
113
+ /// Diagnostics collector and reporter.
114
+ pub struct DiagnosticsCollector {
115
+ engine_version: String,
116
+ server_commit: String,
117
+ protocol_version: u32,
118
+ storage_format: String,
119
+ environment: Environment,
120
+ start_time: u64,
121
+ }
122
+
123
+ impl DiagnosticsCollector {
124
+ /// Creates a new diagnostics collector.
125
+ pub fn new(
126
+ server_commit: String,
127
+ environment: Environment,
128
+ ) -> Self {
129
+ Self {
130
+ engine_version: ENGINE_VERSION.to_string(),
131
+ server_commit,
132
+ protocol_version: PROTOCOL_VERSION,
133
+ storage_format: STORAGE_FORMAT.to_string(),
134
+ environment,
135
+ start_time: now(),
136
+ }
137
+ }
138
+
139
+ /// Generates a diagnostics report.
140
+ pub fn collect_report(
141
+ &self,
142
+ recovery_status: RecoveryStatus,
143
+ ) -> DiagnosticsReport {
144
+ let uptime = now() - self.start_time;
145
+ let overall_status = if recovery_status.status.is_healthy() {
146
+ HealthStatus::Healthy
147
+ } else {
148
+ HealthStatus::Degraded
149
+ };
150
+
151
+ DiagnosticsReport {
152
+ engine_version: self.engine_version.clone(),
153
+ server_commit: self.server_commit.clone(),
154
+ protocol_version: self.protocol_version,
155
+ storage_format: self.storage_format.clone(),
156
+ environment: self.environment,
157
+ status: overall_status,
158
+ recovery: recovery_status,
159
+ uptime_seconds: uptime,
160
+ reported_at: now(),
161
+ subsystems: HashMap::new(),
162
+ }
163
+ }
164
+
165
+ /// Adds subsystem metrics to the report.
166
+ pub fn add_subsystem_metrics(
167
+ report: &mut DiagnosticsReport,
168
+ name: String,
169
+ status: HealthStatus,
170
+ request_count: u64,
171
+ error_count: u64,
172
+ avg_latency_ms: f64,
173
+ details: HashMap<String, serde_json::Value>,
174
+ ) {
175
+ report.subsystems.insert(
176
+ name.clone(),
177
+ SubsystemMetrics {
178
+ name,
179
+ status,
180
+ request_count,
181
+ error_count,
182
+ avg_latency_ms,
183
+ details,
184
+ },
185
+ );
186
+ }
187
+ }
188
+
189
+ impl Default for RecoveryStatus {
190
+ fn default() -> Self {
191
+ Self {
192
+ status: HealthStatus::Healthy,
193
+ transaction_recovery: HealthStatus::Healthy,
194
+ durable_operations: HealthStatus::Healthy,
195
+ last_check_at: now(),
196
+ issues: vec![],
197
+ pending_recovery_count: 0,
198
+ failed_recovery_attempts: 0,
199
+ }
200
+ }
201
+ }
202
+
203
+ /// Builder for RecoveryStatus for easy construction.
204
+ pub struct RecoveryStatusBuilder {
205
+ status: HealthStatus,
206
+ transaction_recovery: HealthStatus,
207
+ durable_operations: HealthStatus,
208
+ issues: Vec<String>,
209
+ pending_recovery_count: usize,
210
+ failed_recovery_attempts: usize,
211
+ }
212
+
213
+ impl RecoveryStatusBuilder {
214
+ pub fn new() -> Self {
215
+ Self {
216
+ status: HealthStatus::Healthy,
217
+ transaction_recovery: HealthStatus::Healthy,
218
+ durable_operations: HealthStatus::Healthy,
219
+ issues: vec![],
220
+ pending_recovery_count: 0,
221
+ failed_recovery_attempts: 0,
222
+ }
223
+ }
224
+
225
+ pub fn with_transaction_recovery(mut self, status: HealthStatus) -> Self {
226
+ self.transaction_recovery = status;
227
+ self
228
+ }
229
+
230
+ pub fn with_durable_operations(mut self, status: HealthStatus) -> Self {
231
+ self.durable_operations = status;
232
+ self
233
+ }
234
+
235
+ pub fn with_issue(mut self, issue: String) -> Self {
236
+ self.issues.push(issue);
237
+ if self.status == HealthStatus::Healthy {
238
+ self.status = HealthStatus::Degraded;
239
+ }
240
+ self
241
+ }
242
+
243
+ pub fn with_pending_recovery_count(mut self, count: usize) -> Self {
244
+ self.pending_recovery_count = count;
245
+ self
246
+ }
247
+
248
+ pub fn with_failed_attempts(mut self, count: usize) -> Self {
249
+ self.failed_recovery_attempts = count;
250
+ self
251
+ }
252
+
253
+ pub fn build(mut self) -> RecoveryStatus {
254
+ // Determine overall status from subsystems
255
+ if self.transaction_recovery != HealthStatus::Healthy
256
+ || self.durable_operations != HealthStatus::Healthy
257
+ {
258
+ self.status = HealthStatus::Degraded;
259
+ }
260
+
261
+ RecoveryStatus {
262
+ status: self.status,
263
+ transaction_recovery: self.transaction_recovery,
264
+ durable_operations: self.durable_operations,
265
+ last_check_at: now(),
266
+ issues: self.issues,
267
+ pending_recovery_count: self.pending_recovery_count,
268
+ failed_recovery_attempts: self.failed_recovery_attempts,
269
+ }
270
+ }
271
+ }
272
+
273
+ impl Default for RecoveryStatusBuilder {
274
+ fn default() -> Self {
275
+ Self::new()
276
+ }
277
+ }
278
+
279
+ fn now() -> u64 {
280
+ std::time::SystemTime::now()
281
+ .duration_since(std::time::UNIX_EPOCH)
282
+ .unwrap_or_default()
283
+ .as_secs()
284
+ }
285
+
286
+ #[cfg(test)]
287
+ mod tests {
288
+ use super::*;
289
+
290
+ #[test]
291
+ fn create_diagnostics_collector() {
292
+ let collector = DiagnosticsCollector::new(
293
+ "abc123def456".to_string(),
294
+ Environment::Prod,
295
+ );
296
+
297
+ assert_eq!(collector.engine_version, ENGINE_VERSION);
298
+ assert_eq!(collector.protocol_version, PROTOCOL_VERSION);
299
+ assert_eq!(collector.storage_format, STORAGE_FORMAT);
300
+ assert_eq!(collector.environment, Environment::Prod);
301
+ }
302
+
303
+ #[test]
304
+ fn collect_report_includes_uptime() {
305
+ let collector = DiagnosticsCollector::new(
306
+ "abc123def456".to_string(),
307
+ Environment::Staging,
308
+ );
309
+
310
+ let recovery_status = RecoveryStatus::default();
311
+ let report = collector.collect_report(recovery_status);
312
+
313
+ assert_eq!(report.engine_version, ENGINE_VERSION);
314
+ let _uptime = report.uptime_seconds;
315
+ }
316
+
317
+ #[test]
318
+ fn recovery_status_builder() {
319
+ let recovery = RecoveryStatusBuilder::new()
320
+ .with_transaction_recovery(HealthStatus::Healthy)
321
+ .with_durable_operations(HealthStatus::Degraded)
322
+ .with_issue("durable operations slow".to_string())
323
+ .with_pending_recovery_count(5)
324
+ .build();
325
+
326
+ assert_eq!(recovery.transaction_recovery, HealthStatus::Healthy);
327
+ assert_eq!(recovery.durable_operations, HealthStatus::Degraded);
328
+ assert_eq!(recovery.status, HealthStatus::Degraded);
329
+ assert_eq!(recovery.pending_recovery_count, 5);
330
+ assert!(recovery.issues.contains(&"durable operations slow".to_string()));
331
+ }
332
+
333
+ #[test]
334
+ fn health_status_propagates_to_overall() {
335
+ let recovery = RecoveryStatusBuilder::new()
336
+ .with_transaction_recovery(HealthStatus::Unhealthy)
337
+ .build();
338
+
339
+ assert_eq!(recovery.status, HealthStatus::Degraded);
340
+ }
341
+
342
+ #[test]
343
+ fn environment_serialization() {
344
+ let envs = vec![
345
+ Environment::Dev,
346
+ Environment::Staging,
347
+ Environment::Prod,
348
+ ];
349
+
350
+ for env in envs {
351
+ let json = serde_json::to_string(&env).expect("serialize failed");
352
+ let deserialized: Environment =
353
+ serde_json::from_str(&json).expect("deserialize failed");
354
+ assert_eq!(env, deserialized);
355
+ }
356
+ }
357
+
358
+ #[test]
359
+ fn diagnostics_report_serialization() {
360
+ let collector = DiagnosticsCollector::new(
361
+ "abc123def456".to_string(),
362
+ Environment::Prod,
363
+ );
364
+
365
+ let recovery_status = RecoveryStatusBuilder::new()
366
+ .with_pending_recovery_count(3)
367
+ .build();
368
+
369
+ let report = collector.collect_report(recovery_status);
370
+ let json = serde_json::to_string(&report).expect("serialize failed");
371
+ let deserialized: DiagnosticsReport =
372
+ serde_json::from_str(&json).expect("deserialize failed");
373
+
374
+ assert_eq!(deserialized.engine_version, report.engine_version);
375
+ assert_eq!(deserialized.server_commit, report.server_commit);
376
+ assert_eq!(deserialized.recovery.pending_recovery_count, 3);
377
+ }
378
+
379
+ #[test]
380
+ fn add_subsystem_metrics() {
381
+ let collector = DiagnosticsCollector::new(
382
+ "abc123def456".to_string(),
383
+ Environment::Dev,
384
+ );
385
+
386
+ let mut report = collector.collect_report(RecoveryStatus::default());
387
+ let mut details = HashMap::new();
388
+ details.insert("cached_items".to_string(), serde_json::json!(1024));
389
+
390
+ DiagnosticsCollector::add_subsystem_metrics(
391
+ &mut report,
392
+ "cache".to_string(),
393
+ HealthStatus::Healthy,
394
+ 10000,
395
+ 5,
396
+ 1.2,
397
+ details,
398
+ );
399
+
400
+ assert!(report.subsystems.contains_key("cache"));
401
+ let cache_metrics = &report.subsystems["cache"];
402
+ assert_eq!(cache_metrics.request_count, 10000);
403
+ assert_eq!(cache_metrics.error_count, 5);
404
+ assert_eq!(cache_metrics.avg_latency_ms, 1.2);
405
+ }
406
+
407
+ #[test]
408
+ fn default_recovery_status_is_healthy() {
409
+ let recovery = RecoveryStatus::default();
410
+ assert_eq!(recovery.status, HealthStatus::Healthy);
411
+ assert!(recovery.is_healthy());
412
+ }
413
+ }