Publish AWS Cloudwatch Log with Jenkins

Prashant Vats
5 min readMay 8, 2019

DevOps guys are friendly with AWS console but Developers are more into coding and solving the logical stuff. They often request for Log from production server where they don’t have direct access(Obviously). Why not automate this process using Jenkins where they can specify log stream, time frame, and search pattern. Jenkins can archive the requested or filtered log and archive it so that it is available for download and future reference.

Jenkins Pipeline

Jenkins pipeline is one of the best ways to automate the execution of a process pipeline in Jenkins. Pipeline syntax is a pool of predefined syntax, which is a wrapper over groovy language. Being in the form of code it supports reusability, increase in efficiency and lessen the error probability. We are using dynamic input parameter to list available log group and log stream and user input to get parameter value.

import groovy.time.TimeCategory
import java.text.SimpleDateFormat
def currentDate(){
date = new Date()
sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'")
return sdf.format(date)
}
def before1day(){
currentDate = new Date()
use( TimeCategory ) {
int timeframe = java.lang.Integer.parseInt("1");
before1day = currentDate - timeframe
}
sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'")
return sdf.format(before1day)
}
def getLogGroups() {
LOG_GROUP = sh (
script: "aws logs describe-log-groups --region us-east-1 --output text|awk '{print \$4}'",
returnStdout: true
).trim().toString()
//values = UUID.randomUUID().toString().split('-').join('\n')
return LOG_GROUP
}
def getLogStream(log) {
LOG_STREAM = sh (
script: "aws logs describe-log-streams --log-group-name ${log} --region us-east-1 --output text |awk '{print \$7}'",
returnStdout: true
).trim().toString()
//values = UUID.randomUUID().toString().split('-').join('\n')
return LOG_STREAM
}
pipeline {
agent any
environment {
MY_LOG_STREAM = "None"
}

stages {
stage("Choose LogGroup") {
steps {
timeout(time: 30, unit: 'SECONDS') {
script {
def INPUT_PARAMS = input message: 'Please Provide Parameters', ok: 'Next',
parameters: [
choice(name: 'LOG_GROUP', choices: getLogGroups(), description: 'Available Log Group'),
booleanParam(defaultValue: false, description: 'Want to choose log stream', name: 'CHOOSE_STREAM'),
string(defaultValue: before1day(), description: 'Starting Time', name: 'START_TIME'),
string(defaultValue: currentDate(), description: 'Ending Time', name: 'END_TIME'),
string(defaultValue: "error", description: 'Search Pattern', name: 'PATTERN')
]
env.LOG_GROUP = INPUT_PARAMS.LOG_GROUP
env.START_TIME = INPUT_PARAMS.START_TIME
env.END_TIME = INPUT_PARAMS.END_TIME
env.LOG_STREAM_FLAG = INPUT_PARAMS.CHOOSE_STREAM
env.PATTERN = INPUT_PARAMS.PATTERN
}
}
script {
echo "FLAG: ${env.LOG_STREAM_FLAG}"
}
}
}
stage("Choose LogStream") {
when {
expression {
return env.LOG_STREAM_FLAG == 'true';
}
}
steps {
timeout(time: 30, unit: 'SECONDS') {
script {
def userInput = input(
id: 'userInput', message: 'Enter LogStream?',
parameters: [
choice(choices: getLogStream("${env.LOG_GROUP}"),
description: 'LogStream',
name: 'MY_LOG_STREAM')
])
MY_LOG_STREAM="${userInput}"
echo "Log Stream: ${MY_LOG_STREAM}"

}

}
}
}
stage("List Logs") {
steps {
sh "python3.7 /var/jenkins_home/log.py ${env.LOG_GROUP} --start=${env.START_TIME} --end=${env.END_TIME} --log-stream-names=${MY_LOG_STREAM} --filter-pattern=${env.PATTERN} > generatedLog.txt"
echo "\n\n\n+++++++++++++++++++++++++++++++++++++++++++++++"
echo "+++++++++++++++++++++++++++++++++++++++++++++++++"
echo "+++++++++++++++++++++++++++++++++++++++++++++++++"
echo "Sample Log"
sh "tail -n 30 generatedLog.txt"
echo "\n\n\n+++++++++++++++++++++++++++++++++++++++++++++++"
echo "+++++++++++++++++++++++++++++++++++++++++++++++++"
echo "+++++++++++++++++++++++++++++++++++++++++++++++++"
// sh "zip generatedLog.zip generatedLog.txt"
archiveArtifacts artifacts: 'generatedLog.txt', onlyIfSuccessful: true
}
}
}
}

There are 3 stages in the above pipeline script

  • Choose Log Group
  • Choose Log Stream
  • List Logs

Choose log group will ask for the required parameter and we can also precise our search to a specific log stream.

Log Group

The dynamic select option is implemented by user input method of Jenkins pipeline. The output of getLogGroups() function is used as choices for user input.

Log Stream

List Log stage of the pipeline is used to archive the generated log as artifact and can be seen in the artifact section and can be download for review.

Artifact

You may have noticed it is using a log.py which is taking all the Jenkins parameter as options. log.py is a python script compatible with python3.7 and using boto3 to communicate with AWS CloudWatch Log. You can find the embedded python code for log.py

Python Module

We are using boto3, docopt and maya python modules, so you need to install these modules, prior to running below python script.

pip install maya --user
pip install boto3 --user
pip install docopt --user
#!/usr/bin/env python
# -*- encoding: utf-8
"""Print log event messages from a CloudWatch log group.
Usage: log.py <LOG_GROUP_NAME> [--start=<START>] [--end=<END>] [--log-stream-names=<NAME>] [--filter-pattern=<PATTERN>]
log.py -h --help
Options:
<LOG_GROUP_NAME> Name of the CloudWatch log group.
--start=<START> Only print events with a timestamp after this time.
--end=<END> Only print events with a timestamp before this time.
--log-stream-names=<NAME>
--filter-pattern=<PATTERN>
-h --help Show this screen.
"""import boto3
import docopt
import maya
def get_log_events(log_group, start_time=None, end_time=None,filter_pattern=None,log_stream_name=None):
"""Generate all the log events from a CloudWatch group.
"""
client = boto3.client('logs')
kwargs = {
'logGroupName': log_group,
'limit': 10000
}
log_stream_names = [log_stream_name]if start_time is not None:
kwargs['startTime'] = start_time
if end_time is not None:
kwargs['endTime'] = end_time
if filter_pattern is not None:
kwargs['filterPattern'] = filter_pattern
if log_stream_name is not None:
kwargs['logStreamNames'] = [log_stream_name]
while True:
resp = client.filter_log_events(**kwargs)
yield from resp['events']
try:
kwargs['nextToken'] = resp['nextToken']
except KeyError:
break
def milliseconds_since_epoch(time_string):
dt = maya.when(time_string)
seconds = dt.epoch
return seconds * 1000
if __name__ == '__main__':
args = docopt.docopt(__doc__)
log_group = args['<LOG_GROUP_NAME>']if args['--start']:
try:
start_time = milliseconds_since_epoch(args['--start'])
except ValueError:
exit(f'Invalid datetime input as --start: {args["--start"]}')
else:
start_time = None
if args['--end']:
try:
end_time = milliseconds_since_epoch(args['--end'])
except ValueError:
exit(f'Invalid datetime input as --end: {args["--end"]}')
else:
end_time = None
if args['--log-stream-names']:
try:
log_stream_name = args['--log-stream-names']
except ValueError:
exit(f'Invalid input: {args["--log-stream-names"]}')
else:
log_stream_name = None
if args['--filter-pattern']:
try:
filter_pattern = args['--filter-pattern']
except ValueError:
exit(f'Invalid input: {args["--filter-pattern"]}')
else:
filter_pattern = None
if log_stream_name == "None":
log_stream_name = None
logs = get_log_events(
log_group=log_group,
start_time=start_time,
end_time=end_time,
log_stream_name=log_stream_name,
filter_pattern=filter_pattern
)
for event in logs:
print(event['message'].rstrip())

Questions and Comments are welcome. Please Clap and Share, if this helped you.

Thanks!

Prashant Vats

A DevOps Guy

--

--