Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/dotnet/HoldFast.Backend.slnx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<Solution>
<Folder Name="/src/">
<Project Path="src/HoldFast.Analytics/HoldFast.Analytics.csproj" />
<Project Path="src/HoldFast.Api/HoldFast.Api.csproj" />
<Project Path="src/HoldFast.Data.ClickHouse/HoldFast.Data.ClickHouse.csproj" />
<Project Path="src/HoldFast.Data/HoldFast.Data.csproj" />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
using System.Globalization;
using System.Text;

namespace HoldFast.Data.ClickHouse;
namespace HoldFast.Analytics;

/// <summary>
/// Encodes and decodes pagination cursors matching Go's cursor.go format.
/// Format: base64("{RFC3339},{uuid}")
///
/// Lives in HoldFast.Analytics (not a backend-specific project) because the
/// cursor format is part of the public GraphQL API contract — every backend
/// must produce/consume the same cursors so frontend pagination state survives
/// a backend swap.
/// </summary>
public static class CursorHelper
{
Expand Down
23 changes: 23 additions & 0 deletions src/dotnet/src/HoldFast.Analytics/HoldFast.Analytics.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">

<!--
HoldFast.Analytics — backend-neutral abstractions for analytics storage
(logs, traces, sessions, errors, metrics, events, alert state).
Carved out of HoldFast.Data.ClickHouse in HOL-25 to enable a future
Postgres backend (HOL-26+) without touching call sites again.

This project must depend ONLY on HoldFast.Domain. No client SDKs
(ClickHouse.Client, Npgsql, Dapper). Adding one would re-couple the
abstractions to a specific backend and defeat the point of the seam.
-->
<ItemGroup>
<ProjectReference Include="..\HoldFast.Domain\HoldFast.Domain.csproj" />
</ItemGroup>

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

</Project>
37 changes: 37 additions & 0 deletions src/dotnet/src/HoldFast.Analytics/IAlertStateStore.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using HoldFast.Analytics.Models;

namespace HoldFast.Analytics;

/// <summary>
/// Backend-neutral store for alert state-change history. Alert evaluation
/// reads recent state changes to detect transitions and avoid duplicate
/// notifications; the worker writes new state changes after evaluation.
/// </summary>
public interface IAlertStateStore
{
Task<List<AlertStateChangeRow>> GetLastAlertStateChangesAsync(
int projectId,
int alertId,
DateTime startDate,
DateTime endDate,
CancellationToken ct = default);

Task<List<AlertStateChangeRow>> GetAlertingAlertStateChangesAsync(
int projectId,
int alertId,
DateTime startDate,
DateTime endDate,
CancellationToken ct = default);

Task<List<AlertStateChangeRow>> GetLastAlertingStatesAsync(
int projectId,
int alertId,
DateTime startDate,
DateTime endDate,
CancellationToken ct = default);

Task WriteAlertStateChangesAsync(
int projectId,
IEnumerable<AlertStateChangeRow> rows,
CancellationToken ct = default);
}
47 changes: 47 additions & 0 deletions src/dotnet/src/HoldFast.Analytics/IErrorAnalyticsStore.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using HoldFast.Analytics.Models;

namespace HoldFast.Analytics;

/// <summary>
/// Backend-neutral store for error analytics (error groups + error objects
/// search, histograms, key/value discovery for the dashboard errors filter
/// UI, and worker-side ingest writes).
/// </summary>
public interface IErrorAnalyticsStore
{
Task<(List<int> Ids, long Total)> QueryErrorGroupIdsAsync(
int projectId,
QueryInput query,
int count,
int page,
CancellationToken ct = default);

Task<List<HistogramBucket>> ReadErrorObjectsHistogramAsync(
int projectId,
QueryInput query,
CancellationToken ct = default);

Task<List<QueryKey>> GetErrorsKeysAsync(
int projectId,
DateTime startDate,
DateTime endDate,
string? query,
CancellationToken ct = default);

Task<List<string>> GetErrorsKeyValuesAsync(
int projectId,
string keyName,
DateTime startDate,
DateTime endDate,
string? query,
int? count,
CancellationToken ct = default);

Task WriteErrorGroupsAsync(
IEnumerable<ErrorGroupRowInput> errorGroups,
CancellationToken ct = default);

Task WriteErrorObjectsAsync(
IEnumerable<ErrorObjectRowInput> errorObjects,
CancellationToken ct = default);
}
28 changes: 28 additions & 0 deletions src/dotnet/src/HoldFast.Analytics/IEventFieldStore.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using HoldFast.Analytics.Models;

namespace HoldFast.Analytics;

/// <summary>
/// Backend-neutral store for the events-key/value discovery surface
/// (used by the dashboard's event-search autocomplete on top of session events).
/// </summary>
public interface IEventFieldStore
{
Task<List<QueryKey>> GetEventsKeysAsync(
int projectId,
DateTime startDate,
DateTime endDate,
string? query,
string? eventName,
CancellationToken ct = default);

Task<List<string>> GetEventsKeyValuesAsync(
int projectId,
string keyName,
DateTime startDate,
DateTime endDate,
string? query,
int? count,
string? eventName,
CancellationToken ct = default);
}
44 changes: 44 additions & 0 deletions src/dotnet/src/HoldFast.Analytics/ILogStore.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
using HoldFast.Analytics.Models;

namespace HoldFast.Analytics;

/// <summary>
/// Backend-neutral store for application log analytics.
/// One implementation per analytics backend (currently ClickHouse only;
/// HOL-26+ will add Postgres).
/// </summary>
public interface ILogStore
{
Task<LogConnection> ReadLogsAsync(
int projectId,
QueryInput query,
ClickHousePagination pagination,
CancellationToken ct = default);

Task<List<HistogramBucket>> ReadLogsHistogramAsync(
int projectId,
QueryInput query,
CancellationToken ct = default);

Task<List<string>> GetLogKeysAsync(
int projectId,
QueryInput query,
CancellationToken ct = default);

Task<List<string>> GetLogKeyValuesAsync(
int projectId,
string key,
QueryInput query,
CancellationToken ct = default);

Task<long> CountLogsAsync(
int projectId,
string? query,
DateTime startDate,
DateTime endDate,
CancellationToken ct = default);

Task WriteLogsAsync(
IEnumerable<LogRowInput> logs,
CancellationToken ct = default);
}
28 changes: 28 additions & 0 deletions src/dotnet/src/HoldFast.Analytics/IMetricStore.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using HoldFast.Analytics.Models;

namespace HoldFast.Analytics;

/// <summary>
/// Backend-neutral store for metric time-series queries and writes.
/// </summary>
public interface IMetricStore
{
Task<MetricsBuckets> ReadMetricsAsync(
int projectId,
QueryInput query,
string bucketBy,
List<string>? groupBy,
string aggregator,
string? column,
CancellationToken ct = default);

Task WriteMetricAsync(
int projectId,
string metricName,
double metricValue,
string? category,
DateTime timestamp,
Dictionary<string, string>? tags,
string? sessionSecureId,
CancellationToken ct = default);
}
48 changes: 48 additions & 0 deletions src/dotnet/src/HoldFast.Analytics/ISessionAnalyticsStore.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
using HoldFast.Analytics.Models;

namespace HoldFast.Analytics;

/// <summary>
/// Backend-neutral store for session analytics queries (sessions search,
/// histograms, key/value discovery for the dashboard sessions filter UI).
///
/// Note: actual session-replay payloads (events, snapshots) live in blob
/// storage, not in the analytics store. This interface only covers the
/// search/aggregation surface.
/// </summary>
public interface ISessionAnalyticsStore
{
Task<List<HistogramBucket>> ReadSessionsHistogramAsync(
int projectId,
QueryInput query,
CancellationToken ct = default);

Task<(List<int> Ids, long Total)> QuerySessionIdsAsync(
int projectId,
QueryInput query,
int count,
int page,
string? sortField = null,
bool sortDesc = true,
CancellationToken ct = default);

Task<List<QueryKey>> GetSessionsKeysAsync(
int projectId,
DateTime startDate,
DateTime endDate,
string? query,
CancellationToken ct = default);

Task<List<string>> GetSessionsKeyValuesAsync(
int projectId,
string keyName,
DateTime startDate,
DateTime endDate,
string? query,
int? count,
CancellationToken ct = default);

Task WriteSessionsAsync(
IEnumerable<SessionRowInput> sessions,
CancellationToken ct = default);
}
36 changes: 36 additions & 0 deletions src/dotnet/src/HoldFast.Analytics/ITraceStore.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using HoldFast.Analytics.Models;

namespace HoldFast.Analytics;

/// <summary>
/// Backend-neutral store for distributed-trace span analytics.
/// </summary>
public interface ITraceStore
{
Task<TraceConnection> ReadTracesAsync(
int projectId,
QueryInput query,
ClickHousePagination pagination,
bool omitBody = false,
CancellationToken ct = default);

Task<List<HistogramBucket>> ReadTracesHistogramAsync(
int projectId,
QueryInput query,
CancellationToken ct = default);

Task<List<string>> GetTraceKeysAsync(
int projectId,
QueryInput query,
CancellationToken ct = default);

Task<List<string>> GetTraceKeyValuesAsync(
int projectId,
string key,
QueryInput query,
CancellationToken ct = default);

Task WriteTracesAsync(
IEnumerable<TraceRowInput> traces,
CancellationToken ct = default);
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
namespace HoldFast.Data.ClickHouse.Models;
namespace HoldFast.Analytics.Models;

/// <summary>
/// Row from the alert_state_changes ClickHouse table.
/// Row from the alert_state_changes table.
/// Mirrors Go's clickhouse.AlertStateChangeRow.
/// </summary>
public class AlertStateChangeRow
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
using HoldFast.Analytics;
using HoldFast.Domain.Enums;

namespace HoldFast.Data.ClickHouse.Models;
namespace HoldFast.Analytics.Models;

/// <summary>
/// Represents a row in the ClickHouse logs table.
/// Read-only model — logs are written via Kafka consumers, read via ClickHouse queries.
/// Represents a row in the logs table.
/// Read-only model — logs are written via the ingest pipeline, read via analytics queries.
/// </summary>
public class LogRow
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
namespace HoldFast.Data.ClickHouse.Models;
namespace HoldFast.Analytics.Models;

/// <summary>
/// A time-bucketed aggregation result from ClickHouse metrics queries.
/// A time-bucketed aggregation result from metrics queries.
/// </summary>
public class MetricsBucket
{
Expand Down Expand Up @@ -65,7 +65,10 @@ public class QueryInput
}

/// <summary>
/// Pagination parameters for cursor-based ClickHouse queries.
/// Pagination parameters for cursor-based analytics queries.
///
/// Naming: kept as ClickHousePagination to minimize churn across ~20 callers.
/// A future cleanup PR can rename to CursorPagination.
/// </summary>
public class ClickHousePagination
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
using HoldFast.Analytics;
using HoldFast.Domain.Enums;

namespace HoldFast.Data.ClickHouse.Models;
namespace HoldFast.Analytics.Models;

/// <summary>
/// Represents a row in the ClickHouse traces table.
/// Read-only model — traces are written via OTLP collector, read via ClickHouse queries.
/// Represents a row in the traces table.
/// Read-only model — traces are written via OTLP ingest, read via analytics queries.
/// </summary>
public class TraceRow
{
Expand Down
Loading
Loading