# -*- tab-width: 4 -*- ;; Emacs
# vi: set filetype=sh tabstop=8 shiftwidth=8 noexpandtab :: Vi/ViM
############################################################ IDENT(1)
#
# $Title: dwatch(8) JSON module for dtrace_io(4) $
# $Copyright: 2014-2022 Devin Teske. All rights reserved. $
# $FrauBSD: dwatch-json/json-io-config-raw 2022-08-22 15:36:11 -0700 freebsdfrau $
# $Version: 1.0 $
#
############################################################ DESCRIPTION
#
# Produce JSON custom log format for disk I/O
#
############################################################ PROBE

: "${PROBE_SECONDS:=10}"
: "${PROBE:=profile-${PROBE_SECONDS}s}"

############################################################ INCLUDES

. /usr/share/bsdconfig/strings.subr || exit

############################################################ FUNCTIONS

#
# JSON helpers
#
jfmt(){ printf "\\\\\"%s\\\\\":$1" "$2" "$3"; }
jraw(){ jfmt %s "$1" "$2"; }
jstr(){ jraw "$1" "\\\"$2\\\""; }
_jstr(){ printf ,; jstr "$@"; }
_jraw(){ printf ,; jraw "$@"; }

############################################################ CONFIG

IFS=" 	$NL" # space tab newline

#
# Defaults
#
: "${JSON_IO_CONFIG:=/usr/local/etc/dwatch-${PROFILE%-config*}.conf}"
: "${HOSTNAME:=$( hostname )}"
: "${REPORT_TYPE:=${PROFILE%-raw}}"
: "${TAG_NAME:=name}"
: "${UNMATCHED_LABEL:=}"

#
# Tabulate variables prior to loading config
#
vars_ignore="$( set | awk '
	match($0, /^[a-zA-Z_][a-zA-Z0-9_]*=/) {
		print substr($0, 1, RLENGTH - 1)
	}
' ) vars_ignore"

#
# Load config file
#
# NB: Allowing the config to tell us which vars we should unset via IGNORE
# NB: Making sure to nullify IGNORE before we load the config
#
IGNORE=
. "$JSON_IO_CONFIG" || exit

#
# Set default values
#
: "${UNMATCHED_LABEL:=unlabeled}"

#
# Determine which variables were set by config
# NB: Ignoring variables set prior to loading config [vars_ignore]
# NB: Ignoring "*_label" variables
#
vars_ignore="IGNORE $IGNORE $vars_ignore"
export vars_ignore # for awk(1) ENVIRON
confvars=$( set | awk '
	BEGIN {
		confvars = ""
		delete env
		nvars = split(ENVIRON["vars_ignore"], vars, /[[:space:]]+/)
		for (n = 1; n <= nvars; n++) ignore[vars[n]]
	}
	match($0, /^[a-zA-Z_][a-zA-Z0-9_]*=/) {
		name = substr($0, 1, RLENGTH - 1)
		if (name in ignore) next
		if (name ~ /_label$/) next
		confvars = confvars " " name
	}
	END { print substr(confvars, 2) }
' )

############################################################ OVERRIDES

#
# Override dwatch(1) settings
#
MAX_ARGS=0		# -B num
MAX_DEPTH=0		# -K num

#
# Unsupported dwatch(1) features
#
unset EXECREGEX		# -z regex
unset GROUP		# -g group
unset PID		# -p pid
unset PROBE_COALESCE	# -F
unset PSTREE		# -R
unset USER		# -u user

############################################################ EVENT ACTION

_EVENT_TEST="${EVENT_TEST:+($EVENT_TEST)}"
_EVENT_TEST="${CUSTOM_TEST:+$CUSTOM_TEST${EVENT_TEST:+ && }}$_EVENT_TEST"
if [ "$JID" ]; then
	pr_id="curthread->td_proc->p_ucred->cr_prison->pr_id"
	_EVENT_TEST="$pr_id == $JID${_EVENT_TEST:+ && ($_EVENT_TEST)}"
	unset JID
fi
EVENT_TEST="cpu == 0"
CUSTOM_TEST=

_CONF_OTHER=
IFS=" "
nconf=0
for var in $confvars; do
	nconf=$(( $nconf + 1 ))
	eval val=\"\$$var\"
	_CONF_OTHER="$_CONF_OTHER || ($val)"
done
[ "$_CONF_OTHER" ] && _CONF_OTHER="!(${_CONF_OTHER# || })"

############################################################ ACTIONS

BIO_COMMANDS="
	BIO_CMD0
	BIO_CMD1
	BIO_CMD2
	BIO_DELETE
	BIO_FLUSH
	BIO_GETATTR
	BIO_READ
	BIO_WRITE
	BIO_ZONE
" # END-QUOTE

BIO_CMD_LIST=$( echo "$BIO_COMMANDS" | awk '
	/^[[:space:]]*(#|$)/ { next }
	{
		sub(/#.*/, "")
		sub(/^[[:space:]]*/, "")
		sub(/[[:space:]]*$/, "")
		n = split($0, f, /[[:space:]]+/)
		for (i = 1; i <= n; i++) list = list " " f[i]
	}
	END { print substr(list, 2) }
' )

bio_cmd_list=$( echo "$BIO_CMD_LIST" | awk '{
	sub(/^BIO_/, "")
	gsub(/ BIO_/, " ")
	print tolower($0)
}' )

stat_list="start done error"

bytes_list="read_bytes bytes_read write_bytes bytes_written"

exec 9<<EOF
this struct bio	*bio;
this bufinfo_t	bufinfo;
this uint8_t	b_error;
this int	b_flags;
this int	bio_cmd;
this long	bio_length;

this devinfo_t	devinfo;
this string	device_entry;
this string	device_if;
this string	device_type;

$( IFS=" "
	for bytes in $bytes_list; do
		printf "uint64_t\t%s;\n" "$bytes"
	done

	for cmd in $bio_cmd_list unknown; do
		for stat in $stat_list; do
			printf "uint64_t\t%s_%s;\n" "$cmd" "$stat"
		done
	done
	printf "\n"

	for var in $confvars; do
		for bytes in $bytes_list; do
			printf "uint64_t\t%s_%s;\n" "$var" "$bytes"
		done
		for cmd in $bio_cmd_list unknown; do
			for stat in $stat_list; do
				printf "uint64_t\t%s_%s_%s;\n" \
					"$var" "$cmd" "$stat"
			done
		done
		printf "\n"
	done

	printf "inline int set_globals_to[int num] =\n"
	for bytes in $bytes_list; do
		printf "\t%s =\n" "$bytes"
	done
	for cmd in $bio_cmd_list unknown; do
		for stat in $stat_list; do
			printf "\t%s_%s =\n" "$cmd" "$stat"
		done
	done
	printf "\tnum;\n\n"

	for var in $confvars; do
		printf "inline int %s_set_globals_to[int num] = \n" "$var"
		for bytes in $bytes_list; do
			printf "\t%s_%s =\n" "$var" "$bytes"
		done
		for cmd in $bio_cmd_list; do
			for stat in $stat_list; do
				printf "\t%s_%s_%s =\n" "$var" "$cmd" "$stat"
			done
		done
		printf "\tnum;\n\n"
	done
)

#pragma D binding "1.13" device_name
inline string device_name[devinfo_t di] = di.dev_name != "" ?
	strjoin(di.dev_name, lltostr(di.dev_minor)) :
	strjoin("[", strjoin(
		strjoin(lltostr(di.dev_major), ","),
		strjoin(lltostr(di.dev_minor), "]")));

inline int add_bio_cmd_start_bytes[int bio_cmd, uint64_t bytes] =
	bio_cmd == BIO_READ ? read_bytes += bytes :
	bio_cmd == BIO_WRITE ? write_bytes += bytes :
	0;

inline int add_bio_cmd_done_bytes[int bio_cmd, uint64_t bytes] =
	bio_cmd == BIO_READ ? bytes_read += bytes :
	bio_cmd == BIO_WRITE ? bytes_written += bytes :
	0;

$( IFS=" "
	for stat in $stat_list; do
		printf "inline int increment_bio_cmd_%s" "$stat"
		printf "[int bio_cmd] =\n"
		for cmd in $BIO_CMD_LIST; do
			key=$( echo "${cmd#BIO_}" | awk '{print tolower($0)}' )
			printf "\tbio_cmd == %s ? %s_%s++ :\n" \
				"$cmd" "$key" "$stat"
		done
		printf "\tunknown_%s++;\n" "$stat"
		printf "\n"
	done

	for var in $confvars; do
		printf "inline int %s_add_bio_cmd_start_bytes[" "$var"
		printf "int bio_cmd, uint64_t bytes] =\n"
		printf "\tbio_cmd == BIO_READ ? %s_read_bytes +=" "$var"
		printf " this->bio_length :\n"
		printf "\tbio_cmd == BIO_WRITE ? %s_write_bytes +=" "$var"
		printf " this->bio_length :\n"
		printf "\t0;\n\n"

		printf "inline int %s_add_bio_cmd_done_bytes[" "$var"
		printf "int bio_cmd, uint64_t bytes] =\n"
		printf "\tbio_cmd == BIO_READ ? %s_bytes_read +=" "$var"
		printf " bytes :\n"
		printf "\tbio_cmd == BIO_WRITE ? %s_bytes_written +=" "$var"
		printf " bytes :\n"
		printf "\t0;\n\n"

		for stat in $stat_list; do
			printf "inline int %s_increment_bio_cmd_%s" \
				"$var" "$stat"
			printf "[int bio_cmd] =\n"
			for cmd in $BIO_CMD_LIST; do
				key=$var_$( echo "${cmd#BIO_}" |
					awk '{print tolower($0)}' )
				printf "\tbio_cmd == %s ? %s_%s_%s++ :\n" \
					"$cmd" "$var" "$key" "$stat"
			done
			printf "\t%s_unknown_%s++;\n" "$var" "$stat"
			printf "\n"
		done
	done
)

BEGIN /* probe ID $ID */
{${TRACE:+
	printf("<$ID>");}
	set_globals_to[0];
$( IFS=" "
	for var in $confvars; do
		printf "\n\t%s_set_globals_to[0];" "$var"
	done
)
}

/****************************** I/O ******************************/

io:::start, io:::done /* probe ID $(( $ID + 1 )) */
{${TRACE:+
	printf("<$(( $ID + 1 ))>");}
	this->bio = (struct bio *)args[0];
}

io:::start, io:::done /this->bio != NULL/ /* probe ID $(( $ID + 2 )) */
{${TRACE:+
	printf("<$(( $ID + 2 ))>");
}
	/*
	 * struct bio *
	 */
	this->bufinfo = xlate <bufinfo_t> ((struct bio *)this->bio);
	this->b_flags = (int)this->bufinfo.b_flags;
	this->b_error = this->b_flags & BIO_ERROR == BIO_ERROR ? 1 : 0;
	this->bio_cmd = (int)this->bufinfo.b_cmd;
	this->bio_length = (long)this->bufinfo.b_bcount;

	/*
	 * struct devstat *
	 */
	this->devinfo = xlate <devinfo_t> ((struct devstat *)args[1]);
	this->device_entry = device_name[this->devinfo];
	this->device_if = device_if[(int)this->devinfo.dev_type];
	this->device_type = device_type[(int)this->devinfo.dev_type];

	/* De-duplicate events */
	this->bio = (this->device_entry == "[0,-1]" ? NULL : this->bio);
}

$( IFS=" "
	id=$(( $ID + 3 ))
	for var in $confvars; do
		eval val=\"\$$var\"

		printf "io:::start\n"
		printf "\t/this->bio != NULL"
		[ "$_EVENT_TEST" ] && printf " && %s" "$_EVENT_TEST"
		printf " && (%s)/\n" "$val"
		printf "\t/* probe ID %u */\n" "$id"
		printf "{${TRACE:+\n\tprintf(\"<$id>\");}\n"
		printf "\t%s_increment_bio_cmd_start[this->bio_cmd];\n" "$var"
		printf "\t%s_add_bio_cmd_start_bytes[this->bio_cmd," "$var"
		printf " this->bio_length];\n"
		printf "}\n\n"
		id=$(( $id + 1 ))

		printf "io:::done\n"
		printf "\t/this->bio != NULL"
		[ "$_EVENT_TEST" ] && printf " && %s" "$_EVENT_TEST"
		printf " && (%s)/\n" "$val"
		printf "\t/* probe ID %u */\n" "$id"
		printf "{${TRACE:+\n\tprintf(\"<$id>\");}\n"
		printf "\t%s_increment_bio_cmd_done[this->bio_cmd];\n" "$var"
		printf "\t%s_add_bio_cmd_done_bytes[this->bio_cmd," "$var"
		printf " this->bio_length];\n"
		printf "}\n\n"
		id=$(( $id + 1 ))

		printf "io:::start, io:::done /this->bio != NULL &&\n"
		printf "\tthis->b_error"
		[ "$_EVENT_TEST" ] && printf " && %s" "$_EVENT_TEST"
		printf " && (%s)/\n" "$val"
		printf "\t/* probe ID %u */\n" "$id"
		printf "{${TRACE:+\n\tprintf(\"<$id>\");}\n"
		printf "\t%s_increment_bio_cmd_error[this->bio_cmd];\n" "$var"
		printf "}\n\n"
		id=$(( $id + 1 ))
	done
)

io:::start /this->bio != NULL${_EVENT_TEST:+ &&
	$_EVENT_TEST}${_CONF_OTHER:+ &&
	$_CONF_OTHER}/
	/* probe ID $(( $ID + 3 + $nconf * 3 )) */
{${TRACE:+
	printf("<$(( $ID + 3 + $nconf * 3 ))>");
}
	increment_bio_cmd_start[this->bio_cmd];
	add_bio_cmd_start_bytes[this->bio_cmd, this->bio_length];
}

io:::done /this->bio != NULL${_EVENT_TEST:+ &&
	$_EVENT_TEST}${_CONF_OTHER:+ &&
	$_CONF_OTHER}/
	/* probe ID $(( $ID + 4 + $nconf * 3 )) */
{${TRACE:+
	printf("<$(( $ID + 4 + $nconf * 3 ))>");
}
	increment_bio_cmd_done[this->bio_cmd];
	add_bio_cmd_done_bytes[this->bio_cmd, this->bio_length];
}

io:::start, io:::done /this->bio != NULL &&
	this->b_flags & BIO_ERROR == BIO_ERROR${_EVENT_TEST:+ &&
	$_EVENT_TEST}${_CONF_OTHER:+ &&
	$_CONF_OTHER}/
	/* probe ID $(( $ID + 5 + $nconf * 3 )) */
{${TRACE:+
	printf("<$(( $ID + 5 ))>");}
	increment_bio_cmd_error[this->bio_cmd];
}
EOF
ACTIONS=$( cat <&9 )
ID=$(( $ID + 6 + $nconf * 3 ))

############################################################ EVENT TAG

# The EVENT_TAG is run inside the print action after the timestamp has been
# printed. By default, `UID.GID CMD[PID]: ' of the process is printed.

EVENT_TAG="printf(\"%s: \", \"${PROFILE%-raw}\")"

############################################################ EVENT DETAILS

SECONDS="walltimestamp / 1000000000"

exec 9<<EOF
	/*
	 * Print JSON
	 * NB: D does NOT allow for floating point calculations
	 */
	printf("{$( IFS=" "
		 jstr report_type "$REPORT_TYPE"
		_jstr hostname "$HOSTNAME"
		_jstr "$TAG_NAME" "$UNMATCHED_LABEL"
		_jraw epoch %u
		for cmd in $bio_cmd_list unknown; do
			for stat in $stat_list; do
				_jraw ${cmd}_$stat %lu
				_jraw ${cmd}_${stat}_rate %lu
			done
		done
		for bytes in $bytes_list; do
			_jraw $bytes %lu
			_jraw ${bytes}_rate %lu
		done
		_jraw start_bytes %lu
		_jraw start_bytes_rate %lu
		_jraw bytes_done %lu
		_jraw bytes_done_rate %lu
	)}\n",
		$SECONDS,
		$( IFS=" "
			for cmd in $bio_cmd_list unknown; do
				for stat in $stat_list; do
					key=${cmd}_$stat
					printf "\n\t\t%s," "$key"
					printf "\n\t\t%s / %u," \
						"$key" "$PROBE_SECONDS"
				done
			done
			for bytes in $bytes_list; do
				printf "\n\t\t%s," "$bytes"
				printf "\n\t\t%s / %u," \
					"$bytes" "$PROBE_SECONDS"
			done
		)
		read_bytes + write_bytes,
		(read_bytes + write_bytes) / $PROBE_SECONDS,
		bytes_read + bytes_written,
		(bytes_read + bytes_written) / $PROBE_SECONDS
	);
	$( IFS=" "
	   for var in $confvars; do
	   	eval label=\"\$${var}_label\"
		printf "\n\tprintf(\"{"
			 jstr report_type "$REPORT_TYPE"
			_jstr hostname "$HOSTNAME"
			_jstr "$TAG_NAME" "${label:-$var}"
			_jraw epoch %u
			for cmd in $bio_cmd_list unknown; do
				for stat in $stat_list; do
					key=${cmd}_$stat
					_jraw $key %lu
					_jraw ${key}_rate %lu
				done
			done
			for bytes in $bytes_list; do
				key=$bytes
				_jraw $key %lu
				_jraw ${key}_rate %lu
			done
			_jraw start_bytes %lu
			_jraw start_bytes_rate %lu
			_jraw bytes_done %lu
			_jraw bytes_done_rate %lu
		printf "}\\\n\""
		printf ", %s" "$SECONDS"
		for cmd in $bio_cmd_list unknown; do
			for stat in $stat_list; do
				key=${var}_${cmd}_$stat
				printf ", %s" "$key"
				printf ", %s / %u" "$key" "$PROBE_SECONDS"
			done
		done
		for bytes in $bytes_list; do
			key=${var}_$bytes
			printf ", %s" "$key"
			printf ", %s / %u" "$key" "$PROBE_SECONDS"
		done
		printf ", %s_read_bytes + %s_write_bytes" "$var" "$var"
		printf ", (%s_read_bytes + %s_write_bytes) / %u" \
			"$var" "$var" "$PROBE_SECONDS"
		printf ", %s_bytes_read + %s_bytes_written" "$var" "$var"
		printf ", (%s_bytes_read + %s_bytes_written) / %u" \
			"$var" "$var" "$PROBE_SECONDS"
		printf ");"
	   done
	)

	/*
	 * Reset counters
	 */
	set_globals_to[0];
$( IFS=" "
	for var in $confvars; do
		printf "\t%s_set_globals_to[0];\n" "$var"
	done
)
EOF
EVENT_DETAILS=$( cat <&9 )

################################################################################
# END
################################################################################
