z/OSの新しい管理方法を探る - (4) RMF MonitorIIIレポートのElasticsearchへの取り込み

# はじめに
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への取り込み](https://qiita.com/tomotagwork/items/362b0db5d4479eecebac)
[z/OSの新しい管理方法を探る - (2)-1 SYSLOGのElasticsearchへの取り込み](https://qiita.com/tomotagwork/items/867dd353cf8c985eab76)
[z/OSの新しい管理方法を探る - (2)-2 SYSLOGのElasticsearchへの取り込み part2](https://qiita.com/tomotagwork/items/e2c6c59a40f7ea00160b)
[z/OSの新しい管理方法を探る - (2)-3 SYSLOGのElasticsearchへの取り込み part3](https://qiita.com/tomotagwork/items/79faddc4e6afa28849ec)
[z/OSの新しい管理方法を探る - (3)CICSヒストリカルデータのElasticsearchへの取り込み](https://qiita.com/tomotagwork/items/c7cd4b9e5ffcb8ede0f1)
[z/OSの新しい管理方法を探る - (4) RMF MonitorIIIレポートのElasticsearchへの取り込み](https://qiita.com/tomotagwork/items/7e7e9b8b6432739b3e3a)

# 全体像
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/132331/69efeb44-9d39-9c81-c322-5fea3b8c8c0e.png)

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関連設定](https://qiita.com/tomotagwork/items/7be3c192e5e1c1d7e3bc#rmf%E9%96%A2%E9%80%A3%E8%A8%AD%E5%AE%9A)

# Linux側構成

## Elasticsearch/Kibana/fluentd準備
各OSSをインストールして動かせるようにしておきます。これは色々情報が転がってるのでここでは割愛。
参考:
[fluentd/Elasticsearch/kibanaを試す](https://qiita.com/tomotagwork/items/6c8e7ad59095bce9a21e)
[fluentdメモ](https://qiita.com/tomotagwork/items/5765b8c1300b89590a7e)

## データ取得用シェル・スクリプト
さて、ここが今回の仕組みの肝となる所です。
以下に示されているように、DDSを使うと各種パフォーマンスデータをHTTPリクエストでRESTっぽく取得することができます。
参考: [RMF Distributed Data Server を使用したパフォーマンス・データへのアクセス](https://www.ibm.com/support/knowledgecenter/ja/SSLTBW_2.3.0/com.ibm.zos.v2r3.erbb700/ddsapi.htm)
RESTっぽくという言い方をしたのは、返ってくる結果がXMLであり、かつ、ちょっと面倒な構造になっているのでRESTと呼ぶのが憚られるからです。DDSから返されるXMLそのままのフォーマットだと取扱いが非常に厄介なため、このシェル・スクリプトでfluentd経由で取り込みやすいようにデータの加工も行うことにします。

※データの取得/加工の際に以下のコマンドを使用しています。無ければ必要に応じてパッケージ追加してください。

- curl
- xmllint
- xsltproc
- jq

### DDS経由でのデータ取得
まずcurlコマンドでHTTPリクエストを発行し、XMLデータを取得します。
参考: [レポート 要求の指定方法](https://www.ibm.com/support/knowledgecenter/ja/SSLTBW_2.3.0/com.ibm.zos.v2r3.erbb700/report.htm)
※当記事は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](https://github.com/tomotagwork/zOS_with_ELK/blob/master/RMF_MonitorIII/SampleXML/sample_SYSINFO.xml)

ちなみにrangeパラメーター指定でレポート対象の範囲を指定することもできます(指定しない場合直近のデータが取得されるようです)。
参考: [range パラメーター](https://www.ibm.com/support/knowledgecenter/ja/SSLTBW_2.3.0/com.ibm.zos.v2r3.erbb700/erbb70026.htm)

XMLの構造は以下に説明されている通りですが、これをElasticsearchに取り込もうとするとそのままの形ではかなり難しそうです。
[DDS から返された XML 文書の解釈の仕方 - Report エレメント](https://www.ibm.com/support/knowledgecenter/ja/SSLTBW_2.3.0/com.ibm.zos.v2r3.erbb700/xmlreport.htm)

実際のデータはcaption要素とrow要素以下がメインとなります。内容はレポートによりますが、SYSINFOだとcaptionは全体の情報、rowはWLMグループごとの情報が出力されています。

```xml:caption部分抜粋
    <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要素はさらに厳しい。

```xml: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](https://www.ibm.com/support/knowledgecenter/SSLTBW_2.3.0/com.ibm.zos.v2r3.erbb700/erbb700165.htm)

もう一つPROCUレポートを取得してみると、結果はこんな感じです。こちらはcaption部分は無くrowのみです。
[sample_PROCU.xml](https://github.com/tomotagwork/zOS_with_ELK/blob/master/RMF_MonitorIII/SampleXML/sample_PROCU.xml)

### XML to JSON変換
さて、取得できるXMLフォーマットがそのままの形ではどうにもならないので、扱いやすいように加工することにします。ここではxsltを使ってJSON形式に変換するために以下のようなxsltを作成しました。

```xml:rmf_xml2json.xsl
<?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) &gt; 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) &gt; 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())]) &gt; 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](https://github.com/tomotagwork/zOS_with_ELK/blob/master/RMF_MonitorIII/SampleJSON/sample_SYSINFO.json)
[sample_PROCU.json](https://github.com/tomotagwork/zOS_with_ELK/blob/master/RMF_MonitorIII/SampleJSON/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](https://github.com/tomotagwork/zOS_with_ELK/blob/master/RMF_MonitorIII/SampleOutput/SYSINFO_caption_20200916.txt)
[SYSINFO_row_20200916.txt](https://github.com/tomotagwork/zOS_with_ELK/blob/master/RMF_MonitorIII/SampleOutput/SYSINFO_row_20200916.txt)

### シェル・スクリプトまとめ

ここまでの処理をまとめて、シェル・スクリプト化するとこんな感じになりました。

データ取得&ファイル出力用の汎用シェル・スクリプト

```sh:curl
#!/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}
```

制御用の親シェル・スクリプト

```sh:getRMFIII.sh
#!/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](https://www.ibm.com/support/knowledgecenter/SSLTBW_2.3.0/com.ibm.zos.v2r3.erbb700/erbb700151.htm)

```
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種類のデータを取り込むための構成ファイルを作成します。

```xxx:td-agent-rmf3.conf
<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
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/132331/aa8e2ccb-218f-5b3f-b614-906cd7adc0b4.png)
CPU関連のフィールド(SYSCUVVC, SYSTSVVC, SYSTSEVC)を時系列の折れ線グラフにしたもの

### PROCU - row
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/132331/52ff5cfd-7c00-17ad-b875-152b36fcfffc.png)
CP EAppl%のフィールド(PRUPCPE)をJOB名(PRUPJOB)ごとに時系列で積み上げグラフにしたもの(左)
指定範囲内でのJOB名ごとのCP EAppl%の最小、平均、最大を示したテーブル(右)

### DELAY - row
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/132331/477ab451-18fa-c89d-91a5-423ebcb17fd4.png)
DELAYの種類の内訳(左)
Processor DelayのJOB名内訳(右)

# おわりに
fluentd経由でElasticsearchにRMF MonitroIIIレポートを取り込む一連の流れをやってみました。あとは具体的にどのレポートのどの項目をどのように可視化したいのか、ということによって、もう少し細かいカスタマイズを適宜加えていくことになると思います。