-
Notifications
You must be signed in to change notification settings - Fork 0
Backend: Reliable deployment status and audit trail pipeline #149
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
ludovit-scholtz
merged 6 commits into
master
from
copilot/add-deployment-status-pipeline
Feb 4, 2026
Merged
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
5844d1c
Initial plan
Copilot d0a0232
Add enhanced error handling and new deployment states (Indexed, Cance…
Copilot 8a0d3d0
Add audit trail export service with JSON/CSV formats and idempotency
Copilot 13feb00
Add deployment metrics endpoint with comprehensive analytics
Copilot 7c5f501
Add comprehensive documentation and performance optimization notes
Copilot 6a39e74
Add comprehensive lifecycle integration tests and test plan documenta…
Copilot File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -20,18 +20,22 @@ namespace BiatecTokensApi.Controllers | |
| public class DeploymentStatusController : ControllerBase | ||
| { | ||
| private readonly IDeploymentStatusService _deploymentStatusService; | ||
| private readonly IDeploymentAuditService _auditService; | ||
| private readonly ILogger<DeploymentStatusController> _logger; | ||
|
|
||
| /// <summary> | ||
| /// Initializes a new instance of the <see cref="DeploymentStatusController"/> class. | ||
| /// </summary> | ||
| /// <param name="deploymentStatusService">The deployment status service</param> | ||
| /// <param name="auditService">The deployment audit service</param> | ||
| /// <param name="logger">The logger instance</param> | ||
| public DeploymentStatusController( | ||
| IDeploymentStatusService deploymentStatusService, | ||
| IDeploymentAuditService auditService, | ||
| ILogger<DeploymentStatusController> logger) | ||
| { | ||
| _deploymentStatusService = deploymentStatusService; | ||
| _auditService = auditService; | ||
| _logger = logger; | ||
| } | ||
|
|
||
|
|
@@ -232,5 +236,301 @@ public async Task<IActionResult> GetDeploymentHistory([FromRoute] string deploym | |
| }); | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Cancels a pending deployment | ||
| /// </summary> | ||
| /// <param name="deploymentId">The deployment ID</param> | ||
| /// <param name="request">Cancellation request with reason</param> | ||
| /// <returns>Cancellation result</returns> | ||
| /// <remarks> | ||
| /// Cancels a deployment that is in Queued status. Once a deployment has been | ||
| /// submitted to the blockchain, it cannot be cancelled through this endpoint. | ||
| /// | ||
| /// **Use Cases:** | ||
| /// - User changes mind before transaction submission | ||
| /// - User wants to modify parameters | ||
| /// - User realizes incorrect configuration | ||
| /// | ||
| /// **Restrictions:** | ||
| /// - Only deployments in Queued status can be cancelled | ||
| /// - Submitted transactions cannot be cancelled (blockchain immutability) | ||
| /// </remarks> | ||
| [HttpPost("{deploymentId}/cancel")] | ||
| [ProducesResponseType(typeof(object), StatusCodes.Status200OK)] | ||
| [ProducesResponseType(StatusCodes.Status404NotFound)] | ||
| [ProducesResponseType(StatusCodes.Status400BadRequest)] | ||
| public async Task<IActionResult> CancelDeployment( | ||
| [FromRoute] string deploymentId, | ||
| [FromBody] CancelDeploymentRequest request) | ||
| { | ||
| try | ||
| { | ||
| if (string.IsNullOrWhiteSpace(deploymentId)) | ||
| { | ||
| return BadRequest(new | ||
| { | ||
| success = false, | ||
| errorMessage = "DeploymentId is required" | ||
| }); | ||
| } | ||
|
|
||
| if (string.IsNullOrWhiteSpace(request?.Reason)) | ||
| { | ||
| return BadRequest(new | ||
| { | ||
| success = false, | ||
| errorMessage = "Cancellation reason is required" | ||
| }); | ||
| } | ||
|
|
||
| var result = await _deploymentStatusService.CancelDeploymentAsync(deploymentId, request.Reason); | ||
|
|
||
| if (!result) | ||
| { | ||
| return BadRequest(new | ||
| { | ||
| success = false, | ||
| errorMessage = "Deployment cannot be cancelled. It may not exist or may have already been submitted." | ||
| }); | ||
| } | ||
|
|
||
| _logger.LogInformation("Deployment cancelled: DeploymentId={DeploymentId}, Reason={Reason}", | ||
| LoggingHelper.SanitizeLogInput(deploymentId), LoggingHelper.SanitizeLogInput(request.Reason)); | ||
|
|
||
| return Ok(new | ||
| { | ||
| success = true, | ||
| message = "Deployment cancelled successfully" | ||
| }); | ||
| } | ||
| catch (Exception ex) | ||
| { | ||
| _logger.LogError(ex, "Error cancelling deployment: DeploymentId={DeploymentId}", LoggingHelper.SanitizeLogInput(deploymentId)); | ||
| return StatusCode(StatusCodes.Status500InternalServerError, new | ||
| { | ||
| success = false, | ||
| errorMessage = "An error occurred while cancelling the deployment" | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Exports audit trail for a specific deployment | ||
| /// </summary> | ||
| /// <param name="deploymentId">The deployment ID</param> | ||
| /// <param name="format">Export format (json or csv)</param> | ||
| /// <returns>Audit trail in requested format</returns> | ||
| /// <remarks> | ||
| /// Exports complete audit trail for a deployment including: | ||
| /// - All status transitions with timestamps | ||
| /// - Compliance checks performed | ||
| /// - Error details if applicable | ||
| /// - Transaction information | ||
| /// - Duration metrics | ||
| /// | ||
| /// **Use Cases:** | ||
| /// - Regulatory compliance reporting | ||
| /// - Incident investigation | ||
| /// - Performance analysis | ||
| /// - Customer support documentation | ||
| /// | ||
| /// **Formats:** | ||
| /// - JSON: Full structured data with nested objects | ||
| /// - CSV: Flattened data suitable for spreadsheet analysis | ||
| /// </remarks> | ||
| [HttpGet("{deploymentId}/audit-trail")] | ||
| [ProducesResponseType(typeof(string), StatusCodes.Status200OK, "application/json")] | ||
| [ProducesResponseType(typeof(string), StatusCodes.Status200OK, "text/csv")] | ||
| [ProducesResponseType(StatusCodes.Status404NotFound)] | ||
| [ProducesResponseType(StatusCodes.Status400BadRequest)] | ||
| public async Task<IActionResult> ExportAuditTrail( | ||
| [FromRoute] string deploymentId, | ||
| [FromQuery] string format = "json") | ||
| { | ||
| try | ||
| { | ||
| if (string.IsNullOrWhiteSpace(deploymentId)) | ||
| { | ||
| return BadRequest(new | ||
| { | ||
| success = false, | ||
| errorMessage = "DeploymentId is required" | ||
| }); | ||
| } | ||
|
|
||
| // Validate format | ||
| if (format.ToLower() != "json" && format.ToLower() != "csv") | ||
| { | ||
| return BadRequest(new | ||
| { | ||
| success = false, | ||
| errorMessage = "Format must be 'json' or 'csv'" | ||
| }); | ||
| } | ||
|
|
||
| string data; | ||
| string contentType; | ||
| string fileName; | ||
|
|
||
| if (format.ToLower() == "json") | ||
| { | ||
| data = await _auditService.ExportAuditTrailAsJsonAsync(deploymentId); | ||
| contentType = "application/json"; | ||
| fileName = $"audit-trail-{deploymentId}.json"; | ||
| } | ||
| else | ||
| { | ||
| data = await _auditService.ExportAuditTrailAsCsvAsync(deploymentId); | ||
| contentType = "text/csv"; | ||
| fileName = $"audit-trail-{deploymentId}.csv"; | ||
|
||
| } | ||
|
|
||
| _logger.LogInformation("Exported audit trail: DeploymentId={DeploymentId}, Format={Format}, Size={Size}", | ||
| LoggingHelper.SanitizeLogInput(deploymentId), format, data.Length); | ||
|
|
||
| return File(System.Text.Encoding.UTF8.GetBytes(data), contentType, fileName); | ||
| } | ||
| catch (InvalidOperationException ex) | ||
| { | ||
| _logger.LogWarning("Deployment not found for audit trail: DeploymentId={DeploymentId}, Error={Error}", | ||
| LoggingHelper.SanitizeLogInput(deploymentId), ex.Message); | ||
| return NotFound(new | ||
| { | ||
| success = false, | ||
| errorMessage = ex.Message | ||
| }); | ||
| } | ||
| catch (Exception ex) | ||
| { | ||
| _logger.LogError(ex, "Error exporting audit trail: DeploymentId={DeploymentId}", | ||
| LoggingHelper.SanitizeLogInput(deploymentId)); | ||
| return StatusCode(StatusCodes.Status500InternalServerError, new | ||
| { | ||
| success = false, | ||
| errorMessage = "An error occurred while exporting the audit trail" | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Exports audit trails for multiple deployments | ||
| /// </summary> | ||
| /// <param name="request">Export request with filters</param> | ||
| /// <returns>Audit trails in requested format</returns> | ||
| /// <remarks> | ||
| /// Exports audit trails for multiple deployments with filtering and pagination. | ||
| /// Supports idempotency through the X-Idempotency-Key header for large exports. | ||
| /// | ||
| /// **Use Cases:** | ||
| /// - Bulk compliance reporting | ||
| /// - Historical analysis | ||
| /// - Data migration | ||
| /// - Backup and archival | ||
| /// | ||
| /// **Idempotency:** | ||
| /// - Include X-Idempotency-Key header for large exports | ||
| /// - Results are cached for 1 hour | ||
| /// - Repeated requests with same key return cached results | ||
| /// - Key must be unique per unique request parameters | ||
| /// | ||
| /// **Pagination:** | ||
| /// - Default: 100 records per page | ||
| /// - Maximum: 1000 records per page | ||
| /// - Use multiple requests for larger datasets | ||
| /// </remarks> | ||
| [HttpPost("audit-trail/export")] | ||
| [ProducesResponseType(typeof(AuditExportResult), StatusCodes.Status200OK)] | ||
| [ProducesResponseType(StatusCodes.Status400BadRequest)] | ||
| public async Task<IActionResult> ExportAuditTrails([FromBody] AuditExportRequest request) | ||
| { | ||
| try | ||
| { | ||
| // Get idempotency key from header | ||
| var idempotencyKey = Request.Headers["X-Idempotency-Key"].FirstOrDefault(); | ||
|
|
||
| var result = await _auditService.ExportAuditTrailsAsync(request, idempotencyKey); | ||
|
|
||
| if (!result.Success) | ||
| { | ||
| return BadRequest(result); | ||
| } | ||
|
|
||
| _logger.LogInformation("Exported audit trails: Count={Count}, Format={Format}, Cached={Cached}", | ||
| result.RecordCount, request.Format, result.IsCached); | ||
|
|
||
| return Ok(result); | ||
| } | ||
| catch (Exception ex) | ||
| { | ||
| _logger.LogError(ex, "Error exporting audit trails"); | ||
| return StatusCode(StatusCodes.Status500InternalServerError, new AuditExportResult | ||
| { | ||
| Success = false, | ||
| ErrorMessage = "An error occurred while exporting audit trails" | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Gets deployment metrics for monitoring and analytics | ||
| /// </summary> | ||
| /// <param name="request">Metrics request with filters</param> | ||
| /// <returns>Deployment metrics</returns> | ||
| /// <remarks> | ||
| /// Returns comprehensive deployment metrics including: | ||
| /// - Success/failure rates | ||
| /// - Duration statistics (average, median, P95) | ||
| /// - Failure breakdown by category | ||
| /// - Deployment counts by network and token type | ||
| /// - Average duration by status transition | ||
| /// - Retry statistics | ||
| /// | ||
| /// **Use Cases:** | ||
| /// - Monitoring dashboard creation | ||
| /// - SLA tracking and reporting | ||
| /// - Performance optimization | ||
| /// - Capacity planning | ||
| /// - Customer success metrics | ||
| /// | ||
| /// **Time Period:** | ||
| /// - Default: Last 24 hours | ||
| /// - Custom: Specify fromDate and toDate | ||
| /// - Maximum recommended period: 30 days (for performance) | ||
| /// | ||
| /// **Filtering:** | ||
| /// - By network (e.g., "voimain-v1.0", "base-mainnet") | ||
| /// - By token type (e.g., "ERC20_Mintable", "ARC200_Mintable") | ||
| /// - By deployer address | ||
| /// </remarks> | ||
| [HttpGet("metrics")] | ||
| [ProducesResponseType(typeof(DeploymentMetricsResponse), StatusCodes.Status200OK)] | ||
| [ProducesResponseType(StatusCodes.Status400BadRequest)] | ||
| public async Task<IActionResult> GetDeploymentMetrics([FromQuery] GetDeploymentMetricsRequest request) | ||
| { | ||
| try | ||
| { | ||
| var metrics = await _deploymentStatusService.GetDeploymentMetricsAsync(request); | ||
|
|
||
| _logger.LogInformation("Calculated deployment metrics: Period={FromDate} to {ToDate}, Total={Total}, Success={Success}, Failed={Failed}", | ||
| metrics.PeriodStart, metrics.PeriodEnd, metrics.TotalDeployments, | ||
| metrics.SuccessfulDeployments, metrics.FailedDeployments); | ||
|
|
||
| return Ok(new DeploymentMetricsResponse | ||
| { | ||
| Success = true, | ||
| Metrics = metrics | ||
| }); | ||
| } | ||
| catch (Exception ex) | ||
| { | ||
| _logger.LogError(ex, "Error calculating deployment metrics"); | ||
| return StatusCode(StatusCodes.Status500InternalServerError, new DeploymentMetricsResponse | ||
| { | ||
| Success = false, | ||
| ErrorMessage = "An error occurred while calculating deployment metrics" | ||
| }); | ||
| } | ||
| } | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The fileName constructed using string interpolation directly includes the unsanitized deploymentId parameter. While the deploymentId should be a GUID, if a malicious user provides path traversal characters or special characters, it could potentially cause issues with file download behavior in some browsers. Consider sanitizing the deploymentId or using a safe filename pattern that doesn't directly expose user input.