Publish AWS Cloudwatch Log with Jenkins
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.SimpleDateFormatdef 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.
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.
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.
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 --helpOptions:
<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 mayadef 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:
breakdef milliseconds_since_epoch(time_string):
dt = maya.when(time_string)
seconds = dt.epoch
return seconds * 1000if __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 = Noneif args['--end']:
try:
end_time = milliseconds_since_epoch(args['--end'])
except ValueError:
exit(f'Invalid datetime input as --end: {args["--end"]}')
else:
end_time = Noneif 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 = Noneif args['--filter-pattern']:
try:
filter_pattern = args['--filter-pattern']
except ValueError:
exit(f'Invalid input: {args["--filter-pattern"]}')
else:
filter_pattern = Noneif log_stream_name == "None":
log_stream_name = Nonelogs = 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