1use std::collections::HashMap;
8use std::path::Path;
9use std::time::Duration;
10
11use anyhow::{Context, Result};
12use figment::Figment;
13use figment::providers::{Env, Format, Toml};
14use serde::{Deserialize, Serialize};
15
16#[derive(Debug, Clone, Default, Serialize, Deserialize)]
18#[serde(default)]
19pub struct FolkConfig {
20 pub server: ServerConfig,
21 pub workers: WorkersConfig,
22 pub log: LogConfig,
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
26#[serde(default)]
27pub struct ServerConfig {
28 pub rpc_socket: String,
30 #[serde(with = "humantime_serde")]
32 pub shutdown_timeout: Duration,
33}
34
35impl Default for ServerConfig {
36 fn default() -> Self {
37 Self {
38 rpc_socket: "/tmp/folk.sock".into(),
39 shutdown_timeout: Duration::from_secs(30),
40 }
41 }
42}
43
44#[derive(Debug, Clone, Serialize, Deserialize)]
45#[serde(default)]
46pub struct WorkersConfig {
47 pub script: String,
49 pub php: String,
51 pub count: usize,
53 pub max_jobs: u64,
55 #[serde(with = "humantime_serde")]
57 pub ttl: Duration,
58 pub max_memory_mb: u64,
60 #[serde(with = "humantime_serde")]
62 pub exec_timeout: Duration,
63 #[serde(with = "humantime_serde")]
65 pub boot_timeout: Duration,
66 pub warmup: bool,
69}
70
71impl Default for WorkersConfig {
72 fn default() -> Self {
73 Self {
74 script: "vendor/bin/folk-worker".into(),
75 php: "php".into(),
76 count: 4,
77 max_jobs: 1000,
78 ttl: Duration::from_secs(3600),
79 max_memory_mb: 256,
80 exec_timeout: Duration::from_secs(30),
81 boot_timeout: Duration::from_secs(30),
82 warmup: true,
83 }
84 }
85}
86
87#[derive(Debug, Clone, Serialize, Deserialize)]
88#[serde(default)]
89pub struct LogConfig {
90 pub filter: String,
92 pub format: LogFormat,
94 #[serde(default)]
98 pub plugins: HashMap<String, String>,
99}
100
101impl Default for LogConfig {
102 fn default() -> Self {
103 Self {
104 filter: "info".into(),
105 format: LogFormat::Text,
106 plugins: HashMap::new(),
107 }
108 }
109}
110
111impl LogConfig {
112 pub fn effective_filter(&self) -> String {
116 if self.plugins.is_empty() {
117 return self.filter.clone();
118 }
119
120 let mut parts = vec![self.filter.clone()];
121 for (plugin, level) in &self.plugins {
122 let target = plugin_name_to_target(plugin);
123 parts.push(format!("{target}={level}"));
124 }
125 parts.join(",")
126 }
127}
128
129fn plugin_name_to_target(name: &str) -> &str {
130 match name {
131 "http" => "folk_plugin_http",
132 "jobs" => "folk_plugin_jobs",
133 "grpc" => "folk_plugin_grpc",
134 "metrics" => "folk_plugin_metrics",
135 "process" => "folk_plugin_process",
136 "core" => "folk_core",
137 "ext" => "folk_ext",
138 _ => name,
139 }
140}
141
142#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
143#[serde(rename_all = "snake_case")]
144pub enum LogFormat {
145 Text,
146 Json,
147 Pretty,
148}
149
150impl FolkConfig {
151 pub fn load() -> Result<Self> {
154 Self::load_from(Path::new("folk.toml"))
155 }
156
157 pub fn load_from(path: impl AsRef<Path>) -> Result<Self> {
159 let path = path.as_ref();
160 let mut fig = Figment::from(figment::providers::Serialized::defaults(Self::default()));
161 if path.exists() {
162 fig = fig.merge(Toml::file(path));
163 }
164 fig = fig.merge(Env::prefixed("FOLK_").split("_"));
165 fig.extract().context("failed to parse Folk configuration")
166 }
167}