Skip to content

Commit 4a376af

Browse files
wgutmannclaude
andcommitted
add Grafana Alloy log shipper + data capture suite logging gaps
- Add Alloy service to local observability docker-compose stack to tail plugin-structured.jsonl and push to Loki - Add config.alloy with JSONL parsing and label extraction (level, component, event, domain) - Data capture suite dashboard and plugin logging improvements - Deploy script and dashboard bridge updates Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 15d70a9 commit 4a376af

10 files changed

Lines changed: 235 additions & 3 deletions

File tree

.env.example

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,16 @@ SIMSTEWARD_LOG_DEBUG= # set to 1 for local debug mode only
7474
# For OTLP HTTP on 4318: also set OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
7575
# Prometheus UI (host): http://localhost:9090 — Grafana datasource UID: prometheus_local
7676

77+
# --- Sentry (error tracking + release tracking) ---
78+
# 3 separate Sentry projects — each component has its own DSN:
79+
# simhub-plugin: C# plugin (DSN used in plugin Init, configurable via env var)
80+
# index-dashboard: Main dashboard (DSN hardcoded in index.html)
81+
# test-dashboard: Data capture suite (DSN hardcoded in data-capture-suite.html)
82+
# Plugin DSN (overrides hardcoded default):
83+
# SIMSTEWARD_SENTRY_DSN=https://ab2d0a6f7cd97033a46f4fa7d90dabab@o4511097126780928.ingest.us.sentry.io/4511102961319936
84+
# Auth token for deploy.ps1 release/deploy tracking (Organization:Read, Release:Admin scopes):
85+
# SENTRY_AUTH_TOKEN=sntrys_...
86+
7787
# --- Local observability stack (observability/local/docker-compose.yml) ---
7888
# GRAFANA_STORAGE_PATH= # e.g. S:/sim-steward-grafana-storage (Loki, Grafana, Prometheus TSDB, local stack data)
7989
# LOKI_PUSH_TOKEN= # optional; if Loki gateway requires auth

deploy.ps1

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,8 @@ $PluginDlls = @(
107107
"Fleck.dll",
108108
"Newtonsoft.Json.dll",
109109
"IRSDKSharper.dll",
110-
"YamlDotNet.dll"
110+
"YamlDotNet.dll",
111+
"Sentry.dll"
111112
)
112113

113114
function Read-PluginDllProductVersion {
@@ -411,6 +412,62 @@ if (-not [string]::IsNullOrWhiteSpace($script:SimStewardPluginVersionDeployed))
411412
}
412413
Write-Host ""
413414

415+
# ── Sentry release + deploy tracking ────────────────────────────────────────
416+
$sentryOrg = 'sim-steward'
417+
$sentryProjects = @('simhub-plugin', 'web-dashboards')
418+
$sentryAuthToken = if (-not [string]::IsNullOrWhiteSpace($env:SENTRY_AUTH_TOKEN)) { $env:SENTRY_AUTH_TOKEN }
419+
elseif (-not [string]::IsNullOrWhiteSpace($env:SENTRY_ELEVATED_API_KEY)) { $env:SENTRY_ELEVATED_API_KEY }
420+
else { $null }
421+
$sentryRelease = if (-not [string]::IsNullOrWhiteSpace($script:SimStewardPluginVersionDeployed)) { $script:SimStewardPluginVersionDeployed } else { $null }
422+
423+
function Push-SentryApi {
424+
param([string]$Path, [hashtable]$Body)
425+
if ([string]::IsNullOrWhiteSpace($sentryAuthToken) -or [string]::IsNullOrWhiteSpace($sentryRelease)) { return }
426+
try {
427+
$url = "https://sentry.io/api/0/organizations/$sentryOrg/$Path"
428+
$json = $Body | ConvertTo-Json -Compress -Depth 5
429+
$headers = @{ Authorization = "Bearer $sentryAuthToken"; 'Content-Type' = 'application/json' }
430+
Invoke-RestMethod -Uri $url -Method Post -Headers $headers -Body $json -ErrorAction Stop | Out-Null
431+
} catch {
432+
# Non-fatal: deploy must not fail because Sentry API is down
433+
Write-Warning "Sentry API ($Path): $($_.Exception.Message)"
434+
}
435+
}
436+
437+
if (-not [string]::IsNullOrWhiteSpace($sentryAuthToken) -and -not [string]::IsNullOrWhiteSpace($sentryRelease)) {
438+
Write-Host "Registering Sentry release: $sentryRelease (3 projects)"
439+
440+
# Create release across all 3 projects with commits
441+
$fullSha = ''
442+
try { $fullSha = (& git -C $PluginRoot rev-parse HEAD 2>$null) } catch {}
443+
Push-SentryApi "releases/" @{
444+
version = $sentryRelease
445+
projects = $sentryProjects
446+
refs = @(@{ repository = "simsteward/simhub-plugin"; commit = $fullSha })
447+
}
448+
449+
# Deploy: simhub-plugin (C# DLLs)
450+
Push-SentryApi "releases/$sentryRelease/deploys/" @{
451+
environment = 'local'
452+
name = 'simhub-plugin'
453+
}
454+
455+
# Deploy: web-dashboards (all HTML/JS dashboards)
456+
Push-SentryApi "releases/$sentryRelease/deploys/" @{
457+
environment = 'local'
458+
name = 'web-dashboards'
459+
}
460+
461+
Write-Host "Sentry release + 2 deploys registered (simhub-plugin, web-dashboards)."
462+
Push-LokiEvent 'deploy_sentry_release' 'INFO' "Sentry release registered: $sentryRelease" @{
463+
sentry_release = $sentryRelease
464+
sentry_org = $sentryOrg
465+
sentry_projects = ($sentryProjects -join ',')
466+
}
467+
} elseif ([string]::IsNullOrWhiteSpace($sentryAuthToken)) {
468+
Write-Host "Skipping Sentry release tracking (SENTRY_AUTH_TOKEN not set)."
469+
}
470+
414471
# ── 5. Re-launch SimHub ─────────────────────────────────────────────────────
415472
$skipLaunch = $env:SIMHUB_SKIP_LAUNCH -eq "1"
416473
if ($skipLaunch) {

observability/local/config.alloy

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Grafana Alloy — tail plugin-structured.jsonl → Loki
2+
// Docs: https://grafana.com/docs/alloy/latest/
3+
4+
local.file_match "simsteward_structured" {
5+
path_targets = [{"__path__" = "/var/log/simsteward/plugin-structured.jsonl"}]
6+
sync_period = "5s"
7+
}
8+
9+
loki.source.file "simsteward_structured" {
10+
targets = local.file_match.simsteward_structured.targets
11+
forward_to = [loki.process.simsteward.receiver]
12+
13+
tail_from_end = true
14+
}
15+
16+
loki.process "simsteward" {
17+
forward_to = [loki.write.local.receiver]
18+
19+
// Extract low-cardinality labels from JSON; everything else stays in the log line.
20+
stage.json {
21+
expressions = {
22+
level = "level",
23+
component = "component",
24+
event = "event",
25+
domain = "domain",
26+
}
27+
}
28+
29+
stage.labels {
30+
values = {
31+
level = "",
32+
component = "",
33+
event = "",
34+
domain = "",
35+
}
36+
}
37+
}
38+
39+
loki.write "local" {
40+
endpoint {
41+
url = "http://loki:3100/loki/api/v1/push"
42+
}
43+
}

observability/local/docker-compose.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,17 @@ services:
8181
- ${GRAFANA_STORAGE_PATH:-S:/sim-steward-grafana-storage}/grafana:/var/lib/grafana
8282
- ./grafana/provisioning:/etc/grafana/provisioning:ro
8383

84+
alloy:
85+
image: grafana/alloy:v1.5.1
86+
depends_on:
87+
loki:
88+
condition: service_healthy
89+
volumes:
90+
- ./config.alloy:/etc/alloy/config.alloy:ro
91+
- ${SIMSTEWARD_DATA_PATH}:/var/log/simsteward:ro
92+
- ${GRAFANA_STORAGE_PATH:-S:/sim-steward-grafana-storage}/alloy:/tmp/positions
93+
command: ["run", "/etc/alloy/config.alloy", "--storage.path=/tmp/positions"]
94+
8495
data-api:
8596
build: ./data-api
8697
ports:

src/SimSteward.Dashboard/data-capture-suite.html

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,15 @@
244244
.filter-clear { background: none; border: none; color: var(--muted); cursor: pointer; font-size: 0.82rem; padding: 0 4px; margin-left: -28px; margin-right: 4px; transition: color 0.15s; z-index: 1; }
245245
.filter-clear:hover { color: var(--text); }
246246
</style>
247+
<script src="https://browser.sentry-cdn.com/9.27.0/bundle.min.js" crossorigin="anonymous"></script>
248+
<script>
249+
Sentry.init({
250+
dsn: 'https://46ad94b35a51979ba4223eeffe38f5c8@o4511097126780928.ingest.us.sentry.io/4511103122210816',
251+
environment: 'local',
252+
tracesSampleRate: 1.0,
253+
});
254+
Sentry.setTag('dashboard', 'data-capture-suite');
255+
</script>
247256
</head>
248257
<body>
249258
<div class="wrap">
@@ -368,6 +377,9 @@
368377
// ── Microinteraction helpers ─────────────────────────────────────────
369378
function logUI(elementId, eventType, message) {
370379
send({ action: 'log', event: 'dashboard_ui_event', element_id: elementId, event_type: eventType, message: message });
380+
if (typeof Sentry !== 'undefined') {
381+
Sentry.addBreadcrumb({ category: 'ui.' + eventType, message: message, data: { element_id: elementId }, level: 'info' });
382+
}
371383
}
372384

373385
function flashEl(el, cls, ms) {
@@ -877,18 +889,26 @@
877889
}
878890
// Log after open so send() works
879891
logUI('ws-pill', 'state_change', 'WebSocket connected');
892+
if (typeof Sentry !== 'undefined') {
893+
Sentry.addBreadcrumb({ category: 'ws', message: 'WebSocket connected', level: 'info' });
894+
Sentry.setTag('ws_connected', 'true');
895+
}
880896
});
881897
ws.addEventListener('close', () => {
882898
_wsOk = false;
883899
_pluginSeen = false;
884900
setPill(false);
901+
if (typeof Sentry !== 'undefined') {
902+
Sentry.addBreadcrumb({ category: 'ws', message: 'WebSocket disconnected', level: 'warning' });
903+
Sentry.setTag('ws_connected', 'false');
904+
}
885905
setTimeout(connect, 2000);
886906
});
887907
ws.addEventListener('message', ev => {
888908
try {
889909
const msg = JSON.parse(ev.data);
890910
if (msg.type === 'state') { _pluginSeen = true; handleState(msg); }
891-
} catch {}
911+
} catch (e) { if (typeof Sentry !== 'undefined') Sentry.captureException(e); }
892912
});
893913
}
894914

@@ -1009,6 +1029,9 @@
10091029
// Version display
10101030
var verEl = document.getElementById('lbl-version');
10111031
if (verEl && msg.pluginVersion) verEl.textContent = 'v' + msg.pluginVersion;
1032+
if (msg.pluginVersion && typeof Sentry !== 'undefined') {
1033+
Sentry.setTag('plugin_version', msg.pluginVersion);
1034+
}
10121035

10131036
// Signals
10141037
setSig('sig-ws', _wsOk);

src/SimSteward.Dashboard/index.html

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -475,6 +475,16 @@
475475
::-webkit-scrollbar-track { background: transparent; }
476476
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
477477
</style>
478+
<script src="https://browser.sentry-cdn.com/9.27.0/bundle.min.js" crossorigin="anonymous"></script>
479+
<script>
480+
Sentry.init({
481+
dsn: 'https://46ad94b35a51979ba4223eeffe38f5c8@o4511097126780928.ingest.us.sentry.io/4511103122210816',
482+
environment: 'local',
483+
release: '',
484+
tracesSampleRate: 1.0,
485+
initialScope: { tags: { dashboard: 'index' } },
486+
});
487+
</script>
478488
</head>
479489
<body>
480490
<div class="app">
@@ -766,15 +776,26 @@
766776
renderInc();
767777
renderStd();
768778
log('health','INFO ','ws_connected','WebSocket connected to plugin at ' + url);
779+
if (typeof Sentry !== 'undefined') {
780+
Sentry.addBreadcrumb({ category: 'ws', message: 'WebSocket connected', level: 'info' });
781+
Sentry.setTag('ws_connected', true);
782+
}
769783
};
770784
ws.onclose = () => {
771785
ws = null; mockPaused = false;
772786
setWs('disconnected');
773787
log('health','WARN ','ws_closed','Connection lost — retrying in 3 s');
788+
if (typeof Sentry !== 'undefined') {
789+
Sentry.addBreadcrumb({ category: 'ws', message: 'WebSocket disconnected', level: 'warning' });
790+
Sentry.setTag('ws_connected', false);
791+
}
774792
setTimeout(connectWs, 3000);
775793
};
776794
ws.onerror = () => { ws = null; mockPaused = false; };
777-
ws.onmessage = e => { try { onMsg(JSON.parse(e.data)); } catch {} };
795+
ws.onmessage = e => {
796+
try { onMsg(JSON.parse(e.data)); }
797+
catch (err) { if (typeof Sentry !== 'undefined') Sentry.captureException(err); }
798+
};
778799
} catch {
779800
setWs('disconnected');
780801
setTimeout(connectWs, 3000);
@@ -789,6 +810,14 @@
789810

790811
/** Structured UI telemetry for Loki (plugin → plugin-structured.jsonl; ingestion outside plugin). */
791812
function sendDashboardUiEvent({ element_id, event_type, message, value }) {
813+
if (typeof Sentry !== 'undefined') {
814+
Sentry.addBreadcrumb({
815+
category: 'ui.' + event_type,
816+
message: message,
817+
data: { element_id },
818+
level: 'info',
819+
});
820+
}
792821
if (!ws || ws.readyState !== WebSocket.OPEN) return false;
793822
const o = { action: 'log', event: 'dashboard_ui_event', element_id, event_type, message };
794823
if (value !== undefined && value !== null) o.value = value;
@@ -829,6 +858,11 @@
829858
const pv = m.pluginVersion ? String(m.pluginVersion) : '';
830859
pvEl.textContent = pv ? ('v' + pv) : 'v—';
831860
pvEl.title = pv ? ('Sim Steward plugin ' + pv) : 'Sim Steward plugin build';
861+
if (pv && typeof Sentry !== 'undefined') {
862+
const client = Sentry.getClient();
863+
if (client && client.getOptions) client.getOptions().release = pv;
864+
Sentry.setTag('plugin_version', pv);
865+
}
832866
}
833867
const pill = document.getElementById('mode-pill');
834868
pill.className = 'mode-pill ' + (mode==='Replay'?'replay':'waiting');

src/SimSteward.Plugin/DashboardBridge.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using Fleck;
44
using Newtonsoft.Json;
55
using Newtonsoft.Json.Linq;
6+
using Sentry;
67

78
namespace SimSteward.Plugin
89
{
@@ -72,6 +73,7 @@ public void Start(string bindAddress, int port, string authToken)
7273
}
7374
catch (Exception ex)
7475
{
76+
SentrySdk.CaptureException(ex);
7577
_logger?.Warn($"DashboardBridge: getStateForNewClient failed: {ex.Message}");
7678
}
7779
try
@@ -82,6 +84,7 @@ public void Start(string bindAddress, int port, string authToken)
8284
}
8385
catch (Exception ex)
8486
{
87+
SentrySdk.CaptureException(ex);
8588
_logger?.Warn($"DashboardBridge: getLogTailForNewClient failed: {ex.Message}");
8689
}
8790
};
@@ -106,6 +109,7 @@ public void Start(string bindAddress, int port, string authToken)
106109
}
107110
catch (Exception ex)
108111
{
112+
SentrySdk.CaptureException(ex);
109113
_logger?.Error($"DashboardBridge: failed to start: {ex.Message}", ex);
110114
throw;
111115
}
@@ -117,6 +121,7 @@ public void Stop()
117121
try { _server.Dispose(); }
118122
catch (Exception ex)
119123
{
124+
SentrySdk.CaptureException(ex);
120125
_logger?.Warn($"DashboardBridge: dispose error: {ex.Message}");
121126
}
122127
_server = null;

src/SimSteward.Plugin/SimSteward.Plugin.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
<PackageReference Include="System.Net.Http" Version="4.3.4" />
3636
<PackageReference Include="OpenTelemetry" Version="1.9.0" />
3737
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.9.0" />
38+
<PackageReference Include="Sentry" Version="4.13.0" />
3839
</ItemGroup>
3940

4041
</Project>

src/SimSteward.Plugin/SimStewardPlugin.DataCaptureSuite.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Diagnostics;
66
using System.Linq;
77
using IRSDKSharper;
8+
using Sentry;
89

910
namespace SimSteward.Plugin
1011
{
@@ -151,6 +152,7 @@ private void ProcessDataCaptureSuiteTick()
151152
}
152153
catch (Exception ex)
153154
{
155+
SentrySdk.CaptureException(ex);
154156
_preflightSnapshot.Phase = "error";
155157
_preflightSnapshot.Error = "BeginPreflight: " + ex.GetType().Name + ": " + ex.Message;
156158
_preflightStep = PreflightStep.Complete;
@@ -161,6 +163,7 @@ private void ProcessDataCaptureSuiteTick()
161163
try { TickPreflight(); }
162164
catch (Exception ex)
163165
{
166+
SentrySdk.CaptureException(ex);
164167
_preflightSnapshot.Phase = "error";
165168
_preflightSnapshot.Error = "TickPreflight@" + _preflightStep + ": " + ex.GetType().Name + ": " + ex.Message;
166169
_preflightStep = PreflightStep.Complete;
@@ -411,6 +414,7 @@ private void TickPreflight()
411414
}
412415
catch (Exception ex)
413416
{
417+
SentrySdk.CaptureException(ex);
414418
SetPfTest("PC_CHECKERED", false, "seek_failed: " + ex.Message);
415419
SetPfTest("PC_RESULTS", false, "seek_failed");
416420
_preflightSnapshot.Error = "seek_failed: " + ex.Message;
@@ -681,6 +685,8 @@ private void BeginDataCaptureSuite()
681685

682686
EmitSuiteLifecycleEvent(DataCaptureSuiteConstants.EventSuiteStarted,
683687
$"Data capture suite started. test_run_id={_suiteTestRunId}", "T_start");
688+
SentrySdk.AddBreadcrumb("Data capture suite started", "lifecycle",
689+
data: new Dictionary<string, string> { ["test_run_id"] = _suiteTestRunId });
684690
_logger?.Info($"DataCaptureSuite started. test_run_id={_suiteTestRunId}");
685691
}
686692

0 commit comments

Comments
 (0)