Skip to content
Open
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
17 changes: 17 additions & 0 deletions .cursor/rules/aevatar-core.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
description:
globs:
alwaysApply: true
---
# Background
(1) The project is developed using C# and uses the Orleans technology stack. GAgent is a wrapper for Orleans Grain.

# Reference Files
• README.md([README.md](mdc:README.md)):
The aevatar Framework is a distributed actor-based framework built on Microsoft Orleans for creating scalable event-sourced applications. Its core component, GAgentBase, provides event sourcing, pub/sub messaging, state management, and hierarchical agent relationship capabilities. Key features include automatic event forwarding, type-safe state containers, dynamic agent registration, and built-in stream processing. The README demonstrates usage examples for creating agents, handling events, and managing registrations, along with best practices.

• DIRECTORY_STRUCTURE.md([DIRECTORY_STRUCTURE.md](mdc:docs/DIRECTORY_STRUCTURE.md)):
The DIRECTORY_STRUCTURE.md document provides a comprehensive overview of the Aevatar Framework's organization, highlighting its modular architecture. The framework is structured with clear separation between source code, samples, and tests. The source code (src/) is organized into several key components including Aevatar.Core (containing the GAgentBase implementation), Core.Abstractions (defining interfaces), EventSourcing modules, and plugin systems. The samples directory showcases various implementation examples like ArtifactGAgent, MessagingGAgent, and SimpleAIGAgent. The test directory provides extensive test coverage for all framework components. Built on .NET, the framework leverages event sourcing with MongoDB integration, Orleans for distributed computing, a plugin-based extensibility system, and Generative Agents (GAgents) for AI integration. Key components include GAgents, event sourcing, plugin systems, Orleans integration, and state projections through event streams.

• Aevatar.Core.md([Aevatar.Core.md](mdc:docs/Aevatar.Core.md))
The Aevatar.Core.md document illustrates the foundation of the Aevatar Framework through sequence and relationship diagrams. This core module implements the Generative Agent (GAgent) concept - intelligent, stateful, event-driven entities that can communicate through events. Key components include GAgentBase (providing event publishing, subscription, and handling capabilities), GAgentFactory (creating agent instances), GAgentManager (managing agent lifecycle), StateProjection (handling state updates based on events), and EventDispatcher (routing events between agents). The module implements a lightweight event-sourcing pattern where agent state changes are driven by events, creating a network of communicating agents. It supports specialized agent types such as ArtifactGAgent and StateProjectionGAgentBase for different operational needs and advanced state management. The diagrams effectively illustrate the data flow from client interactions through event processing and the relationships between the various components.
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
17 changes: 17 additions & 0 deletions features/catch-and-publish-exception.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Background
(1) The project is developed using C# and uses the Orleans technology stack. GAgent is a wrapper for Orleans Grain.
(2) It is necessary to capture exceptions in implemented GAgent EventHandlers and notify subscribers of the exception information and corresponding context information through Orleans Stream.
(3) Subscribers do not need to handle exceptions through GAgent's EventHandle, but instead assemble their own StreamProvider and subscribe to the stream.

# Reference Files
• README.md
• DIRECTORY_STRUCTURE.md
• Aevatar.Core.md

# Features
Implement a method in the Aevatar.Core project to write exceptions to Orleans Stream, with the following capabilities:
a. Must include exception information, context of the exception, and information such as the time the exception was thrown
b. Requires a separate Kafka Topic to handle exceptions, isolated from business topics, with the exception topic name read from configuration

# Note
a. Be sure not to modify existing designs and implementations.
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");
}
}
4 changes: 4 additions & 0 deletions src/Aevatar.Core.Abstractions/AevatarOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,9 @@ public class AevatarOptions
public string StateProjectionStreamNamespace { get; set; } = "AevatarStateProjection";

public string BroadCastStreamNamespace { get; set; } = "AevatarBroadCast";
public string ExceptionStreamNamespace { get; set; } = "AevatarDLQ";
public string ExceptionStreamKey { get; set; } = "Global";

public int ExceptionStackMaxLength { get; set; } = 1024;
//public int ElasticSearchProcessors { get; set; } = 10;
}
Loading