itsi-scheduler 0.1.5 → 0.1.14

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.

Potentially problematic release.


This version of itsi-scheduler might be problematic. Click here for more details.

Files changed (112) hide show
  1. checksums.yaml +4 -4
  2. data/CODE_OF_CONDUCT.md +7 -0
  3. data/Cargo.lock +83 -22
  4. data/README.md +5 -0
  5. data/_index.md +7 -0
  6. data/ext/itsi_error/src/from.rs +26 -29
  7. data/ext/itsi_error/src/lib.rs +10 -1
  8. data/ext/itsi_error/target/debug/build/clang-sys-da71b0344e568175/out/common.rs +355 -0
  9. data/ext/itsi_error/target/debug/build/clang-sys-da71b0344e568175/out/dynamic.rs +276 -0
  10. data/ext/itsi_error/target/debug/build/clang-sys-da71b0344e568175/out/macros.rs +49 -0
  11. data/ext/itsi_error/target/debug/build/rb-sys-49f554618693db24/out/bindings-0.9.110-mri-arm64-darwin23-3.4.2.rs +8865 -0
  12. data/ext/itsi_error/target/debug/incremental/itsi_error-1mmt5sux7jb0i/s-h510z7m8v9-0bxu7yd.lock +0 -0
  13. data/ext/itsi_error/target/debug/incremental/itsi_error-2vn3jey74oiw0/s-h5113n0e7e-1v5qzs6.lock +0 -0
  14. data/ext/itsi_error/target/debug/incremental/itsi_error-37uv9dicz7awp/s-h510ykifhe-0tbnep2.lock +0 -0
  15. data/ext/itsi_error/target/debug/incremental/itsi_error-37uv9dicz7awp/s-h510yyocpj-0tz7ug7.lock +0 -0
  16. data/ext/itsi_error/target/debug/incremental/itsi_error-37uv9dicz7awp/s-h510z0xc8g-14ol18k.lock +0 -0
  17. data/ext/itsi_error/target/debug/incremental/itsi_error-3g5qf4y7d54uj/s-h5113n0e7d-1trk8on.lock +0 -0
  18. data/ext/itsi_error/target/debug/incremental/itsi_error-3lpfftm45d3e2/s-h510z7m8r3-1pxp20o.lock +0 -0
  19. data/ext/itsi_error/target/debug/incremental/itsi_error-3o4qownhl3d7n/s-h510ykifek-1uxasnk.lock +0 -0
  20. data/ext/itsi_error/target/debug/incremental/itsi_error-3o4qownhl3d7n/s-h510yyocki-11u37qm.lock +0 -0
  21. data/ext/itsi_error/target/debug/incremental/itsi_error-3o4qownhl3d7n/s-h510z0xc93-0pmy0zm.lock +0 -0
  22. data/ext/itsi_rb_helpers/Cargo.toml +1 -0
  23. data/ext/itsi_rb_helpers/src/heap_value.rs +18 -0
  24. data/ext/itsi_rb_helpers/src/lib.rs +59 -9
  25. data/ext/itsi_rb_helpers/target/debug/build/clang-sys-da71b0344e568175/out/common.rs +355 -0
  26. data/ext/itsi_rb_helpers/target/debug/build/clang-sys-da71b0344e568175/out/dynamic.rs +276 -0
  27. data/ext/itsi_rb_helpers/target/debug/build/clang-sys-da71b0344e568175/out/macros.rs +49 -0
  28. data/ext/itsi_rb_helpers/target/debug/build/rb-sys-eb9ed4ff3a60f995/out/bindings-0.9.110-mri-arm64-darwin23-3.4.2.rs +8865 -0
  29. data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-040pxg6yhb3g3/s-h5113n7a1b-03bwlt4.lock +0 -0
  30. data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-131g1u4dzkt1a/s-h51113xnh3-1eik1ip.lock +0 -0
  31. data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-131g1u4dzkt1a/s-h5111704jj-0g4rj8x.lock +0 -0
  32. data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-1q2d3drtxrzs5/s-h5113n79yl-0bxcqc5.lock +0 -0
  33. data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-374a9h7ovycj0/s-h51113xoox-10de2hp.lock +0 -0
  34. data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-374a9h7ovycj0/s-h5111704w7-0vdq7gq.lock +0 -0
  35. data/ext/itsi_server/Cargo.lock +2956 -0
  36. data/ext/itsi_server/Cargo.toml +69 -26
  37. data/ext/itsi_server/src/env.rs +43 -0
  38. data/ext/itsi_server/src/lib.rs +81 -75
  39. data/ext/itsi_server/src/{body_proxy → ruby_types/itsi_body_proxy}/big_bytes.rs +10 -5
  40. data/ext/itsi_server/src/{body_proxy/itsi_body_proxy.rs → ruby_types/itsi_body_proxy/mod.rs} +22 -3
  41. data/ext/itsi_server/src/ruby_types/itsi_grpc_request.rs +147 -0
  42. data/ext/itsi_server/src/ruby_types/itsi_grpc_response.rs +19 -0
  43. data/ext/itsi_server/src/ruby_types/itsi_grpc_stream/mod.rs +216 -0
  44. data/ext/itsi_server/src/{request/itsi_request.rs → ruby_types/itsi_http_request.rs} +108 -103
  45. data/ext/itsi_server/src/{response/itsi_response.rs → ruby_types/itsi_http_response.rs} +79 -38
  46. data/ext/itsi_server/src/ruby_types/itsi_server/file_watcher.rs +225 -0
  47. data/ext/itsi_server/src/ruby_types/itsi_server/itsi_server_config.rs +355 -0
  48. data/ext/itsi_server/src/ruby_types/itsi_server.rs +82 -0
  49. data/ext/itsi_server/src/ruby_types/mod.rs +55 -0
  50. data/ext/itsi_server/src/server/bind.rs +33 -20
  51. data/ext/itsi_server/src/server/byte_frame.rs +32 -0
  52. data/ext/itsi_server/src/server/cache_store.rs +74 -0
  53. data/ext/itsi_server/src/server/itsi_service.rs +172 -0
  54. data/ext/itsi_server/src/server/lifecycle_event.rs +3 -0
  55. data/ext/itsi_server/src/server/listener.rs +197 -106
  56. data/ext/itsi_server/src/server/middleware_stack/middleware.rs +153 -0
  57. data/ext/itsi_server/src/server/middleware_stack/middlewares/allow_list.rs +47 -0
  58. data/ext/itsi_server/src/server/middleware_stack/middlewares/auth_api_key.rs +58 -0
  59. data/ext/itsi_server/src/server/middleware_stack/middlewares/auth_basic.rs +82 -0
  60. data/ext/itsi_server/src/server/middleware_stack/middlewares/auth_jwt.rs +264 -0
  61. data/ext/itsi_server/src/server/middleware_stack/middlewares/cache_control.rs +139 -0
  62. data/ext/itsi_server/src/server/middleware_stack/middlewares/compression.rs +300 -0
  63. data/ext/itsi_server/src/server/middleware_stack/middlewares/cors.rs +287 -0
  64. data/ext/itsi_server/src/server/middleware_stack/middlewares/deny_list.rs +48 -0
  65. data/ext/itsi_server/src/server/middleware_stack/middlewares/error_response.rs +127 -0
  66. data/ext/itsi_server/src/server/middleware_stack/middlewares/etag.rs +191 -0
  67. data/ext/itsi_server/src/server/middleware_stack/middlewares/grpc_service.rs +72 -0
  68. data/ext/itsi_server/src/server/middleware_stack/middlewares/header_interpretation.rs +85 -0
  69. data/ext/itsi_server/src/server/middleware_stack/middlewares/intrusion_protection.rs +195 -0
  70. data/ext/itsi_server/src/server/middleware_stack/middlewares/log_requests.rs +82 -0
  71. data/ext/itsi_server/src/server/middleware_stack/middlewares/mod.rs +82 -0
  72. data/ext/itsi_server/src/server/middleware_stack/middlewares/proxy.rs +216 -0
  73. data/ext/itsi_server/src/server/middleware_stack/middlewares/rate_limit.rs +124 -0
  74. data/ext/itsi_server/src/server/middleware_stack/middlewares/redirect.rs +76 -0
  75. data/ext/itsi_server/src/server/middleware_stack/middlewares/request_headers.rs +43 -0
  76. data/ext/itsi_server/src/server/middleware_stack/middlewares/response_headers.rs +34 -0
  77. data/ext/itsi_server/src/server/middleware_stack/middlewares/ruby_app.rs +93 -0
  78. data/ext/itsi_server/src/server/middleware_stack/middlewares/static_assets.rs +162 -0
  79. data/ext/itsi_server/src/server/middleware_stack/middlewares/string_rewrite.rs +158 -0
  80. data/ext/itsi_server/src/server/middleware_stack/middlewares/token_source.rs +12 -0
  81. data/ext/itsi_server/src/server/middleware_stack/mod.rs +315 -0
  82. data/ext/itsi_server/src/server/mod.rs +8 -1
  83. data/ext/itsi_server/src/server/process_worker.rs +44 -11
  84. data/ext/itsi_server/src/server/rate_limiter.rs +565 -0
  85. data/ext/itsi_server/src/server/request_job.rs +11 -0
  86. data/ext/itsi_server/src/server/serve_strategy/cluster_mode.rs +129 -46
  87. data/ext/itsi_server/src/server/serve_strategy/mod.rs +9 -6
  88. data/ext/itsi_server/src/server/serve_strategy/single_mode.rs +337 -163
  89. data/ext/itsi_server/src/server/signal.rs +25 -2
  90. data/ext/itsi_server/src/server/static_file_server.rs +984 -0
  91. data/ext/itsi_server/src/server/thread_worker.rs +164 -88
  92. data/ext/itsi_server/src/server/tls/locked_dir_cache.rs +55 -17
  93. data/ext/itsi_server/src/server/tls.rs +104 -28
  94. data/ext/itsi_server/src/server/types.rs +43 -0
  95. data/ext/itsi_tracing/Cargo.toml +1 -0
  96. data/ext/itsi_tracing/src/lib.rs +222 -34
  97. data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-0994n8rpvvt9m/s-h510hfz1f6-1kbycmq.lock +0 -0
  98. data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-0bob7bf4yq34i/s-h5113125h5-0lh4rag.lock +0 -0
  99. data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-2fcodulrxbbxo/s-h510h2infk-0hp5kjw.lock +0 -0
  100. data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-2iak63r1woi1l/s-h510h2in4q-0kxfzw1.lock +0 -0
  101. data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-2kk4qj9gn5dg2/s-h5113124kv-0enwon2.lock +0 -0
  102. data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-2mwo0yas7dtw4/s-h510hfz1ha-1udgpei.lock +0 -0
  103. data/lib/itsi/scheduler/version.rb +1 -1
  104. data/lib/itsi/scheduler.rb +2 -2
  105. metadata +79 -14
  106. data/ext/itsi_server/extconf.rb +0 -6
  107. data/ext/itsi_server/src/body_proxy/mod.rs +0 -2
  108. data/ext/itsi_server/src/request/mod.rs +0 -1
  109. data/ext/itsi_server/src/response/mod.rs +0 -1
  110. data/ext/itsi_server/src/server/itsi_ca/itsi_ca.crt +0 -13
  111. data/ext/itsi_server/src/server/itsi_ca/itsi_ca.key +0 -5
  112. data/ext/itsi_server/src/server/itsi_server.rs +0 -244
@@ -2,21 +2,29 @@ use base64::{engine::general_purpose, Engine as _};
2
2
  use itsi_error::Result;
3
3
  use itsi_tracing::info;
4
4
  use locked_dir_cache::LockedDirCache;
5
- use rcgen::{CertificateParams, DnType, KeyPair, SanType};
6
- use rustls::pki_types::{CertificateDer, PrivateKeyDer};
5
+ use rcgen::{
6
+ generate_simple_self_signed, CertificateParams, CertifiedKey, DnType, KeyPair, SanType,
7
+ };
8
+ use rustls::{
9
+ pki_types::{CertificateDer, PrivateKeyDer},
10
+ ClientConfig, RootCertStore,
11
+ };
7
12
  use rustls_pemfile::{certs, pkcs8_private_keys};
8
13
  use std::{
9
14
  collections::HashMap,
10
- env, fs,
15
+ fs,
11
16
  io::{BufReader, Error},
12
17
  sync::Arc,
13
18
  };
14
19
  use tokio::sync::Mutex;
15
20
  use tokio_rustls::{rustls::ServerConfig, TlsAcceptor};
16
21
  use tokio_rustls_acme::{AcmeAcceptor, AcmeConfig, AcmeState};
22
+
23
+ use crate::env::{
24
+ ITSI_ACME_CACHE_DIR, ITSI_ACME_CA_PEM_PATH, ITSI_ACME_CONTACT_EMAIL, ITSI_ACME_DIRECTORY_URL,
25
+ ITSI_LOCAL_CA_DIR,
26
+ };
17
27
  mod locked_dir_cache;
18
- const ITS_CA_CERT: &str = include_str!("./itsi_ca/itsi_ca.crt");
19
- const ITS_CA_KEY: &str = include_str!("./itsi_ca/itsi_ca.key");
20
28
 
21
29
  #[derive(Clone)]
22
30
  pub enum ItsiTlsAcceptor {
@@ -28,35 +36,72 @@ pub enum ItsiTlsAcceptor {
28
36
  ),
29
37
  }
30
38
 
31
- // Generates a TLS configuration based on either :
32
- // * Input "cert" and "key" options (either paths or Base64-encoded strings) or
33
- // * Performs automatic certificate generation/retrieval. Generated certs use an internal self-signed Isti CA.
34
- // If a non-local host or optional domain parameter is provided,
35
- // an automated certificate will attempt to be fetched using let's encrypt.
39
+ /// Generates a TLS configuration based on either :
40
+ /// * Input "cert" and "key" options (either paths or Base64-encoded strings) or
41
+ /// * Performs automatic certificate generation/retrieval. Generated certs use an internal self-signed Isti CA.
42
+ ///
43
+ /// If a non-local host or optional domain parameter is provided,
44
+ /// an automated certificate will attempt to be fetched using let's encrypt.
36
45
  pub fn configure_tls(
37
46
  host: &str,
38
47
  query_params: &HashMap<String, String>,
39
48
  ) -> Result<ItsiTlsAcceptor> {
40
49
  let domains = query_params
41
50
  .get("domains")
42
- .map(|v| v.split(',').map(String::from).collect::<Vec<_>>());
51
+ .map(|v| v.split(',').map(String::from).collect::<Vec<_>>())
52
+ .or_else(|| query_params.get("domain").map(|v| vec![v.to_string()]));
43
53
 
44
- if query_params.get("cert").is_none() || query_params.get("key").is_none() {
54
+ if query_params.get("cert").is_some_and(|c| c == "acme") {
45
55
  if let Some(domains) = domains {
46
- let directory_url = env::var("ACME_DIRECTORY_URL")
47
- .unwrap_or_else(|_| "https://acme-v02.api.letsencrypt.org/directory".to_string());
56
+ let directory_url = &*ITSI_ACME_DIRECTORY_URL;
48
57
  info!(
49
58
  domains = format!("{:?}", domains),
50
59
  directory_url, "Requesting acme cert"
51
60
  );
52
- let acme_state = AcmeConfig::new(domains)
53
- .contact(["mailto:wc@pico.net.nz"])
54
- .cache(LockedDirCache::new("./rustls_acme_cache"))
55
- .directory(directory_url)
56
- .state();
57
- let rustls_config = ServerConfig::builder()
61
+ let acme_contact_email = query_params
62
+ .get("acme_email")
63
+ .map(|s| s.to_string())
64
+ .or_else(|| (*ITSI_ACME_CONTACT_EMAIL).as_ref().ok().map(|s| s.to_string()))
65
+ .ok_or_else(|| itsi_error::ItsiError::ArgumentError(
66
+ "acme_email query param or ITSI_ACME_CONTACT_EMAIL must be set before you can auto-generate let's encrypt certificates".to_string(),
67
+ ))?;
68
+
69
+ let acme_config = AcmeConfig::new(domains)
70
+ .contact([format!("mailto:{}", acme_contact_email)])
71
+ .cache(LockedDirCache::new(&*ITSI_ACME_CACHE_DIR))
72
+ .directory(directory_url);
73
+
74
+ let acme_state = if let Ok(ca_pem_path) = &*ITSI_ACME_CA_PEM_PATH {
75
+ let mut root_cert_store = RootCertStore::empty();
76
+
77
+ let ca_pem = fs::read(ca_pem_path).expect("failed to read CA pem file");
78
+ let mut ca_reader = BufReader::new(&ca_pem[..]);
79
+ let der_certs: Vec<CertificateDer> = certs(&mut ca_reader)
80
+ .collect::<std::result::Result<Vec<CertificateDer>, _>>()
81
+ .map_err(|e| {
82
+ itsi_error::ItsiError::ArgumentError(format!(
83
+ "Invalid ACME CA Pem path {:?}",
84
+ e
85
+ ))
86
+ })?;
87
+ root_cert_store.add_parsable_certificates(der_certs);
88
+
89
+ let client_config = ClientConfig::builder()
90
+ .with_root_certificates(root_cert_store)
91
+ .with_no_client_auth();
92
+ acme_config
93
+ .client_tls_config(Arc::new(client_config))
94
+ .state()
95
+ } else {
96
+ acme_config.state()
97
+ };
98
+
99
+ let mut rustls_config = ServerConfig::builder()
58
100
  .with_no_client_auth()
59
101
  .with_cert_resolver(acme_state.resolver());
102
+
103
+ rustls_config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
104
+
60
105
  let acceptor = acme_state.acceptor();
61
106
  return Ok(ItsiTlsAcceptor::Automatic(
62
107
  acceptor,
@@ -73,7 +118,7 @@ pub fn configure_tls(
73
118
  let key = load_private_key(key_path);
74
119
  (certs, key)
75
120
  } else {
76
- generate_ca_signed_cert(vec![host.to_owned()])?
121
+ generate_ca_signed_cert(domains.unwrap_or(vec![host.to_owned()]))?
77
122
  };
78
123
 
79
124
  let mut config = ServerConfig::builder()
@@ -144,10 +189,19 @@ pub fn load_private_key(path: &str) -> PrivateKeyDer<'static> {
144
189
  pub fn generate_ca_signed_cert(
145
190
  domains: Vec<String>,
146
191
  ) -> Result<(Vec<CertificateDer<'static>>, PrivateKeyDer<'static>)> {
147
- info!("Generating New Itsi CA - Self signed Certificate. Use `itsi ca export` to export the CA certificate for import into your local trust store.");
192
+ info!(
193
+ domains = format!("{}", domains.join(", ")),
194
+ "Self signed cert",
195
+ );
196
+ info!(
197
+ "Add {} to your system's trusted cert store to resolve certificate errors.",
198
+ format!("{}/itsi_dev_ca.crt", ITSI_LOCAL_CA_DIR.to_str().unwrap())
199
+ );
200
+ info!("Dev CA path can be overridden by setting env var: `ITSI_LOCAL_CA_DIR`.");
201
+ let (ca_key_pem, ca_cert_pem) = get_or_create_local_dev_ca()?;
148
202
 
149
- let ca_kp = KeyPair::from_pem(ITS_CA_KEY).expect("Failed to load embedded CA key");
150
- let ca_cert = CertificateParams::from_ca_cert_pem(ITS_CA_CERT)
203
+ let ca_kp = KeyPair::from_pem(&ca_key_pem).expect("Failed to load CA key");
204
+ let ca_cert = CertificateParams::from_ca_cert_pem(&ca_cert_pem)
151
205
  .expect("Failed to parse embedded CA certificate")
152
206
  .self_signed(&ca_kp)
153
207
  .expect("Failed to self-sign embedded CA cert");
@@ -155,10 +209,6 @@ pub fn generate_ca_signed_cert(
155
209
  let ee_key = KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256).unwrap();
156
210
  let mut ee_params = CertificateParams::default();
157
211
 
158
- info!(
159
- "Generated certificate will be valid for domains {:?}",
160
- domains
161
- );
162
212
  use std::net::IpAddr;
163
213
 
164
214
  ee_params.subject_alt_names = domains
@@ -187,3 +237,29 @@ pub fn generate_ca_signed_cert(
187
237
  PrivateKeyDer::try_from(ee_key.serialize_der()).unwrap(),
188
238
  ))
189
239
  }
240
+
241
+ fn get_or_create_local_dev_ca() -> Result<(String, String)> {
242
+ let ca_dir = &*ITSI_LOCAL_CA_DIR;
243
+ fs::create_dir_all(ca_dir)?;
244
+
245
+ let key_path = ca_dir.join("itsi_dev_ca.key");
246
+ let cert_path = ca_dir.join("itsi_dev_ca.crt");
247
+
248
+ if key_path.exists() && cert_path.exists() {
249
+ // Already have a local CA
250
+ let key_pem = fs::read_to_string(&key_path)?;
251
+ let cert_pem = fs::read_to_string(&cert_path)?;
252
+
253
+ Ok((key_pem, cert_pem))
254
+ } else {
255
+ let subject_alt_names = vec!["dev.itsi.fyi".to_string(), "localhost".to_string()];
256
+
257
+ let CertifiedKey { cert, key_pair } =
258
+ generate_simple_self_signed(subject_alt_names).unwrap();
259
+
260
+ fs::write(&key_path, key_pair.serialize_pem())?;
261
+ fs::write(&cert_path, cert.pem())?;
262
+
263
+ Ok((key_pair.serialize_pem(), cert.pem()))
264
+ }
265
+ }
@@ -0,0 +1,43 @@
1
+ use std::convert::Infallible;
2
+
3
+ use bytes::Bytes;
4
+ use http::{Request, Response};
5
+ use http_body_util::combinators::BoxBody;
6
+ use hyper::body::Incoming;
7
+
8
+ pub type HttpResponse = Response<BoxBody<Bytes, Infallible>>;
9
+ pub type HttpRequest = Request<Incoming>;
10
+
11
+ pub trait RequestExt {
12
+ fn content_type(&self) -> Option<&str>;
13
+ fn accept(&self) -> Option<&str>;
14
+ fn header(&self, header_name: &str) -> Option<&str>;
15
+ fn query_param(&self, query_name: &str) -> Option<&str>;
16
+ }
17
+
18
+ impl RequestExt for HttpRequest {
19
+ fn content_type(&self) -> Option<&str> {
20
+ self.headers()
21
+ .get("content-type")
22
+ .map(|hv| hv.to_str().unwrap_or(""))
23
+ }
24
+
25
+ fn accept(&self) -> Option<&str> {
26
+ self.headers()
27
+ .get("accept")
28
+ .map(|hv| hv.to_str().unwrap_or(""))
29
+ }
30
+
31
+ fn header(&self, header_name: &str) -> Option<&str> {
32
+ self.headers()
33
+ .get(header_name)
34
+ .map(|hv| hv.to_str().unwrap_or(""))
35
+ }
36
+
37
+ fn query_param(&self, query_name: &str) -> Option<&str> {
38
+ self.uri()
39
+ .query()
40
+ .and_then(|query| query.split('&').find(|param| param.starts_with(query_name)))
41
+ .map(|param| param.split('=').nth(1).unwrap_or(""))
42
+ }
43
+ }
@@ -14,3 +14,4 @@ tracing-subscriber = { version = "0.3.19", features = [
14
14
  ] }
15
15
  tracing-attributes = "0.1"
16
16
  atty = "0.2.14"
17
+ tracing-appender = "0.2.3"
@@ -1,41 +1,229 @@
1
- use std::env;
2
-
3
1
  use atty::{Stream, is};
4
- pub use tracing::{debug, error, info, trace, warn};
5
- pub use tracing_attributes::instrument; // Explicitly export from tracing-attributes
6
- use tracing_subscriber::{
7
- EnvFilter,
8
- fmt::{self, format},
2
+ use std::{
3
+ env,
4
+ sync::{Mutex, OnceLock},
9
5
  };
6
+ pub use tracing::{debug, error, info, trace, warn};
7
+ use tracing_appender::rolling;
8
+ use tracing_subscriber::Layer;
9
+ use tracing_subscriber::fmt::writer::BoxMakeWriter;
10
+ use tracing_subscriber::{EnvFilter, fmt, prelude::*, reload};
11
+
12
+ // Global reload handle for changing the level at runtime.
13
+ static RELOAD_HANDLE: OnceLock<
14
+ Mutex<Option<reload::Handle<EnvFilter, tracing_subscriber::Registry>>>,
15
+ > = OnceLock::new();
16
+
17
+ /// Log format: Plain or JSON.
18
+ #[derive(Debug, Clone)]
19
+ pub enum LogFormat {
20
+ Plain,
21
+ Json,
22
+ }
23
+
24
+ /// Log target: STDOUT, File, or Both.
25
+ #[derive(Debug, Clone)]
26
+ pub enum LogTarget {
27
+ Stdout,
28
+ File(String), // file name (rotated daily)
29
+ Both(String), // file name (rotated daily) plus STDOUT
30
+ }
31
+
32
+ /// Logger configuration.
33
+ #[derive(Debug, Clone)]
34
+ pub struct LogConfig {
35
+ /// Log level as a string (e.g. "info", "debug").
36
+ pub level: String,
37
+ /// Format: Plain (with optional ANSI) or JSON.
38
+ pub format: LogFormat,
39
+ /// Target: STDOUT, File, or Both.
40
+ pub target: LogTarget,
41
+ /// Whether to enable ANSI coloring (for plain text).
42
+ pub use_ansi: bool,
43
+ }
10
44
 
11
- #[instrument]
45
+ impl Default for LogConfig {
46
+ fn default() -> Self {
47
+ let level = env::var("ITSI_LOG").unwrap_or_else(|_| "info".into());
48
+ let format = match env::var("ITSI_LOG_FORMAT").as_deref() {
49
+ Ok("json") => LogFormat::Json,
50
+ _ => LogFormat::Plain,
51
+ };
52
+ let target = match env::var("ITSI_LOG_TARGET").as_deref() {
53
+ Ok("file") => {
54
+ let file = env::var("ITSI_LOG_FILE").unwrap_or_else(|_| "app.log".into());
55
+ LogTarget::File(file)
56
+ }
57
+ Ok("both") => {
58
+ let file = env::var("ITSI_LOG_FILE").unwrap_or_else(|_| "app.log".into());
59
+ LogTarget::Both(file)
60
+ }
61
+ _ => LogTarget::Stdout,
62
+ };
63
+ // If ITSI_LOG_ANSI is set, use that; otherwise, use ANSI if stdout is a TTY.
64
+ let use_ansi = env::var("ITSI_LOG_ANSI")
65
+ .map(|s| s == "true")
66
+ .unwrap_or_else(|_| is(Stream::Stdout));
67
+ Self {
68
+ level,
69
+ format,
70
+ target,
71
+ use_ansi,
72
+ }
73
+ }
74
+ }
75
+
76
+ /// Initialize the global tracing subscriber with the default configuration.
12
77
  pub fn init() {
13
- let env_filter = EnvFilter::builder()
14
- .with_env_var("ITSI_LOG")
15
- .try_from_env()
16
- .unwrap_or_else(|_| EnvFilter::new("info"));
17
-
18
- let format = fmt::format()
19
- .compact()
20
- .with_file(false)
21
- .with_level(true)
22
- .with_line_number(false)
23
- .with_source_location(false)
24
- .with_target(false)
25
- .with_thread_ids(false);
26
-
27
- let is_tty = is(Stream::Stdout);
28
-
29
- let subscriber = tracing_subscriber::fmt()
30
- .event_format(format)
31
- .with_env_filter(env_filter);
32
-
33
- if (is_tty && env::var("ITSI_LOG_PLAIN").is_err()) || env::var("ITSI_LOG_ANSI").is_ok() {
34
- subscriber.with_ansi(true).init();
78
+ init_with_config(LogConfig::default());
79
+ }
80
+
81
+ /// Initialize the global tracing subscriber with a given configuration.
82
+ pub fn init_with_config(config: LogConfig) {
83
+ // Build an EnvFilter from the configured level.
84
+ let env_filter = EnvFilter::new(config.level);
85
+
86
+ // Build the formatting layer based on target and format.
87
+ let fmt_layer = match config.target {
88
+ LogTarget::Stdout => match config.format {
89
+ LogFormat::Plain => fmt::layer()
90
+ .compact()
91
+ .with_file(false)
92
+ .with_line_number(false)
93
+ .with_target(false)
94
+ .with_thread_ids(false)
95
+ .with_writer(BoxMakeWriter::new(std::io::stdout))
96
+ .with_ansi(config.use_ansi)
97
+ .boxed(),
98
+ LogFormat::Json => fmt::layer()
99
+ .compact()
100
+ .with_file(false)
101
+ .with_line_number(false)
102
+ .with_target(false)
103
+ .with_thread_ids(false)
104
+ .with_writer(BoxMakeWriter::new(std::io::stdout))
105
+ .with_ansi(config.use_ansi)
106
+ .json()
107
+ .boxed(),
108
+ },
109
+ LogTarget::File(file) => match config.format {
110
+ LogFormat::Plain => fmt::layer()
111
+ .compact()
112
+ .with_file(false)
113
+ .with_line_number(false)
114
+ .with_target(false)
115
+ .with_thread_ids(false)
116
+ .with_writer(BoxMakeWriter::new({
117
+ let file = file.clone();
118
+ move || rolling::daily(".", file.clone())
119
+ }))
120
+ .with_ansi(false)
121
+ .boxed(),
122
+ LogFormat::Json => fmt::layer()
123
+ .compact()
124
+ .with_file(false)
125
+ .with_line_number(false)
126
+ .with_target(false)
127
+ .with_thread_ids(false)
128
+ .with_writer(BoxMakeWriter::new({
129
+ let file = file.clone();
130
+ move || rolling::daily(".", file.clone())
131
+ }))
132
+ .with_ansi(false)
133
+ .json()
134
+ .boxed(),
135
+ },
136
+ LogTarget::Both(file) => {
137
+ // For "Both" target, handle each format separately to avoid type mismatches
138
+ match config.format {
139
+ LogFormat::Plain => {
140
+ let stdout_layer = fmt::layer()
141
+ .compact()
142
+ .with_file(false)
143
+ .with_line_number(false)
144
+ .with_target(false)
145
+ .with_thread_ids(false)
146
+ .with_writer(BoxMakeWriter::new(std::io::stdout))
147
+ .with_ansi(config.use_ansi);
148
+
149
+ let file_layer = fmt::layer()
150
+ .compact()
151
+ .with_file(false)
152
+ .with_line_number(false)
153
+ .with_target(false)
154
+ .with_thread_ids(false)
155
+ .with_writer(BoxMakeWriter::new({
156
+ let file = file.clone();
157
+ move || rolling::daily(".", file.clone())
158
+ }))
159
+ .with_ansi(false);
160
+
161
+ stdout_layer.and_then(file_layer).boxed()
162
+ }
163
+ LogFormat::Json => {
164
+ let stdout_layer = fmt::layer()
165
+ .compact()
166
+ .with_file(false)
167
+ .with_line_number(false)
168
+ .with_target(false)
169
+ .with_thread_ids(false)
170
+ .with_writer(BoxMakeWriter::new(std::io::stdout))
171
+ .with_ansi(config.use_ansi)
172
+ .json();
173
+
174
+ let file_layer = fmt::layer()
175
+ .compact()
176
+ .with_file(false)
177
+ .with_line_number(false)
178
+ .with_target(false)
179
+ .with_thread_ids(false)
180
+ .with_writer(BoxMakeWriter::new({
181
+ let file = file.clone();
182
+ move || rolling::daily(".", file.clone())
183
+ }))
184
+ .with_ansi(false)
185
+ .json();
186
+
187
+ stdout_layer.and_then(file_layer).boxed()
188
+ }
189
+ }
190
+ }
191
+ };
192
+
193
+ // Create a reloadable filter layer so we can update the level at runtime.
194
+ let (filter_layer, handle) = reload::Layer::new(env_filter);
195
+
196
+ // Build the subscriber registry
197
+ let subscriber = tracing_subscriber::registry()
198
+ .with(filter_layer)
199
+ .with(fmt_layer);
200
+
201
+ tracing::subscriber::set_global_default(subscriber)
202
+ .expect("Unable to set global tracing subscriber");
203
+
204
+ RELOAD_HANDLE.set(Mutex::new(Some(handle))).unwrap();
205
+ }
206
+
207
+ /// Change the log level at runtime.
208
+ pub fn set_level(new_level: &str) {
209
+ if let Some(handle) = RELOAD_HANDLE.get().unwrap().lock().unwrap().as_ref() {
210
+ handle
211
+ .modify(|filter| *filter = EnvFilter::new(new_level))
212
+ .expect("Failed to update log level");
35
213
  } else {
36
- subscriber
37
- .fmt_fields(format::JsonFields::default())
38
- .event_format(fmt::format().json())
39
- .init();
214
+ eprintln!("Reload handle not initialized; call init() first.");
40
215
  }
41
216
  }
217
+
218
+ /// Run a function silently by temporarily setting a no-op subscriber.
219
+ pub fn run_silently<F, R>(f: F) -> R
220
+ where
221
+ F: FnOnce() -> R,
222
+ {
223
+ let no_op_subscriber = tracing_subscriber::fmt()
224
+ .with_writer(std::io::sink)
225
+ .with_max_level(tracing_subscriber::filter::LevelFilter::OFF)
226
+ .finish();
227
+ let dispatch = tracing::Dispatch::new(no_op_subscriber);
228
+ tracing::dispatcher::with_default(&dispatch, f)
229
+ }
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Itsi
4
4
  class Scheduler
5
- VERSION = "0.1.5"
5
+ VERSION = "0.1.14"
6
6
  end
7
7
  end
@@ -73,7 +73,7 @@ module Itsi
73
73
  fiber.resume
74
74
  end
75
75
  rescue StandardError => e
76
- warn "Failed to resume fiber #{fiber}: #{e.message}"
76
+ warn "Fiber #{fiber} terminated on exception: #{e.message}"
77
77
  end
78
78
 
79
79
  def resume_fiber_with_readiness((token, readiness))
@@ -81,7 +81,7 @@ module Itsi
81
81
  fiber.resume(readiness)
82
82
  end
83
83
  rescue StandardError => e
84
- warn "Failed to resume fiber #{fiber}: #{e.message}"
84
+ warn "Fiber #{fiber} terminated on exception: #{e.message}"
85
85
  end
86
86
 
87
87
  def resume_blocked(fiber)