#!/usr/bin/env nu # Inspect an ECS service: show task status, or fetch recent CloudWatch logs. # Requires: AWS CLI v2, configured credentials, and permissions for ECS/CloudWatch Logs. # # Examples: # ecs tasks my-cluster my-service # ecs logs my-cluster my-service --tail 100 # ecs logs my-cluster my-service --region us-east-1 const default_tail = 50 def main [ mode: string # `tasks` or `logs` cluster: string # ECS cluster name or ARN service: string # ECS service name or ARN --tail: int = $default_tail # Number of recent log events to show --region: string # AWS region; otherwise AWS CLI configuration is used ] { if $mode not-in [tasks logs] { error make {msg: $"mode must be `tasks` or `logs`; got `($mode)`"} } let aws_args = if ($region | is-empty) { [] } else { [--region $region] } let task_arns = ( ^aws ...$aws_args ecs list-tasks --cluster $cluster --service-name $service --desired-status RUNNING --query 'taskArns' --output json | from json ) if ($task_arns | is-empty) { print $"No RUNNING tasks found for service `($service)` in cluster `($cluster)`." return } if $mode == tasks { ( ^aws ...$aws_args ecs describe-tasks --cluster $cluster --tasks ...$task_arns --query 'tasks[].{taskArn:taskArn,lastStatus:lastStatus,desiredStatus:desiredStatus,healthStatus:healthStatus,startedAt:startedAt,stoppedReason:stoppedReason,containers:containers[].{name:name,lastStatus:lastStatus,exitCode:exitCode,reason:reason}}' --output json | from json | table -e | print ) return } let definition_arn = ( ^aws ...$aws_args ecs describe-tasks --cluster $cluster --tasks ($task_arns | first) --query 'tasks[0].taskDefinitionArn' --output text | str trim ) let container_definitions = ( ^aws ...$aws_args ecs describe-task-definition --task-definition $definition_arn --query 'taskDefinition.containerDefinitions[].{name:name,logDriver:logConfiguration.logDriver,options:logConfiguration.options}' --output json | from json ) let awslogs = ($container_definitions | where logDriver == 'awslogs') if ($awslogs | is-empty) { error make {msg: $"Task definition `($definition_arn)` has no containers using the awslogs log driver."} } for container in $awslogs { let group = ($container.options | get 'awslogs-group') let prefix = ($container.options | get 'awslogs-stream-prefix') let stream_prefix = $"($prefix)/($container.name)/" # get-log-events returns a stream's most recent events by default, so walk # streams newest-first and stop once no remaining stream can hold an event # newer than the ones collected. (describe-log-streams can't combine a # stream prefix with LastEventTime ordering, so filter by prefix here.) print $"\n==> CloudWatch log group: ($group), stream prefix: ($stream_prefix)" let limit = ([$tail 10000] | math min) mut events = [] mut next_token = '' mut done = false loop { let token_args = if ($next_token | is-empty) { [] } else { [--next-token $next_token] } let page = ( ^aws ...$aws_args logs describe-log-streams --log-group-name $group --order-by LastEventTime --descending --limit 50 --output json ...$token_args | from json ) # project the fields we need and keep the page's nextToken let streams = ( $page.logStreams? | default [] | each {|s| {name: $s.logStreamName, last: ($s.lastEventTimestamp? | default null)}} | where name starts-with $stream_prefix ) let next = ($page.nextToken? | default '') for stream in $streams { let last = ($stream.last? | default null) let have_enough = ($events | length) >= $tail let oldest = (if ($events | is-empty) { null } else { $events | get timestamp | math min }) # skip empty streams; stop when this stream's newest event is older # than what we already have (later streams are even older) if $last == null or ($have_enough and $last < $oldest) { if $last != null { $done = true break } continue } let recent = ( ^aws ...$aws_args logs get-log-events --log-group-name $group --log-stream-name $stream.name --limit $limit --query 'events[].{timestamp:timestamp,message:message}' --output json | from json | each {|e| {timestamp: $e.timestamp, message: $e.message, stream: $stream.name}} ) $events = ($events | append $recent | sort-by timestamp | last $tail) } if $done or ($next | is-empty) { break } $next_token = $next } for event in $events { let time = (($event.timestamp | into int) * 1000000 | into datetime | format date '%Y-%m-%dT%H:%M:%S%.3f%:z') let task_id = ($event.stream | split row '/' | last) print $"($time) ($task_id) ($event.message)" } } }