Add Sentry (#19)
* add sentry * add sentry * better log web3proxy * Add managing and worker on sentry * better log web3proxy
This commit is contained in:
78
src/Managing.Core/Exceptions/CustomExceptions.cs
Normal file
78
src/Managing.Core/Exceptions/CustomExceptions.cs
Normal file
@@ -0,0 +1,78 @@
|
||||
namespace Managing.Core.Exceptions;
|
||||
|
||||
/// <summary>
|
||||
/// Exception thrown when validation fails (maps to 400 Bad Request)
|
||||
/// </summary>
|
||||
public class ValidationException : Exception
|
||||
{
|
||||
public ValidationException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
|
||||
public ValidationException(string message, Exception innerException) : base(message, innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Exception thrown when a resource is not found (maps to 404 Not Found)
|
||||
/// </summary>
|
||||
public class NotFoundException : Exception
|
||||
{
|
||||
public NotFoundException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
|
||||
public NotFoundException(string resourceType, string identifier)
|
||||
: base($"{resourceType} with identifier '{identifier}' was not found.")
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Exception thrown when the user does not have permission (maps to 403 Forbidden)
|
||||
/// </summary>
|
||||
public class ForbiddenException : Exception
|
||||
{
|
||||
public ForbiddenException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
|
||||
public ForbiddenException() : base("You do not have permission to access this resource.")
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Exception thrown when there is a conflict with the current state (maps to 409 Conflict)
|
||||
/// </summary>
|
||||
public class ConflictException : Exception
|
||||
{
|
||||
public ConflictException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Exception thrown when a rate limit is exceeded (maps to 429 Too Many Requests)
|
||||
/// </summary>
|
||||
public class RateLimitExceededException : Exception
|
||||
{
|
||||
public RateLimitExceededException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
|
||||
public RateLimitExceededException() : base("Rate limit exceeded. Please try again later.")
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Exception thrown when an external service is unavailable (maps to 503 Service Unavailable)
|
||||
/// </summary>
|
||||
public class ServiceUnavailableException : Exception
|
||||
{
|
||||
public ServiceUnavailableException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
}
|
||||
106
src/Managing.Core/Exceptions/README.md
Normal file
106
src/Managing.Core/Exceptions/README.md
Normal file
@@ -0,0 +1,106 @@
|
||||
# Error Handling in Managing Application
|
||||
|
||||
This document describes the centralized error handling approach used in the Managing applications to ensure consistent error responses and logging, with Sentry integration for error monitoring.
|
||||
|
||||
## Architecture
|
||||
|
||||
The error handling architecture consists of:
|
||||
|
||||
1. **Global Error Handling Middleware**: Captures all unhandled exceptions and formats consistent responses
|
||||
2. **Sentry Integration**: Sends detailed error information to Sentry for monitoring and analysis
|
||||
3. **Custom Exception Types**: Provide appropriate HTTP status code mapping
|
||||
4. **SentryErrorCapture Utility**: Provides methods for manually capturing errors with context
|
||||
|
||||
## Global Error Handling Middleware
|
||||
|
||||
The `GlobalErrorHandlingMiddleware` is registered in `Program.cs` for both the main API and Worker API:
|
||||
|
||||
```csharp
|
||||
app.UseMiddleware(typeof(GlobalErrorHandlingMiddleware));
|
||||
```
|
||||
|
||||
When an exception occurs, the middleware:
|
||||
|
||||
1. Determines the appropriate HTTP status code based on exception type
|
||||
2. Logs the error with request details
|
||||
3. Captures the exception in Sentry with appropriate context
|
||||
4. Returns a standardized JSON error response to the client
|
||||
|
||||
## Sentry Integration
|
||||
|
||||
Sentry is integrated in three ways:
|
||||
|
||||
1. **Global Configuration**: Set up in `Program.cs` with environment-specific settings
|
||||
2. **Error Capture**: In the global middleware and utility methods
|
||||
3. **Diagnostic Endpoint**: The SentryDiagnosticsMiddleware provides a test endpoint at `/api/sentry-diagnostics`
|
||||
|
||||
The captured data includes:
|
||||
|
||||
- HTTP request details (path, method, query strings)
|
||||
- Exception details (type, message, stack trace)
|
||||
- Additional context from exception data
|
||||
- Tags for better categorization and filtering
|
||||
|
||||
## Using Custom Exception Types
|
||||
|
||||
The shared exception types map to appropriate HTTP status codes:
|
||||
|
||||
| Exception Type | HTTP Status Code | Use Case |
|
||||
|----------------|------------------|----------|
|
||||
| ValidationException | 400 Bad Request | Input validation errors |
|
||||
| NotFoundException | 404 Not Found | Resource does not exist |
|
||||
| ForbiddenException | 403 Forbidden | Permission denied |
|
||||
| ConflictException | 409 Conflict | Resource state conflict |
|
||||
| RateLimitExceededException | 429 Too Many Requests | Rate limit exceeded |
|
||||
| ServiceUnavailableException | 503 Service Unavailable | External service down |
|
||||
|
||||
Example:
|
||||
```csharp
|
||||
// Validation error
|
||||
throw new ValidationException("The username must be at least 3 characters");
|
||||
|
||||
// Resource not found with context
|
||||
throw new NotFoundException("User", userId);
|
||||
|
||||
// Permission denied
|
||||
throw new ForbiddenException();
|
||||
```
|
||||
|
||||
## Manual Error Reporting
|
||||
|
||||
For manually reporting errors to Sentry, use the `SentryErrorCapture` utility:
|
||||
|
||||
```csharp
|
||||
// Capture an exception with context
|
||||
SentryErrorCapture.CaptureException(ex, "MyService", new Dictionary<string, object> {
|
||||
{ "userId", user.Id },
|
||||
{ "operation", "ProcessImport" }
|
||||
});
|
||||
|
||||
// Enrich an exception before throwing
|
||||
throw SentryErrorCapture.EnrichException(new ValidationException("Invalid data"),
|
||||
new Dictionary<string, object> {
|
||||
{ "validationErrors", errors }
|
||||
});
|
||||
```
|
||||
|
||||
## Error Response Format
|
||||
|
||||
The standard error response format is:
|
||||
|
||||
```json
|
||||
{
|
||||
"statusCode": 400,
|
||||
"message": "The error message",
|
||||
"traceId": "sentry-event-id",
|
||||
"stackTrace": "Only included in non-production environments"
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Use custom exception types**: Throw the appropriate exception type for each error case
|
||||
2. **Add context to exceptions**: Use the Data dictionary to add context that will be captured
|
||||
3. **Don't duplicate Sentry reporting**: Let the global middleware handle Sentry integration
|
||||
4. **Avoid sensitive data**: Never include sensitive data (passwords, tokens) in error messages or context
|
||||
5. **Use the diagnostic endpoint**: Test Sentry connectivity using the `/api/sentry-diagnostics` endpoint
|
||||
109
src/Managing.Core/Exceptions/SentryErrorCapture.cs
Normal file
109
src/Managing.Core/Exceptions/SentryErrorCapture.cs
Normal file
@@ -0,0 +1,109 @@
|
||||
using Sentry;
|
||||
|
||||
namespace Managing.Core.Exceptions;
|
||||
|
||||
/// <summary>
|
||||
/// Utility class for capturing errors with Sentry across the application
|
||||
/// </summary>
|
||||
public static class SentryErrorCapture
|
||||
{
|
||||
/// <summary>
|
||||
/// Captures an exception in Sentry with additional context
|
||||
/// </summary>
|
||||
/// <param name="exception">The exception to capture</param>
|
||||
/// <param name="contextName">A descriptive name for where the error occurred</param>
|
||||
/// <param name="extraData">Optional dictionary of additional data to include</param>
|
||||
/// <returns>The Sentry event ID</returns>
|
||||
public static SentryId CaptureException(Exception exception, string contextName, IDictionary<string, object> extraData = null)
|
||||
{
|
||||
return SentrySdk.CaptureException(exception, scope =>
|
||||
{
|
||||
// Add context information
|
||||
scope.SetTag("context", contextName);
|
||||
scope.SetTag("error_type", exception.GetType().Name);
|
||||
|
||||
// Add any extra data provided
|
||||
if (extraData != null)
|
||||
{
|
||||
foreach (var kvp in extraData)
|
||||
{
|
||||
scope.SetExtra(kvp.Key, kvp.Value?.ToString() ?? "null");
|
||||
}
|
||||
}
|
||||
|
||||
// Add extra info from the exception's Data dictionary if available
|
||||
foreach (var key in exception.Data.Keys)
|
||||
{
|
||||
if (key is string keyStr && exception.Data[key] != null)
|
||||
{
|
||||
scope.SetExtra($"exception_data_{keyStr}", exception.Data[key].ToString());
|
||||
}
|
||||
}
|
||||
|
||||
// Add a breadcrumb for context
|
||||
scope.AddBreadcrumb(
|
||||
message: $"Exception in {contextName}",
|
||||
category: "error",
|
||||
level: BreadcrumbLevel.Error
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enriches an exception with additional context data before throwing
|
||||
/// </summary>
|
||||
/// <param name="exception">The exception to enrich</param>
|
||||
/// <param name="contextData">Dictionary of context data to add</param>
|
||||
/// <returns>The enriched exception for chaining</returns>
|
||||
public static Exception EnrichException(Exception exception, IDictionary<string, object> contextData)
|
||||
{
|
||||
if (contextData != null)
|
||||
{
|
||||
foreach (var item in contextData)
|
||||
{
|
||||
exception.Data[item.Key] = item.Value;
|
||||
}
|
||||
}
|
||||
|
||||
return exception;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Captures a message in Sentry with additional context
|
||||
/// </summary>
|
||||
/// <param name="message">The message to capture</param>
|
||||
/// <param name="level">The severity level</param>
|
||||
/// <param name="contextName">A descriptive name for where the message originated</param>
|
||||
/// <param name="extraData">Optional dictionary of additional data to include</param>
|
||||
/// <returns>The Sentry event ID</returns>
|
||||
public static SentryId CaptureMessage(string message, SentryLevel level, string contextName, IDictionary<string, object> extraData = null)
|
||||
{
|
||||
// First capture the message with the specified level
|
||||
var id = SentrySdk.CaptureMessage(message, level);
|
||||
|
||||
// Then add context via a scope
|
||||
SentrySdk.ConfigureScope(scope =>
|
||||
{
|
||||
// Add context information
|
||||
scope.SetTag("context", contextName);
|
||||
|
||||
// Add any extra data provided
|
||||
if (extraData != null)
|
||||
{
|
||||
foreach (var kvp in extraData)
|
||||
{
|
||||
scope.SetExtra(kvp.Key, kvp.Value?.ToString() ?? "null");
|
||||
}
|
||||
}
|
||||
|
||||
// Add a breadcrumb for context
|
||||
scope.AddBreadcrumb(
|
||||
message: $"Message from {contextName}",
|
||||
category: "message",
|
||||
level: BreadcrumbLevel.Info
|
||||
);
|
||||
});
|
||||
|
||||
return id;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user