Skip to main content

pitchfork_cli/cli/
json_output.rs

1use crate::log_store::LogEntry;
2use serde::Serialize;
3
4#[derive(Serialize)]
5pub struct JsonListEntry {
6    pub id: String,
7    pub namespace: String,
8    pub name: String,
9    pub pid: Option<u32>,
10    pub status: String,
11    /// True for a `oneshot = true` daemon. Present so a consumer can tell a
12    /// task from a service rather than inferring it from `completed`, which a
13    /// task only reaches once it has run.
14    pub oneshot: bool,
15    pub disabled: bool,
16    pub available: bool,
17    /// Deprecated alias of `url`, kept so existing consumers keep working.
18    pub proxy_url: Option<String>,
19    /// The daemon's proxy URL.
20    pub url: Option<String>,
21    /// How the proxy handles TLS for this daemon: `terminate` or `passthrough`.
22    /// Omitted when the daemon is not routed through the proxy.
23    #[serde(skip_serializing_if = "Option::is_none")]
24    pub proxy_tls: Option<String>,
25    pub error: Option<String>,
26    #[serde(skip_serializing_if = "Option::is_none")]
27    pub active_port: Option<u16>,
28    pub port: Vec<u16>,
29}
30
31#[derive(Serialize)]
32pub struct JsonStatusEntry {
33    pub id: String,
34    pub namespace: String,
35    pub name: String,
36    pub pid: Option<u32>,
37    pub status: String,
38    /// True for a `oneshot = true` daemon. See `JsonListEntry::oneshot`.
39    pub oneshot: bool,
40    #[serde(skip_serializing_if = "Option::is_none")]
41    pub active_port: Option<u16>,
42    pub port: Vec<u16>,
43    /// Deprecated alias of `url`, kept so existing consumers keep working.
44    pub proxy_url: Option<String>,
45    /// The daemon's proxy URL.
46    pub url: Option<String>,
47    /// How the proxy handles TLS for this daemon: `terminate` or `passthrough`.
48    /// Omitted when the daemon is not routed through the proxy.
49    #[serde(skip_serializing_if = "Option::is_none")]
50    pub proxy_tls: Option<String>,
51    /// The daemon's cron expression. Omitted when it is not scheduled; the
52    /// two fields below are omitted with it.
53    #[serde(skip_serializing_if = "Option::is_none")]
54    pub cron_schedule: Option<String>,
55    /// When the cron watcher last started this daemon, RFC 3339. Omitted
56    /// until a scheduled run has actually happened -- a registered schedule
57    /// that has not come due yet has no last run.
58    #[serde(skip_serializing_if = "Option::is_none")]
59    pub cron_last_run: Option<String>,
60    /// The next time the schedule comes due, RFC 3339. A time in the past
61    /// means the window was missed while the supervisor was down and the
62    /// watcher will take it on its next check.
63    #[serde(skip_serializing_if = "Option::is_none")]
64    pub cron_next_run: Option<String>,
65}
66
67#[derive(Serialize)]
68pub struct JsonLogEntry {
69    /// SQLite row id, used by the webui for backward pagination (scroll-up
70    /// history loading). Skipped in serialization when 0 to avoid changing
71    /// the CLI --json output shape for non-web callers.
72    #[serde(skip_serializing_if = "is_zero_id")]
73    pub id: i64,
74    pub timestamp: String,
75    pub daemon_id: String,
76    pub message: String,
77    #[serde(skip_serializing_if = "Option::is_none")]
78    pub level: Option<String>,
79    #[serde(skip_serializing_if = "Option::is_none")]
80    pub msg: Option<String>,
81    #[serde(skip_serializing_if = "Option::is_none")]
82    pub logger: Option<String>,
83    /// Parsed structured fields as a JSON object, or null if the line was
84    /// not structured (plain text).
85    #[serde(skip_serializing_if = "Option::is_none")]
86    pub fields: Option<serde_json::Value>,
87}
88
89fn is_zero_id(id: &i64) -> bool {
90    *id == 0
91}
92
93impl From<LogEntry> for JsonLogEntry {
94    fn from(e: LogEntry) -> Self {
95        let fields = e
96            .fields_json
97            .as_deref()
98            .and_then(|s| serde_json::from_str(s).ok());
99        JsonLogEntry {
100            id: e.id,
101            timestamp: e.timestamp.format("%Y-%m-%d %H:%M:%S").to_string(),
102            daemon_id: e.daemon_id,
103            message: console::strip_ansi_codes(&e.message).to_string(),
104            level: e.level,
105            msg: e.msg,
106            logger: e.logger,
107            fields,
108        }
109    }
110}
111
112#[derive(Serialize)]
113pub struct JsonDaemonConfigEntry {
114    pub id: String,
115    /// As written in the config: a string, or an array for the argv form.
116    pub run: crate::pitchfork_toml::RunCommand,
117}
118
119#[derive(Serialize)]
120pub struct JsonProxyStatus {
121    pub enabled: bool,
122    #[serde(skip_serializing_if = "Option::is_none")]
123    pub scheme: Option<String>,
124    #[serde(skip_serializing_if = "Option::is_none")]
125    pub tld: Option<String>,
126    #[serde(skip_serializing_if = "Option::is_none")]
127    pub port: Option<u16>,
128    #[serde(skip_serializing_if = "Option::is_none")]
129    pub lan: Option<JsonLanInfo>,
130    #[serde(skip_serializing_if = "Option::is_none")]
131    pub tls_cert: Option<String>,
132    #[serde(skip_serializing_if = "Option::is_none")]
133    pub trusted: Option<bool>,
134    pub slugs: Vec<JsonSlugEntry>,
135    /// Automatic hostnames, grouped by project and worktree.
136    #[serde(default, skip_serializing_if = "Vec::is_empty")]
137    pub projects: Vec<JsonProxyProject>,
138    /// Labels claimed by more than one project, worktree or daemon. Nothing is
139    /// routed under them until the clash is resolved.
140    #[serde(default, skip_serializing_if = "Vec::is_empty")]
141    pub conflicts: Vec<String>,
142}
143
144#[derive(Serialize)]
145pub struct JsonProxyProject {
146    pub project: String,
147    pub dir: String,
148    /// URL of the project page.
149    pub url: String,
150    pub daemons: Vec<JsonProxyHost>,
151    #[serde(skip_serializing_if = "Vec::is_empty")]
152    pub worktrees: Vec<JsonProxyWorktree>,
153}
154
155#[derive(Serialize)]
156pub struct JsonProxyWorktree {
157    pub worktree: String,
158    pub dir: String,
159    /// URL of the stack page.
160    pub url: String,
161    pub daemons: Vec<JsonProxyHost>,
162}
163
164#[derive(Serialize)]
165pub struct JsonProxyHost {
166    pub daemon: String,
167    /// Hostname without the TLD.
168    pub host: String,
169    pub url: String,
170    pub status: String,
171    #[serde(skip_serializing_if = "Option::is_none")]
172    pub port: Option<u16>,
173}
174
175#[derive(Serialize)]
176pub struct JsonLanInfo {
177    pub enabled: bool,
178    pub ip: String,
179}
180
181#[derive(Serialize)]
182pub struct JsonSlugEntry {
183    pub slug: String,
184    /// Absent when the slug is not routable (see `status`).
185    #[serde(skip_serializing_if = "Option::is_none")]
186    pub url: Option<String>,
187    pub dir: String,
188    pub daemon: String,
189    pub status: String,
190    #[serde(skip_serializing_if = "Option::is_none")]
191    pub port: Option<u16>,
192}
193
194#[derive(Serialize)]
195pub struct JsonSupervisorStatus {
196    pub status: &'static str,
197    #[serde(skip_serializing_if = "Option::is_none")]
198    pub web_ui: Option<String>,
199    #[serde(skip_serializing_if = "Option::is_none")]
200    pub error: Option<String>,
201}
202
203#[derive(Serialize)]
204pub struct JsonSettingEntry {
205    pub key: String,
206    pub value: String,
207    #[serde(skip_serializing_if = "Option::is_none")]
208    pub default: Option<String>,
209    #[serde(skip_serializing_if = "Option::is_none")]
210    pub r#type: Option<&'static str>,
211    #[serde(skip_serializing_if = "Option::is_none")]
212    pub env_var: Option<&'static str>,
213    /// Where the winning value came from, named so a script can act on it:
214    /// `PITCHFORK_LOG`, `/path/to/pitchfork.toml#general.log_level`, `the
215    /// default`. Absent only for a setting nothing set and that has no
216    /// default.
217    #[serde(skip_serializing_if = "Option::is_none")]
218    pub origin: Option<String>,
219}
220
221pub fn print_json<T: Serialize>(value: &T) -> crate::Result<()> {
222    let json = serde_json::to_string_pretty(value)
223        .map_err(|e| miette::miette!("failed to serialize JSON: {e}"))?;
224    println!("{json}");
225    Ok(())
226}