Skip to content

Commit 7ab8b70

Browse files
author
Timothy Dodd
committed
Update .gitignore for quartz-dashboard project
Add patterns to ignore temporary/generated files such as `__azurite*` and `__blobstorage*`. Include a rule to ignore Nx cache files (`**/.nx/cache/`) and exclude `/src/.github/copilot-instructions.md` from version control. These changes ensure that unnecessary files are not tracked in the repository.
1 parent 56cd8c5 commit 7ab8b70

31 files changed

Lines changed: 2476 additions & 1118 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,3 +167,4 @@ __blobstorage*
167167

168168
# Nx
169169
**/.nx/cache/
170+
/src/.github/copilot-instructions.md

src/LogMkAgent/Common/ApiJsonSerializerContext.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ namespace LogMkAgent;
99
[JsonSerializable(typeof(IEnumerable<LatestDeploymentEntry>))]
1010
[JsonSerializable(typeof(IEnumerable<LogLine>))]
1111
[JsonSerializable(typeof(ValidationSettings))]
12+
[JsonSerializable(typeof(AgentHeartbeat))]
13+
[JsonSerializable(typeof(AgentConfig))]
1214
public partial class ApiJsonSerializerContext : JsonSerializerContext
1315
{
1416
}

src/LogMkAgent/LogMkAgent.csproj

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,10 @@
1313
</PropertyGroup>
1414

1515
<ItemGroup>
16-
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="10.0.3" />
17-
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.3" />
18-
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.3" />
19-
<PackageReference Include="System.Text.Json" Version="10.0.3" />
16+
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="10.0.7" />
17+
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.7" />
18+
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.7" />
19+
<PackageReference Include="System.Text.Json" Version="10.0.7" />
2020
</ItemGroup>
2121

2222
<ItemGroup>

src/LogMkAgent/Program.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,12 @@ public static async Task Main(string[] args)
6363
});
6464
});
6565

66-
// Register hosted service
67-
services.AddHostedService<LogWatcher>();
66+
// Register hosted services. LogWatcher is registered as a singleton
67+
// so other services (AgentRegistrationService) can inject it for runtime
68+
// config updates, then exposed as a hosted service via factory.
69+
services.AddSingleton<LogWatcher>();
70+
services.AddHostedService(sp => sp.GetRequiredService<LogWatcher>());
71+
services.AddHostedService<AgentRegistrationService>();
6872
}).Build();
6973

7074
await host.RunAsync();
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
using System.Net.NetworkInformation;
2+
using System.Net.Sockets;
3+
using System.Reflection;
4+
using LogMkCommon;
5+
6+
namespace LogMkAgent.Services;
7+
8+
/// <summary>
9+
/// Periodically posts a heartbeat with runtime stats and pulls down the latest
10+
/// agent-specific config so log filtering and batching can be tuned from the
11+
/// web portal without restarting the agent.
12+
/// </summary>
13+
public class AgentRegistrationService : BackgroundService
14+
{
15+
private readonly LogApiClient _apiClient;
16+
private readonly BatchingService _batchingService;
17+
private readonly LogWatcher _logWatcher;
18+
private readonly ILogger<AgentRegistrationService> _logger;
19+
20+
private readonly TimeSpan _heartbeatInterval = TimeSpan.FromSeconds(30);
21+
private readonly TimeSpan _configPollInterval = TimeSpan.FromSeconds(60);
22+
23+
private readonly DateTime _startedAt = DateTime.UtcNow;
24+
private readonly string _agentId;
25+
private readonly string _hostname;
26+
private readonly string? _version;
27+
private readonly string? _ipAddress;
28+
29+
public AgentRegistrationService(
30+
LogApiClient apiClient,
31+
BatchingService batchingService,
32+
LogWatcher logWatcher,
33+
ILogger<AgentRegistrationService> logger)
34+
{
35+
_apiClient = apiClient;
36+
_batchingService = batchingService;
37+
_logWatcher = logWatcher;
38+
_logger = logger;
39+
40+
_hostname = Environment.MachineName;
41+
_agentId = _hostname; // DaemonSet → one agent per node, hostname is unique
42+
_version = Assembly.GetExecutingAssembly().GetName().Version?.ToString();
43+
_ipAddress = TryGetLocalIp();
44+
45+
_logger.LogInformation("AgentRegistrationService starting: AgentId={AgentId}, Hostname={Hostname}, Version={Version}",
46+
_agentId, _hostname, _version);
47+
}
48+
49+
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
50+
{
51+
// Pull config once on startup before the regular polling loop
52+
await PullConfigAsync(stoppingToken).ConfigureAwait(false);
53+
54+
var lastConfigPull = DateTime.UtcNow;
55+
56+
while (!stoppingToken.IsCancellationRequested)
57+
{
58+
try
59+
{
60+
await SendHeartbeatAsync(stoppingToken).ConfigureAwait(false);
61+
}
62+
catch (Exception ex)
63+
{
64+
_logger.LogWarning(ex, "Heartbeat failed");
65+
}
66+
67+
if (DateTime.UtcNow - lastConfigPull >= _configPollInterval)
68+
{
69+
try
70+
{
71+
await PullConfigAsync(stoppingToken).ConfigureAwait(false);
72+
lastConfigPull = DateTime.UtcNow;
73+
}
74+
catch (Exception ex)
75+
{
76+
_logger.LogWarning(ex, "Config pull failed");
77+
}
78+
}
79+
80+
try
81+
{
82+
await Task.Delay(_heartbeatInterval, stoppingToken).ConfigureAwait(false);
83+
}
84+
catch (OperationCanceledException)
85+
{
86+
break;
87+
}
88+
}
89+
}
90+
91+
private async Task SendHeartbeatAsync(CancellationToken cancellationToken)
92+
{
93+
var stats = _batchingService.GetStats();
94+
var heartbeat = new AgentHeartbeat
95+
{
96+
AgentId = _agentId,
97+
Hostname = _hostname,
98+
Version = _version,
99+
IpAddress = _ipAddress,
100+
StartedAt = _startedAt,
101+
LastBatchSentAt = stats.LastSuccessfulSend,
102+
QueuedItems = stats.QueuedItems,
103+
TotalItemsProcessed = stats.TotalItemsProcessed,
104+
TotalBatchesSent = stats.TotalBatchesSent,
105+
TotalFailures = stats.TotalFailures,
106+
TotalDropped = stats.TotalDropped,
107+
CircuitBreakerState = stats.CircuitBreakerState
108+
};
109+
110+
var response = await _apiClient.PostJsonAsync("api/agents/heartbeat", heartbeat, cancellationToken)
111+
.ConfigureAwait(false);
112+
113+
if (!response.IsSuccessStatusCode)
114+
{
115+
_logger.LogDebug("Heartbeat returned non-success status {StatusCode}", response.StatusCode);
116+
}
117+
}
118+
119+
private async Task PullConfigAsync(CancellationToken cancellationToken)
120+
{
121+
try
122+
{
123+
var config = await _apiClient.GetDataAsync<AgentConfig>($"api/agents/{Uri.EscapeDataString(_agentId)}/config")
124+
.ConfigureAwait(false);
125+
126+
if (config == null)
127+
{
128+
_logger.LogDebug("No agent config returned for {AgentId}", _agentId);
129+
return;
130+
}
131+
132+
_logWatcher.ApplyConfig(config);
133+
_batchingService.ApplyConfig(config);
134+
}
135+
catch (HttpRequestException ex)
136+
{
137+
_logger.LogDebug(ex, "Could not reach API to pull config");
138+
}
139+
}
140+
141+
private static string? TryGetLocalIp()
142+
{
143+
try
144+
{
145+
foreach (var ni in NetworkInterface.GetAllNetworkInterfaces())
146+
{
147+
if (ni.OperationalStatus != OperationalStatus.Up) continue;
148+
if (ni.NetworkInterfaceType == NetworkInterfaceType.Loopback) continue;
149+
150+
foreach (var ip in ni.GetIPProperties().UnicastAddresses)
151+
{
152+
if (ip.Address.AddressFamily == AddressFamily.InterNetwork)
153+
{
154+
return ip.Address.ToString();
155+
}
156+
}
157+
}
158+
}
159+
catch
160+
{
161+
// best-effort
162+
}
163+
return null;
164+
}
165+
}

src/LogMkAgent/Services/BatchService.cs

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@ public class BatchingService : IDisposable
3131
// Circuit breaker
3232
private readonly CircuitBreaker _circuitBreaker;
3333

34-
// Rate limiting
35-
private readonly int _maxBatchesPerMinute;
34+
// Rate limiting (mutable so it can be tuned from server-side config)
35+
private int _maxBatchesPerMinute;
3636
private readonly Queue<DateTime> _batchSendTimestamps = new();
3737
private readonly object _rateLimitLock = new();
3838

@@ -97,6 +97,24 @@ private void ValidateOptions()
9797
throw new ArgumentException("SendTimeout must be positive");
9898
}
9999

100+
public void ApplyConfig(AgentConfig config)
101+
{
102+
if (config == null) return;
103+
104+
if (config.MaxBatchSize > 0)
105+
_options.MaxBatchSize = config.MaxBatchSize;
106+
107+
if (config.MaxBatchesPerMinute > 0)
108+
_maxBatchesPerMinute = config.MaxBatchesPerMinute;
109+
110+
if (config.MaxQueueSize > 0)
111+
_options.MaxQueueSize = config.MaxQueueSize;
112+
113+
_logger.LogInformation(
114+
"Applied batching config: MaxBatchSize={MaxBatchSize}, MaxBatchesPerMinute={MaxBatchesPerMinute}, MaxQueueSize={MaxQueueSize}",
115+
_options.MaxBatchSize, _maxBatchesPerMinute, _options.MaxQueueSize);
116+
}
117+
100118
public void AddData(LogLine data)
101119
{
102120
if (_disposed)

src/LogMkAgent/Services/LogApiClient.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,13 @@ public async Task<HttpResponseMessage> SendDataAsync<T>(string url, T data, Canc
5050
return await _httpClient.PostAsync(url, requestContent, cancellationToken);
5151
}
5252

53+
public async Task<HttpResponseMessage> PostJsonAsync<T>(string url, T data, CancellationToken cancellationToken)
54+
{
55+
var json = JsonSerializer.Serialize(data, jsonOptions);
56+
using var content = new StringContent(json, Encoding.UTF8, "application/json");
57+
return await _httpClient.PostAsync(url, content, cancellationToken);
58+
}
59+
5360

5461
private byte[] CompressData<T>(T data)
5562
{

src/LogMkAgent/Services/LogWatcher.cs

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@ public class LogWatcher : BackgroundService
3232
private LogLevel _defaultLogLevel = LogLevel.Information;
3333
private readonly SemaphoreSlim _fileSemaphore = new(Environment.ProcessorCount);
3434

35+
// Tracks names that came from the local config file vs runtime overrides
36+
// (so re-applying a remote config can clear stale runtime entries without
37+
// wiping out file-based defaults)
38+
private readonly HashSet<string> _runtimePodOverrides = new(StringComparer.OrdinalIgnoreCase);
39+
3540
// Static readonly for better performance
3641
// Moved to LogParser class in LogMkCommon
3742

@@ -46,7 +51,7 @@ public class LogWatcher : BackgroundService
4651
{
4752
".log", ".txt", ".csv", ".evtx" // Add .evtx
4853
};
49-
private readonly int _maxDaysOld = 30; // Maximum age of logs to process
54+
private int _maxDaysOld = 30; // Maximum age of logs to process
5055

5156
private readonly StateService _stateService;
5257

@@ -210,6 +215,44 @@ private async Task InitializeDeploymentTimesAsync(LogApiClient client)
210215
}
211216
}
212217

218+
public void ApplyConfig(AgentConfig config)
219+
{
220+
if (config == null) return;
221+
222+
_defaultLogLevel = AgentConfig.ParseLogLevel(config.DefaultLogLevel, LogLevel.Information);
223+
_maxDaysOld = config.MaxDaysOld > 0 ? config.MaxDaysOld : 30;
224+
225+
// Reset previous runtime overrides so removed entries don't linger.
226+
// We keep file-based overrides untouched (those weren't in _runtimePodOverrides).
227+
foreach (var pod in _runtimePodOverrides.ToList())
228+
{
229+
if (_podSettings.TryGetValue(pod, out var existing))
230+
{
231+
existing.LogLevel = _defaultLogLevel;
232+
existing.Ignore = false;
233+
}
234+
}
235+
_runtimePodOverrides.Clear();
236+
237+
foreach (var kvp in config.PodLogLevels)
238+
{
239+
var settings = GetPodSettings(kvp.Key);
240+
settings.LogLevel = AgentConfig.ParseLogLevel(kvp.Value, _defaultLogLevel);
241+
_runtimePodOverrides.Add(kvp.Key);
242+
}
243+
244+
foreach (var pod in config.IgnoredPods)
245+
{
246+
var settings = GetPodSettings(pod);
247+
settings.Ignore = true;
248+
_runtimePodOverrides.Add(pod);
249+
}
250+
251+
_logger.LogInformation(
252+
"Applied agent config: DefaultLogLevel={DefaultLevel}, PodOverrides={PodCount}, IgnoredPods={IgnoredCount}, MaxDaysOld={MaxDaysOld}",
253+
_defaultLogLevel, config.PodLogLevels.Count, config.IgnoredPods.Count, _maxDaysOld);
254+
}
255+
213256
private PodSettings GetPodSettings(string podName)
214257
{
215258
return _podSettings.GetOrAdd(podName, _ => new PodSettings

0 commit comments

Comments
 (0)