Files
linux-scripts/jira-metrics.sh
T
chiefgeek a1a17e81a1 Sync all scripts from website downloads — 352 scripts total
Includes updated JS challenge scripts with Claude-User whitelist,
same-site referer bypass, Blackbox-Exporter allowed bot, and all
new exporters, cheat sheets, and automation scripts.
2026-05-25 03:31:08 +02:00

1149 lines
44 KiB
Bash
Executable File

#!/bin/bash
################################################################################
# Script Name: jira-metrics.sh
# Version: 1.4
# Author: Phil Connor <contact@mylinux.work>
# License: MIT
# Description: Prometheus exporter for Jira metrics via REST API
#
# Exports metrics for:
# - Issue counts by status (To Do, In Progress, Done, etc.)
# - Issue counts by priority (Highest, High, Medium, Low, Lowest)
# - Issue counts by type (Bug, Story, Task, Epic, Sub-task)
# - Issue counts by project
# - Backlog size and age (unresolved issues)
# - Created vs resolved issue rates (24h, 7d)
# - Average resolution time (resolved last 7d)
# - Overdue issues count
# - Sprint active issue counts (if boards configured)
# - Component issue counts
# - Assignee workload (top 15)
# - Unassigned issue count
# - SLA/resolution time buckets
# - Login activity from security log (logins, failed logins, per-user)
# - Plugin/add-on inventory (installed, enabled, disabled, user-installed)
# - Service Desk queues, request types, SLA metrics, CSAT, organizations
# - Scrape metadata
#
# Modes:
# --textfile Write to node_exporter textfile collector
# --http Run HTTP server for direct Prometheus scraping
# stdout Default: print metrics to stdout
#
# Requirements:
# - curl and jq must be available
# - Jira base URL, username, and API token (or password)
# - For Jira Cloud: use email + API token
# - For Jira Server/DC: use username + password or PAT
#
# Configuration:
# Set environment variables or edit the defaults below:
# JIRA_BASE_URL - Full URL including port
# e.g. https://yourcompany.atlassian.net (Cloud)
# e.g. http://localhost:8080 (Server/DC)
# JIRA_USER - e.g. user or user@company.com (not needed for PAT)
# JIRA_API_TOKEN - API token, password, or Personal Access Token (PAT)
# JIRA_PROJECTS - Comma-separated project keys (default: all)
# JIRA_HOME - Jira home/data directory (for security log parsing)
# e.g. /mnt/ebs/application-data/jira
#
# Authentication:
# Uses Bearer token authentication (Authorization: Bearer <token>).
# - Jira Cloud: Generate API token at
# https://id.atlassian.com/manage-profile/security/api-tokens
# - Jira Server/DC: Generate a PAT via Profile → Personal Access Tokens
# (requires Jira 8.14+)
#
# Troubleshooting:
# All metrics return zero:
# 1. Verify JIRA_BASE_URL includes the port (e.g. http://localhost:8080,
# NOT http://localhost). Test with:
# curl -H "Authorization: Bearer $JIRA_API_TOKEN" \
# "$JIRA_BASE_URL/rest/api/2/search?jql=&maxResults=0"
# You should see a JSON response with "total" > 0.
# 2. If the above returns blank, the URL or port is wrong.
# 3. If it returns HTML or a 404, check for a context path
# (e.g. http://localhost:8080/jira).
#
# 403 Forbidden / AUTHENTICATION_DENIED:
# - Basic auth may be blocked. This script uses Bearer token auth
# which requires a PAT (Server/DC) or API token (Cloud).
# - CAPTCHA may be triggered from prior failed logins. Clear it via
# Jira Admin → User Management → find user → Reset failed login count.
# - Ensure the X-Atlassian-Token: no-check header is present (included
# by default in this script).
#
# 401 Unauthorized:
# - PAT may be expired or revoked. Generate a new one.
# - Verify the token is correct (no trailing whitespace/newline).
#
# Partial metrics (some sections zero):
# - The user associated with the PAT must have Browse Projects
# permission on the target projects.
# - Login metrics require JIRA_HOME to be set correctly and the
# script must have read access to the Jira security log at
# $JIRA_HOME/log/atlassian-jira-security.log
# - Service Desk metrics require Jira Service Management.
#
# Login metrics all zero:
# - Verify JIRA_HOME points to the correct Jira data directory.
# - Check the security log exists:
# ls $JIRA_HOME/log/atlassian-jira-security.log
# - Ensure the user running this script can read the log file.
# - Note: The audit log REST API (/rest/api/2/auditing/record)
# does not exist on Jira 10.x. This script parses the security
# log file directly instead.
#
# Changelog:
# 1.4 - Auto-discover all projects when JIRA_PROJECTS is not set
# Fixed per-project metrics (HELP/TYPE headers always emitted)
# Fixed missing metrics: all HELP/TYPE headers now always present
# even when data is empty (assignee, component, plugin, service
# desk queue/org, user logins, resolution time)
# Per-project metrics now grouped by metric name per Prometheus
# exposition format (HELP/TYPE then all values, not interleaved)
# Switched login metrics from audit log REST API (404 on Jira 10.x)
# to parsing $JIRA_HOME/log/atlassian-jira-security.log directly
# Added JIRA_HOME configuration variable and --jira-home CLI option
# Added troubleshooting section for login metrics
# 1.3 - Switched authentication from basic auth to Bearer token (PAT)
# Fixed JQL URL encoding (use curl --data-urlencode)
# Added troubleshooting documentation
# 1.2 - Added plugin/add-on inventory metrics from UPM API
# Added Service Desk metrics (queues, requests, SLAs, CSAT,
# request types, organizations)
# 1.1 - Added login activity metrics from Jira audit log
# (total logins, failed logins, unique users, per-user counts)
# 1.0 - Initial release
################################################################################
SCRIPT_VERSION="1.4"
TEXTFILE_DIR="/var/lib/node_exporter"
OUTPUT_FILE=""
HTTP_MODE=false
HTTP_PORT=9418
LOCK_FILE="/var/run/jira-metrics.lock"
# Jira connection settings (edit these or override via environment variables)
JIRA_BASE_URL="${JIRA_BASE_URL:-https://yourcompany.atlassian.net}"
JIRA_USER="${JIRA_USER:-user@company.com}"
JIRA_API_TOKEN="${JIRA_API_TOKEN:-your-api-token-here}"
JIRA_PROJECTS="${JIRA_PROJECTS:-}" # comma-separated, empty = all (e.g. "PROJ1,PROJ2")
JIRA_HOME="${JIRA_HOME:-/mnt/ebs/application-data/jira}" # Jira home directory (for log parsing)
# Timeouts
CURL_TIMEOUT=30
MAX_RESULTS=1000
show_usage() {
cat <<EOF
Usage: $0 [OPTIONS]
Export Jira metrics as Prometheus metrics (v${SCRIPT_VERSION}).
MODES:
--textfile Write to node_exporter textfile collector
--http Run HTTP server on port $HTTP_PORT
OPTIONS:
-p, --port HTTP port (default: $HTTP_PORT)
-o, --output Output file
-u, --url Jira base URL
--user Jira username/email
--token Jira API token
--projects Comma-separated project keys
-h, --help Show help
ENVIRONMENT VARIABLES:
JIRA_BASE_URL Jira instance URL
JIRA_USER Jira username or email
JIRA_API_TOKEN API token or password
JIRA_PROJECTS Comma-separated project keys
REQUIREMENTS:
- curl and jq must be installed
- Valid Jira credentials
EOF
exit 0
}
parse_args() {
while [[ $# -gt 0 ]]; do
case $1 in
-h|--help) show_usage ;;
--textfile) OUTPUT_FILE="$TEXTFILE_DIR/jira_metrics.prom"; shift ;;
--http) HTTP_MODE=true; shift ;;
-p|--port) HTTP_PORT="$2"; shift 2 ;;
-o|--output) OUTPUT_FILE="$2"; shift 2 ;;
-u|--url) JIRA_BASE_URL="$2"; shift 2 ;;
--user) JIRA_USER="$2"; shift 2 ;;
--token) JIRA_API_TOKEN="$2"; shift 2 ;;
--projects) JIRA_PROJECTS="$2"; shift 2 ;;
--jira-home) JIRA_HOME="$2"; shift 2 ;;
*) echo "Unknown: $1"; exit 1 ;;
esac
done
}
preflight_check() {
local fail=0
if ! command -v curl >/dev/null 2>&1; then
echo "ERROR: curl is required" >&2; fail=1
fi
if ! command -v jq >/dev/null 2>&1; then
echo "ERROR: jq is required" >&2; fail=1
fi
if [ -z "$JIRA_BASE_URL" ]; then
echo "ERROR: JIRA_BASE_URL not set" >&2; fail=1
fi
if [ -z "$JIRA_USER" ] || [ -z "$JIRA_API_TOKEN" ]; then
echo "ERROR: JIRA_USER and JIRA_API_TOKEN must be set" >&2; fail=1
fi
[ "$fail" -eq 1 ] && exit 1
}
acquire_lock() {
if [ -f "$LOCK_FILE" ]; then
local pid
pid=$(cat "$LOCK_FILE" 2>/dev/null)
if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
echo "ERROR: Another instance is already running (PID: $pid)" >&2
exit 1
else
echo "Removing stale lock file" >&2
rm -f "$LOCK_FILE"
fi
fi
echo $$ > "$LOCK_FILE"
trap cleanup EXIT INT TERM
}
cleanup() {
rm -f "$LOCK_FILE"
}
# ---------------------------------------------------------------------------
# Jira API helper
# ---------------------------------------------------------------------------
jira_api() {
local endpoint="$1"
local url="${JIRA_BASE_URL}/rest/api/2${endpoint}"
curl -s --max-time "$CURL_TIMEOUT" \
-H "Authorization: Bearer ${JIRA_API_TOKEN}" \
-H "Content-Type: application/json" \
-H "X-Atlassian-Token: no-check" \
"$url" 2>/dev/null
}
jira_search() {
local jql="$1"
local max_results="${2:-0}"
local fields="${3:-}"
local url="${JIRA_BASE_URL}/rest/api/2/search"
local data_args=("--data-urlencode" "jql=${jql}" "--data-urlencode" "maxResults=${max_results}")
if [ -n "$fields" ]; then
data_args+=("--data-urlencode" "fields=${fields}")
fi
curl -s -G --max-time "$CURL_TIMEOUT" \
-H "Authorization: Bearer ${JIRA_API_TOKEN}" \
-H "Content-Type: application/json" \
-H "X-Atlassian-Token: no-check" \
"${data_args[@]}" \
"$url" 2>/dev/null
}
jira_search_count() {
local jql="$1"
local result
result=$(jira_search "$jql" 0)
echo "$result" | jq -r '.total // 0' 2>/dev/null
}
# Build project filter clause
get_project_filter() {
if [ -z "$JIRA_PROJECTS" ]; then
echo ""
return
fi
local clause="project in ("
local first=true
IFS=',' read -ra PROJ_ARRAY <<< "$JIRA_PROJECTS"
for proj in "${PROJ_ARRAY[@]}"; do
proj=$(echo "$proj" | tr -d ' ')
if [ "$first" = true ]; then
clause="${clause}\"${proj}\""
first=false
else
clause="${clause},\"${proj}\""
fi
done
clause="${clause})"
echo "$clause"
}
# ---------------------------------------------------------------------------
# Data collection (cached in temp dir)
# ---------------------------------------------------------------------------
CACHE_DIR=""
cache_all_jira_data() {
CACHE_DIR=$(mktemp -d /tmp/jira_metrics_cache.XXXXXX)
local pf
pf=$(get_project_filter)
local pf_and=""
[ -n "$pf" ] && pf_and="${pf} AND "
# Issue counts by status
for status in "To Do" "In Progress" "In Review" "Done" "Closed" "Open" "Reopened" "Resolved"; do
local safe_status
safe_status=$(echo "$status" | tr ' ' '_' | tr '[:upper:]' '[:lower:]')
jira_search_count "${pf_and}status = \"${status}\"" > "$CACHE_DIR/status_${safe_status}" &
done
# Issue counts by priority
for priority in "Highest" "High" "Medium" "Low" "Lowest"; do
local safe_priority
safe_priority=$(echo "$priority" | tr '[:upper:]' '[:lower:]')
jira_search_count "${pf_and}priority = \"${priority}\"" > "$CACHE_DIR/priority_${safe_priority}" &
done
# Issue counts by type
for itype in "Bug" "Story" "Task" "Epic" "Sub-task" "Incident"; do
local safe_type
safe_type=$(echo "$itype" | tr ' ' '_' | tr '-' '_' | tr '[:upper:]' '[:lower:]')
jira_search_count "${pf_and}issuetype = \"${itype}\"" > "$CACHE_DIR/type_${safe_type}" &
done
# Total unresolved (backlog)
jira_search_count "${pf_and}resolution = Unresolved" > "$CACHE_DIR/backlog_total" &
# Overdue issues
jira_search_count "${pf_and}due < now() AND resolution = Unresolved" > "$CACHE_DIR/overdue" &
# Unassigned issues
jira_search_count "${pf_and}assignee is EMPTY AND resolution = Unresolved" > "$CACHE_DIR/unassigned" &
# Created in last 24h
jira_search_count "${pf_and}created >= -1d" > "$CACHE_DIR/created_24h" &
# Resolved in last 24h
jira_search_count "${pf_and}resolved >= -1d" > "$CACHE_DIR/resolved_24h" &
# Created in last 7d
jira_search_count "${pf_and}created >= -7d" > "$CACHE_DIR/created_7d" &
# Resolved in last 7d
jira_search_count "${pf_and}resolved >= -7d" > "$CACHE_DIR/resolved_7d" &
# Created in last 30d
jira_search_count "${pf_and}created >= -30d" > "$CACHE_DIR/created_30d" &
# Resolved in last 30d
jira_search_count "${pf_and}resolved >= -30d" > "$CACHE_DIR/resolved_30d" &
# Aged backlog buckets (unresolved, created more than X days ago)
jira_search_count "${pf_and}resolution = Unresolved AND created <= -7d" > "$CACHE_DIR/backlog_older_7d" &
jira_search_count "${pf_and}resolution = Unresolved AND created <= -30d" > "$CACHE_DIR/backlog_older_30d" &
jira_search_count "${pf_and}resolution = Unresolved AND created <= -90d" > "$CACHE_DIR/backlog_older_90d" &
jira_search_count "${pf_and}resolution = Unresolved AND created <= -365d" > "$CACHE_DIR/backlog_older_365d" &
# High-priority unresolved
jira_search_count "${pf_and}priority in (Highest, High) AND resolution = Unresolved" > "$CACHE_DIR/high_priority_unresolved" &
# Critical bugs unresolved
jira_search_count "${pf_and}issuetype = Bug AND priority in (Highest, High) AND resolution = Unresolved" > "$CACHE_DIR/critical_bugs" &
wait
}
cache_project_data() {
local proj_list="$JIRA_PROJECTS"
if [ -z "$proj_list" ]; then
proj_list=$(jira_api "/project" | jq -r '.[].key' 2>/dev/null | paste -sd ',' -)
if [ -z "$proj_list" ]; then
return
fi
JIRA_PROJECTS="$proj_list"
fi
IFS=',' read -ra PROJ_ARRAY <<< "$proj_list"
for proj in "${PROJ_ARRAY[@]}"; do
proj=$(echo "$proj" | tr -d ' ')
local safe_proj
safe_proj=$(echo "$proj" | tr '[:upper:]' '[:lower:]')
jira_search_count "project = \"${proj}\" AND resolution = Unresolved" > "$CACHE_DIR/proj_open_${safe_proj}" &
jira_search_count "project = \"${proj}\"" > "$CACHE_DIR/proj_total_${safe_proj}" &
jira_search_count "project = \"${proj}\" AND created >= -7d" > "$CACHE_DIR/proj_created_7d_${safe_proj}" &
jira_search_count "project = \"${proj}\" AND resolved >= -7d" > "$CACHE_DIR/proj_resolved_7d_${safe_proj}" &
done
wait
}
cache_assignee_data() {
local pf
pf=$(get_project_filter)
local pf_and=""
[ -n "$pf" ] && pf_and="${pf} AND "
local result
result=$(jira_search "${pf_and}resolution = Unresolved AND assignee is not EMPTY ORDER BY assignee ASC" "$MAX_RESULTS" "assignee")
if [ -n "$result" ]; then
echo "$result" | jq -r '
[.issues[]? | .fields.assignee.displayName // .fields.assignee.name // "unknown"] |
group_by(.) |
map({name: .[0], count: length}) |
sort_by(-.count) |
.[:15][] |
"\(.name)|\(.count)"
' 2>/dev/null > "$CACHE_DIR/assignees"
fi
}
cache_component_data() {
local pf
pf=$(get_project_filter)
local pf_and=""
[ -n "$pf" ] && pf_and="${pf} AND "
local result
result=$(jira_search "${pf_and}resolution = Unresolved AND component is not EMPTY" "$MAX_RESULTS" "components")
if [ -n "$result" ] && echo "$result" | jq -e '.issues' >/dev/null 2>&1; then
echo "$result" | jq -r '
[.issues[]? | .fields.components[]?.name // "unknown"] |
group_by(.) |
map({name: .[0], count: length}) |
sort_by(-.count) |
.[:20][] |
"\(.name)|\(.count)"
' 2>/dev/null > "$CACHE_DIR/components"
fi
}
cache_resolution_time_data() {
local pf
pf=$(get_project_filter)
local pf_and=""
[ -n "$pf" ] && pf_and="${pf} AND "
local result
result=$(jira_search "${pf_and}resolved >= -7d" 100 "created,resolutiondate")
if [ -n "$result" ]; then
echo "$result" | jq -r '
[.issues[]? |
select(.fields.resolutiondate != null and .fields.created != null) |
((.fields.resolutiondate | sub("\\.[0-9]+.*";"") | strptime("%Y-%m-%dT%H:%M:%S") | mktime) -
(.fields.created | sub("\\.[0-9]+.*";"") | strptime("%Y-%m-%dT%H:%M:%S") | mktime)) / 3600
] |
if length > 0 then
{ avg: (add / length), min: min, max: max, count: length }
else
{ avg: 0, min: 0, max: 0, count: 0 }
end |
"\(.avg)|\(.min)|\(.max)|\(.count)"
' 2>/dev/null > "$CACHE_DIR/resolution_times"
fi
}
cache_login_data() {
local security_log="${JIRA_HOME}/log/atlassian-jira-security.log"
if [ ! -f "$security_log" ]; then
echo "0" > "$CACHE_DIR/logins_total"
echo "0" > "$CACHE_DIR/logins_success"
echo "0" > "$CACHE_DIR/logins_failed"
echo "0" > "$CACHE_DIR/logins_unique_users"
return
fi
local since_date
since_date=$(date -d '24 hours ago' '+%Y-%m-%d %H:%M' 2>/dev/null || \
date -v-1d '+%Y-%m-%d %H:%M' 2>/dev/null)
# Extract lines from last 24h (compare date prefix YYYY-MM-DD HH:MM)
local recent_lines
recent_lines=$(awk -v since="$since_date" '$0 ~ /^[0-9]{4}-[0-9]{2}-[0-9]{2}/ { ts=substr($0,1,16); if (ts >= since) print }' "$security_log" 2>/dev/null)
if [ -z "$recent_lines" ]; then
echo "0" > "$CACHE_DIR/logins_total"
echo "0" > "$CACHE_DIR/logins_success"
echo "0" > "$CACHE_DIR/logins_failed"
echo "0" > "$CACHE_DIR/logins_unique_users"
return
fi
# Successful logins: "has PASSED authentication"
local success_count
success_count=$(echo "$recent_lines" | grep -c 'has PASSED authentication' 2>/dev/null)
echo "${success_count:-0}" > "$CACHE_DIR/logins_success"
# Failed logins: "tried to login but"
local failed_count
failed_count=$(echo "$recent_lines" | grep -c 'tried to login but' 2>/dev/null)
echo "${failed_count:-0}" > "$CACHE_DIR/logins_failed"
# Total login events
local total_count
total_count=$(( success_count + failed_count ))
echo "${total_count:-0}" > "$CACHE_DIR/logins_total"
# Unique users (extract username from 'user' has PASSED)
local unique_users
unique_users=$(echo "$recent_lines" | grep 'has PASSED authentication' | \
grep -oP "The user '\K[^']+" 2>/dev/null | \
sort -u | wc -l)
echo "${unique_users:-0}" > "$CACHE_DIR/logins_unique_users"
# Per-user login counts (top 15)
echo "$recent_lines" | grep 'has PASSED authentication' | \
grep -oP "The user '\K[^']+" 2>/dev/null | \
sort | uniq -c | sort -rn | head -15 | \
awk '{print $2"|"$1}' > "$CACHE_DIR/logins_per_user" 2>/dev/null
}
# ---------------------------------------------------------------------------
# Plugin / add-on metrics (UPM API - Jira Server/DC)
# ---------------------------------------------------------------------------
cache_plugin_data() {
local result
result=$(curl -s --max-time "$CURL_TIMEOUT" \
-H "Authorization: Bearer ${JIRA_API_TOKEN}" \
-H "Accept: application/vnd.atl.plugins.installed+json" \
"${JIRA_BASE_URL}/rest/plugins/1.0/" 2>/dev/null)
if [ -n "$result" ] && echo "$result" | jq -e '.plugins' >/dev/null 2>&1; then
echo "$result" | jq -r '.plugins | length' > "$CACHE_DIR/plugins_total" 2>/dev/null
echo "$result" | jq -r '[.plugins[]? | select(.enabled == true)] | length' > "$CACHE_DIR/plugins_enabled" 2>/dev/null
echo "$result" | jq -r '[.plugins[]? | select(.enabled == false)] | length' > "$CACHE_DIR/plugins_disabled" 2>/dev/null
echo "$result" | jq -r '[.plugins[]? | select(.userInstalled == true)] | length' > "$CACHE_DIR/plugins_user_installed" 2>/dev/null
echo "$result" | jq -r '[.plugins[]? | select(.userInstalled == false or .userInstalled == null)] | length' > "$CACHE_DIR/plugins_system" 2>/dev/null
echo "$result" | jq -r '
[.plugins[]? | select(.userInstalled == true)] |
.[] |
"\(.name // .key)|\(if .enabled then 1 else 0 end)|\(.version // "unknown")"
' 2>/dev/null > "$CACHE_DIR/plugins_detail"
else
echo "0" > "$CACHE_DIR/plugins_total"
echo "0" > "$CACHE_DIR/plugins_enabled"
echo "0" > "$CACHE_DIR/plugins_disabled"
echo "0" > "$CACHE_DIR/plugins_user_installed"
echo "0" > "$CACHE_DIR/plugins_system"
fi
}
# ---------------------------------------------------------------------------
# Service Desk metrics (Jira Service Desk / Service Management API)
# ---------------------------------------------------------------------------
servicedesk_api() {
local endpoint="$1"
curl -s --max-time "$CURL_TIMEOUT" \
-H "Authorization: Bearer ${JIRA_API_TOKEN}" \
-H "Content-Type: application/json" \
-H "X-ExperimentalApi: opt-in" \
"${JIRA_BASE_URL}/rest/servicedeskapi${endpoint}" 2>/dev/null
}
cache_servicedesk_data() {
local sd_list
sd_list=$(servicedesk_api "/servicedesk")
if [ -z "$sd_list" ] || ! echo "$sd_list" | jq -e '.values' >/dev/null 2>&1; then
echo "0" > "$CACHE_DIR/sd_available"
return
fi
echo "1" > "$CACHE_DIR/sd_available"
local sd_count
sd_count=$(echo "$sd_list" | jq -r '.size // (.values | length)' 2>/dev/null)
echo "${sd_count:-0}" > "$CACHE_DIR/sd_count"
echo "$sd_list" | jq -r '.values[]? | "\(.id)|\(.projectKey // "unknown")|\(.projectName // "unknown")"' \
2>/dev/null > "$CACHE_DIR/sd_list"
local tmp_queues="$CACHE_DIR/sd_queues_all"
local tmp_request_types="$CACHE_DIR/sd_request_types_all"
local tmp_orgs="$CACHE_DIR/sd_orgs_all"
: > "$tmp_queues"
: > "$tmp_request_types"
: > "$tmp_orgs"
while IFS='|' read -r sd_id sd_key _sd_name; do
[ -z "$sd_id" ] && continue
local queues
queues=$(servicedesk_api "/servicedesk/${sd_id}/queue")
if [ -n "$queues" ] && echo "$queues" | jq -e '.values' >/dev/null 2>&1; then
echo "$queues" | jq -r --arg proj "$sd_key" '
.values[]? | "\($proj)|\(.id)|\(.name // "unknown")|\(.issueCount // 0)"
' 2>/dev/null >> "$tmp_queues"
fi
local rtypes
rtypes=$(servicedesk_api "/servicedesk/${sd_id}/requesttype")
if [ -n "$rtypes" ] && echo "$rtypes" | jq -e '.values' >/dev/null 2>&1; then
echo "$rtypes" | jq -r --arg proj "$sd_key" '
.values[]? | "\($proj)|\(.id)|\(.name // "unknown")"
' 2>/dev/null >> "$tmp_request_types"
fi
local orgs
orgs=$(servicedesk_api "/servicedesk/${sd_id}/organization")
if [ -n "$orgs" ] && echo "$orgs" | jq -e '.values' >/dev/null 2>&1; then
local org_count
org_count=$(echo "$orgs" | jq -r '.size // (.values | length)' 2>/dev/null)
echo "${sd_key}|${org_count:-0}" >> "$tmp_orgs"
fi
done < "$CACHE_DIR/sd_list"
local pf
pf=$(get_project_filter)
local pf_and=""
[ -n "$pf" ] && pf_and="${pf} AND "
jira_search_count "${pf_and}\"Customer Request Type\" is not EMPTY AND resolution = Unresolved" \
> "$CACHE_DIR/sd_open_requests" 2>/dev/null &
jira_search_count "${pf_and}\"Customer Request Type\" is not EMPTY AND resolved >= -24h" \
> "$CACHE_DIR/sd_resolved_24h" 2>/dev/null &
jira_search_count "${pf_and}\"Customer Request Type\" is not EMPTY AND resolved >= -7d" \
> "$CACHE_DIR/sd_resolved_7d" 2>/dev/null &
jira_search_count "${pf_and}\"Customer Request Type\" is not EMPTY AND created >= -24h" \
> "$CACHE_DIR/sd_created_24h" 2>/dev/null &
jira_search_count "${pf_and}\"Customer Request Type\" is not EMPTY AND created >= -7d" \
> "$CACHE_DIR/sd_created_7d" 2>/dev/null &
jira_search_count "${pf_and}\"Customer Request Type\" is not EMPTY AND priority in (Highest, High) AND resolution = Unresolved" \
> "$CACHE_DIR/sd_high_priority_open" 2>/dev/null &
jira_search_count "${pf_and}\"Customer Request Type\" is not EMPTY AND due < now() AND resolution = Unresolved" \
> "$CACHE_DIR/sd_breached_sla" 2>/dev/null &
wait
local csat_result
csat_result=$(jira_search "${pf_and}\"Customer Request Type\" is not EMPTY AND resolved >= -30d AND \"Satisfaction rating\" is not EMPTY" \
100 "customfield_10421,satisfaction")
if [ -n "$csat_result" ] && echo "$csat_result" | jq -e '.issues' >/dev/null 2>&1; then
local csat_total
csat_total=$(echo "$csat_result" | jq -r '.total // 0' 2>/dev/null)
echo "${csat_total}" > "$CACHE_DIR/sd_csat_responses"
else
echo "0" > "$CACHE_DIR/sd_csat_responses"
fi
}
# ---------------------------------------------------------------------------
# Read cached values
# ---------------------------------------------------------------------------
read_cache() {
local key="$1" default="${2:-0}"
local val
val=$(tr -d '[:space:]' < "$CACHE_DIR/$key" 2>/dev/null)
if [ -z "$val" ] || [ "$val" = "null" ]; then
echo "$default"
else
echo "$val"
fi
}
# ---------------------------------------------------------------------------
# Metric generation
# ---------------------------------------------------------------------------
generate_metrics() {
local start_time
start_time=$(date +%s)
# Read all cached values
local status_todo status_in_progress status_in_review status_done status_closed status_open status_reopened status_resolved
status_todo=$(read_cache "status_to_do")
status_in_progress=$(read_cache "status_in_progress")
status_in_review=$(read_cache "status_in_review")
status_done=$(read_cache "status_done")
status_closed=$(read_cache "status_closed")
status_open=$(read_cache "status_open")
status_reopened=$(read_cache "status_reopened")
status_resolved=$(read_cache "status_resolved")
local priority_highest priority_high priority_medium priority_low priority_lowest
priority_highest=$(read_cache "priority_highest")
priority_high=$(read_cache "priority_high")
priority_medium=$(read_cache "priority_medium")
priority_low=$(read_cache "priority_low")
priority_lowest=$(read_cache "priority_lowest")
local type_bug type_story type_task type_epic type_sub_task type_incident
type_bug=$(read_cache "type_bug")
type_story=$(read_cache "type_story")
type_task=$(read_cache "type_task")
type_epic=$(read_cache "type_epic")
type_sub_task=$(read_cache "type_sub_task")
type_incident=$(read_cache "type_incident")
local backlog_total overdue unassigned
backlog_total=$(read_cache "backlog_total")
overdue=$(read_cache "overdue")
unassigned=$(read_cache "unassigned")
local created_24h resolved_24h created_7d resolved_7d created_30d resolved_30d
created_24h=$(read_cache "created_24h")
resolved_24h=$(read_cache "resolved_24h")
created_7d=$(read_cache "created_7d")
resolved_7d=$(read_cache "resolved_7d")
created_30d=$(read_cache "created_30d")
resolved_30d=$(read_cache "resolved_30d")
local backlog_older_7d backlog_older_30d backlog_older_90d backlog_older_365d
backlog_older_7d=$(read_cache "backlog_older_7d")
backlog_older_30d=$(read_cache "backlog_older_30d")
backlog_older_90d=$(read_cache "backlog_older_90d")
backlog_older_365d=$(read_cache "backlog_older_365d")
local high_priority_unresolved critical_bugs
high_priority_unresolved=$(read_cache "high_priority_unresolved")
critical_bugs=$(read_cache "critical_bugs")
cat <<EOF
# HELP jira_exporter_info Jira exporter metadata
# TYPE jira_exporter_info gauge
jira_exporter_info{version="${SCRIPT_VERSION}",jira_url="${JIRA_BASE_URL}"} 1
# HELP jira_issues_by_status Issue count by status
# TYPE jira_issues_by_status gauge
jira_issues_by_status{status="to_do"} $status_todo
jira_issues_by_status{status="in_progress"} $status_in_progress
jira_issues_by_status{status="in_review"} $status_in_review
jira_issues_by_status{status="done"} $status_done
jira_issues_by_status{status="closed"} $status_closed
jira_issues_by_status{status="open"} $status_open
jira_issues_by_status{status="reopened"} $status_reopened
jira_issues_by_status{status="resolved"} $status_resolved
# HELP jira_issues_by_priority Issue count by priority
# TYPE jira_issues_by_priority gauge
jira_issues_by_priority{priority="highest"} $priority_highest
jira_issues_by_priority{priority="high"} $priority_high
jira_issues_by_priority{priority="medium"} $priority_medium
jira_issues_by_priority{priority="low"} $priority_low
jira_issues_by_priority{priority="lowest"} $priority_lowest
# HELP jira_issues_by_type Issue count by type
# TYPE jira_issues_by_type gauge
jira_issues_by_type{type="bug"} $type_bug
jira_issues_by_type{type="story"} $type_story
jira_issues_by_type{type="task"} $type_task
jira_issues_by_type{type="epic"} $type_epic
jira_issues_by_type{type="sub_task"} $type_sub_task
jira_issues_by_type{type="incident"} $type_incident
# HELP jira_backlog_total Total unresolved issues
# TYPE jira_backlog_total gauge
jira_backlog_total $backlog_total
# HELP jira_overdue_issues Unresolved issues past due date
# TYPE jira_overdue_issues gauge
jira_overdue_issues $overdue
# HELP jira_unassigned_issues Unresolved issues with no assignee
# TYPE jira_unassigned_issues gauge
jira_unassigned_issues $unassigned
# HELP jira_high_priority_unresolved High/Highest priority unresolved issues
# TYPE jira_high_priority_unresolved gauge
jira_high_priority_unresolved $high_priority_unresolved
# HELP jira_critical_bugs_unresolved High/Highest priority unresolved bugs
# TYPE jira_critical_bugs_unresolved gauge
jira_critical_bugs_unresolved $critical_bugs
# HELP jira_issues_created Issues created in time period
# TYPE jira_issues_created gauge
jira_issues_created{period="24h"} $created_24h
jira_issues_created{period="7d"} $created_7d
jira_issues_created{period="30d"} $created_30d
# HELP jira_issues_resolved Issues resolved in time period
# TYPE jira_issues_resolved gauge
jira_issues_resolved{period="24h"} $resolved_24h
jira_issues_resolved{period="7d"} $resolved_7d
jira_issues_resolved{period="30d"} $resolved_30d
# HELP jira_backlog_age_issues Unresolved issues older than threshold
# TYPE jira_backlog_age_issues gauge
jira_backlog_age_issues{older_than="7d"} $backlog_older_7d
jira_backlog_age_issues{older_than="30d"} $backlog_older_30d
jira_backlog_age_issues{older_than="90d"} $backlog_older_90d
jira_backlog_age_issues{older_than="365d"} $backlog_older_365d
EOF
# Resolution time metrics
local rt_avg=0 rt_min=0 rt_max=0 rt_count=0
if [ -f "$CACHE_DIR/resolution_times" ]; then
local rt_data
rt_data=$(cat "$CACHE_DIR/resolution_times" 2>/dev/null)
if [ -n "$rt_data" ] && [ "$rt_data" != "null" ]; then
rt_avg=$(echo "$rt_data" | cut -d'|' -f1)
rt_min=$(echo "$rt_data" | cut -d'|' -f2)
rt_max=$(echo "$rt_data" | cut -d'|' -f3)
rt_count=$(echo "$rt_data" | cut -d'|' -f4)
fi
fi
cat <<EOF
# HELP jira_resolution_time_hours Issue resolution time in hours (last 7d)
# TYPE jira_resolution_time_hours gauge
jira_resolution_time_hours{stat="avg"} ${rt_avg:-0}
jira_resolution_time_hours{stat="min"} ${rt_min:-0}
jira_resolution_time_hours{stat="max"} ${rt_max:-0}
# HELP jira_resolution_time_sample_count Number of resolved issues used for resolution time calculation
# TYPE jira_resolution_time_sample_count gauge
jira_resolution_time_sample_count ${rt_count:-0}
EOF
# Per-project metrics
if [ -n "$JIRA_PROJECTS" ]; then
IFS=',' read -ra PROJ_ARRAY <<< "$JIRA_PROJECTS"
echo "# HELP jira_project_issues_open Unresolved issues per project"
echo "# TYPE jira_project_issues_open gauge"
for proj in "${PROJ_ARRAY[@]}"; do
proj=$(echo "$proj" | tr -d '[:space:]')
[ -z "$proj" ] && continue
local safe_proj
safe_proj=$(echo "$proj" | tr '[:upper:]' '[:lower:]')
echo "jira_project_issues_open{project=\"${proj}\"} $(read_cache "proj_open_${safe_proj}")"
done
echo ""
echo "# HELP jira_project_issues_total Total issues per project"
echo "# TYPE jira_project_issues_total gauge"
for proj in "${PROJ_ARRAY[@]}"; do
proj=$(echo "$proj" | tr -d '[:space:]')
[ -z "$proj" ] && continue
local safe_proj
safe_proj=$(echo "$proj" | tr '[:upper:]' '[:lower:]')
echo "jira_project_issues_total{project=\"${proj}\"} $(read_cache "proj_total_${safe_proj}")"
done
echo ""
echo "# HELP jira_project_issues_created_7d Issues created in last 7d per project"
echo "# TYPE jira_project_issues_created_7d gauge"
for proj in "${PROJ_ARRAY[@]}"; do
proj=$(echo "$proj" | tr -d '[:space:]')
[ -z "$proj" ] && continue
local safe_proj
safe_proj=$(echo "$proj" | tr '[:upper:]' '[:lower:]')
echo "jira_project_issues_created_7d{project=\"${proj}\"} $(read_cache "proj_created_7d_${safe_proj}")"
done
echo ""
echo "# HELP jira_project_issues_resolved_7d Issues resolved in last 7d per project"
echo "# TYPE jira_project_issues_resolved_7d gauge"
for proj in "${PROJ_ARRAY[@]}"; do
proj=$(echo "$proj" | tr -d '[:space:]')
[ -z "$proj" ] && continue
local safe_proj
safe_proj=$(echo "$proj" | tr '[:upper:]' '[:lower:]')
echo "jira_project_issues_resolved_7d{project=\"${proj}\"} $(read_cache "proj_resolved_7d_${safe_proj}")"
done
echo ""
fi
# Assignee workload metrics
echo "# HELP jira_assignee_open_issues Open issues per assignee (top 15)"
echo "# TYPE jira_assignee_open_issues gauge"
if [ -f "$CACHE_DIR/assignees" ] && [ -s "$CACHE_DIR/assignees" ]; then
while IFS='|' read -r name count; do
[ -z "$name" ] && continue
local safe_name
safe_name=${name//\"/\\\"}
echo "jira_assignee_open_issues{assignee=\"${safe_name}\"} ${count}"
done < "$CACHE_DIR/assignees"
fi
echo ""
# Component metrics
echo "# HELP jira_component_open_issues Open issues per component (top 20)"
echo "# TYPE jira_component_open_issues gauge"
if [ -f "$CACHE_DIR/components" ] && [ -s "$CACHE_DIR/components" ]; then
while IFS='|' read -r name count; do
[ -z "$name" ] && continue
local safe_name
safe_name=${name//\"/\\\"}
echo "jira_component_open_issues{component=\"${safe_name}\"} ${count}"
done < "$CACHE_DIR/components"
fi
echo ""
# Plugin / add-on metrics
local plugins_total plugins_enabled plugins_disabled plugins_user plugins_system
plugins_total=$(read_cache "plugins_total")
plugins_enabled=$(read_cache "plugins_enabled")
plugins_disabled=$(read_cache "plugins_disabled")
plugins_user=$(read_cache "plugins_user_installed")
plugins_system=$(read_cache "plugins_system")
cat <<PLUGINEOF
# HELP jira_plugins_total Total installed plugins/add-ons
# TYPE jira_plugins_total gauge
jira_plugins_total $plugins_total
# HELP jira_plugins_enabled Enabled plugins
# TYPE jira_plugins_enabled gauge
jira_plugins_enabled $plugins_enabled
# HELP jira_plugins_disabled Disabled plugins
# TYPE jira_plugins_disabled gauge
jira_plugins_disabled $plugins_disabled
# HELP jira_plugins_user_installed User-installed plugins (non-system)
# TYPE jira_plugins_user_installed gauge
jira_plugins_user_installed $plugins_user
# HELP jira_plugins_system System/bundled plugins
# TYPE jira_plugins_system gauge
jira_plugins_system $plugins_system
PLUGINEOF
echo "# HELP jira_plugin_status Per user-installed plugin status (1=enabled, 0=disabled)"
echo "# TYPE jira_plugin_status gauge"
if [ -f "$CACHE_DIR/plugins_detail" ] && [ -s "$CACHE_DIR/plugins_detail" ]; then
while IFS='|' read -r pname penabled pversion; do
[ -z "$pname" ] && continue
local safe_pname
safe_pname=${pname//\"/\\\"}
echo "jira_plugin_status{plugin=\"${safe_pname}\",version=\"${pversion}\"} ${penabled}"
done < "$CACHE_DIR/plugins_detail"
fi
echo ""
# Service Desk metrics
local sd_available
sd_available=$(read_cache "sd_available")
if [ "$sd_available" = "1" ]; then
local sd_count sd_open sd_resolved_24h sd_resolved_7d sd_created_24h sd_created_7d
local sd_high_priority sd_breached sd_csat
sd_count=$(read_cache "sd_count")
sd_open=$(read_cache "sd_open_requests")
sd_resolved_24h=$(read_cache "sd_resolved_24h")
sd_resolved_7d=$(read_cache "sd_resolved_7d")
sd_created_24h=$(read_cache "sd_created_24h")
sd_created_7d=$(read_cache "sd_created_7d")
sd_high_priority=$(read_cache "sd_high_priority_open")
sd_breached=$(read_cache "sd_breached_sla")
sd_csat=$(read_cache "sd_csat_responses")
cat <<SDEOF
# HELP jira_servicedesk_count Number of service desks
# TYPE jira_servicedesk_count gauge
jira_servicedesk_count $sd_count
# HELP jira_servicedesk_open_requests Total open service desk requests
# TYPE jira_servicedesk_open_requests gauge
jira_servicedesk_open_requests $sd_open
# HELP jira_servicedesk_requests_created Service desk requests created in period
# TYPE jira_servicedesk_requests_created gauge
jira_servicedesk_requests_created{period="24h"} $sd_created_24h
jira_servicedesk_requests_created{period="7d"} $sd_created_7d
# HELP jira_servicedesk_requests_resolved Service desk requests resolved in period
# TYPE jira_servicedesk_requests_resolved gauge
jira_servicedesk_requests_resolved{period="24h"} $sd_resolved_24h
jira_servicedesk_requests_resolved{period="7d"} $sd_resolved_7d
# HELP jira_servicedesk_high_priority_open High priority open service desk requests
# TYPE jira_servicedesk_high_priority_open gauge
jira_servicedesk_high_priority_open $sd_high_priority
# HELP jira_servicedesk_sla_breached Requests past SLA due date (unresolved)
# TYPE jira_servicedesk_sla_breached gauge
jira_servicedesk_sla_breached $sd_breached
# HELP jira_servicedesk_csat_responses_30d Customer satisfaction responses in last 30d
# TYPE jira_servicedesk_csat_responses_30d gauge
jira_servicedesk_csat_responses_30d $sd_csat
SDEOF
# Per-queue issue counts
echo "# HELP jira_servicedesk_queue_size Issue count per service desk queue"
echo "# TYPE jira_servicedesk_queue_size gauge"
if [ -f "$CACHE_DIR/sd_queues_all" ] && [ -s "$CACHE_DIR/sd_queues_all" ]; then
while IFS='|' read -r q_proj _q_id q_name q_count; do
[ -z "$q_proj" ] && continue
local safe_qname
safe_qname=${q_name//\"/\\\"}
echo "jira_servicedesk_queue_size{project=\"${q_proj}\",queue=\"${safe_qname}\"} ${q_count}"
done < "$CACHE_DIR/sd_queues_all"
fi
echo ""
# Request types per service desk
echo "# HELP jira_servicedesk_request_type Available request types per service desk"
echo "# TYPE jira_servicedesk_request_type gauge"
if [ -f "$CACHE_DIR/sd_request_types_all" ] && [ -s "$CACHE_DIR/sd_request_types_all" ]; then
while IFS='|' read -r rt_proj _rt_id rt_name; do
[ -z "$rt_proj" ] && continue
local safe_rtname
safe_rtname=${rt_name//\"/\\\"}
echo "jira_servicedesk_request_type{project=\"${rt_proj}\",request_type=\"${safe_rtname}\"} 1"
done < "$CACHE_DIR/sd_request_types_all"
fi
echo ""
# Organizations per service desk
echo "# HELP jira_servicedesk_organizations Organization count per service desk"
echo "# TYPE jira_servicedesk_organizations gauge"
if [ -f "$CACHE_DIR/sd_orgs_all" ] && [ -s "$CACHE_DIR/sd_orgs_all" ]; then
while IFS='|' read -r o_proj o_count; do
[ -z "$o_proj" ] && continue
echo "jira_servicedesk_organizations{project=\"${o_proj}\"} ${o_count}"
done < "$CACHE_DIR/sd_orgs_all"
fi
echo ""
fi
# Login activity metrics (from audit log, last 24h)
local logins_total logins_success logins_failed logins_unique
logins_total=$(read_cache "logins_total")
logins_success=$(read_cache "logins_success")
logins_failed=$(read_cache "logins_failed")
logins_unique=$(read_cache "logins_unique_users")
cat <<LOGINEOF
# HELP jira_logins_total_24h Total login events in last 24h
# TYPE jira_logins_total_24h gauge
jira_logins_total_24h $logins_total
# HELP jira_logins_success_24h Successful logins in last 24h
# TYPE jira_logins_success_24h gauge
jira_logins_success_24h $logins_success
# HELP jira_logins_failed_24h Failed login attempts in last 24h
# TYPE jira_logins_failed_24h gauge
jira_logins_failed_24h $logins_failed
# HELP jira_logins_unique_users_24h Unique users who logged in during last 24h
# TYPE jira_logins_unique_users_24h gauge
jira_logins_unique_users_24h $logins_unique
LOGINEOF
echo "# HELP jira_user_logins_24h Login count per user in last 24h (top 15)"
echo "# TYPE jira_user_logins_24h gauge"
if [ -f "$CACHE_DIR/logins_per_user" ] && [ -s "$CACHE_DIR/logins_per_user" ]; then
while IFS='|' read -r user count; do
[ -z "$user" ] && continue
local safe_user
safe_user=${user//\"/\\\"}
echo "jira_user_logins_24h{user=\"${safe_user}\"} ${count}"
done < "$CACHE_DIR/logins_per_user"
fi
echo ""
cat <<EOF
# HELP jira_scrape_timestamp_seconds Unix timestamp of metric generation
# TYPE jira_scrape_timestamp_seconds gauge
jira_scrape_timestamp_seconds $(date +%s)
# HELP jira_scrape_duration_seconds Time to generate all metrics
# TYPE jira_scrape_duration_seconds gauge
jira_scrape_duration_seconds $(($(date +%s) - start_time))
# HELP jira_scrape_success Whether the last scrape was successful (1=yes, 0=no)
# TYPE jira_scrape_success gauge
jira_scrape_success 1
EOF
echo ""
rm -rf "$CACHE_DIR"
}
# ---------------------------------------------------------------------------
# HTTP server
# ---------------------------------------------------------------------------
run_http_server() {
echo "Starting Jira exporter on port $HTTP_PORT..." >&2
while true; do
{
read -r request
if [[ "$request" =~ ^GET\ /metrics ]]; then
printf "HTTP/1.1 200 OK\r\nContent-Type: text/plain; version=0.0.4; charset=utf-8\r\n\r\n"
cache_all_jira_data
cache_project_data
cache_assignee_data
cache_component_data
cache_resolution_time_data
cache_login_data
cache_plugin_data
cache_servicedesk_data
generate_metrics
else
printf "HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=utf-8\r\n\r\n"
echo "<h1>Jira Exporter v${SCRIPT_VERSION}</h1><a href='/metrics'>Metrics</a>"
fi
} | nc -l -p "$HTTP_PORT" -q 1 2>/dev/null
done
}
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
main() {
parse_args "$@"
preflight_check
[ "$HTTP_MODE" != true ] && acquire_lock
if [ "$HTTP_MODE" = true ]; then
run_http_server
elif [ -n "$OUTPUT_FILE" ]; then
cache_all_jira_data
cache_project_data
cache_assignee_data
cache_component_data
cache_resolution_time_data
cache_login_data
cache_plugin_data
cache_servicedesk_data
mkdir -p "$(dirname "$OUTPUT_FILE")"
local temp_file
temp_file=$(mktemp /tmp/jira_metrics.XXXXXX)
generate_metrics > "$temp_file"
rm -f "$OUTPUT_FILE"
mv "$temp_file" "$OUTPUT_FILE"
chmod 644 "$OUTPUT_FILE"
sync
else
cache_all_jira_data
cache_project_data
cache_assignee_data
cache_component_data
cache_resolution_time_data
cache_login_data
cache_plugin_data
cache_servicedesk_data
generate_metrics
fi
}
main "$@"