commit fcac9574787db96a21ad49a18a3e62d7d8b72df2 from: mischa date: Sat Jun 3 15:09:17 2023 UTC added RSS feed, moved health processing to a function commit - 38f7df88e7abb54f7e900e9f3355048d1be8558f commit + fcac9574787db96a21ad49a18a3e62d7d8b72df2 blob - 247ebe0cc3a0e4101fd0e23493571aa8573b6197 blob + 81a621b8298df89795bd077e111c493a5a61035b --- README.md +++ README.md @@ -10,7 +10,8 @@ uptimeatomic alerts when downtime happens and generate * Minimal dependencies (curl, nc and coreutils) * Easy configuration and customisation * Tiny (~1kb) optimized result page -* Incident history (manual) +* Incident and maintenance history (manual) +* RSS feed for incidents and maintenance messages * Crontab friendly ## Demo @@ -52,13 +53,23 @@ Note: `port4` and `port6` require OpenBSD `nc` binary. The default timeout is set in `uptimeatomic` but can be set in `checks.csv` per host. +## Incidents / Maintenance + +The syntax of the `incidents.txt` and `pastincidents.txt` files is: + +``` +Datetime, Type, Description +202305231230, Incident, There is a service interuption on 2023-05-23 at 12:23 UTC +202306031700, Maintenance, Server maintenance is scheduled on 2023-07-13 at 00:00 UTC + ## Parameters ``` -./uptimeatomic -c CHECKFILE -i INCIDENTSFILE -o HTMLFILE +./uptimeatomic -c CHECKFILE -i INCIDENTSFILE -o HTMLFILE -r RSSFILE Default: -c = checks.csv -i = incidents.txt -p = pastincidents.txt -o = index.html + -r = rss file (no default) ``` blob - e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 blob + cdacba60ffcb91ea16f68fb1626d02fb5786b5c5 --- incidents.txt +++ incidents.txt @@ -0,0 +1 @@ +202306021200,Maintenance,On 2023-06-15 between 22:30-02:30 UTC our upstream provider has scheduled network maintenance blob - a4a2be5b2d4c7ff1b81ea7a89f08731372555602 blob + 6f16f269fa335b2429f95c2c20aaa0c40487bb91 --- pastincidents.txt +++ pastincidents.txt @@ -1 +1 @@ -2023-05-28 21:17 - Short network outage was observed at our upstream provider between 2023-05-28 21:17:28 UTC and 2023-05-28 21:19:17 UTC +202305282120,Incident,Between 21:17:28 and 21:19:17 UTC on 2023-05-28 a network outage happened at our upstream provider. Resolved blob - 91346ebb7728d9f62eadb755134a5fd72c7a2fc6 blob + ecd843c495eb0c3ded0a0692d22e701c842a858e --- uptimeatomic +++ uptimeatomic @@ -1,6 +1,6 @@ #!/bin/ksh # -# Uptime Atomic v20230531 +# Uptime Atomic v20230603 # https://git.high5.nl/uptimeatomic/ # export PATH=/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin @@ -16,9 +16,12 @@ PUSHOVER_CONF="/home/mischa/_pushover.conf" PUSHOVER_STATUS="status" RECIPIENT="alerts@high5.nl" REFRESH=60 +RSS_DESCRIPTION="Incidents and Maintenance Updates" +RSS_FILE="" +RSS_URL="https://ops.lowfive.nl" SENDER="uptimeatomic@high5.nl" TIMEOUT=10 -TITLE="Uptime Atomic" +TITLE="Uptime Atomic - OpenBSD Amsterdam" USERAGENT="User-Agent: Mozilla/5.0 (OpenBSD; Intel OpenBSD 7.4; rv:109.0) Gecko/20100101 Firefox/113.0" WORKDIR=/home/mischa/uptimeatomic @@ -27,82 +30,161 @@ usage() { exit } +date_rss() { + if [ -n "${1}" ]; then + date -j '+%a, %d %b %Y %H:%M:%S %z' $(echo "$1") + else + date -j '+%a, %d %b %Y %H:%M:%S %z' + fi +} +date_incident() { + if [ -n "${1}" ]; then + date -j '+%F %H:%M %Z' $(echo "$1") + else + date -j '+%F %H:%M %Z' + fi +} + get_element() { echo "${2}" | awk -v col="${1}" -F',' '{gsub(/^[ \t]+|[ \t]+$/, "", $col); print $col}' } +incidents() { + _date=${1} + _type=${2} + _description=${3} + [[ ${_type} == "Incident" ]] && color="failed" + [[ ${_type} == "Maintenance" ]] && color="maint" + echo "

$(date_incident ${_date}) - ${_type}
${_description}

" >> ${_HTMLFILE} + if [ -n ${RSS_FILE} ]; then +cat << EOF >> ${RSS_FILE} + +${_type} +${RSS_URL}/rss.xml +${RSS_URL}/rss.xml?$(date -j '+%s' $(echo "${_date}")) +$(date_rss "${_date}") + + +EOF + fi +} + notify() { - name="${1}" - status="${2}" - priority="${3}" + _name="${1}" + _status="${2}" + _priority="${3}" - if [ ${priority} == "KO" ]; then - echo "${status}." | mail -r "${TITLE} <${SENDER}>" -s "${name} DOWN" ${RECIPIENT} - ${PUSHOVER} -c ${PUSHOVER_CONF} -t "${TITLE}" -m "${name} DOWN ${status}." -p 1 >/dev/null 2>&1 - touch "${PUSHOVER_STATUS}/${name}" + if [ ${_priority} == "ko" ]; then + echo "${_status}." | mail -r "${TITLE} <${SENDER}>" -s "${_name} DOWN" ${RECIPIENT} + ${PUSHOVER} -c ${PUSHOVER_CONF} -t "${TITLE}" -m "${_name} DOWN ${_status}." -p 1 >/dev/null 2>&1 + touch "${PUSHOVER_STATUS}/${_name}" fi - if [ ${priority} == "OK" ]; then - _seconds=$(expr $(date +%s) - $(stat -r "${PUSHOVER_STATUS}/${name}" | awk '{print $11}')) + if [ ${_priority} == "ok" ]; then + _seconds=$(expr $(date +%s) - $(stat -r "${PUSHOVER_STATUS}/${_name}" | awk '{print $11}')) _downtime=$(date -r${_seconds} -u +%H:%M:%S) - echo "${status} - down for ${_downtime}" | mail -r "${TITLE} <${SENDER}>" -s "${name} OK" ${RECIPIENT} - ${PUSHOVER} -c ${PUSHOVER_CONF} -t "${TITLE}" -m "${name} OK ${status} - down for ${_downtime}" >/dev/null 2>&1 - rm -rf "${PUSHOVER_STATUS}/${name}" + echo "${_status} - down for ${_downtime}" | mail -r "${TITLE} <${SENDER}>" -s "${_name} OK" ${RECIPIENT} + ${PUSHOVER} -c ${PUSHOVER_CONF} -t "${TITLE}" -m "${_name} OK ${_status} - down for ${_downtime}" >/dev/null 2>&1 + rm -rf "${PUSHOVER_STATUS}/${_name}" fi } check() { - ctype="${1}" - host="${2}" - name="${3}" - expectedcode="${4}" - timeout="${5}" - IPv="${ctype#(http|ping|port)}" - [[ -n "${timeout}" ]] && TIMEOUT=${timeout} + _ctype="${1}" + _expectedcode="${2}" + _name="${3}" + _host="${4}" + _timeout="${5}" + IPv="${_ctype#(http|ping|port)}" + [[ -n "${_timeout}" ]] && TIMEOUT=${_timeout} - case "${ctype}" in + case "${_ctype}" in http*) - statuscode="$(curl -${IPv}sSkLo /dev/null -H "${USERAGENT}" -m "${TIMEOUT}" -w "%{http_code}" "${host}" 2> "${_tmp}/ko/${name}.error")" - if [ "${statuscode}" -ne "${expectedcode}" ]; then - if [ -s "${_tmp}/ko/${name}.error" ]; then - sed -e 's,curl: ([0-9]*) ,,' "${_tmp}/ko/${name}.error" > "${_tmp}/ko/${name}.status" + statuscode="$(curl -${IPv}sSkLo /dev/null -H "${USERAGENT}" -m "${TIMEOUT}" -w "%{http_code}" "${_host}" 2> "${_TMP}/ko/${_name}.error")" + if [ "${statuscode}" -ne "${_expectedcode}" ]; then + if [ -s "${_TMP}/ko/${_name}.error" ]; then + sed -e 's,curl: ([0-9]*) ,,' "${_TMP}/ko/${_name}.error" > "${_TMP}/ko/${_name}.status" else - echo "Status code: ${statuscode}" > "${_tmp}/ko/${name}.status" + echo "Status code: ${statuscode}" > "${_TMP}/ko/${_name}.status" fi else - echo "Status code: ${statuscode}" > "${_tmp}/ok/${name}.status" + echo "Status code: ${statuscode}" > "${_TMP}/ok/${_name}.status" fi ;; ping*) - ping -${IPv}w "${TIMEOUT}" -c 1 "${host}" >/dev/null 2>&1 + ping -${IPv}w "${TIMEOUT}" -c 1 "${_host}" >/dev/null 2>&1 statuscode=$? - if [ "${statuscode}" -ne "${expectedcode}" ]; then - echo "Host unreachable" > "${_tmp}/ko/${name}.status" + if [ "${statuscode}" -ne "${_expectedcode}" ]; then + echo "Host unreachable" > "${_TMP}/ko/${_name}.status" else - echo "Host reachable" > "${_tmp}/ok/${name}.status" + echo "Host reachable" > "${_TMP}/ok/${_name}.status" fi ;; port*) - error="$(nc -${IPv}w "${TIMEOUT}" -zv ${host} 2>&1)" + error="$(nc -${IPv}w "${TIMEOUT}" -zv ${_host} 2>&1)" statuscode=$? - if [ "${statuscode}" -ne "${expectedcode}" ]; then - echo "Connection refused" > "${_tmp}/ko/${name}.status" + if [ "${statuscode}" -ne "${_expectedcode}" ]; then + echo "Connection refused" > "${_TMP}/ko/${_name}.status" else - echo "Connection succeeded" > "${_tmp}/ok/${name}.status" + echo "Connection succeeded" > "${_TMP}/ok/${_name}.status" fi ;; maint*) - echo "Maintenance" > "${_tmp}/maint/${name}.status" + echo "Maintenance" > "${_TMP}/maint/${_name}.status" ;; esac } -while getopts c:i:o:h arg; do +process_health() { + _health=${1} + + ls ${_TMP}/${_health}/*.status 2>/dev/null | sort -V | while read file; do + [ -e "${file}" ] || continue + _name="$(basename "${file%.status}")" + _status="$(cat "${file}")" + + if [ ${_health} == "ko" ]; then + echo "
  • ${_name} (${_status})Disrupted
  • " >> ${_HTMLFILE} + if [ ! -e "${PUSHOVER_STATUS}/${_name}" ]; then + notify "${_name}" "${status}" "${_health}" + fi + fi + if [ ${_health} == "maint" ]; then + echo "
  • ${_name} Maintenance
  • " >> ${_HTMLFILE} + fi + if [ ${_health} == "ok" ]; then + echo "
  • ${_name} Operational
  • " >> ${_HTMLFILE} + if [ -e "${PUSHOVER_STATUS}/${_name}" ]; then + notify "${_name}" "${_status}" "${_health}" + fi + fi + done +} + +parse_file() { + _file=${1} + while IFS="$(printf '\n')" read -r line; do + _col1="$(get_element 1 "${line}")" + _col2="$(get_element 2 "${line}")" + _col3="$(get_element 3 "${line}")" + _col4="$(get_element 4 "${line}")" + _col5="$(get_element 5 "${line}")" + if [[ ${_file} == *".csv" ]]; then + check "${_col1}" "${_col2}" "${_col3}" "${_col4}" "${_col5}" & + else + incidents "${_col1}" "${_col2}" "${_col3}" + fi + done < "${_file}" + wait +} + +while getopts c:i:o:r:h arg; do case ${arg} in c) CHECKFILE=${OPTARG};; i) INCIDENTSFILE=${OPTARG};; p) PASTINCIDENTSFILE=${OPTARG};; o) HTMLFILE=${OPTARG};; + r) RSS_FILE=${OPTARG};; h) usage;; *) usage;; esac @@ -114,21 +196,16 @@ if [ ! -e "${CHECKFILE}" ]; then exit fi -_tmp="$(mktemp -d)" -mkdir -p "${_tmp}/ok" "${_tmp}/ko" "${_tmp}/maint" || exit 1 -_htmlfile="${_tmp}/${HTMLFILE}" +_TMP="$(mktemp -d)" +mkdir -p "${_TMP}/ok" "${_TMP}/ko" "${_TMP}/maint" || exit 1 +_HTMLFILE="${_TMP}/${HTMLFILE}" -while IFS="$(printf '\n')" read -r line; do - ctype="$(get_element 1 "${line}")" - code="$(get_element 2 "${line}")" - name="$(get_element 3 "${line}")" - host="$(get_element 4 "${line}")" - timeout="$(get_element 5 "${line}")" - check "${ctype}" "${host}" "${name}" "${code}" "${timeout}" & -done < "${CHECKFILE}" -wait +parse_file "${CHECKFILE}" -cat << EOF >> ${_htmlfile} +# +# HTML +# +cat << EOF > ${_HTMLFILE} @@ -157,62 +234,74 @@ li { list-style: none; margin-bottom: 2px; padding: 5p

    ${HEADER}

    EOF -_outage_count="$(find "${_tmp}/ko" -mindepth 1 | grep -c 'status$')" -_maint_count="$(find "${_tmp}/maint" -mindepth 1 | grep -c 'status$')" + +# +# RSS +# +if [ -n ${RSS_FILE} ]; then +cat << EOF > ${RSS_FILE} + + + + +${TITLE} +${RSS_DESCRIPTION} +${RSS_URL} +$(date_rss) +EOF +fi + +_outage_count="$(find "${_TMP}/ko" -mindepth 1 | grep -c 'status$')" +_maint_count="$(find "${_TMP}/maint" -mindepth 1 | grep -c 'status$')" if [ "${_outage_count}" -ne 0 ]; then - echo "
    • ${_outage_count} Outage(s)
    " >> ${_htmlfile} + echo "
    • ${_outage_count} Outage(s)
    " >> ${_HTMLFILE} fi if [ "${_maint_count}" -ne 0 ]; then - echo "
    • ${_maint_count} Maintenance
    " >> ${_htmlfile} + echo "
    • ${_maint_count} Maintenance
    " >> ${_HTMLFILE} fi if [[ "${_outage_count}" -eq 0 && "${_maint_count}" -eq 0 ]]; then - echo "
    • All Systems Operational
    " >> ${_htmlfile} + echo "
    • All Systems Operational
    " >> ${_HTMLFILE} fi + if [ -s "${INCIDENTSFILE}" ]; then - echo '

    Incidents

    ' >> ${_htmlfile} - sed 's|^\(.*\)$|

    \1

    |' "${INCIDENTSFILE}" >> ${_htmlfile} + echo '

    Incidents / Maintenance

    ' >> ${_HTMLFILE} + parse_file "${INCIDENTSFILE}" fi -cat << EOF >> ${_htmlfile} -

    Services

    -
      -EOF -ls ${_tmp}/ko/*.status 2>/dev/null | sort -V | while read file; do - [ -e "${file}" ] || continue - name="$(basename "${file%.status}")" - status="$(cat "${file}")" - echo "
    • ${name} (${status})Disrupted
    • " >> ${_htmlfile} - if [ ! -e "${PUSHOVER_STATUS}/${name}" ]; then - notify "${name}" "${status}" "KO" - fi -done -ls ${_tmp}/maint/*.status 2>/dev/null | sort -V | while read file; do - [ -e "${file}" ] || continue - name="$(basename "${file%.status}")" - echo "
    • ${name} Maintenance
    • " >> ${_htmlfile} -done -ls ${_tmp}/ok/*.status 2>/dev/null | sort -V | while read file; do - [ -e "${file}" ] || continue - name="$(basename "${file%.status}")" - status="$(cat "${file}")" - echo "
    • ${name} Operational
    • " >> ${_htmlfile} - if [ -e "${PUSHOVER_STATUS}/${name}" ]; then - notify "${name}" "${status}" "OK" - fi -done -cat << EOF >> ${_htmlfile} -
    -

    Last check: $(date +%FT%T%z)

    -EOF + +echo "

    Services

    " >> ${_HTMLFILE} +echo "
      " >> ${_HTMLFILE} + +process_health "ko" +process_health "maint" +process_health "ok" + +echo "
    " >> ${_HTMLFILE} +echo "

    Last check: $(date '+%FT%T %Z')

    " >> ${_HTMLFILE} + if [ -s "${PASTINCIDENTSFILE}" ]; then - echo '

    Past Incidents

    ' >> ${_htmlfile} - sed 's|^\(.*\)$|

    \1

    |' "${PASTINCIDENTSFILE}" >> ${_htmlfile} + echo '

    Past Incidents / Maintenance

    ' >> ${_HTMLFILE} + parse_file "${PASTINCIDENTSFILE}" fi -cat << EOF >> ${_htmlfile} + +# +# END HTML +# +cat << EOF >> ${_HTMLFILE}

    Uptime Atomic loosely based on Tinystatus

    EOF -cp ${_htmlfile} ${HTMLDIR}/${HTMLFILE} -rm -r "${_tmp}" 2>/dev/null +# +# END RSS +# +if [ -n ${RSS_FILE} ]; then +cat << EOF >> ${RSS_FILE} + + +EOF +fi + +cp ${_HTMLFILE} ${HTMLDIR}/${HTMLFILE} +rm -r "${_TMP}" 2>/dev/null blob - /dev/null blob + 5e7d2734cfc60289debf74293817c0a8f572ff32 (mode 644) --- /dev/null +++ status/.gitignore @@ -0,0 +1,4 @@ +# Ignore everything in this directory +* +# Except this file +!.gitignore