a1a17e81a1
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.
1149 lines
44 KiB
Bash
Executable File
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 "$@"
|