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}
67
68impl Default for WorkersConfig {
69 fn default() -> Self {
70 Self {
71 script: "vendor/bin/folk-worker".into(),
72 php: "php".into(),
73 count: 4,
74 max_jobs: 1000,
75 ttl: Duration::from_secs(3600),
76 max_memory_mb: 256,
77 exec_timeout: Duration::from_secs(30),
78 boot_timeout: Duration::from_secs(30),
79 }
80 }
81}
82
83#[derive(Debug, Clone, Serialize, Deserialize)]
84#[serde(default)]
85pub struct LogConfig {
86 pub filter: String,
88 pub format: LogFormat,
90 #[serde(default)]
94 pub plugins: HashMap<String, String>,
95}
96
97impl Default for LogConfig {
98 fn default() -> Self {
99 Self {
100 filter: "info".into(),
101 format: LogFormat::Text,
102 plugins: HashMap::new(),
103 }
104 }
105}
106
107impl LogConfig {
108 pub fn effective_filter(&self) -> String {
112 if self.plugins.is_empty() {
113 return self.filter.clone();
114 }
115
116 let mut parts = vec![self.filter.clone()];
117 for (plugin, level) in &self.plugins {
118 let target = plugin_name_to_target(plugin);
119 parts.push(format!("{target}={level}"));
120 }
121 parts.join(",")
122 }
123}
124
125fn plugin_name_to_target(name: &str) -> &str {
126 match name {
127 "http" => "folk_plugin_http",
128 "jobs" => "folk_plugin_jobs",
129 "grpc" => "folk_plugin_grpc",
130 "metrics" => "folk_plugin_metrics",
131 "process" => "folk_plugin_process",
132 "core" => "folk_core",
133 "ext" => "folk_ext",
134 _ => name,
135 }
136}
137
138#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
139#[serde(rename_all = "snake_case")]
140pub enum LogFormat {
141 Text,
142 Json,
143 Pretty,
144}
145
146impl FolkConfig {
147 pub fn load() -> Result<Self> {
150 Self::load_from(Path::new("folk.toml"))
151 }
152
153 pub fn load_from(path: impl AsRef<Path>) -> Result<Self> {
155 let path = path.as_ref();
156 let mut fig = Figment::from(figment::providers::Serialized::defaults(Self::default()));
157 if path.exists() {
158 fig = fig.merge(Toml::file(path));
159 }
160 fig = fig.merge(Env::prefixed("FOLK_").split("_"));
161 fig.extract().context("failed to parse Folk configuration")
162 }
163}