Skip to content

Commit 82c9f66

Browse files
committed
feat(runtime): extract Diagnostics into Runtime.Diagnostics namespace
Move MutationAuditEntryFactory and StateSizeEstimator out of Internal into a dedicated Runtime.Diagnostics namespace to align with the existing Runtime.Audit, Runtime.Metrics, and Runtime.Policies pattern. Diagnostics components are now accessible from Runtime-level consumers without coupling to the internal execution namespace.
1 parent e504133 commit 82c9f66

2 files changed

Lines changed: 239 additions & 0 deletions

File tree

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
using ModularityKit.Mutator.Abstractions.Audit;
2+
using ModularityKit.Mutator.Abstractions.Changes;
3+
using ModularityKit.Mutator.Abstractions.Context;
4+
using ModularityKit.Mutator.Abstractions.Engine;
5+
using ModularityKit.Mutator.Abstractions.Effects;
6+
using ModularityKit.Mutator.Abstractions.History;
7+
using ModularityKit.Mutator.Abstractions.Policies;
8+
using ModularityKit.Mutator.Abstractions.Results;
9+
10+
namespace ModularityKit.Mutator.Runtime.Diagnostics;
11+
12+
/// <summary>
13+
/// Factory for creating audit and history entries for mutations.
14+
/// </summary>
15+
internal static class MutationAuditEntryFactory
16+
{
17+
/// <summary>
18+
/// Creates a successful mutation audit entry.
19+
/// </summary>
20+
/// <typeparam name="TState">The state type handled by the mutation.</typeparam>
21+
/// <param name="mutation">The mutation that was executed.</param>
22+
/// <param name="result">The result of the mutation execution.</param>
23+
/// <param name="policyDecision">The policy decision applied to the mutation.</param>
24+
/// <param name="executionId">The unique identifier of the execution.</param>
25+
/// <param name="duration">The execution duration.</param>
26+
/// <returns>A configured <see cref="MutationAuditEntry"/> representing success.</returns>
27+
public static MutationAuditEntry CreateSuccess<TState>(
28+
IMutation<TState> mutation,
29+
MutationResult<TState> result,
30+
PolicyDecision policyDecision,
31+
string executionId,
32+
TimeSpan duration)
33+
{
34+
return Create(
35+
mutation,
36+
executionId,
37+
duration,
38+
isSuccess: true,
39+
changes: result.Changes,
40+
policyDecisions: result.PolicyDecisions.Count > 0 ? result.PolicyDecisions : [policyDecision],
41+
sideEffects: result.SideEffects,
42+
sourceIpAddress: mutation.Context.SourceIpAddress,
43+
userAgent: mutation.Context.UserAgent);
44+
}
45+
46+
/// <summary>
47+
/// Creates a failed mutation audit entry.
48+
/// </summary>
49+
/// <typeparam name="TState">The state type handled by the mutation.</typeparam>
50+
/// <param name="mutation">The mutation that was executed.</param>
51+
/// <param name="result">The result of the mutation execution.</param>
52+
/// <param name="executionId">The unique identifier of the execution.</param>
53+
/// <param name="duration">The execution duration.</param>
54+
/// <returns>A configured <see cref="MutationAuditEntry"/> representing failure.</returns>
55+
public static MutationAuditEntry CreateFailure<TState>(
56+
IMutation<TState> mutation,
57+
MutationResult<TState> result,
58+
string executionId,
59+
TimeSpan duration)
60+
{
61+
return Create(
62+
mutation,
63+
executionId,
64+
duration,
65+
isSuccess: false,
66+
changes: result.Changes,
67+
errorMessage: string.Join("; ", result.ValidationResult.Errors.Select(e => e.Message)),
68+
policyDecisions: result.PolicyDecisions,
69+
sideEffects: result.SideEffects);
70+
}
71+
72+
/// <summary>
73+
/// Creates a failed mutation audit entry due to an exception.
74+
/// </summary>
75+
/// <typeparam name="TState">The state type handled by the mutation.</typeparam>
76+
/// <param name="mutation">The mutation that was executed.</param>
77+
/// <param name="exception">The exception that occurred.</param>
78+
/// <param name="executionId">The unique identifier of the execution.</param>
79+
/// <param name="duration">The execution duration.</param>
80+
/// <returns>A configured <see cref="MutationAuditEntry"/> representing an exception failure.</returns>
81+
public static MutationAuditEntry CreateException<TState>(
82+
IMutation<TState> mutation,
83+
Exception exception,
84+
string executionId,
85+
TimeSpan duration)
86+
{
87+
return Create(
88+
mutation,
89+
executionId,
90+
duration,
91+
isSuccess: false,
92+
errorMessage: exception.Message);
93+
}
94+
95+
/// <summary>
96+
/// Creates a mutation history entry for persistence.
97+
/// </summary>
98+
/// <typeparam name="TState">The state type handled by the mutation.</typeparam>
99+
/// <param name="mutation">The mutation that was executed.</param>
100+
/// <param name="result">The result of the mutation execution.</param>
101+
/// <param name="executionId">The unique identifier of the execution.</param>
102+
/// <param name="stateId">The identifier of the target state.</param>
103+
/// <param name="duration">The execution duration.</param>
104+
/// <returns>A configured <see cref="MutationHistoryEntry"/>.</returns>
105+
public static MutationHistoryEntry CreateHistoryEntry<TState>(
106+
IMutation<TState> mutation,
107+
MutationResult<TState> result,
108+
string executionId,
109+
string stateId,
110+
TimeSpan duration)
111+
{
112+
return new MutationHistoryEntry
113+
{
114+
ExecutionId = executionId,
115+
StateId = stateId,
116+
Intent = mutation.Intent,
117+
Context = mutation.Context,
118+
Changes = result.Changes,
119+
SideEffects = result.SideEffects.ToList(),
120+
Timestamp = mutation.Context.Timestamp,
121+
ExecutionTime = duration
122+
};
123+
}
124+
125+
/// <summary>
126+
/// Resolves the state identifier from the mutation context.
127+
/// </summary>
128+
/// <param name="context">The mutation context.</param>
129+
/// <returns>The resolved state ID or correlation ID.</returns>
130+
public static string? ResolveStateId(MutationContext context) =>
131+
context.StateId ?? context.CorrelationId;
132+
133+
/// <summary>
134+
/// Helper method to create a mutation audit entry.
135+
/// </summary>
136+
private static MutationAuditEntry Create<TState>(
137+
IMutation<TState> mutation,
138+
string executionId,
139+
TimeSpan duration,
140+
bool isSuccess,
141+
ChangeSet? changes = null,
142+
string? errorMessage = null,
143+
IReadOnlyList<PolicyDecision>? policyDecisions = null,
144+
IReadOnlyList<SideEffect>? sideEffects = null,
145+
string? sourceIpAddress = null,
146+
string? userAgent = null)
147+
{
148+
return new MutationAuditEntry
149+
{
150+
ExecutionId = executionId,
151+
StateId = ResolveStateId(mutation.Context),
152+
StateType = typeof(TState).Name,
153+
MutationIntent = mutation.Intent,
154+
Context = mutation.Context,
155+
Changes = changes ?? ChangeSet.Empty,
156+
IsSuccess = isSuccess,
157+
ErrorMessage = errorMessage,
158+
PolicyDecisions = policyDecisions ?? [],
159+
SideEffects = sideEffects ?? [],
160+
Timestamp = mutation.Context.Timestamp,
161+
Duration = duration,
162+
SourceIpAddress = sourceIpAddress,
163+
UserAgent = userAgent
164+
};
165+
}
166+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
using System.Collections;
2+
using System.Text;
3+
4+
namespace ModularityKit.Mutator.Runtime.Diagnostics;
5+
6+
/// <summary>
7+
/// Provides a best-effort estimate of the in-memory size of a state object in bytes.
8+
/// </summary>
9+
internal static class StateSizeEstimator
10+
{
11+
private static readonly IReadOnlyDictionary<Type, int> PrimitiveTypeSizes = new Dictionary<Type, int>
12+
{
13+
[typeof(bool)] = sizeof(bool),
14+
[typeof(byte)] = sizeof(byte),
15+
[typeof(sbyte)] = sizeof(sbyte),
16+
[typeof(char)] = sizeof(char),
17+
[typeof(short)] = sizeof(short),
18+
[typeof(ushort)] = sizeof(ushort),
19+
[typeof(int)] = sizeof(int),
20+
[typeof(uint)] = sizeof(uint),
21+
[typeof(long)] = sizeof(long),
22+
[typeof(ulong)] = sizeof(ulong),
23+
[typeof(float)] = sizeof(float),
24+
[typeof(double)] = sizeof(double),
25+
[typeof(decimal)] = sizeof(decimal),
26+
[typeof(Guid)] = 16
27+
};
28+
29+
/// <summary>
30+
/// Estimates the size of the given state in bytes.
31+
/// </summary>
32+
/// <param name="state">The state object to estimate. Can be <see langword="null" />.</param>
33+
/// <returns>
34+
/// The estimated byte size: UTF-8 byte count for strings, byte length for primitive arrays,
35+
/// element count for collections, or <c>0</c> for unrecognized or null values.
36+
/// </returns>
37+
public static long Estimate(object? state)
38+
{
39+
if (state is null)
40+
return 0;
41+
42+
if (state is string text)
43+
return Encoding.UTF8.GetByteCount(text);
44+
45+
if (TryEstimateArraySize(state, out var arraySize))
46+
return arraySize;
47+
48+
return state is ICollection collection ? collection.Count : 0;
49+
}
50+
51+
/// <summary>
52+
/// Attempts to estimate the byte size of a primitive array.
53+
/// </summary>
54+
/// <param name="state">The object to inspect.</param>
55+
/// <param name="sizeInBytes">When successful, contains the estimated byte size of the array.</param>
56+
/// <returns><see langword="true" /> if <paramref name="state" /> is a primitive array with a known element size; otherwise <see langword="false" />.</returns>
57+
private static bool TryEstimateArraySize(object state, out long sizeInBytes)
58+
{
59+
sizeInBytes = 0;
60+
if (state is not Array array)
61+
return false;
62+
63+
var elementType = state.GetType().GetElementType();
64+
if (elementType is null)
65+
return false;
66+
67+
if (!PrimitiveTypeSizes.TryGetValue(elementType, out var elementSize))
68+
return false;
69+
70+
sizeInBytes = array.LongLength * elementSize;
71+
return true;
72+
}
73+
}

0 commit comments

Comments
 (0)