SDK for .NET (v4)를 사용한 CloudWatch Logs 예제 - AWS SDK for .NET (V4)

의 버전 4(V4) AWS SDK for .NET 가 릴리스되었습니다!

변경 사항 해제 및 애플리케이션 마이그레이션에 대한 자세한 내용은 마이그레이션 주제를 참조하세요.

기계 번역으로 제공되는 번역입니다. 제공된 번역과 원본 영어의 내용이 상충하는 경우에는 영어 버전이 우선합니다.

SDK for .NET (v4)를 사용한 CloudWatch Logs 예제

다음 코드 예제에서는 CloudWatch Logs와 함께 AWS SDK for .NET (v4)를 사용하여 작업을 수행하고 일반적인 시나리오를 구현하는 방법을 보여줍니다.

작업은 대규모 프로그램에서 발췌한 코드이며 컨텍스트에 맞춰 실행해야 합니다. 작업은 개별 서비스 함수를 직접 호출하는 방법을 보여주며, 관련 시나리오의 컨텍스트에 맞는 작업을 볼 수 있습니다.

시나리오는 서비스 내에서 또는 다른 AWS 서비스와 결합된 상태에서 여러 함수를 직접적 호출하여 특정 작업을 수행하는 방법을 보여주는 코드 예제입니다.

각 예시에는 전체 소스 코드에 대한 링크가 포함되어 있으며, 여기에서 컨텍스트에 맞춰 코드를 설정하고 실행하는 방법에 대한 지침을 찾을 수 있습니다.

작업

다음 코드 예시는 GetQueryResults의 사용 방법을 보여 줍니다.

SDK for .NET (v4)
참고

GitHub에 더 많은 내용이 있습니다. AWS 코드 예 리포지토리에서 전체 예를 찾고 설정 및 실행하는 방법을 배워보세요.

/// <summary> /// Gets the results of a CloudWatch Logs Insights query. /// </summary> /// <param name="queryId">The ID of the query.</param> /// <returns>The query results response.</returns> public async Task<GetQueryResultsResponse?> GetQueryResultsAsync(string queryId) { try { var request = new GetQueryResultsRequest { QueryId = queryId }; var response = await _amazonCloudWatchLogs.GetQueryResultsAsync(request); return response; } catch (ResourceNotFoundException ex) { _logger.LogError($"Query not found: {ex.Message}"); return null; } catch (Exception ex) { _logger.LogError($"An error occurred while getting query results: {ex.Message}"); return null; } }
  • API 세부 정보는 AWS SDK for .NET API 참조GetQueryResults를 참조하세요.

다음 코드 예시는 StartQuery의 사용 방법을 보여 줍니다.

SDK for .NET (v4)
참고

GitHub에 더 많은 내용이 있습니다. AWS 코드 예 리포지토리에서 전체 예를 찾고 설정 및 실행하는 방법을 배워보세요.

/// <summary> /// Starts a CloudWatch Logs Insights query. /// </summary> /// <param name="logGroupName">The name of the log group to query.</param> /// <param name="queryString">The CloudWatch Logs Insights query string.</param> /// <param name="startTime">The start time for the query (seconds since epoch).</param> /// <param name="endTime">The end time for the query (seconds since epoch).</param> /// <param name="limit">The maximum number of results to return.</param> /// <returns>The query ID if successful, null otherwise.</returns> public async Task<string?> StartQueryAsync( string logGroupName, string queryString, long startTime, long endTime, int limit = 10000) { try { var request = new StartQueryRequest { LogGroupName = logGroupName, QueryString = queryString, StartTime = startTime, EndTime = endTime, Limit = limit }; var response = await _amazonCloudWatchLogs.StartQueryAsync(request); return response.QueryId; } catch (InvalidParameterException ex) { _logger.LogError($"Invalid parameter for query: {ex.Message}"); return null; } catch (ResourceNotFoundException ex) { _logger.LogError($"Log group not found: {ex.Message}"); return null; } catch (Exception ex) { _logger.LogError($"An error occurred while starting query: {ex.Message}"); return null; } }
  • API 세부 정보는 AWS SDK for .NET API 참조의 StartQuery를 참조하세요.

시나리오

다음 코드 예시에서는 CloudWatch Logs를 사용하여 1만 개 이상의 레코드를 쿼리하는 방법을 보여줍니다.

SDK for .NET (v4)
참고

GitHub에 더 많은 내용이 있습니다. AWS 코드 예 리포지토리에서 전체 예를 찾고 설정 및 실행하는 방법을 배워보세요.

이는 대규모 쿼리 시나리오를 보여주는 주요 워크플로입니다.

using System.Diagnostics; using System.Text.RegularExpressions; using Amazon.CloudFormation; using Amazon.CloudFormation.Model; using Amazon.CloudWatchLogs; using Amazon.CloudWatchLogs.Model; using CloudWatchLogsActions; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; namespace CloudWatchLogsScenario; public class LargeQueryWorkflow { /* Before running this .NET code example, set up your development environment, including your credentials. This .NET code example performs the following tasks for the CloudWatch Logs Large Query workflow: 1. Prepare the Application: - Prompt the user to deploy CloudFormation stack and generate sample logs. - Deploy the CloudFormation template for resource creation. - Generate 50,000 sample log entries using CloudWatch Logs API. - Wait 5 minutes for logs to be fully ingested. 2. Execute Large Query: - Perform recursive queries to retrieve all logs using binary search. - Display progress for each query executed. - Show total execution time and logs found. 3. Clean up: - Prompt the user to delete the CloudFormation stack and all resources. - Destroy the CloudFormation stack and wait until removed. */ public static ILogger<LargeQueryWorkflow> _logger = null!; public static CloudWatchLogsWrapper _wrapper = null!; public static IAmazonCloudFormation _amazonCloudFormation = null!; private static string _logGroupName = "/workflows/cloudwatch-logs/large-query"; private static string _logStreamName = "stream1"; private static long _queryStartDate; private static long _queryEndDate; public static bool _interactive = true; public static string _stackName = "CloudWatchLargeQueryStack"; private static string _stackResourcePath = "../../../../../../../scenarios/features/cloudwatch_logs_large_query/resources/stack.yaml"; public static async Task Main(string[] args) { using var host = Host.CreateDefaultBuilder(args) .ConfigureLogging(logging => logging.AddFilter("System", LogLevel.Debug) .AddFilter("Microsoft", LogLevel.Information)) .ConfigureServices((_, services) => services.AddAWSService<IAmazonCloudWatchLogs>() .AddAWSService<IAmazonCloudFormation>() .AddTransient<CloudWatchLogsWrapper>() ) .Build(); if (_interactive) { _logger = LoggerFactory.Create(builder => { builder.AddConsole(); }) .CreateLogger<LargeQueryWorkflow>(); _wrapper = host.Services.GetRequiredService<CloudWatchLogsWrapper>(); _amazonCloudFormation = host.Services.GetRequiredService<IAmazonCloudFormation>(); } Console.WriteLine(new string('-', 80)); Console.WriteLine("Welcome to the CloudWatch Logs Large Query Scenario."); Console.WriteLine(new string('-', 80)); Console.WriteLine("This scenario demonstrates how to perform large-scale queries on"); Console.WriteLine("CloudWatch Logs using recursive binary search to retrieve more than"); Console.WriteLine("the 10,000 result limit."); Console.WriteLine(); try { Console.WriteLine(new string('-', 80)); var prepareSuccess = await PrepareApplication(); Console.WriteLine(new string('-', 80)); if (prepareSuccess) { Console.WriteLine(new string('-', 80)); await ExecuteLargeQuery(); Console.WriteLine(new string('-', 80)); } Console.WriteLine(new string('-', 80)); await Cleanup(); Console.WriteLine(new string('-', 80)); } catch (Exception ex) { _logger.LogError(ex, "There was a problem with the scenario, initiating cleanup..."); _interactive = false; await Cleanup(); } Console.WriteLine("CloudWatch Logs Large Query scenario completed."); } /// <summary> /// Runs the scenario workflow. Used for testing. /// </summary> public static async Task RunScenario() { Console.WriteLine(new string('-', 80)); Console.WriteLine("Welcome to the CloudWatch Logs Large Query Scenario."); Console.WriteLine(new string('-', 80)); Console.WriteLine("This scenario demonstrates how to perform large-scale queries on"); Console.WriteLine("CloudWatch Logs using recursive binary search to retrieve more than"); Console.WriteLine("the 10,000 result limit."); Console.WriteLine(); try { Console.WriteLine(new string('-', 80)); var prepareSuccess = await PrepareApplication(); Console.WriteLine(new string('-', 80)); if (prepareSuccess) { Console.WriteLine(new string('-', 80)); await ExecuteLargeQuery(); Console.WriteLine(new string('-', 80)); } Console.WriteLine(new string('-', 80)); await Cleanup(); Console.WriteLine(new string('-', 80)); } catch (Exception ex) { _logger.LogError(ex, "There was a problem with the scenario, initiating cleanup..."); _interactive = false; await Cleanup(); } Console.WriteLine("CloudWatch Logs Large Query scenario completed."); } /// <summary> /// Prepares the application by creating the necessary resources. /// </summary> /// <returns>True if the application was prepared successfully.</returns> public static async Task<bool> PrepareApplication() { Console.WriteLine("Preparing the application..."); Console.WriteLine(); try { var deployStack = !_interactive || GetYesNoResponse( "Would you like to deploy the CloudFormation stack and generate sample logs? (y/n) "); if (deployStack) { if (_interactive) { Console.Write( $"Enter a path for the CloudFormation stack resource .yaml file (or press Enter for default '{_stackResourcePath}'): "); string? inputPath = Console.ReadLine(); if (!string.IsNullOrWhiteSpace(inputPath)) { _stackResourcePath = inputPath; } } _stackName = PromptUserForStackName(); var deploySuccess = await DeployCloudFormationStack(_stackName); if (deploySuccess) { Console.WriteLine(); Console.WriteLine("Generating 50,000 sample log entries..."); var generateSuccess = await GenerateSampleLogs(); if (generateSuccess) { Console.WriteLine(); Console.WriteLine("Sample logs created. Waiting 5 minutes for logs to be fully ingested..."); await WaitWithCountdown(300); Console.WriteLine("Application preparation complete."); return true; } } } else { _logGroupName = PromptUserForInput("Enter the log group name ", _logGroupName); _logStreamName = PromptUserForInput("Enter the log stream name ", _logStreamName); var startDateMs = PromptUserForLong("Enter the query start date (milliseconds since epoch): "); var endDateMs = PromptUserForLong("Enter the query end date (milliseconds since epoch): "); _queryStartDate = startDateMs / 1000; _queryEndDate = endDateMs / 1000; Console.WriteLine("Application preparation complete."); return true; } } catch (Exception ex) { _logger.LogError(ex, "An error occurred while preparing the application."); } Console.WriteLine("Application preparation failed."); return false; } /// <summary> /// Deploys the CloudFormation stack with the necessary resources. /// </summary> /// <param name="stackName">The name of the CloudFormation stack.</param> /// <returns>True if the stack was deployed successfully.</returns> private static async Task<bool> DeployCloudFormationStack(string stackName) { Console.WriteLine($"\nDeploying CloudFormation stack: {stackName}"); try { var request = new CreateStackRequest { StackName = stackName, TemplateBody = await File.ReadAllTextAsync(_stackResourcePath) }; var response = await _amazonCloudFormation.CreateStackAsync(request); if (response.HttpStatusCode == System.Net.HttpStatusCode.OK) { Console.WriteLine($"CloudFormation stack creation started: {stackName}"); bool stackCreated = await WaitForStackCompletion(response.StackId); if (stackCreated) { Console.WriteLine("CloudFormation stack created successfully."); return true; } else { _logger.LogError($"CloudFormation stack creation failed: {stackName}"); return false; } } else { _logger.LogError($"Failed to create CloudFormation stack: {stackName}"); return false; } } catch (AlreadyExistsException) { _logger.LogWarning($"CloudFormation stack '{stackName}' already exists. Please provide a unique name."); var newStackName = PromptUserForStackName(); return await DeployCloudFormationStack(newStackName); } catch (Exception ex) { _logger.LogError(ex, $"An error occurred while deploying the CloudFormation stack: {stackName}"); return false; } } /// <summary> /// Waits for the CloudFormation stack to be in the CREATE_COMPLETE state. /// </summary> /// <param name="stackId">The ID of the CloudFormation stack.</param> /// <returns>True if the stack was created successfully.</returns> private static async Task<bool> WaitForStackCompletion(string stackId) { int retryCount = 0; const int maxRetries = 30; const int retryDelay = 10000; while (retryCount < maxRetries) { var describeStacksRequest = new DescribeStacksRequest { StackName = stackId }; var describeStacksResponse = await _amazonCloudFormation.DescribeStacksAsync(describeStacksRequest); if (describeStacksResponse.Stacks.Count > 0) { if (describeStacksResponse.Stacks[0].StackStatus == StackStatus.CREATE_COMPLETE) { return true; } if (describeStacksResponse.Stacks[0].StackStatus == StackStatus.CREATE_FAILED || describeStacksResponse.Stacks[0].StackStatus == StackStatus.ROLLBACK_COMPLETE) { return false; } } Console.WriteLine("Waiting for CloudFormation stack creation to complete..."); await Task.Delay(retryDelay); retryCount++; } _logger.LogError("Timed out waiting for CloudFormation stack creation to complete."); return false; } /// <summary> /// Generates sample logs directly using CloudWatch Logs API. /// Creates 50,000 log entries spanning 5 minutes. /// </summary> /// <returns>True if logs were generated successfully.</returns> private static async Task<bool> GenerateSampleLogs() { const int totalEntries = 50000; const int entriesPerBatch = 10000; const int fiveMinutesMs = 5 * 60 * 1000; try { // Calculate timestamps var startTimeMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); var timestampIncrement = fiveMinutesMs / totalEntries; Console.WriteLine($"Generating {totalEntries} log entries..."); var entryCount = 0; var currentTimestamp = startTimeMs; var numBatches = totalEntries / entriesPerBatch; // Generate and upload logs in batches for (int batchNum = 0; batchNum < numBatches; batchNum++) { var logEvents = new List<InputLogEvent>(); for (int i = 0; i < entriesPerBatch; i++) { logEvents.Add(new InputLogEvent { Timestamp = DateTimeOffset.FromUnixTimeMilliseconds(currentTimestamp).UtcDateTime, Message = $"Entry {entryCount}" }); entryCount++; currentTimestamp += timestampIncrement; } // Upload batch var success = await _wrapper.PutLogEventsAsync(_logGroupName, _logStreamName, logEvents); if (!success) { _logger.LogError($"Failed to upload batch {batchNum + 1}/{numBatches}"); return false; } Console.WriteLine($"Uploaded batch {batchNum + 1}/{numBatches}"); } // Set query date range (convert milliseconds to seconds for query API) _queryStartDate = startTimeMs / 1000; _queryEndDate = (currentTimestamp - timestampIncrement) / 1000; Console.WriteLine($"Query start date: {DateTimeOffset.FromUnixTimeSeconds(_queryStartDate):yyyy-MM-ddTHH:mm:ss.fffZ}"); Console.WriteLine($"Query end date: {DateTimeOffset.FromUnixTimeSeconds(_queryEndDate):yyyy-MM-ddTHH:mm:ss.fffZ}"); Console.WriteLine($"Successfully uploaded {totalEntries} log entries"); return true; } catch (Exception ex) { _logger.LogError(ex, "An error occurred while generating sample logs."); return false; } } /// <summary> /// Executes the large query workflow. /// </summary> public static async Task ExecuteLargeQuery() { Console.WriteLine("Starting recursive query to retrieve all logs..."); Console.WriteLine(); var queryLimit = PromptUserForInteger("Enter the query limit (max 10000) ", 10000); if (queryLimit > 10000) queryLimit = 10000; var queryString = "fields @timestamp, @message | sort @timestamp asc"; var stopwatch = Stopwatch.StartNew(); var allResults = await PerformLargeQuery(_logGroupName, queryString, _queryStartDate, _queryEndDate, queryLimit); stopwatch.Stop(); Console.WriteLine(); Console.WriteLine($"Queries finished in {stopwatch.Elapsed.TotalSeconds:F3} seconds."); Console.WriteLine($"Total logs found: {allResults.Count}"); // Check for duplicates Console.WriteLine(); Console.WriteLine("Checking for duplicate logs..."); var duplicates = FindDuplicateLogs(allResults); if (duplicates.Count > 0) { Console.WriteLine($"WARNING: Found {duplicates.Count} duplicate log entries!"); Console.WriteLine("Duplicate entries (showing first 10):"); foreach (var dup in duplicates.Take(10)) { Console.WriteLine($" [{dup.Timestamp}] {dup.Message} (appears {dup.Count} times)"); } var uniqueCount = allResults.Count - duplicates.Sum(d => d.Count - 1); Console.WriteLine($"Unique logs: {uniqueCount}"); } else { Console.WriteLine("No duplicates found. All logs are unique."); } Console.WriteLine(); var viewSample = !_interactive || GetYesNoResponse("Would you like to see a sample of the logs? (y/n) "); if (viewSample) { Console.WriteLine(); Console.WriteLine($"Sample logs (first 10 of {allResults.Count}):"); for (int i = 0; i < Math.Min(10, allResults.Count); i++) { var timestamp = allResults[i].Find(f => f.Field == "@timestamp")?.Value ?? "N/A"; var message = allResults[i].Find(f => f.Field == "@message")?.Value ?? "N/A"; Console.WriteLine($"[{timestamp}] {message}"); } } } /// <summary> /// Performs a large query using recursive binary search. /// </summary> private static async Task<List<List<ResultField>>> PerformLargeQuery( string logGroupName, string queryString, long startTime, long endTime, int limit) { var queryId = await _wrapper.StartQueryAsync(logGroupName, queryString, startTime, endTime, limit); if (queryId == null) { return new List<List<ResultField>>(); } var results = await PollQueryResults(queryId); if (results == null || results.Count == 0) { return new List<List<ResultField>>(); } var startDate = DateTimeOffset.FromUnixTimeSeconds(startTime).ToString("yyyy-MM-ddTHH:mm:ss.fffZ"); var endDate = DateTimeOffset.FromUnixTimeSeconds(endTime).ToString("yyyy-MM-ddTHH:mm:ss.fffZ"); Console.WriteLine($"Query date range: {startDate} ({startTime}s) to {endDate} ({endTime}s). Found {results.Count} logs."); if (results.Count < limit) { Console.WriteLine($" -> Returning {results.Count} logs (less than limit of {limit})"); return results; } Console.WriteLine($" -> Hit limit of {limit}. Need to split and recurse."); // Get the timestamp of the last log (sorted to find the actual last one) var lastLogTimestamp = GetLastLogTimestamp(results); if (lastLogTimestamp == null) { Console.WriteLine($" -> No timestamp found in results. Returning {results.Count} logs."); return results; } Console.WriteLine($" -> Last log timestamp: {lastLogTimestamp}"); // Parse the timestamp and add 1 millisecond to avoid querying the same log again var lastLogDate = DateTimeOffset.Parse(lastLogTimestamp + " +0000"); Console.WriteLine($" -> Last log as DateTimeOffset: {lastLogDate:yyyy-MM-ddTHH:mm:ss.fffZ} ({lastLogDate.ToUnixTimeSeconds()}s)"); var offsetLastLogDate = lastLogDate.AddMilliseconds(1); Console.WriteLine($" -> Offset timestamp (last + 1ms): {offsetLastLogDate:yyyy-MM-ddTHH:mm:ss.fffZ} ({offsetLastLogDate.ToUnixTimeSeconds()}s)"); // Convert to seconds, but round UP to the next second to avoid overlapping with logs in the same second // This ensures we don't re-query logs that share the same second as the last log var offsetLastLogTime = offsetLastLogDate.ToUnixTimeSeconds(); if (offsetLastLogDate.Millisecond > 0) { offsetLastLogTime++; // Move to the next full second Console.WriteLine($" -> Adjusted to next full second: {offsetLastLogTime}s ({DateTimeOffset.FromUnixTimeSeconds(offsetLastLogTime):yyyy-MM-ddTHH:mm:ss.fffZ})"); } Console.WriteLine($" -> Comparing: offsetLastLogTime={offsetLastLogTime}s vs endTime={endTime}s"); Console.WriteLine($" -> End time as date: {DateTimeOffset.FromUnixTimeSeconds(endTime):yyyy-MM-ddTHH:mm:ss.fffZ}"); // Check if there's any time range left to query if (offsetLastLogTime >= endTime) { Console.WriteLine($" -> No time range left to query. Offset time ({offsetLastLogTime}s) >= end time ({endTime}s)"); return results; } // Split the remaining date range in half var (range1Start, range1End, range2Start, range2End) = SplitDateRange(offsetLastLogTime, endTime); var range1StartDate = DateTimeOffset.FromUnixTimeSeconds(range1Start).ToString("yyyy-MM-ddTHH:mm:ss.fffZ"); var range1EndDate = DateTimeOffset.FromUnixTimeSeconds(range1End).ToString("yyyy-MM-ddTHH:mm:ss.fffZ"); var range2StartDate = DateTimeOffset.FromUnixTimeSeconds(range2Start).ToString("yyyy-MM-ddTHH:mm:ss.fffZ"); var range2EndDate = DateTimeOffset.FromUnixTimeSeconds(range2End).ToString("yyyy-MM-ddTHH:mm:ss.fffZ"); Console.WriteLine($" -> Splitting remaining range:"); Console.WriteLine($" Range 1: {range1StartDate} ({range1Start}s) to {range1EndDate} ({range1End}s)"); Console.WriteLine($" Range 2: {range2StartDate} ({range2Start}s) to {range2EndDate} ({range2End}s)"); // Query both halves recursively Console.WriteLine($" -> Querying range 1..."); var results1 = await PerformLargeQuery(logGroupName, queryString, range1Start, range1End, limit); Console.WriteLine($" -> Range 1 returned {results1.Count} logs"); Console.WriteLine($" -> Querying range 2..."); var results2 = await PerformLargeQuery(logGroupName, queryString, range2Start, range2End, limit); Console.WriteLine($" -> Range 2 returned {results2.Count} logs"); // Combine all results var allResults = new List<List<ResultField>>(results); allResults.AddRange(results1); allResults.AddRange(results2); Console.WriteLine($" -> Combined total: {allResults.Count} logs ({results.Count} + {results1.Count} + {results2.Count})"); return allResults; } /// <summary> /// Gets the timestamp string of the most recent log from a list of logs. /// Sorts timestamps to find the actual last one. /// </summary> private static string? GetLastLogTimestamp(List<List<ResultField>> logs) { var timestamps = logs .Select(log => log.Find(f => f.Field == "@timestamp")?.Value) .Where(t => !string.IsNullOrEmpty(t)) .OrderBy(t => t) .ToList(); if (timestamps.Count == 0) { return null; } return timestamps[timestamps.Count - 1]; } /// <summary> /// Splits a date range in half. /// Range 2 starts at midpoint + 1 second to avoid overlap. /// </summary> private static (long range1Start, long range1End, long range2Start, long range2End) SplitDateRange(long startTime, long endTime) { var midpoint = startTime + (endTime - startTime) / 2; // Range 2 starts at midpoint + 1 to avoid querying the same second twice return (startTime, midpoint, midpoint + 1, endTime); } /// <summary> /// Polls for query results until complete. /// </summary> private static async Task<List<List<ResultField>>?> PollQueryResults(string queryId) { int retryCount = 0; const int maxRetries = 60; const int retryDelay = 1000; while (retryCount < maxRetries) { var response = await _wrapper.GetQueryResultsAsync(queryId); if (response == null) { return null; } if (response.Status == QueryStatus.Complete) { return response.Results; } if (response.Status == QueryStatus.Failed || response.Status == QueryStatus.Cancelled || response.Status == QueryStatus.Timeout || response.Status == QueryStatus.Unknown) { _logger.LogError($"Query failed with status: {response.Status}"); return null; } await Task.Delay(retryDelay); retryCount++; } _logger.LogError("Timed out waiting for query results."); return null; } /// <summary> /// Cleans up the resources created during the scenario. /// </summary> public static async Task<bool> Cleanup() { var cleanup = !_interactive || GetYesNoResponse( "Do you want to delete the CloudFormation stack and all resources? (y/n) "); if (cleanup) { try { var stackDeleteSuccess = await DeleteCloudFormationStack(_stackName, false); return stackDeleteSuccess; } catch (Exception ex) { _logger.LogError(ex, "An error occurred while cleaning up the resources."); return false; } } Console.WriteLine($"Resources will remain. Stack name: {_stackName}, Log group: {_logGroupName}"); _logger.LogInformation("CloudWatch Logs Large Query scenario is complete."); return true; } /// <summary> /// Deletes the CloudFormation stack and waits for confirmation. /// </summary> private static async Task<bool> DeleteCloudFormationStack(string stackName, bool forceDelete) { var request = new DeleteStackRequest { StackName = stackName, }; if (forceDelete) { request.DeletionMode = DeletionMode.FORCE_DELETE_STACK; } await _amazonCloudFormation.DeleteStackAsync(request); Console.WriteLine($"CloudFormation stack '{stackName}' is being deleted. This may take a few minutes."); bool stackDeleted = await WaitForStackDeletion(stackName, forceDelete); if (stackDeleted) { Console.WriteLine($"CloudFormation stack '{stackName}' has been deleted."); return true; } else { _logger.LogError($"Failed to delete CloudFormation stack '{stackName}'."); return false; } } /// <summary> /// Waits for the stack to be deleted. /// </summary> private static async Task<bool> WaitForStackDeletion(string stackName, bool forceDelete) { int retryCount = 0; const int maxRetries = 30; const int retryDelay = 10000; while (retryCount < maxRetries) { var describeStacksRequest = new DescribeStacksRequest { StackName = stackName }; try { var describeStacksResponse = await _amazonCloudFormation.DescribeStacksAsync(describeStacksRequest); if (describeStacksResponse.Stacks.Count == 0 || describeStacksResponse.Stacks[0].StackStatus == StackStatus.DELETE_COMPLETE) { return true; } if (!forceDelete && describeStacksResponse.Stacks[0].StackStatus == StackStatus.DELETE_FAILED) { return await DeleteCloudFormationStack(stackName, true); } } catch (AmazonCloudFormationException ex) when (ex.ErrorCode == "ValidationError") { return true; } Console.WriteLine($"Waiting for CloudFormation stack '{stackName}' to be deleted..."); await Task.Delay(retryDelay); retryCount++; } _logger.LogError($"Timed out waiting for CloudFormation stack '{stackName}' to be deleted."); return false; } /// <summary> /// Waits with a countdown display. /// </summary> private static async Task WaitWithCountdown(int seconds) { for (int i = seconds; i > 0; i--) { Console.Write($"\rWaiting: {i} seconds remaining... "); await Task.Delay(1000); } Console.WriteLine("\rWait complete. "); } /// <summary> /// Helper method to get a yes or no response from the user. /// </summary> private static bool GetYesNoResponse(string question) { Console.WriteLine(question); var ynResponse = Console.ReadLine(); var response = ynResponse != null && ynResponse.Equals("y", StringComparison.InvariantCultureIgnoreCase); return response; } /// <summary> /// Prompts the user for a stack name. /// </summary> private static string PromptUserForStackName() { if (_interactive) { Console.Write($"Enter a name for the CloudFormation stack (press Enter for default '{_stackName}'): "); string? input = Console.ReadLine(); if (!string.IsNullOrWhiteSpace(input)) { var regex = "[a-zA-Z][-a-zA-Z0-9]*"; if (!Regex.IsMatch(input, regex)) { Console.WriteLine($"Invalid stack name. Using default: {_stackName}"); return _stackName; } return input; } } return _stackName; } /// <summary> /// Prompts the user for input with a default value. /// </summary> private static string PromptUserForInput(string prompt, string defaultValue) { if (_interactive) { Console.Write($"{prompt}(press Enter for default '{defaultValue}'): "); string? input = Console.ReadLine(); return string.IsNullOrWhiteSpace(input) ? defaultValue : input; } return defaultValue; } /// <summary> /// Prompts the user for an integer value. /// </summary> private static int PromptUserForInteger(string prompt, int defaultValue) { if (_interactive) { Console.Write($"{prompt}(press Enter for default '{defaultValue}'): "); string? input = Console.ReadLine(); if (string.IsNullOrWhiteSpace(input) || !int.TryParse(input, out var result)) { return defaultValue; } return result; } return defaultValue; } /// <summary> /// Prompts the user for a long value. /// </summary> private static long PromptUserForLong(string prompt) { if (_interactive) { Console.Write(prompt); string? input = Console.ReadLine(); if (long.TryParse(input, out var result)) { return result; } } return 0; } /// <summary> /// Finds duplicate log entries based on timestamp and message. /// </summary> private static List<(string Timestamp, string Message, int Count)> FindDuplicateLogs(List<List<ResultField>> logs) { var logSignatures = new Dictionary<string, int>(); foreach (var log in logs) { var timestamp = log.Find(f => f.Field == "@timestamp")?.Value ?? ""; var message = log.Find(f => f.Field == "@message")?.Value ?? ""; var signature = $"{timestamp}|{message}"; if (logSignatures.ContainsKey(signature)) { logSignatures[signature]++; } else { logSignatures[signature] = 1; } } return logSignatures .Where(kvp => kvp.Value > 1) .Select(kvp => { var parts = kvp.Key.Split('|'); return (Timestamp: parts[0], Message: parts[1], Count: kvp.Value); }) .OrderByDescending(x => x.Count) .ToList(); } }