11// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
22// SPDX-License-Identifier: Apache-2.0
33
4- // snippet-start:[CloudWatchLogs.dotnetv3 .LargeQueryWorkflow]
4+ // snippet-start:[CloudWatchLogs.dotnetv4 .LargeQueryWorkflow]
55using System . Diagnostics ;
66using System . Text . RegularExpressions ;
77using Amazon . CloudFormation ;
@@ -24,7 +24,7 @@ public class LargeQueryWorkflow
2424 1. Prepare the Application:
2525 - Prompt the user to deploy CloudFormation stack and generate sample logs.
2626 - Deploy the CloudFormation template for resource creation.
27- - Generate 50,000 sample log entries using a Python script .
27+ - Generate 50,000 sample log entries using CloudWatch Logs API .
2828 - Wait 5 minutes for logs to be fully ingested.
2929
3030 2. Execute Large Query:
@@ -48,8 +48,7 @@ public class LargeQueryWorkflow
4848
4949 public static bool _interactive = true ;
5050 private static string _stackName = "CloudWatchLargeQueryStack" ;
51- private static string _stackResourcePath = "../../../../../../scenarios/features/cloudwatch_logs_large_query/resources/stack.yaml" ;
52- private static string _pythonScriptPath = "../../../../../../scenarios/features/cloudwatch_logs_large_query/resources/create_logs.py" ;
51+ private static string _stackResourcePath = "../../../../../../../scenarios/features/cloudwatch_logs_large_query/resources/stack.yaml" ;
5352
5453 public static async Task Main ( string [ ] args )
5554 {
@@ -147,8 +146,8 @@ public static async Task<bool> PrepareApplication()
147146 }
148147 else
149148 {
150- _logGroupName = PromptUserForInput ( "Enter the log group name: " , _logGroupName ) ;
151- _logStreamName = PromptUserForInput ( "Enter the log stream name: " , _logStreamName ) ;
149+ _logGroupName = PromptUserForInput ( "Enter the log group name " , _logGroupName ) ;
150+ _logStreamName = PromptUserForInput ( "Enter the log stream name " , _logStreamName ) ;
152151
153152 var startDateMs = PromptUserForLong ( "Enter the query start date (milliseconds since epoch): " ) ;
154153 var endDateMs = PromptUserForLong ( "Enter the query end date (milliseconds since epoch): " ) ;
@@ -267,65 +266,65 @@ private static async Task<bool> WaitForStackCompletion(string stackId)
267266 }
268267
269268 /// <summary>
270- /// Generates sample logs using a Python script.
269+ /// Generates sample logs directly using CloudWatch Logs API.
270+ /// Creates 50,000 log entries spanning 5 minutes.
271271 /// </summary>
272272 /// <returns>True if logs were generated successfully.</returns>
273273 private static async Task < bool > GenerateSampleLogs ( )
274274 {
275+ const int totalEntries = 50000 ;
276+ const int entriesPerBatch = 10000 ;
277+ const int fiveMinutesMs = 5 * 60 * 1000 ;
278+
275279 try
276280 {
277- if ( ! File . Exists ( _pythonScriptPath ) )
278- {
279- _logger . LogError ( $ "Python script not found at: { _pythonScriptPath } ") ;
280- Console . WriteLine ( "Please run the script manually from:" ) ;
281- Console . WriteLine ( $ " { _pythonScriptPath } ") ;
282- return false ;
283- }
281+ // Calculate timestamps
282+ var startTimeMs = DateTimeOffset . UtcNow . ToUnixTimeMilliseconds ( ) ;
283+ var timestampIncrement = fiveMinutesMs / totalEntries ;
284284
285- var processStartInfo = new ProcessStartInfo
286- {
287- FileName = "python" ,
288- Arguments = _pythonScriptPath ,
289- RedirectStandardOutput = true ,
290- RedirectStandardError = true ,
291- UseShellExecute = false ,
292- CreateNoWindow = true
293- } ;
285+ Console . WriteLine ( $ "Generating { totalEntries } log entries...") ;
294286
295- using var process = Process . Start ( processStartInfo ) ;
296- if ( process == null )
287+ var entryCount = 0 ;
288+ var currentTimestamp = startTimeMs ;
289+ var numBatches = totalEntries / entriesPerBatch ;
290+
291+ // Generate and upload logs in batches
292+ for ( int batchNum = 0 ; batchNum < numBatches ; batchNum ++ )
297293 {
298- _logger . LogError ( "Failed to start Python process." ) ;
299- return false ;
300- }
294+ var logEvents = new List < InputLogEvent > ( ) ;
301295
302- var output = await process . StandardOutput . ReadToEndAsync ( ) ;
303- var error = await process . StandardError . ReadToEndAsync ( ) ;
304- await process . WaitForExitAsync ( ) ;
296+ for ( int i = 0 ; i < entriesPerBatch ; i ++ )
297+ {
298+ logEvents . Add ( new InputLogEvent
299+ {
300+ Timestamp = DateTimeOffset . FromUnixTimeMilliseconds ( currentTimestamp ) . UtcDateTime ,
301+ Message = $ "Entry { entryCount } "
302+ } ) ;
305303
306- if ( process . ExitCode != 0 )
307- {
308- _logger . LogError ( $ "Python script failed: { error } ") ;
309- return false ;
304+ entryCount ++ ;
305+ currentTimestamp += timestampIncrement ;
306+ }
307+
308+ // Upload batch
309+ var success = await _wrapper . PutLogEventsAsync ( _logGroupName , _logStreamName , logEvents ) ;
310+ if ( ! success )
311+ {
312+ _logger . LogError ( $ "Failed to upload batch { batchNum + 1 } /{ numBatches } ") ;
313+ return false ;
314+ }
315+
316+ Console . WriteLine ( $ "Uploaded batch { batchNum + 1 } /{ numBatches } ") ;
310317 }
311318
312- var startMatch = Regex . Match ( output , @"QUERY_START_DATE=(\d+)" ) ;
313- var endMatch = Regex . Match ( output , @"QUERY_END_DATE=(\d+)" ) ;
319+ // Set query date range (convert milliseconds to seconds for query API)
320+ _queryStartDate = startTimeMs / 1000 ;
321+ _queryEndDate = ( currentTimestamp - timestampIncrement ) / 1000 ;
314322
315- if ( startMatch . Success && endMatch . Success )
316- {
317- _queryStartDate = long . Parse ( startMatch . Groups [ 1 ] . Value ) / 1000 ;
318- _queryEndDate = long . Parse ( endMatch . Groups [ 1 ] . Value ) / 1000 ;
323+ Console . WriteLine ( $ "Query start date: { DateTimeOffset . FromUnixTimeSeconds ( _queryStartDate ) : yyyy-MM-ddTHH:mm:ss.fffZ} ") ;
324+ Console . WriteLine ( $ "Query end date: { DateTimeOffset . FromUnixTimeSeconds ( _queryEndDate ) : yyyy-MM-ddTHH:mm:ss.fffZ} ") ;
325+ Console . WriteLine ( $ "Successfully uploaded { totalEntries } log entries") ;
319326
320- Console . WriteLine ( $ "Query start date: { DateTimeOffset . FromUnixTimeSeconds ( _queryStartDate ) : yyyy-MM-ddTHH:mm:ss.fffZ} ") ;
321- Console . WriteLine ( $ "Query end date: { DateTimeOffset . FromUnixTimeSeconds ( _queryEndDate ) : yyyy-MM-ddTHH:mm:ss.fffZ} ") ;
322- return true ;
323- }
324- else
325- {
326- _logger . LogError ( "Failed to parse timestamps from script output." ) ;
327- return false ;
328- }
327+ return true ;
329328 }
330329 catch ( Exception ex )
331330 {
@@ -342,7 +341,7 @@ public static async Task ExecuteLargeQuery()
342341 Console . WriteLine ( "Starting recursive query to retrieve all logs..." ) ;
343342 Console . WriteLine ( ) ;
344343
345- var queryLimit = PromptUserForInteger ( "Enter the query limit (default 10000, max 10000): " , 10000 ) ;
344+ var queryLimit = PromptUserForInteger ( "Enter the query limit (max 10000) " , 10000 ) ;
346345 if ( queryLimit > 10000 ) queryLimit = 10000 ;
347346
348347 var queryString = "fields @timestamp, @message | sort @timestamp asc" ;
@@ -407,17 +406,70 @@ private static async Task<List<List<ResultField>>> PerformLargeQuery(
407406 return results ;
408407 }
409408
409+ // Parse the timestamp - CloudWatch returns ISO 8601 format with milliseconds
410410 var lastTime = DateTimeOffset . Parse ( lastTimestamp ) . ToUnixTimeSeconds ( ) ;
411+
412+ // Check if there's any time range left to query
413+ if ( lastTime >= endTime )
414+ {
415+ return results ;
416+ }
417+
418+ // Calculate midpoint between last result and end time
411419 var midpoint = ( lastTime + endTime ) / 2 ;
420+
421+ // Ensure we have enough range to split
422+ if ( midpoint <= lastTime || midpoint >= endTime )
423+ {
424+ // Range too small to split, just query the remaining range
425+ var remainingResults = await PerformLargeQuery ( logGroupName , queryString , lastTime , endTime , limit ) ;
426+
427+ var allResults = new List < List < ResultField > > ( results ) ;
428+ // Skip the first result if it's a duplicate of the last result from previous query
429+ if ( remainingResults . Count > 0 )
430+ {
431+ var firstTimestamp = remainingResults [ 0 ] . Find ( f => f . Field == "@timestamp" ) ? . Value ;
432+ if ( firstTimestamp == lastTimestamp )
433+ {
434+ remainingResults . RemoveAt ( 0 ) ;
435+ }
436+ }
437+ allResults . AddRange ( remainingResults ) ;
438+ return allResults ;
439+ }
412440
441+ // Split the remaining range in half
413442 var results1 = await PerformLargeQuery ( logGroupName , queryString , lastTime , midpoint , limit ) ;
414443 var results2 = await PerformLargeQuery ( logGroupName , queryString , midpoint , endTime , limit ) ;
415444
416- var allResults = new List < List < ResultField > > ( results ) ;
417- allResults . AddRange ( results1 ) ;
418- allResults . AddRange ( results2 ) ;
445+ var combinedResults = new List < List < ResultField > > ( results ) ;
446+
447+ // Remove duplicate from results1 if it matches the last result
448+ if ( results1 . Count > 0 )
449+ {
450+ var firstTimestamp1 = results1 [ 0 ] . Find ( f => f . Field == "@timestamp" ) ? . Value ;
451+ if ( firstTimestamp1 == lastTimestamp )
452+ {
453+ results1 . RemoveAt ( 0 ) ;
454+ }
455+ }
456+
457+ combinedResults . AddRange ( results1 ) ;
458+
459+ // Remove duplicate from results2 if it matches the last result from results1
460+ if ( results2 . Count > 0 && results1 . Count > 0 )
461+ {
462+ var lastTimestamp1 = results1 [ results1 . Count - 1 ] . Find ( f => f . Field == "@timestamp" ) ? . Value ;
463+ var firstTimestamp2 = results2 [ 0 ] . Find ( f => f . Field == "@timestamp" ) ? . Value ;
464+ if ( firstTimestamp2 == lastTimestamp1 )
465+ {
466+ results2 . RemoveAt ( 0 ) ;
467+ }
468+ }
469+
470+ combinedResults . AddRange ( results2 ) ;
419471
420- return allResults ;
472+ return combinedResults ;
421473 }
422474
423475 /// <summary>
@@ -592,9 +644,9 @@ private static bool GetYesNoResponse(string question)
592644 /// </summary>
593645 private static string PromptUserForStackName ( )
594646 {
595- Console . WriteLine ( $ "Enter a name for the CloudFormation stack (default: { _stackName } ): ") ;
596647 if ( _interactive )
597648 {
649+ Console . Write ( $ "Enter a name for the CloudFormation stack (press Enter for default '{ _stackName } '): ") ;
598650 string ? input = Console . ReadLine ( ) ;
599651 if ( ! string . IsNullOrWhiteSpace ( input ) )
600652 {
@@ -617,7 +669,7 @@ private static string PromptUserForInput(string prompt, string defaultValue)
617669 {
618670 if ( _interactive )
619671 {
620- Console . Write ( prompt ) ;
672+ Console . Write ( $ " { prompt } (press Enter for default ' { defaultValue } '): " ) ;
621673 string ? input = Console . ReadLine ( ) ;
622674 return string . IsNullOrWhiteSpace ( input ) ? defaultValue : input ;
623675 }
@@ -631,7 +683,7 @@ private static int PromptUserForInteger(string prompt, int defaultValue)
631683 {
632684 if ( _interactive )
633685 {
634- Console . Write ( prompt ) ;
686+ Console . Write ( $ " { prompt } (press Enter for default ' { defaultValue } '): " ) ;
635687 string ? input = Console . ReadLine ( ) ;
636688 if ( string . IsNullOrWhiteSpace ( input ) || ! int . TryParse ( input , out var result ) )
637689 {
@@ -659,4 +711,4 @@ private static long PromptUserForLong(string prompt)
659711 return 0 ;
660712 }
661713}
662- // snippet-end:[CloudWatchLogs.dotnetv3 .LargeQueryWorkflow]
714+ // snippet-end:[CloudWatchLogs.dotnetv4 .LargeQueryWorkflow]
0 commit comments