1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
//! Local ops telemetry — the single owned source of truth for the path and
//! writer that records operational metrics (`lf sync`, `lf wt create`, …).
//!
//! Telemetry lives under the git-ignored `.lf/tmp/metrics/ops.jsonl` tree so
//! read-only operations never dirty a tracked worktree. The previous design
//! appended to a *tracked* `.lf/metrics/ops.jsonl`; that made every read-only
//! op dirty the checkout, which then made dispatch refuse the "previously
//! clean" checkout and broke `lf task run` on the dogfood loop. The path
//! contract here — ignored tree, never tracked — is guarded by tests below so
//! a regression back to a tracked path fails the build.
use std::io::Write;
use std::path::{Path, PathBuf};
/// Where ops telemetry is recorded: `.lf/tmp/metrics/ops.jsonl` under the
/// worktree. `.lf/tmp/` is git-ignored, so recording a metric never touches a
/// tracked file. One source of truth for the path.
pub(crate) fn ops_metrics_path(repo: &Path) -> PathBuf {
repo.join(".lf")
.join("tmp")
.join("metrics")
.join("ops.jsonl")
}
/// Best-effort append of one ops-metric event. Never fails the caller: a
/// telemetry write that errors is dropped, not propagated, so an op never
/// breaks because its metric couldn't land. A `ts` is stamped on every event.
pub(crate) fn record_ops_metric(repo: &Path, mut event: serde_json::Value) {
let Some(object) = event.as_object_mut() else {
return;
};
object.insert(
"ts".to_string(),
serde_json::Value::String(chrono::Utc::now().to_rfc3339()),
);
let path = ops_metrics_path(repo);
let Some(parent) = path.parent() else {
return;
};
if std::fs::create_dir_all(parent).is_err() {
return;
}
let Ok(mut file) = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(path)
else {
return;
};
if serde_json::to_writer(&mut file, &event).is_ok() {
let _ = writeln!(file);
}
}
#[cfg(test)]
mod tests {
use super::{ops_metrics_path, record_ops_metric};
use std::path::Path;
use std::process::Command;
fn git(repo: &Path, args: &[&str]) {
let ok = Command::new("git")
.args(args)
.current_dir(repo)
.status()
.expect("git runs")
.success();
assert!(ok, "git {args:?} failed");
}
fn porcelain(repo: &Path) -> String {
let out = Command::new("git")
.args(["status", "--porcelain"])
.current_dir(repo)
.output()
.expect("git status runs");
String::from_utf8(out.stdout).expect("utf8")
}
fn ignored(repo: &Path, path: &Path) -> bool {
Command::new("git")
.args(["check-ignore", "--quiet", "--"])
.arg(path)
.current_dir(repo)
.status()
.map(|status| status.success())
.unwrap_or(false)
}
fn clean_fixture() -> tempfile::TempDir {
let dir = tempfile::tempdir().expect("tempdir");
let repo = dir.path();
git(repo, &["init", "-q"]);
git(repo, &["config", "user.email", "test@example.com"]);
git(repo, &["config", "user.name", "Test"]);
// The ignored-path contract: `.lf/tmp/` is never committed.
std::fs::write(repo.join(".gitignore"), ".lf/tmp/\n").expect("write .gitignore");
git(repo, &["add", "."]);
git(repo, &["commit", "-qm", "init"]);
assert_eq!(porcelain(repo), "", "fixture should start clean");
dir
}
/// The path contract: telemetry targets the git-ignored `.lf/tmp/` tree and
/// never the legacy tracked `.lf/metrics/` path that used to dirty checkouts.
/// A change that moves the path back under a tracked directory fails here
/// before it can reach production.
#[test]
fn ops_metrics_path_targets_ignored_tree_not_legacy_tracked_path() {
let dir = tempfile::tempdir().expect("tempdir");
let repo = dir.path();
let path = ops_metrics_path(repo);
assert!(
path.starts_with(repo.join(".lf").join("tmp")),
"telemetry must live under the ignored .lf/tmp/ tree, got {}",
path.display()
);
assert!(
!path.starts_with(repo.join(".lf").join("metrics")),
"telemetry must never target the legacy tracked .lf/metrics/ path, got {}",
path.display()
);
assert_eq!(
path.file_name(),
Some(std::ffi::OsStr::new("ops.jsonl")),
"telemetry file name is stable"
);
}
/// The dogfood regression: recording ops telemetry into a clean checkout
/// must leave it clean. The tracked-path bug (`.lf/metrics/ops.jsonl`) made
/// every read-only op dirty the worktree and blocked `lf task run`; the fix
/// routes telemetry under the git-ignored `.lf/tmp/` tree.
#[test]
fn recording_telemetry_leaves_a_clean_checkout() {
let dir = clean_fixture();
let repo = dir.path();
record_ops_metric(repo, serde_json::json!({ "op": "sync", "class": "noop" }));
// Telemetry is not disabled: the record lands on disk...
let path = ops_metrics_path(repo);
assert!(path.exists(), "telemetry file must be written");
// ...and git reports the path as ignored, so it cannot dirty the tree...
assert!(
ignored(repo, &path),
"telemetry path {} must be git-ignored",
path.display()
);
// ...so the checkout stays clean and dispatch/sync/status never refuse.
assert_eq!(
porcelain(repo),
"",
"recording telemetry must not dirty a clean worktree"
);
}
/// A malformed (non-object) event is dropped silently rather than panicking
/// or writing a torn line — telemetry never breaks the op that emits it.
#[test]
fn non_object_event_is_dropped_not_written() {
let dir = clean_fixture();
let repo = dir.path();
record_ops_metric(repo, serde_json::json!(["not", "an", "object"]));
let path = ops_metrics_path(repo);
assert!(
!path.exists(),
"a non-object event must not create the file"
);
assert_eq!(porcelain(repo), "", "dropped event must not dirty the tree");
}
}