Many current tools and web services use JSON format for both input and output data. The jq tool proves very useful for extracting and processing JSON data for single-use situations or small scripts, especially shell scripts. Recently for a client I needed to create a small script to process API responses from Apache Marathon container orchestration system to identify any deployments exceeding a certain time limit. I took the opportunity to use the development of the script to present a brief tutorial on the use and capabilities of jq.
jq operates on the pipeline principle similar to many other UNIX utilities. However instead of single lines of text, the unit for jq is a JSON object. PIpeline operations can be chained together to extract data, construct summaries, or build new JSON objects and structures.
The Marathon API call "/v2/deployments" returns a JSON array of hashes, each representing a deployment in progress. Below is a sample Marathon JSON output. You may wish to adjust the dates in the "version" field so one or more of the deployments is not past the 30 minute "overdue" threshold.
[ { "id": "bc12d628-f668-41af-8a3c-050aba1c8cb8", "version": "2018-01-27T00:22:10.905Z", "affectedApps": [ "/dev/serviceapp1" ], "affectedPods": [], "steps": [ { "actions": [ { "action": "ScaleApplication", "app": "/dev/serviceapp1" } ] } ], "currentActions": [ { "action": "ScaleApplication", "app": "/dev/serviceapp1", "readinessCheckResults": [] } ], "currentStep": 1, "totalSteps": 1 }, { "id": "48bca295-f668-41af-933acf938231c8cb8", "version": "2018-01-27T05:00:10.905Z", "affectedApps": [ "/qa/testapp" ], "affectedPods": [], "steps": [ { "actions": [ { "action": "ScaleApplication", "app": "/qa/testapp" } ] } ], "currentActions": [ { "action": "ScaleApplication", "app": "/qa/testapp", "readinessCheckResults": [] } ], "currentStep": 1, "totalSteps": 1 }, { "id": "bc12d628-f668-41af-8a3c-4aab392cc34d", "version": "2018-01-27T00:55:10.905Z", "affectedApps": [ "/prod/region1/app1a", "/prod/region1/app1b" ], "affectedPods": [], "steps": [ { "actions": [ { "action": "ScaleApplication", "app": "/prod/region1/app1b" } ] } ], "currentActions": [ { "action": "ScaleApplication", "app": "/prod/region1/app1b", "readinessCheckResults": [] } ], "currentStep": 1, "totalSteps": 1 } ]
Below is the Bash shell script, including intermediate stages to show the development and use of some jq features:
#!/bin/bash # Evolution of a jq script to parse Marathon API output to identify # "stuck" deployments i.e. exceeding 30 minutes USER_PW="username:password" URL="http://marathon-hostname:8080/v2/deployments" input=deployments.json curl -s -u ${USER_PW} ${URL} > $input # pretty print JSON data jq '.' $input echo "=============" # extract first array element jq '.[0]' $input echo "=============" # extract single field from first array element jq '.[0].version' $input echo "=============" # extract single field from each array element jq '.[].version' $input echo "=============" # To extract multiple fields from an array element, construct a new JSON hash object { } jq '.[0]|{timestamp: .version, appList: .affectedApps}' $input echo "=============" # if you want only to extract the fields, retaining the names, you can use this shorthand jq '.[0]|{version, affectedApps}' $input echo "=============" # create the same object for each element in the array # Note the output is not a JSON array, just a list of objects jq '.[]|{version, affectedApps}|.affectedApps |= join(",")|.version+","+.affectedApps' $input echo "=============" # to produce a JSON array of hashes, wrap the entire expression in [ ] jq '[.[]|{version, affectedApps}]' $input echo "=============" # The |= operator updates the value of a JSON element # Here we convert the version value to UNIX epoc format in 3 steps jq '.[]|{version, affectedApps}|.version |= (.[:-5]|strptime("%Y-%m-%dT%H:%M:%S")|mktime)' $input echo "=============" # Mark deployments that have exceeded 30 minutes by adding # an "overdue" element to each object with the result jq '.[]|{version, affectedApps}|.version |= (.[:-5]|strptime("%Y-%m-%dT%H:%M:%S")|mktime)|if .version < (now - 1800) then .overdue = true else .overdue = false end' $input echo "=============" # Alternatively use the "empty" operator to skip those objects # that are not overdue jq '.[]|{version, affectedApps}|.version |= (.[:-5]|strptime("%Y-%m-%dT%H:%M:%S")|mktime)|if .version < (now - 1800) then . else empty end' $input echo "=============" # We want to use the human readable time format in the final # output, so move the UNIX epoch conversion into the if # statement expression jq '.[]|{version, affectedApps}|if (.version[:-5]|strptime("%Y-%m-%dT%H:%M:%S")|mktime) < (now - 1800) then . else empty end' $input echo "=============" # Join apps list into one comma delimited string, add some # explanatory text, add -r option to omit quotes around JSON values jq -r '.[]|{version, affectedApps}|if (.version[:-5]|strptime("%Y-%m-%dT%H:%M:%S")|mktime) < (now - 1800) then "Deploying since " + .version + ", apps affected: " + (.affectedApps|join(", ")) else empty end' $input echo "=============" r=$(jq -r '.[]|{version, affectedApps}|if (.version[:-5]|strptime("%Y-%m-%dT%H:%M:%S")|mktime) < (now - 1800) then "Deploying since " + .version + ", Apps affected: " + (.affectedApps|join(", ")) else empty end' $input) if [[ -n "$r" ]] then echo "List of overdue deployments:" echo "$r" fi
A small script such as this can't show the entire array of jq features, but should be enough to illustrate some basic concepts and provide a basis for further development. I find this iterative method useful in building up and testing complex operations, it lends itself well to experimentation and debugging.
For more information, see the jq manual