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