Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
126 changes: 126 additions & 0 deletions docs/exception-handling.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
# Exception Capturing and Publishing

This document describes how to use the exception capturing and publishing features of the Aevatar framework. This functionality allows exception information to be written to Orleans Stream for centralized processing, monitoring, and analysis.

## Feature Overview

The exception capturing and publishing functionality sends exception information to a dedicated exception handling channel via Orleans Stream. Key features include:

- Publishing detailed exception information to a dedicated Orleans Stream
- Supporting the recording of exception context information
- Automatically capturing calling method and class name
- Providing easy-to-use helper methods to simplify the exception handling process
- Using a separate Kafka Topic, isolated from business Topics

## Configuration

Add the following configuration to the `appsettings.json` file:

```json
{
"Aevatar": {
"ExceptionStreamNamespace": "AevatarException"
}
}
```

Where `ExceptionStreamNamespace` specifies the Stream namespace for exception events, with a default value of "AevatarException".

## Usage

### Direct Exception Publishing

```csharp
public class MyGAgent : GAgentBase<MyState, MyStateLogEvent>
{
public async Task DoSomethingAsync()
{
try
{
// Business logic
await ProcessDataAsync();
}
catch (Exception ex)
{
// Publish exception
var contextData = new { UserId = "user123", Action = "ProcessData" };
await this.PublishExceptionAsync(ex, contextData);

// Can choose to rethrow or handle the exception
throw;
}
}
}
```

### Using Helper Methods to Automatically Capture and Publish Exceptions

```csharp
public class MyGAgent : GAgentBase<MyState, MyStateLogEvent>
{
public async Task DoSomethingAsync()
{
var contextData = new { UserId = "user123", Action = "ProcessData" };

// Automatically capture and publish exceptions, rethrown by default
await this.CatchAndPublishExceptionAsync(async () =>
{
await ProcessDataAsync();
}, contextData);
}

public async Task<Result> GetDataAsync()
{
var contextData = new { UserId = "user123", Action = "GetData" };

// For cases with return values, without rethrowing the exception
var (result, exceptionId) = await this.CatchAndPublishExceptionAsync(
async () => await FetchDataAsync(),
new Result { Success = false }, // Default value
contextData,
rethrowException: false);

if (exceptionId != Guid.Empty)
{
// Exception occurred, using default value
Logger.LogWarning("Exception occurred, using default value. ExceptionId: {ExceptionId}", exceptionId);
}

return result;
}
}
```

## Exception Event Format

The published exception event contains the following information:

```csharp
public class ExceptionEvent : EventBase
{
public GrainId GrainId { get; set; } // Grain ID where the exception occurred
public string ExceptionMessage { get; set; } // Exception message
public string ExceptionType { get; set; } // Exception type
public string StackTrace { get; set; } // Stack trace
public string ContextData { get; set; } // Context data (JSON format)
public DateTime Timestamp { get; set; } // Exception timestamp (UTC)
public string? MethodName { get; set; } // Method name where the exception occurred
public string? ClassName { get; set; } // Class name where the exception occurred
}
```

## Exception Handling Process

1. Exception is captured in the GAgent
2. Exception information is encapsulated as an ExceptionEvent
3. ExceptionEvent is published to a dedicated Orleans Stream
4. Via Kafka, exception events are routed to consumers for processing
5. Exception handling services can aggregate, analyze, and alert on exceptions

## Best Practices

- Add exception capturing and publishing for important or complex operations
- Include sufficient information in the context data to facilitate troubleshooting
- When handling sensitive data, be careful not to include personal privacy information in the context
- For high-frequency operations, consider setting an exception sampling rate to avoid too many exception events affecting performance
- Implement exception consumer services for real-time monitoring and analysis of exceptions
209 changes: 209 additions & 0 deletions samples/ExceptionHandling/ExceptionHandlingSample.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
using System;
using System.Threading.Tasks;
using Aevatar.Core;
using Aevatar.Core.Abstractions;
using Aevatar.Core.Extensions;
using Microsoft.Extensions.Logging;

namespace Aevatar.Samples.ExceptionHandling;

/// <summary>
/// Sample State Class
/// </summary>
[GenerateSerializer]
public class SampleState : StateBase
{
[Id(0)] public int Counter { get; set; }
}

/// <summary>
/// Sample State Log Event Class
/// </summary>
[GenerateSerializer]
public class SampleStateLogEvent : StateLogEventBase<SampleStateLogEvent>
{
}

/// <summary>
/// Sample GAgent demonstrating how to use exception capturing and publishing features
/// </summary>
[GAgent]
public class ExceptionHandlingSampleGAgent : GAgentBase<SampleState, SampleStateLogEvent>
{
private readonly ILogger<ExceptionHandlingSampleGAgent> _logger;

public ExceptionHandlingSampleGAgent(ILogger<ExceptionHandlingSampleGAgent> logger) : base(logger)
{
_logger = logger;
}

/// <summary>
/// Demonstrates how to use exception publishing feature directly
/// </summary>
public async Task DemoDirectExceptionPublishingAsync()
{
_logger.LogInformation("Starting direct exception publishing demo");

try
{
// Simulate an exception
throw new InvalidOperationException("This is a test exception");
}
catch (Exception ex)
{
// Create context data
var contextData = new
{
Operation = "DemoDirectExceptionPublishing",
Timestamp = DateTime.UtcNow,
GrainId = this.GetGrainId().ToString()
};

// Publish exception directly
var exceptionId = await this.PublishExceptionAsync(ex, contextData);

_logger.LogInformation("Published exception with ID: {ExceptionId}", exceptionId);

// In real applications, you might choose to rethrow or handle the exception
// throw;
}

_logger.LogInformation("Completed direct exception publishing demo");
}

/// <summary>
/// Demonstrates how to use helper method to catch and publish exceptions (without return value)
/// </summary>
public async Task DemoExceptionHandlingWithoutResultAsync()
{
_logger.LogInformation("Starting exception handling demo without result");

var contextData = new
{
Operation = "DemoExceptionHandlingWithoutResult",
Timestamp = DateTime.UtcNow,
GrainId = this.GetGrainId().ToString()
};

// Use helper method to catch and publish exceptions, without rethrowing the exception
var exceptionId = await this.CatchAndPublishExceptionAsync(
async () =>
{
// Simulate an exception
await Task.Delay(100);
throw new ArgumentException("Invalid argument in operation");
},
contextData,
rethrowException: false);

if (exceptionId != Guid.Empty)
{
_logger.LogInformation("Exception occurred and published with ID: {ExceptionId}", exceptionId);
}

_logger.LogInformation("Completed exception handling demo without result");
}

/// <summary>
/// Demonstrates how to use helper method to catch and publish exceptions (with return value)
/// </summary>
public async Task<(bool Success, string Message)> DemoExceptionHandlingWithResultAsync()
{
_logger.LogInformation("Starting exception handling demo with result");

var contextData = new
{
Operation = "DemoExceptionHandlingWithResult",
Timestamp = DateTime.UtcNow,
Parameters = new { Id = "sample-id", RequestType = "GET" }
};

// Use helper method to catch and publish exceptions, with return value, without rethrowing the exception
var (result, exceptionId) = await this.CatchAndPublishExceptionAsync(
async () =>
{
// Simulate a successful operation
await Task.Delay(100);

// May throw an exception based on conditions
if (DateTime.UtcNow.Millisecond % 2 == 0)
{
throw new TimeoutException("Operation timed out");
}

return (Success: true, Message: "Operation completed successfully");
},
(Success: false, Message: "Operation failed due to an exception"), // Default value
contextData,
rethrowException: false);

if (exceptionId != Guid.Empty)
{
_logger.LogInformation("Exception occurred and published with ID: {ExceptionId}", exceptionId);
_logger.LogInformation("Using default result: {Result}", result);
}
else
{
_logger.LogInformation("Operation completed successfully: {Result}", result);
}

_logger.LogInformation("Completed exception handling demo with result");

return result;
}

/// <summary>
/// Demonstrates how to use exception capturing and publishing in actual business logic
/// </summary>
public async Task<int> PerformBusinessOperationAsync(int value)
{
_logger.LogInformation("Performing business operation with value: {Value}", value);

// Catch and publish exceptions with business context data
var contextData = new
{
OperationName = "PerformBusinessOperation",
InputValue = value
};

var (result, exceptionId) = await this.CatchAndPublishExceptionAsync(
async () =>
{
// Simulate business logic
await Task.Delay(100);

if (value < 0)
{
throw new ArgumentOutOfRangeException(nameof(value), "Value cannot be negative");
}

// Update state
RaiseEvent(new SampleStateLogEvent());
State.Counter += value;

return State.Counter;
},
-1, // Default value, indicating operation failure
contextData,
rethrowException: false);

if (exceptionId != Guid.Empty)
{
_logger.LogWarning("Business operation failed with exception ID: {ExceptionId}", exceptionId);
}
else
{
_logger.LogInformation("Business operation completed successfully, new counter value: {Counter}", result);
}

return result;
}

/// <summary>
/// GAgent description
/// </summary>
public override Task<string> GetDescriptionAsync()
{
return Task.FromResult("Exception Handling Sample GAgent");
}
}
2 changes: 2 additions & 0 deletions src/Aevatar.Core.Abstractions/AevatarOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,7 @@ public class AevatarOptions
public string StateProjectionStreamNamespace { get; set; } = "AevatarStateProjection";

public string BroadCastStreamNamespace { get; set; } = "AevatarBroadCast";
public string ExceptionStreamNamespace { get; set; } = "AevatarException";
public string ExceptionStreamKey { get; set; } = "global-exceptions";
Comment thread
Jeff-Xu23 marked this conversation as resolved.
Outdated
//public int ElasticSearchProcessors { get; set; } = 10;
}
14 changes: 14 additions & 0 deletions src/Aevatar.Core.Abstractions/Events/ExceptionEvent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace Aevatar.Core.Abstractions;

[GenerateSerializer]
public class ExceptionEvent : EventBase

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could you add GrainType, EventType and the original Event object

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

{
[Id(0)] public required GrainId GrainId { get; set; }
[Id(1)] public required string ExceptionMessage { get; set; }
[Id(2)] public required string ExceptionType { get; set; }
[Id(3)] public required string StackTrace { get; set; }
Comment thread
Jeff-Xu23 marked this conversation as resolved.
[Id(4)] public required string ContextData { get; set; }
[Id(5)] public required DateTime Timestamp { get; set; } = DateTime.UtcNow;
[Id(6)] public string? MethodName { get; set; }
[Id(7)] public string? ClassName { get; set; }
}
Loading