Forge
csharpdeeb25a2
1#nullable enable
2using System.Diagnostics;
3using System.Globalization;
4using CascadeIDE.Services;
5
6namespace CascadeIDE.Features.Chat;
7
8/// <summary>
9/// Трассировка отправки Intercom (attach @ send, провайдер). Файл: <c>{workspace}/.cascade-ide/intercom-send-trace.log</c>.
10/// </summary>
11/// <remarks>
12/// Включение: <c>%LocalAppData%\CascadeIDE\settings.toml</c> — <c>[logging.intercom] send_trace = true</c> (по умолчанию false).
13/// Границы фаз — через <see cref="Run"/>, <see cref="RunAsync"/> и <see cref="IntercomSendPhase.Detail"/>.
14/// </remarks>
15public static class IntercomSendTrace
16{
17 private const int MaxLogBytes = 512 * 1024;
18
19 private static bool _settingsCacheValid;
20 private static bool _settingsSendTrace;
21
22 public static bool IsEnabled => ReadSettingsSendTrace();
23
24 /// <summary>Сбросить кэш после <see cref="SettingsService.Load"/> / <see cref="SettingsService.Save"/>.</summary>
25 public static void InvalidateSettingsCache() => _settingsCacheValid = false;
26
27 public static T Run<T>(string? workspaceRoot, string phase, Func<IntercomSendPhase, T> body)
28 {
29 using var _ = Begin(workspaceRoot, phase);
30 return body(new IntercomSendPhase(workspaceRoot, phase));
31 }
32
33 public static void Run(string? workspaceRoot, string phase, Action<IntercomSendPhase> body)
34 {
35 using var _ = Begin(workspaceRoot, phase);
36 body(new IntercomSendPhase(workspaceRoot, phase));
37 }
38
39 public static async Task RunAsync(string? workspaceRoot, string phase, Func<IntercomSendPhase, Task> body)
40 {
41 using var _ = Begin(workspaceRoot, phase);
42 await body(new IntercomSendPhase(workspaceRoot, phase)).ConfigureAwait(false);
43 }
44
45 public static async Task<T> RunAsync<T>(
46 string? workspaceRoot,
47 string phase,
48 Func<IntercomSendPhase, Task<T>> body)
49 {
50 using var _ = Begin(workspaceRoot, phase);
51 return await body(new IntercomSendPhase(workspaceRoot, phase)).ConfigureAwait(false);
52 }
53
54 public static IDisposable Begin(string? workspaceRoot, string phase) =>
55 IsEnabled ? new Phase(workspaceRoot, phase) : NoopPhase.Instance;
56
57 public static void Write(string? workspaceRoot, string phase, string detail)
58 {
59 if (!IsEnabled)
60 return;
61
62 var line = $"[{UtcStamp()}] {phase} {detail}";
63 System.Diagnostics.Debug.WriteLine("[intercom-send] " + line);
64 TryAppendFile(workspaceRoot, line);
65 }
66
67 private static bool ReadSettingsSendTrace()
68 {
69 if (_settingsCacheValid)
70 return _settingsSendTrace;
71
72 _settingsCacheValid = true;
73 try
74 {
75 _settingsSendTrace = SettingsService.Load().Logging.Intercom.SendTrace;
76 }
77 catch
78 {
79 _settingsSendTrace = false;
80 }
81
82 return _settingsSendTrace;
83 }
84
85 private static string UtcStamp() =>
86 DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss.fff", CultureInfo.InvariantCulture) + "Z";
87
88 private static void TryAppendFile(string? workspaceRoot, string line)
89 {
90 if (string.IsNullOrWhiteSpace(workspaceRoot))
91 return;
92
93 try
94 {
95 var root = workspaceRoot.Trim();
96 if (!Directory.Exists(root))
97 return;
98
99 var logDir = Path.Combine(root, ".cascade-ide");
100 Directory.CreateDirectory(logDir);
101 var logPath = Path.Combine(logDir, "intercom-send-trace.log");
102 trimLogIfNeeded(logPath);
103 File.AppendAllText(logPath, line + Environment.NewLine);
104 }
105 catch
106 {
107 // best-effort
108 }
109 }
110
111 private static void trimLogIfNeeded(string logPath)
112 {
113 try
114 {
115 if (!File.Exists(logPath))
116 return;
117
118 var info = new FileInfo(logPath);
119 if (info.Length <= MaxLogBytes)
120 return;
121
122 var keepBytes = MaxLogBytes / 2;
123 using var stream = new FileStream(logPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
124 stream.Seek(-keepBytes, SeekOrigin.End);
125 var tail = new byte[keepBytes];
126 _ = stream.Read(tail, 0, keepBytes);
127 File.WriteAllBytes(logPath, tail);
128 }
129 catch
130 {
131 // best-effort
132 }
133 }
134
135 private sealed class Phase : IDisposable
136 {
137 private readonly string? _workspaceRoot;
138 private readonly string _phase;
139 private readonly Stopwatch _sw = Stopwatch.StartNew();
140
141 public Phase(string? workspaceRoot, string phase)
142 {
143 _workspaceRoot = workspaceRoot;
144 _phase = phase;
145 Write(workspaceRoot, phase, "start");
146 }
147
148 public void Dispose()
149 {
150 _sw.Stop();
151 Write(_workspaceRoot, _phase, $"done elapsed_ms={_sw.ElapsedMilliseconds}");
152 }
153 }
154
155 private sealed class NoopPhase : IDisposable
156 {
157 public static readonly NoopPhase Instance = new();
158 public void Dispose() { }
159 }
160}
161
162/// <summary>Контекст активной фазы трассировки (детали внутри <see cref="IntercomSendTrace.Run"/>).</summary>
163public readonly struct IntercomSendPhase
164{
165 private readonly string? _workspaceRoot;
166 private readonly string _phase;
167
168 internal IntercomSendPhase(string? workspaceRoot, string phase)
169 {
170 _workspaceRoot = workspaceRoot;
171 _phase = phase;
172 }
173
174 public void Detail(string detail) => IntercomSendTrace.Write(_workspaceRoot, _phase, detail);
175}
176
View only · write via MCP/CIDE