はじめに
z/OS上のインフラ関連の情報をElasticsearchに取り込むシリーズの1つ。今回はRMF MonitorIIIレポートです。
DDS(Distributed Data Server)というz/OS提供のコンポーネントを立てれば、RMFのMonitorIIIレポートをRESTっぽくHTTP経由で取得することができます。ここではそれを利用してRMF MonitorIIIレポートを取得してElasticsearchにデータを取り込んでみたいと思います。(DDSはz/OS提供のコンポーネントなので追加のS/Wライセンスなどは不要です。)
関連記事
z/OSの新しい管理方法を探る - (1)DAパネル情報のElasticsearchへの取り込み
z/OSの新しい管理方法を探る - (2)-1 SYSLOGのElasticsearchへの取り込み
z/OSの新しい管理方法を探る - (2)-2 SYSLOGのElasticsearchへの取り込み part2
z/OSの新しい管理方法を探る - (2)-3 SYSLOGのElasticsearchへの取り込み part3
z/OSの新しい管理方法を探る - (3)CICSヒストリカルデータのElasticsearchへの取り込み
z/OSの新しい管理方法を探る - (4) RMF MonitorIIIレポートのElasticsearchへの取り込み
全体像
Linux上のシェル・スクリプトからcurlコマンドでhttpリクエストをDDSに対して発行してRMF MonitorIIIレポートを取得し、結果をLinux上のファイルに書きだします。
fluentdはそのファイルを読み取ってElasticsearchにデータを投入します。
データを取得するシェル・スクリプトはcronなどを使用して一定間隔でデータを取得するイメージです(5分間隔など)。
環境情報
【Host】
z/OS V2.3
【Linux】
RHEL V7.5
Elasticsearch V7.6.2
Kibana V7.6.2
fluentd V1.0
z/OS側構成
別の記事でCDPz(Common Data Provider for z/OS)という有償製品を使った方法についても記載していますが、そちらもDDS経由でデータを取得することになります。その記事でRMF MonitorIII、および、DDSの有効化について記載していますので、構成についてはそちらをご参照ください。
参考: CDPzを利用したz/OS-ELK連携 - (8)カスタマイズ-RMF Monitor III - RMF関連設定
Linux側構成
Elasticsearch/Kibana/fluentd準備
各OSSをインストールして動かせるようにしておきます。これは色々情報が転がってるのでここでは割愛。
参考:
fluentd/Elasticsearch/kibanaを試す
fluentdメモ
データ取得用シェル・スクリプト
さて、ここが今回の仕組みの肝となる所です。
以下に示されているように、DDSを使うと各種パフォーマンスデータをHTTPリクエストでRESTっぽく取得することができます。
参考: RMF Distributed Data Server を使用したパフォーマンス・データへのアクセス
RESTっぽくという言い方をしたのは、返ってくる結果がXMLであり、かつ、ちょっと面倒な構造になっているのでRESTと呼ぶのが憚られるからです。DDSから返されるXMLそのままのフォーマットだと取扱いが非常に厄介なため、このシェル・スクリプトでfluentd経由で取り込みやすいようにデータの加工も行うことにします。
※データの取得/加工の際に以下のコマンドを使用しています。無ければ必要に応じてパッケージ追加してください。
- curl
- xmllint
- xsltproc
- jq
DDS経由でのデータ取得
まずcurlコマンドでHTTPリクエストを発行し、XMLデータを取得します。
参考: レポート 要求の指定方法
※当記事はz/OS V2.3の環境で試した結果を記載しています。z/OS V2.1以前とV2.2以降ではHTTPリクエストの作法が若干異なりますのでV2.1以前の環境を利用する場合はご注意ください。
DDS経由でRMF MonitorIIIのレポートを取得するには、以下のような形式でHTTPリクエストを発行します。
http://ddshost:8803/gpm/rmfm3.xml?report=SYSINFO&resource=,ZOS1,MVS_IMAGE
上の例は、システム名ZOS1に対するSYSINFOレポートを取得するためのものです。
このようなリクエストを発行すると、XMLが結果として返されます。
sample_SYSINFO.xml
ちなみにrangeパラメーター指定でレポート対象の範囲を指定することもできます(指定しない場合直近のデータが取得されるようです)。
参考: range パラメーター
XMLの構造は以下に説明されている通りですが、これをElasticsearchに取り込もうとするとそのままの形ではかなり難しそうです。
DDS から返された XML 文書の解釈の仕方 - Report エレメント
実際のデータはcaption要素とrow要素以下がメインとなります。内容はレポートによりますが、SYSINFOだとcaptionは全体の情報、rowはWLMグループごとの情報が出力されています。
<caption>
<var>
<name>SYSPARVC</name>
<value/>
</var>
<var>
<name>SYSMODVC</name>
<value>2964</value>
</var>
<var>
<name>SYSMDLVC</name>
<value>740</value>
</var>
....
</caption>
<var>
の要素として、<name>
、 <value>
という子要素がありそれぞれが項目名と値を意味します。コレジャナイ感がはんぱない!(書くなら普通こうでしょ=><SYSMODVC>2964</SYSMODVC>
)
row要素はさらに厳しい。
<row refno="1">
<col>*SYSTEM</col>
<col/>
<col>80</col>
<col>134</col>
<col>0</col>
<col>0.42</col>
<col>69</col>
<col/>
<col>0.2</col>
<col>0.2</col>
...
</row>
<row refno="2">
<col>*TSO</col>
<col/>
<col>100</col>
<col>14</col>
<col>0</col>
<col>0.42</col>
<col>0</col>
<col/>
<col>0.0</col>
<col>0.0</col>
...
</row>
...
</row>
<column-headers>
<col type="T">SYSNAMVC</col>
<col type="T">SYSTYPVC</col>
<col type="N">SYSWFLVC</col>
<col type="N">SYSTUSVC</col>
<col type="T">SYSAUSVC</col>
<col type="T">SYSTRSVC</col>
<col type="T">SYSAFCVC</col>
<col type="N">SYSRSPM</col>
<col type="N">SYSAUPVC</col>
...
</column-haders>
row要素の子要素にWLMグループごとのデータが含まれているのですが、全部<col>
という要素名になってて、それらの値が何の項目なのかは、末尾にある<column-headers>
の要素とマッピングしてあげないと分からない!CSVを無理やりXMLにしました、みたいな構造ですね。
ちなみに各項目の意味はマニュアルの以下の辺りに記載があります。
SYSINFO - Tabular report data table ERBSYST3
もう一つPROCUレポートを取得してみると、結果はこんな感じです。こちらはcaption部分は無くrowのみです。
sample_PROCU.xml
XML to JSON変換
さて、取得できるXMLフォーマットがそのままの形ではどうにもならないので、扱いやすいように加工することにします。ここではxsltを使ってJSON形式に変換するために以下のようなxsltを作成しました。
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text" />
<xsl:variable name="sysname" select="substring-before(substring-after(//report/resource/reslabel,','),',')"/>
<xsl:template match="ddsml">
{
<xsl:apply-templates select="report"/>
}
</xsl:template>
<xsl:template match="report">
<xsl:if test="count(caption) > 0">
"caption": {
"startTime":"<xsl:value-of select="//report/time-data/display-start"/>",
"endTime":"<xsl:value-of select="//report/time-data/display-end"/>",
"sysname":"<xsl:value-of select="$sysname"/>"
<xsl:for-each select="caption/var">
<xsl:if test="value != '' and value !='-'">
,"<xsl:value-of select="name"/>":"<xsl:value-of select="value"/>"
</xsl:if>
</xsl:for-each>
},
</xsl:if>
<xsl:if test="count(row) > 0">
"row":[
<xsl:for-each select="row">
{
"startTime":"<xsl:value-of select="//report/time-data/display-start"/>",
"endTime":"<xsl:value-of select="//report/time-data/display-end"/>",
"sysname":"<xsl:value-of select="$sysname"/>"
<xsl:for-each select="col">
<xsl:if test=". != '' and . !='N/A'">
<xsl:variable name="position" select="position()"/>
,"<xsl:value-of select="//column-headers/col[$position]"/>":"<xsl:value-of select="."/>"
</xsl:if>
</xsl:for-each>
}
<xsl:if test="count(following-sibling::*[name() = name(current())]) > 0">
<xsl:text>,</xsl:text>
</xsl:if>
</xsl:for-each>
]
</xsl:if>
</xsl:template>
</xsl:stylesheet>
このxsltでは以下のような処理を行っています。
- caption要素について、
<name>
、<value>
を組み合わせて"SYSMODVC":"2964"
というような1つの項目として扱います。 - row要素についても、
<column-headers>
と組み合わせて"SYSNAMVC":"*TSO"
というように項目名と値を対応させます。rowには複数要素が含まれているので配列として保持します。 - レポート対象の開始時刻、終了時刻、システム名を、captionの先頭、および、rowの各要素の先頭に含めるようにします。システム名は
<reslabel>
の値(",ZOS1,MVS_IMAGE")からカンマ区切りの真ん中の値を抽出しています。 - 空の要素はJSONデータから除外します。
- captionのvalueが"-"となっているもの、および、rowのcolが"N/A"になっているものはJSONデータから除外します(同じ項目名でも数値情報と文字列情報が混在することになり、後にElasticsarchに取り込む際に不都合が生じるため)。
さて、このxsltを使ってxsltprocコマンドでXMLを加工すると、先のxmlはそれぞれ以下のようなJSONに変換されます(jqコマンドは余分な改行などを削除して整形する目的で使用しています)。
コマンド例:
xsltproc rmf_xml2json.xsl sample_SYSINFO.xml | jq . > sample_SYSINFO.json
xsltproc rmf_xml2json.xsl sample_PROCU.xml | jq . > sample_PROCU.json
結果例:
sample_SYSINFO.json
sample_PROCU.json
これでだいぶ扱いやすくなりました。
割と汎用的にxsltを作れたと思います。少なくともSYSINFO, PROCU, DELAYのレポートが生成できることは確認済み。
ファイルへの出力
fluentdでElasticsearchに取り込みやすくするために、captionとrowは分けて、かつ、Elasticsearchの1レコード相当のデータを1行に1JSONフォーマットで出力するようにします。
JSONデータの必要な要素を抽出するにはjqコマンドが使えます。
caption部分: jq -c .caption sample_SYSINFO.json >> SYSINFO_caption_20200916.txt
row部分: jq -c .row[] sample_SYSINFO.json >> SYSINFO_row_20200916.txt
結果例
SYSINFO_caption_20200916.txt
SYSINFO_row_20200916.txt
シェル・スクリプトまとめ
ここまでの処理をまとめて、シェル・スクリプト化するとこんな感じになりました。
データ取得&ファイル出力用の汎用シェル・スクリプト
#!/bin/sh
### Input parameters
if [[ $# -lt 3 ]]
then
echo Error: invalid arguments
echo " Usage: " `basename $0` "<reportName> <systemName> <interval(min)>"
echo " Example: " `basename $0` "PROCU ZOS1 5"
exit 1
fi
reportName=$1
systemName=$2
intervalMin=$3
### Other parameters
uriBase="http://eplex1:18803/gpm/rmfm3.xml"
user=CICS004
password=xxxxxxxx
outputDir=output
pid=$$
### function for removing temporary files
function func_removeTempFile(){
tempFile=$1
if [[ -e ${tempFile} ]]
then
rm ${tempFile}
fi
}
### Create outputDir
if [[ ! -e ${outputDir} ]]
then
mkdir ${outputDir}
fi
### Calculate startTime and endTime from intervalMin
thisTime=`date "+%Y%m%d %H:%M"`
startTime=`date --date "${thisTime} ${intervalMin} minutes ago" +%Y%m%d%H%M%S`
endTime=`date --date "${thisTime}" +%Y%m%d%H%M%S`
#echo startTime: ${startTime}
#echo endTime: ${endTime}
### Create URI
uriAll="${uriBase}?report=${reportName}&resource=,${systemName},MVS_IMAGE&range=${startTime},${endTime}"
### Issue request and get XML
tempXMLFile=/tmp/temp_${pid}_${reportName}_${systemName}_${endTime}.xml
curl -X GET -s -u ${user}:${password} ${uriAll} | xmllint --format - > ${tempXMLFile}
rc=$?
if [[ ${rc} -ne 0 ]]
then
echo `date --date "${thisTime}" +%Y/%m/%d-%H:%M:%S` : curl error
func_removeTempFile ${tempXMLFile}
exit 1
fi
### Convert from XML to JSON
tempJsonFile=/tmp/temp_${pid}_${reportName}_${systemName}_${endTime}.json
xsltproc rmf_xml2json.xsl ${tempXMLFile} | jq . > ${tempJsonFile}
rc=$?
read line < ${tempJsonFile}
if [[ ${rc} -ne 0 ]] || [[ ${line} == "{}" ]]
then
echo `date --date "${thisTime}" +%Y/%m/%d-%H:%M:%S` : xsltproc error
echo -------------------------------
cat ${tempXMLFile}
echo -------------------------------
func_removeTempFile ${tempXMLFile}
func_removeTempFile ${tempJsonFile}
exit 1
fi
### set output file name
thisDate=`date --date "${thisTime}" +%Y%m%d`
outputFile_row=${outputDir}/${reportName}_row_${thisDate}.txt
outputFile_caption=${outputDir}/${reportName}_caption_${thisDate}.txt
### extract row data in compact format and append to outputFile_row
checkRow=`jq -c .row[] ${tempJsonFile}`
if [[ ${checkRow} != "null" ]]
then
jq -c .row[] ${tempJsonFile} >> ${outputFile_row}
fi
### extract caption data in compact format and append to outputFile_caption
checkCaption=`jq -c .caption ${tempJsonFile}`
if [[ ${checkCaption} != "null" ]]
then
jq -c .caption ${tempJsonFile} >> ${outputFile_caption}
fi
### Remove temporary file
func_removeTempFile ${tempXMLFile}
func_removeTempFile ${tempJsonFile}
制御用の親シェル・スクリプト
#!/bin/sh
reportList="PROCU SYSINFO DELAY"
systemName=ZOS1
interval=5
cd `dirname $0`
for reportName in ${reportList}
do
echo ${reportName}
./curlRMFIII.sh ${reportName} ${systemName} ${interval} &
done
上の例はZOS1に対してPROCU, SYSINFO, DEALYのレポートを取得するスクリプトです。getRMFIII.shをcronで5分間隔で定期的に実行することを想定して作成しています。
スクリプト中のinterval=5は、このスクリプトを実行する間隔に合わせて変更してください(10分間隔でデータ取るのであればinterval=10を指定)。
Elasticsearchの準備
Elasticsearchにデータを取り込む前に、Index Templateを作成しておきます。Index名は"rmf3-sysinfo-row-YYYYMMDD"というような形式を想定します。
まず、今回のレポートで共通の設定を行います。今回はテスト用環境なので1ノードのElasticsearchを想定しているため、シャード数:1, レプリカ数:0に指定します(この辺りは実際の環境に合わせて適宜設定してください)。また、数値フィールドは自動判別させるためnumeric_detectionをtrueに設定します。
PUT _template/rmf3
{
"index_patterns": ["rmf3-*"],
"order" : 0,
"settings": {
"number_of_shards": 1,
"number_of_replicas" : 0
},
"mappings": {
"numeric_detection": true
}
}
続いて、PROCUレポート用のIndex Templateについて追加設定します。
PROCUのレポートを取り込もうとすると、"PRUPCLP"というフィールドに数値と数値以外が混在していて取り込み時にエラーになりました。このフィールドは、"Service class period"を意味しますが、ここには数値以外に"*"が入る場合があるので、明示的に文字列のフィールドとして事前定義しておきます。
参考: PROCU - Tabular report data table ERBPRUT3
PUT _template/rmf3-procu
{
"index_patterns": ["rmf3-procu-*"],
"order" : 1,
"mappings": {
"properties": {
"PRUPCLP": {
"type": "text"
}
}
}
}
fluentdによるデータの取り込み
SYSINFO, PROCU, DELAYのレポートを出力させてそれらをfluentd経由でElasticsearchに取り込むための構成を行います。
上で作成したシェルスクリプトの出力ファイルを読み取り、Elasticsearchが想定するIndex名に合わせてデータを取り込みます。ここでは、SYSINFO(caption,row), PROCU(row), DELAY(row)の合計4種類のデータを取り込むための構成ファイルを作成します。
<source>
@type tail
path output/PROCU_row_*.txt
pos_file output/PROCU_row_pos.txt
tag rmf3.procu.row
read_from_head true
<parse>
@type json
time_key endTime
time_format "%m/%d/%Y %H:%M:%S"
keep_time_key
</parse>
</source>
<source>
@type tail
path output/SYSINFO_row_*.txt
pos_file output/SYSINFO_row_pos.txt
tag rmf3.sysinfo.row
read_from_head true
<parse>
@type json
time_key endTime
time_format "%m/%d/%Y %H:%M:%S"
keep_time_key
</parse>
</source>
<source>
@type tail
path output/SYSINFO_caption_*.txt
pos_file output/SYSINFO_caption_pos.txt
tag rmf3.sysinfo.caption
read_from_head true
<parse>
@type json
time_key endTime
time_format "%m/%d/%Y %H:%M:%S"
keep_time_key
</parse>
</source>
<source>
@type tail
path output/DELAY_row_*.txt
pos_file output/DELAY_row_pos.txt
tag rmf3.delay.row
read_from_head true
<parse>
@type json
time_key endTime
time_format "%m/%d/%Y %H:%M:%S"
keep_time_key
</parse>
</source>
<match rmf3.**>
@type elasticsearch_dynamic
host "localhost"
port 9200
logstash_format true
logstash_prefix "rmf3-${tag_parts[1]}-${tag_parts[2]}"
logstash_dateformat "%Y%m%d"
</match>
レポート取得時に指定したrangeのendTimeの部分を各レコードのタイムスタンプとして使用しています。
Kibanaによる可視化例
Elasticsearchにデータが取り込めればあとは可視化はKibanaの作法で可視化できます。例えば以下のようなグラフが作成可能です。
SYSINFO - caption
CPU関連のフィールド(SYSCUVVC, SYSTSVVC, SYSTSEVC)を時系列の折れ線グラフにしたもの
PROCU - row
CP EAppl%のフィールド(PRUPCPE)をJOB名(PRUPJOB)ごとに時系列で積み上げグラフにしたもの(左)
指定範囲内でのJOB名ごとのCP EAppl%の最小、平均、最大を示したテーブル(右)
DELAY - row
DELAYの種類の内訳(左)
Processor DelayのJOB名内訳(右)
おわりに
fluentd経由でElasticsearchにRMF MonitroIIIレポートを取り込む一連の流れをやってみました。あとは具体的にどのレポートのどの項目をどのように可視化したいのか、ということによって、もう少し細かいカスタマイズを適宜加えていくことになると思います。