z/OSの新しい管理方法を探る - (1)DAパネル情報のElasticsearchへの取り込み

# はじめに
メインフレーム、z/OS環境というのは、その"特異性"というのが利用における大きなネックになっていると思う訳で、UI(PCOMの黒画面)はその顕著な例だと思います。"メインフレームの特異性"というハードルを下げてOSSや標準技術を利用できると、幸せな世界が広がっていくんじゃないかなぁと考える次第です。
z/OSのUIについては以前以下のような記事をアップしましたが、やっぱりなかなかすぐには利用環境は変わっていきませんね。
[z/OSのユーザー・インターフェースについて考える ～teraterm利用のすゝめ～](https://qiita.com/tomotagwork/items/5ae230c7a215f6ef4409)
z/OSも色々と進化をしており、OS標準機能として提供されるz/OS MF(Management Facility)ではブラウザベースのUIやらREST APIやらを提供していたり、さらにそれをベースにOSSを活用したフレームワークとして[Zowe](https://www.ibm.com/blogs/systems/jp-ja/open-source-project-zowe-fast-simple-familiar-zos-development/) ("ゾーウィ"っていう感じで発音するらしい)というものが提供されてきたりしています。
UI以外の所でも、例えばアプリケーション連携については、メインフレーム上のアプリケーション資産(CICS, IMS, MQなどのミドルウェアベースのアプリ)を、REST APIで公開するためのz/OS Connect EE という製品が提供されています(参考: [z/OS Connectによるメインフレーム資産のAPI化](https://www.imagazine.co.jp/z-os-connect%E3%81%AB%E3%82%88%E3%82%8B%E3%83%A1%E3%82%A4%E3%83%B3%E3%83%95%E3%83%AC%E3%83%BC%E3%83%A0%E8%B3%87%E7%94%A3%E3%81%AEapi%E5%8C%96%E3%80%80%EF%BD%9Esor%E3%81%A8soe%E3%82%92%E6%9F%94/))。
また、ログの分析ということについては、SMFのデータなんかをオープンソースを活用して分析するための製品として、IBM Z Operations Analytics、あるいは、IBM Common Data Provider for z Systems という製品が提供されていたりします。

ただ、こういった製品をいきなり使うのはハードルが高い面もあるので(お金の面とか環境面とか)、まずはあまり製品とかを追加せずに、ベース機能でOSSがどの程度活用できるのかというのを探っていこうという試みです。
ということで、ここでは、z/OSのログ系の情報をElasticsearchに取り込んで、Kibanaで可視化する、ってのができるといいんじゃないの？ということでやってみました。
取り込む情報としては、まずはSDSFのDAパネルで見られる情報を対象とします。

## 関連記事
[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)


# やりたいこと
SDSFのDAパネル(Display Active Users Panel)では、その時点でのシステム全体のCPU使用率とか、稼働しているJOB毎のCPU使用率、ストレージ利用状況なんかが分かります。
こんな感じ。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/132331/aa84b347-f8f5-4715-55ae-5d65333dcb5a.png)


PCOMだとエンターを押すたびにその時のリアルタイムの状況が分かりますが、この情報を定期的に取得してそれをElasticsearchにぶち込みます。
データがElasticsearchに入ってしまえば、あとはKibanaで可視化するってのは汎用的なスキルでできるので、そこでz/OSの特異性が排除されることになります。

※ここでは一定間隔で(例えば1分毎とか5分毎など)、このDAパネルの情報をスクリプトで取得することを想定しています。つまりこの仕組みでは、スクリプトが動いた瞬間に実行されているアドレススペースの情報しか収集できません。すぐに終了してしまうJOBについては収拾されないことが多いので、あくまで全体の傾向把握や、ミドルウェア/ロングランニングのバッチなどの負荷の高いジョブの利用状況の把握に利用するイメージです。
より詳細な情報を把握したい場合は、RMFレポートなどを参照する必要があります。

# 実装
## 全体像
![image.png](https://qiita-image-store.s3.amazonaws.com/0/132331/d3649f7a-4ff8-6800-3247-bf5e7413debe.png)


## 前提

- ELKスタック: Elasticsearch, Logstash, Kibanaは、Linux上にサーバーをたててそこに環境を構築する前提です。この部分の構築は当記事では割愛します。
 参考: [fluentd/Elasticsearch/kibanaを試す: (1)インストール](https://qiita.com/tomotagwork/items/572d46d59c3eb5723a4d)
 ※あ、logstashの記事は書いてなかった...

- SSHD: Linuxからssh経由でUSS上のREXXを呼び出す想定ですので、z/OS側にはSSHサーバーの構成を行い、公開鍵認証を有効化しておく必要があります。

- SSH接続用ユーザー: USS上のREXXを実行するためのユーザーを用意しておきます(OMVSセグメントの設定を付与したRACFユーザーが必要)。また、REXXからSDSFを使えるよう適切な権限設定が必要になります。(ここはちょっと分かりにくいので末尾で補足します。)


## USS / 情報取得部分
DAパネルで表示される情報は、REXXを使うと割と簡単に取得することができますので、USS上に情報を取得するためのREXXスクリプトを用意しておきます。
この時、Elasticsearchに取り込みやすいように、時刻情報と合わせてcsv形式にして出力します。個人的にはREXXよりもShellScriptの方が馴染みがあるため、情報取得部分のみREXXを使い、全体の制御や加工はShellScriptで実施しています。
DAパネルで取得できる情報は、システム全体のものと、JOB単位のものが混在しているので、それぞれ別の情報として取得できるよう、出力のさせかたを少し工夫しています。
参考: [DAパネル フィールド情報](https://www.ibm.com/support/knowledgecenter/ja/SSLTBW_2.3.0/com.ibm.zos.v2r3.isfa600/isfa600ug_isfjvdad.htm)


### DAパネル取得用REXX

```rexx:da_sys_job.rex
/* REXX */
/*--------------------------------------------------------*/
/* Arg1: date  (yyyy/mm/dd)                               */
/* Arg2: time  (hh:mm:ss)                                 */
/* Arg3: output file (xxx.txt)                            */
/*--------------------------------------------------------*/

parse arg mydate mytime output_file


rc=isfcalls('ON')

/***************************/
/* Access the DA display   */
/***************************/
Address SDSF "ISFEXEC DA"
lrc=rc
if lrc<>0 then
  exit 20

/***************************/
/* for SYSTEM              */
/***************************/
call stream output_file, 'C','CLEARFILE'
rc = charout(output_file,,)

HEADER = "TIME,SYSNAME,SPAGING,SCPU,SZAAP,SZIIP,SLCPU"
/* rc = charout(output_file, HEADER,) */

LINE = MYDATE" "MYTIME","SYSNAME.1","SPAGING.1","SCPU.1","SZAAP.1","SZIIP.1","SLCPU.1
LINE = LINE || ESC_N
rc = charout(output_file, LINE,)

/***************************/
/* for JOB                 */
/***************************/

HEADER = "TIME,JNAME,STEPN,PROCS,JTYPE,JNUM,JOBID,OWNERID,JCLASS"
HEADER = HEADER",POS,DP,REAL,PAGING,EXCPRT,CPUPR,ASID,ASIDX,EXCP"
HEADER = HEADER",CPU,SWAPR,STATUS,SYSNAME,WORKLOAD,SRVCLASS"
HEADER = HEADER",PERIOD,RESGROUP,SERVER,QUIESCE,ECPU,ECPUPR,CPUCRIT"
HEADER = HEADER",STORCRIT,RPTCLASS,MEMLIMIT,TRANACT,TRANRES,SPIN,SECLABEL"
HEADER = HEADER",GCPTIME,ZAAPTIME,ZAAPCPTM,GCPUSE,ZAAPUSE"
HEADER = HEADER",PROMOTED,ZAAPNTIM,ZIIPTIME,ZIIPCPTM,ZIIPNTIM,ZIIPUSE"
HEADER = HEADER",IOPRIOGRP,JOBCORR"
/* rc = charout(output_file, HEADER,) */

/* Loop for all target jobs *****************************/

do ix=1 to JNAME.0
  LINE = MYDATE" "MYTIME","JNAME.ix","STEPN.ix","PROCS.ix","JTYPE.ix","JNUM.ix","JOBID.ix","OWNERID.ix","JCLASS.ix
  LINE = LINE","POS.ix","DP.ix","REAL.ix","PAGING.ix","EXCPRT.ix","CPUPR.ix","ASID.ix","ASIDX.ix","EXCP.ix
  LINE = LINE","CPU.ix","SWAPR.ix","STATUS.ix","SYSNAME.ix","WORKLOAD.ix","SRVCLASS.ix
  LINE = LINE","PERIOD.ix","RESGROUP.ix","SERVER.ix","QUIESCE.ix","ECPU.ix","ECPUPR.ix","CPUCRIT.ix
  LINE = LINE","STORCRIT.ix","RPTCLASS.ix","MEMLIMIT.ix","TRANACT.ix","TRANRES.ix","SPIN.ix","SECLABEL.ix
  LINE = LINE","GCPTIME.ix","ZAAPTIME.ix","ZAAPCPTM.ix","GCPUSE.ix","ZAAPUSE.ix
  LINE = LINE","PROMOTED.ix","ZAAPNTIM.ix","ZIIPTIME.ix","ZIIPCPTM.ix","ZIIPNTIM.ix","ZIIPUSE.ix
  LINE = LINE","IOPRIOGRP.ix","JOBCORR.ix
  /*say LINE*/
  LINE = LINE || ESC_N
  rc = charout(output_file, LINE,)
end

call stream output_file, 'C', 'CLOSE'

rc=isfcalls('OFF')
exit 0

```

### REXX呼び出し用シェル・スクリプト

※PATHは適宜環境に合わせて変更する必要があります

```sh:getDaSysJob.sh
#!/bin/sh

export PATH=$PATH:/u/tag/REXX:/u/tag/Shell
export TZ=JST-9
thisDateTime=$(date '+%Y/%m/%d %T')

outputFile=daSysJob_temp$(date '+%Y%m%d%H%M%S').txt

#get DA panel info
da_sys_job.rex ${thisDateTime} ${outputFile} > /dev/null 2>&1
rc=$?

### Error Handling
if [ ${rc} -gt 0 ]; then
  # do nothing
  # echo Error!: ${rc}
  rm ${outputFile}
  exit 1
fi

### Output
cat ${outputFile}
rm ${outputFile}

exit 0
```

### 実行例
これをUSS上で単発で実行すると、こんな感じになります。

```xxx:実行例
$ ./getDaSysJob.sh
2019/03/28 16:42:14,CPAC,0,1,0,0,1
2019/03/28 16:42:14,*MASTER*,,,STC,JNUM.1,STC04901,+MASTER+,,NS,FF,2455,0.00,0.00,0.00,1,0001,5519,3.60,,,CPAC,SYSTEM,SYSTEM,1,,NO,,3.60,0.00,NO,NO,,17179869180GB,24:19:21.53,24:19:21.53,NO,SYSHIGH,1.79,0.00,0.00,0.00,0.00,NO,0.00,0.00,0.00,0.00,0.00,NORMAL,
2019/03/28 16:42:14,PCAUTH,PCAUTH,,STC,JNUM.2,,,,NS,FF,142,0.00,0.00,0.00,2,0002,18,0.00,,,CPAC,SYSTEM,SYSTEM,1,,NO,,0.00,0.00,NO,NO,,,24:19:23.48,24:19:23.48,,,0.00,0.00,0.00,0.00,0.00,NO,0.00,0.00,0.00,0.00,0.00,NORMAL,
2019/03/28 16:42:14,RASP,RASP,,STC,JNUM.3,,,,NS,FF,291,0.00,0.00,0.00,3,0003,2,0.25,,,CPAC,SYSTEM,SYSTEM,1,,NO,,0.25,0.00,NO,NO,,,24:19:23.48,24:19:23.48,,,0.00,0.00,0.00,0.00,0.00,NO,0.00,0.00,0.00,0.00,0.00,NORMAL,
2019/03/28 16:42:14,TRACE,TRACE,,STC,JNUM.4,,,,NS,FF,617,0.00,0.00,0.00,4,0004,26,0.00,,,CPAC,SYSTEM,SYSTEM,1,,NO,,0.00,0.00,NO,NO,,36GB,24:19:23.48,24:19:23.48,,,0.00,0.00,0.00,0.00,0.00,NO,0.00,0.00,0.00,0.00,0.00,NORMAL,
2019/03/28 16:42:14,DUMPSRV,DUMPSRV,DUMPSRV,STC,JNUM.5,,,,NS,FF,586,0.00,0.00,0.00,5,0005,125,0.00,,,CPAC,SYSTEM,SYSTEM,1,,NO,,0.00,0.00,NO,NO,,17179869180GB,24:19:19.00,24:19:19.00,,,0.00,0.00,0.00,0.00,0.00,NO,0.00,0.00,0.00,0.00,0.00,NORMAL,
2019/03/28 16:42:14,XCFAS,XCFAS,IEFPROC,STC,JNUM.6,,,,NS,FF,3132,0.00,0.00,0.00,6,0006,163734,5.47,,,CPAC,SYSTEM,SYSTEM,1,,NO,,5.47,0.00,NO,NO,,17179869180GB,24:19:18.98,24:19:18.98,,,5.12,0.00,0.00,0.00,0.00,NO,0.00,0.00,0.00,0.00,0.00,NORMAL,
... 以下省略
```
1行目がシステム全体の情報、2行目以降がJOB毎の情報です。
それぞれを取得するスクリプトを分けてもよいのですが、両者を同じタイミングで取得したかったのでこのような仕様にしています(Linux側で両者を分けて取り込む想定)。
タイムスタンプはシェル・スクリプト上で取得したものを付与するようにしています(全行同じタイムスタンプになるように).

カレントディレクトリに一時ファイルとしてdaSysJob_tempxxxx.txtというようなファイルを作っています。実行後は自動削除されますが、ファイルシステムとして一時的にこれらの情報を保持できる分の余裕が必要です。
USS上にファイルを作らず直接標準出力に書き出すこともできますが、エラー時のハンドリングが容易だと思いこのようにしています。


## Linux-USS間のssh接続
上で準備したUSS上のスクリプトをLogstashが稼働しているLinuxマシンからssh経由で呼び出して結果を受取れるようにします。この時パスワード入力をしなくてすむように、公開鍵認証の設定をしておきます。
sshの公開鍵認証の設定については、以下を参照。
[マルチ・プラットフォームを扱うインフラ屋さんのための連携技術: SSH編](https://qiita.com/tomotagwork/items/b35aa09c1aa1acb3d554)

上で用意したUSS上のスクリプトは結果を標準出力に吐くようにしています。これをLinuxからssh経由で呼び出す想定です。普通sshとかscpではコード変換は行われませんが、z/OSのUSSの場合Ascii-EBCDIC変換を行ってくれるため、ここでは特にコード変換は考慮する必要がありません。
参考: [OpenSSH and globalization](https://www.ibm.com/support/knowledgecenter/en/SSLTBW_2.3.0/com.ibm.zos.v2r3.foto100/fotz127.htm)

sshの公開鍵認証設定が完了した前提で、Linux側からssh経由でスクリプトを実行してみるとこんな感じになります。

```xxx:実行例
$ ssh TAG@hostname REXX/getDaSysJob.sh
2019/03/28 16:47:03,CPAC,0,1,0,0,1
2019/03/28 16:47:03,*MASTER*,,,STC,JNUM.1,STC04901,+MASTER+,,NS,FF,2455,0.00,0.00,0.00,1,0001,5527,3.61,,,CPAC,SYSTEM,SYSTEM,1,,NO,,3.62,0.00,NO,NO,,17179869180GB,24:24:11.29,24:24:11.29,NO,SYSHIGH,1.80,0.00,0.00,0.00,0.00,NO,0.00,0.00,0.00,0.00,0.00,NORMAL,
2019/03/28 16:47:03,PCAUTH,PCAUTH,,STC,JNUM.2,,,,NS,FF,142,0.00,0.00,0.00,2,0002,18,0.00,,,CPAC,SYSTEM,SYSTEM,1,,NO,,0.00,0.00,NO,NO,,,24:24:13.24,24:24:13.24,,,0.00,0.00,0.00,0.00,0.00,NO,0.00,0.00,0.00,0.00,0.00,NORMAL,
2019/03/28 16:47:03,RASP,RASP,,STC,JNUM.3,,,,NS,FF,291,0.00,0.00,0.00,3,0003,2,0.25,,,CPAC,SYSTEM,SYSTEM,1,,NO,,0.25,0.00,NO,NO,,,24:24:13.24,24:24:13.24,,,0.00,0.00,0.00,0.00,0.00,NO,0.00,0.00,0.00,0.00,0.00,NORMAL,
2019/03/28 16:47:03,TRACE,TRACE,,STC,JNUM.4,,,,NS,FF,617,0.00,0.00,0.00,4,0004,26,0.00,,,CPAC,SYSTEM,SYSTEM,1,,NO,,0.00,0.00,NO,NO,,36GB,24:24:13.24,24:24:13.24,,,0.00,0.00,0.00,0.00,0.00,NO,0.00,0.00,0.00,0.00,0.00,NORMAL,
...以下省略
```

## Linux / ssh経由で情報を取得するシェル・スクリプト

上で用意したUSS上のREXXをSSH経由で実行し、DAパネルの情報を取得します。
取得した情報は、システム全体の情報(1行目)と、JOB毎の情報(2行目以降)に分割して、Linux上の別のファイルに格納します。

```sh:sshGetDaSysJob.sh
#!/bin/sh

outputDir=/var/log/zos_da/
yyyymmdd=$(date '+%Y%m%d')
outputFile1=${outputDir}daSys_zosmf_${yyyymmdd}.txt
outputFile2=${outputDir}daJob_zosmf_${yyyymmdd}.txt

idx=0
ssh TAG@hostname "REXX/getDaSysJob.sh" | while read line
do
        if [ ${idx} = 0 ]
        then
                echo ${line} >> ${outputFile1}
        else
                echo ${line} >> ${outputFile2}
        fi

        idx=$((idx+1))
done
```

単純にこのシェル・スクリプトを実行した時刻の情報が格納されるだけなので、cronなどを使って定期的にこのシェル・スクリプトを実行することを想定しています。(例えば1分間隔で実行するなど)
そうすると、勝手にどんどん指定した時間間隔でファイルのDAパネルの情報が蓄積されていくイメージとなります。

## LogstashによるElasticsearchへのデータ取り込み
下準備が整ったので、いよいよLogstashを使ってデータをElasticsearchに取り込みます。

### システム全体の情報

Logstashのfile input pluginを使って、上で取得したファイルを取り込みます。
csv filter pluginを使って、CSVの解釈を行い、各フィールドの型を指定します。また、date filter pluginを使ってタイムスタンプを認識させます。
Configファイルは以下の通り。

```config:da_sys_zosmf.conf
input{
        file{
                path => ["/var/log/zos_da/daSys_zosmf*.txt"]
                start_position => "beginning"
                sincedb_path => "/var/log/zos_da/tempDaSys_zosmf.sincedb"
        }
}

filter{
        csv{
                columns => ["date_time","sysname","spaging","scpu","szaap","sziip","slcpu"]
                remove_field => ["message","command"]
                convert => {
                        "spaging" => "float"
                        "scpu" => "float"
                        "szaap" => "float"
                        "sziip" => "float"
                        "slcpu" => "float"
                }
        }
        date{
                match => ["date_time", "yyyy/MM/dd HH:mm:ss"]
                target => "@timestamp"
                remove_field => ["date_time", "@version", "host"]
        }

}

output{
        elasticsearch{
                hosts => ["http://localhost:9200"]
                index => "da.sys-zosmf-%{+YYYY.MM.dd}"
        }
}
```

これで、da.sys-zosmf-yyyy.MM.dd というindexでElasticsearchに取り込まれます。

### ジョブ毎の情報

同様にジョブ毎の情報を取り込むLogstash構成ファイルを作成します。

```config:da_job_zosmf.conf
input{
        file{
                path => ["/var/log/zos_da/daJob_zosmf*.txt"]
                start_position => "beginning"
                sincedb_path => "/var/log/zos_da/tempDaJob_zosmf.sincedb"
        }
}

filter{
        csv{
                columns => ["date_time","jname","stepn","procs","jtype","jnum","jobid","ownerid","jclass","pos","dp","real","paging","excprt","cpupr","asid","asidx","excp","cpu","swapr",
"status","sysname","workload","srvclass","period","resgroup","server","quiesce","ecpu","ecpupr","cpucrit","storcrit","rptclass","memlimit","tranact","tranres","spin","seclabel","gcptime"
,"zaaptime","zaapcptm","gcpuse","zaapuse","promoted","zaapntim","ziiptime","ziipcptm","ziipntim","ziipuse","iopriogrp","jobcorr"]
                remove_field => ["message","command"]
                convert => {
                        "real" => "float"
                        "paging" => "float"
                        "excprt" => "float"
                        "cpupr" => "float"
                        "asid" => "integer"
                        "excp" => "float"
                        "cpu" => "float"
                        "ecpu" => "float"
                        "ecpupr" => "float"
                        "gcptime" => "float"
                        "zaaptime" => "float"
                        "zaapcptm" => "float"
                        "gcpuse" => "float"
                        "zaapuse" => "float"
                        "zaapntim" => "float"
                        "ziiptime" => "float"
                        "ziipcptm" => "float"
                        "ziipntim" => "float"
                        "ziipuse" => "float"
                }
        }
        date{
                match => ["date_time", "yyyy/MM/dd HH:mm:ss"]
                target => "@timestamp"
                remove_field => ["date_time", "@version", "host"]
        }

}

output{
        elasticsearch{
                hosts => ["http://localhost:9200"]
                index => "da.job-zosmf-%{+YYYY.MM.dd}"
        }
}
```

これで、da.job-zosmf-yyyy.MM.dd というindexでElasticsearchに取り込まれます。


## kibanaで可視化

Elasticsearchにデータが取り込まれてしまえば、あとはz/OSとは関係なく、Elasticsearch, Kibanaの世界でいかようにでも加工できますね。取り込んだデータを元に、いくつかグラフを作成してみましょう。

### インデックス・パターン作成
Kibanaの Management - Intex Patterns から、インデックス・パターンを作成します。
ここでは、システム全体用、JOB用、それぞれ、da.sys-zosmf-\*, da.job-zosmf-\* という名前で作成します。いずれもTime Filter field nameとしてtimestampを指定します。

### システム全体のCPU使用率の時系列グラフ
システム全体のCPU使用率を折れ線グラフで可視化してみます。
KibanaのVisualizeのメニューからLineを選択し、読み込むデータとして先ほど作成したインデックスパターンda.sys-zosmf-*を選択します。

Y軸としてscpu, slcpuを、X軸としてtimestampを指定して折れ線グラフを作成してみます。
こんな感じ。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/132331/af3b659a-e71a-9827-ed8d-8b6f7f501130.png)

参考: [パネル(DA)](https://www.ibm.com/support/knowledgecenter/ja/SSLTBW_2.3.0/com.ibm.zos.v2r3.isfa600/isfa600ug_isfjvdad.htm)より

>scpu: ジョブを処理しているシステムのシステム CPU パーセンテージ。
>slcpu: 最新の間隔内での、LPAR がシステムで使用中である時間のパーセンテージ。

cpu_sys_zosmfという名前で保存しておきます。

### JOB毎のCPU使用率の時系列グラフ(積み上げグラフ)
同様に、JOB毎のCPU使用率を積み上げグラフで可視化してみます。

KibanaのVisualizeのメニューからAreaを選択し、読み込むデータとして先ほど作成したインデックスパターンda.job-zosmf-*を選択します。

Y軸としてcpupr, slcpuを、X軸としてtimestampを指定して折れ線グラフを作成してみます。さらに、jname(JOB名)を系列として追加します。
こんな感じ。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/132331/b4464074-44cc-e86c-848d-994588912c95.png)

![image.png](https://qiita-image-store.s3.amazonaws.com/0/132331/fc1dc5e4-2deb-1ed0-fbb9-3641171476ad.png)



cpu_job_zosmfという名前で保存しておきます。

### JOB毎のCPU使用率平均値テーブル
JOB毎のCPU使用率については、積み上げグラフだけでなく、指定した期間の平均値上位を表形式でも表現してみます。
KibanaのVisualizeのメニューからData Tableを選択し、読み込むデータとして先ほど作成したインデックスパターンda.job-zosmf-*を選択します。

MetricとしてAverage / cpupr,  Max / cpuprを選択します (表示する項目として、cpu使用率の平均と最大値)。
Bucketsで Split RowsでJOB名(jname.keyword)を指定し、平均値上位20件を表示するようにします。

こんな感じ。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/132331/5c14d3b0-f166-6a86-f42b-9bc92d49cd20.png)

cpu_job_table_zosmfという名前で保存しておきます。

### ダッシュボード作成
最後に、上で作成した3つのオブジェクトをダッシュボードとして1画面に並べて表示してみます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/132331/95613ad1-cd52-4e63-dd17-5e4c79f9d0e4.png)

なかなかいい感じに可視化できました！


# 補足
## REXXからSDSFアクセスについて
さて、今回の仕組みはREXXからSDSFにアクセスするためのインターフェースを使っていますので、そのための適切な権限設定を行っておく必要があります。この辺りの考え方が恐ろしく分かりにくかったので補足しておきます(私の理解力が乏しいのを差し引いても分かりにくい仕組みだと思うんですが。よくこんな難解なもの使いこなせますなぁ)。

マニュアルとしてはこれ。
[z/OS SDSF User's Guide](https://www.ibm.com/support/knowledgecenter/ja/SSLTBW_2.3.0/com.ibm.zos.v2r3.isfa600/abstract.htm)
[z/OS SDSF Operation and Customization](https://www.ibm.com/support/knowledgecenter/ja/SSLTBW_2.3.0/com.ibm.zos.v2r3.isfa500/abstract.htm)

このマニュアルの以下の辺り。
[セキュリティーと REXX](https://www.ibm.com/support/knowledgecenter/ja/SSLTBW_2.3.0/com.ibm.zos.v2r3.isfa600/r19rxsec.htm)
[グループ許可パラメーター (GROUP または ISFGRP)](https://www.ibm.com/support/knowledgecenter/ja/SSLTBW_2.3.0/com.ibm.zos.v2r3.isfa500/isfgrpa.htm)

基本的にこの辺のセキュリティー制御はRACFのSDSFクラスで制御されるが、RACF上の定義とISFPARMSとで管理されることになり、その辺が複雑...。

まず前提として、SDSFの権限はグループ単位で制御することになる。管理用のグループだと全ての操作できるけど、オペレーター用のグループだと一部の機能は制限する、とか。この"グループ"の定義は、ISFPARMS(SDSF起動プロシージャーで指定されるISFPRMxx)で設定する。ここで、このグループはこういう権限もってるよ、という定義を行います(これはRACFには持たせられないので、必ずグループの定義はISFPRMxxが使われます)。

で、ユーザーとかREXXプログラムがSDSFの操作を行う場合、最初に、そのユーザーやらプログラムやらがどのグループに属するのか、というのを判断するらしく、それを"グループ・メンバーシップ"の制御と呼んでいるようです。この判定には、RACFを使うか、もしくは、ISFPRMxxのグループ定義を使って制御します。
RACFを使う場合、SDSFクラスのGROUP.xxx リソースのREAD権限をユーザーに付与します。
参考: [SAF の使用による、グループ・メンバーシップの制御](https://www.ibm.com/support/knowledgecenter/ja/SSLTBW_2.3.0/com.ibm.zos.v2r3.isfa500/safgrp6.htm)

ISFPRMxxを使う場合、これが分かりにくいのですが、GROUPステートメントのいくつかのパラメーターでグループ・メンバーシップを制御します。例えばTSOAUTHというパラメータがあります。

```xxx:ISFPRMxx抜粋
GROUP NAME(ISFSPROG),       /* Group name                          */  
  TSOAUTH(JCL,OPER,ACCT),   /* User must have JCL, OPER, ACCT      */  
  AUTH(ALL),                /* All authorized functions            */  
  BROWSE(NONE),             /* Browse default action character     */  
  CMDAUTH(ALL),             /* Commands allowed for all jobs       */  
...

GROUP NAME(ISFOPER),        /* Group name                          */ 
  TSOAUTH(JCL,OPER),        /* User must have JCL and OPER         */ 
  AUTH(ALLOPER),            /* All operator authorized functions   */ 
  BROWSE(NONE),             /* Browse default action character     */ 
  CMDAUTH(ALL),             /* Commands allowed for all jobs       */ 
...

GROUP NAME(ISFUSER),        /* Group name                          */ 
  TSOAUTH(JCL),             /* User must have JCL                  */ 
  AUTH(ALLUSER),            /* All user authorized functions       */ 
  BROWSE(NONE),             /* Browse default action character     */ 
  CMDAUTH(USERID,NOTIFY),   /* Command authority                   */ 
```

上の例だと、TSO権限としてJCL, OPER, ACCTの権限を持つ(AND条件)ユーザー/プログラムは、ISFSPROGグループに属すると判断される、という感じです。
他にも、ログオンプロシージャーやユーザーIDなどを判断条件に加えることができます。
参考: [グループ・メンバーシップ・パラメーターの解説](https://www.ibm.com/support/knowledgecenter/ja/SSLTBW_2.3.0/com.ibm.zos.v2r3.isfa500/grpref.htm)

REXXプログラムからSDSFを使用する場合、ログオンプロシージャーは"REXX"に、TSO権限は"JCL"に固定で設定されます。
参考: [ISFPARMS の使用](https://www.ibm.com/support/knowledgecenter/ja/SSLTBW_2.3.0/com.ibm.zos.v2r3.isfa600/rxprmu.htm)

従って、RACFではなくISFPRMxxでグループ・メンバーシップを制御する場合、以下のような感じのグループをISFPRMxxに定義することになります。

```xxx:ISFPRMxx例
GROUP NAME(REXXGRP),        /* Group name                          */ 
  TSOAUTH(JCL),             /* User must have JCL                  */ 
  ILPROC(PROC1),
  AUTH(DA,H,LOG,O,PREF,ST), /* authorized functions                */ 
  DSPAUTH(ALL),             /* Browse authority                    */
  ...

NTBLNAME(PROC1)
 NTBLENT STRING(REXX),OFFSET(1)
...
```

TSOAUTH(JCL) / ILPROC(PROC1) で、TSO権限がJCL かつ、ログオンプロシージャーがREXXではじまるものを、REXXGRPに属するものと判断します。
そして、AUTHやDSPAUTHで指定された権限設定に基づいて、各オペレーションの実行可否が決まる、ということになります。(上の例だと、このグループ権限では、DA,H,LOG,O,PREF,STのオペレーションを許可し、ブラウズ関連は全てのオペレーションを許可する、という設定。)

うーん、マニュアル読むのイヤになる程わかりにくい！ 今、1から設計し直したら絶対こういう実装にはしないんじゃないかと思うのだが。こういう設定の複雑さ、分かりにくさもz/OSの"特異性"なんだと思う...

SDSFクラスをアクティブにしなければこの辺のチェックが行われないはずなので、テスト環境とかだと、オフにしちゃうのが楽かも。

# おわりに

追加コンポーネントが不要とはいえ、この辺の仕組みを使ったことが無ければ最初ちょっと環境作るまでに壁があるかもしれません。デフォルトでこうなってるといいんですけどね。
でも、一度こういう環境作ってしまえば少しのカスタマイズと工夫で色々と出来ることが広がるのでやる価値は大いにあると思います。

次はSYSLOGをElasticsearchに取り込んでみる予定です。

※ 一応各ファイルは以下のGitHub上にも公開しています。作り物はこのページ上で紹介したものがほぼ全てですけど。
https://github.com/tomotagwork/zOS_with_ELK


# 追記 (2019/04/23)
DAパネルの情報を取得するスクリプトを新しい仕組みに置き換えたものを作成してみました。
具体的には、REXXでCSVにしていたところを、JSON形式で出力させるようにしました。さらにそのJSONデータに、"RecType":"SYSTEM" / "RecType:"JOB" というように、システム全体orジョブ毎を区別するフィールドを追加しました。
これにより、Logstash経由でElasticsearchに取り込む際に、このフィールドを判定してインデックスを分けられるようにしました。

## USS / 情報取得部分
### DAパネル取得用REXX

```rexx:da_sys_job_json.rex
/* REXX */
/*--------------------------------------------------------*/
/* Arg1: date  (yyyy/mm/dd)                               */
/* Arg2: time  (hh:mm:ss)                                 */
/* Arg3: output file (xxx.txt)                            */
/*--------------------------------------------------------*/

parse arg mydate mytime output_file

rc=isfcalls('ON')

/***************************/
/* Access the DA display   */
/***************************/
Address SDSF "ISFEXEC DA"
lrc=rc
if lrc<>0 then
  exit 20

DATETIME=MYDATE||" "||MYTIME

/***************************/
/* for SYSTEM              */
/***************************/
call stream output_file, 'C','CLEARFILE'
rc = charout(output_file,,)


LINE = "{"||jsonStr("RecType", "SYSTEM")||","||jsonStr("date_time",DATETIME)||","||jsonStr("sysname",SYSNAME.1)||","||jsonNum("spaging",SPAGING.1)||","
LINE = LINE||jsonNum("scpu", SCPU.1)||","||jsonNum("szaap",SZAAP.1)||","||jsonNum("sziip",SZIIP.1)||","||jsonNum("slcpu",SLCPU.1)||"}"
LINE = LINE || ESC_N

rc = charout(output_file, LINE,)

/***************************/
/* for JOB                 */
/***************************/


/* Loop for all target jobs *****************************/

do ix=1 to JNAME.0
  LINE = "{"||jsonStr("RecType", "JOB")||","||jsonStr("date_time",DATETIME)||","||jsonStr("sysname",SYSNAME.ix)||","
  LINE = LINE||jsonStr("jname",JNAME.ix)||","||jsonStr("stepn",STEPN.ix)||","||jsonStr("procs",PROCS.ix)||","||jsonStr("jtype",JTYPE.ix)||","||jsonStr("jobid",JOBID.ix)||","||jsonStr("ownerid",OWNERID.ix)||","||jsonStr("jclass",JCLASS.ix)||","
  LINE = LINE||jsonStr("pos",POS.ix)||","||jsonStr("dp",DP.ix)||","||jsonNum("real",REAL.ix)||","||jsonNum("paging",PAGING.ix)||","||jsonNum("excprt",EXCPRT.ix)||","||jsonNum("cpupr",CPUPR.ix)||","||jsonNum("asid",ASID.ix)||","||jsonStr("asidx",ASIDX.ix)||","||jsonNum("excp",EXCP.ix)||","
  LINE = LINE||jsonNum("cpu",CPU.ix)||","||jsonStr("swapr",SWAPR.ix)||","||jsonStr("status",STATUS.ix)||","||jsonStr("workload",WORKLOAD.ix)||","||jsonStr("srvclass",SRVCLASS.ix)||","
  LINE = LINE||jsonNum("period",PERIOD.ix)||","||jsonStr("resgroup",RESGROUP.ix)||","||jsonStr("server",SERVER.ix)||","||jsonStr("quiesce",QUIESCE.ix)||","||jsonNum("ecpu",ECPU.ix)||","||jsonNum("ecpupr",ECPUPR.ix)||","||jsonStr("cpucrit",CPUCRIT.ix)||","
  LINE = LINE||jsonStr("storcrit",STORCRIT.ix)||","||jsonStr("rptclass",RPTCLASS.ix)||","||jsonStr("memlimit",MEMLIMIT.ix)||","||jsonStr("tranact",TRANACT.ix)||","||jsonStr("tranres",TRANRES.ix)||","||jsonStr("spin",SPIN.ix)||","||jsonStr("seclabel",SECLABEL.ix)||","
  LINE = LINE||jsonNum("gcptime",GCPTIME.ix)||","||jsonNum("zaaptime",ZAAPTIME.ix)||","||jsonNum("zaapcptm",ZAAPCPTM.ix)||","||jsonNum("gcpuse",GCPUSE.ix)||","||jsonNum("zaapuse",ZAAPUSE.ix)||","
  LINE = LINE||jsonStr("promoted",PROMOTED.ix)||","||jsonNum("zaapntim",ZAAPNTIM.ix)||","||jsonNum("ziiptime",ZIIPTIME.ix)||","||jsonNum("ziipcptm",ZIIPCPTM.ix)||","||jsonNum("ziipntim",ZIIPNTIM.ix)||","||jsonNum("ziipuse",ZIIPUSE.ix)||","
  LINE = LINE||jsonStr("iopriogrp",IOPRIOGRP.ix)||","||jsonStr("jobcorr",JOBCORR.ix)||"}"
  /*say LINE*/
  LINE = LINE || ESC_N
  rc = charout(output_file, LINE,)
end

call stream output_file, 'C', 'CLOSE'

rc=isfcalls('OFF')
exit 0


/******************************/
/* function for JSON format   */
/******************************/
jsonItem:
 key=arg(1)
 value=arg(2)
 if datatype(value, "N")=1 then do
  /* say value " is Number" */
  result = """"||key||""":"||value
 end
 else do
  /* say value "is not Number" */
  result = """"||key||""":"""||value||""""
 end
 return result

jsonNum:
 key=arg(1)
 value=arg(2)
 if value="" then do
  value=0
 end
 result = """"||key||""":"||value
 return result

jsonStr:
 key=arg(1)
 value=arg(2)
 result = """"||key||""":"""||value||""""
 return result
```

## REXX呼び出し用シェル・スクリプト

ここは基本的に変わらないです(呼び出すrex変えているだけ)。

```sh:getDaSysJobJson.sh
#!/bin/sh

export PATH=$PATH:/u/cics004/REXX:/u/cics004/Shell
export TZ=JST-9
thisDateTime=$(date '+%Y/%m/%d %T')

#thisDate=$(date '+%Y%m%d')
#echo ${thisDateTime} / ${thisDate}

outputFile=daSysJob_temp$(date '+%Y%m%d%H%M%S').txt

#get DA panel info
da_sys_job_json.rex ${thisDateTime} ${outputFile} > /dev/null 2>&1
rc=$?

### Error Handling
if [ ${rc} -gt 0 ]; then
  # do nothing
  # echo Error!: ${rc}
  rm ${outputFile}
  exit 1
fi


### Output
cat ${outputFile}
rm ${outputFile}

exit 0
```

### 実行例

```xxx:実行例
$ ./getDaSysJobJson.sh
{"RecType":"SYSTEM","date_time":"2019/04/23 10:00:52","sysname":"ZOS1","spaging":0,"scpu":20,"szaap":0,"sziip":0,"slcpu":13}
{"RecType":"JOB","date_time":"2019/04/23 10:00:52","sysname":"ZOS1","jname":"*MASTER*","stepn":"","procs":"","jtype":"STC","jobid":"STC00002","ownerid":"+MASTER+","jclass":"","pos":"NS","dp":"FF","real":7391,"paging":0.00,"excprt":0.00,"cpupr":0.00,"asid":1,"asidx":"0001","excp":53823,"cpu":864.35,"swapr":"","status":"","workload":"SYSTEM","srvclass":"SYSTEM","period":1,"resgroup":"","server":"NO","quiesce":"","ecpu":864.80,"ecpupr":0.00,"cpucrit":"NO","storcrit":"NO","rptclass":"","memlimit":"17179869180GB","tranact":"480:59:50.66","tranres":"480:59:50.66","spin":"NO","seclabel":"SYSHIGH","gcptime":0.00,"zaaptime":0.00,"zaapcptm":0.00,"gcpuse":0.00,"zaapuse":0.00,"promoted":"NO","zaapntim":0.00,"ziiptime":0.00,"ziipcptm":0.00,"ziipntim":0.00,"ziipuse":0.00,"iopriogrp":"NORMAL","jobcorr":""}
{"RecType":"JOB","date_time":"2019/04/23 10:00:52","sysname":"ZOS1","jname":"PCAUTH","stepn":"PCAUTH","procs":"","jtype":"STC","jobid":"","ownerid":"","jclass":"","pos":"NS","dp":"FE","real":398,"paging":0.00,"excprt":0.00,"cpupr":0.00,"asid":2,"asidx":"0002","excp":18,"cpu":0.03,"swapr":"","status":"","workload":"SYSTEM","srvclass":"SYSSTC","period":1,"resgroup":"","server":"NO","quiesce":"","ecpu":0.03,"ecpupr":0.00,"cpucrit":"NO","storcrit":"NO","rptclass":"","memlimit":"","tranact":"480:59:50.66","tranres":"480:59:50.66","spin":"","seclabel":"","gcptime":0.00,"zaaptime":0.00,"zaapcptm":0.00,"gcpuse":0.00,"zaapuse":0.00,"promoted":"NO","zaapntim":0.00,"ziiptime":0.00,"ziipcptm":0.00,"ziipntim":0.00,"ziipuse":0.00,"iopriogrp":"NORMAL","jobcorr":""}
{"RecType":"JOB","date_time":"2019/04/23 10:00:52","sysname":"ZOS1","jname":"RASP","stepn":"RASP","procs":"","jtype":"STC","jobid":"","ownerid":"","jclass":"","pos":"NS","dp":"FF","real":523,"paging":0.00,"excprt":0.00,"cpupr":0.00,"asid":3,"asidx":"0003","excp":2,"cpu":24.31,"swapr":"","status":"","workload":"SYSTEM","srvclass":"SYSTEM","period":1,"resgroup":"","server":"NO","quiesce":"","ecpu":24.31,"ecpupr":0.00,"cpucrit":"NO","storcrit":"NO","rptclass":"","memlimit":"","tranact":"480:59:50.66","tranres":"480:59:50.66","spin":"","seclabel":"","gcptime":0.00,"zaaptime":0.00,"zaapcptm":0.00,"gcpuse":0.00,"zaapuse":0.00,"promoted":"NO","zaapntim":0.00,"ziiptime":0.00,"ziipcptm":0.00,"ziipntim":0.00,"ziipuse":0.00,"iopriogrp":"NORMAL","jobcorr":""}
{"RecType":"JOB","date_time":"2019/04/23 10:00:52","sysname":"ZOS1","jname":"TRACE","stepn":"TRACE","procs":"","jtype":"STC","jobid":"","ownerid":"","jclass":"","pos":"NS","dp":"FE","real":604,"paging":0.00,"excprt":0.00,"cpupr":0.00,"asid":4,"asidx":"0004","excp":30,"cpu":0.02,"swapr":"","status":"","workload":"SYSTEM","srvclass":"SYSSTC","period":1,"resgroup":"","server":"NO","quiesce":"","ecpu":0.02,"ecpupr":0.00,"cpucrit":"NO","storcrit":"NO","rptclass":"","memlimit":"36GB","tranact":"480:59:50.66","tranres":"480:59:50.66","spin":"","seclabel":"","gcptime":0.00,"zaaptime":0.00,"zaapcptm":0.00,"gcpuse":0.00,"zaapuse":0.00,"promoted":"NO","zaapntim":0.00,"ziiptime":0.00,"ziipcptm":0.00,"ziipntim":0.00,"ziipuse":0.00,"iopriogrp":"NORMAL","jobcorr":""}
...以下略
```

## Linux / ssh経由で情報を取得するシェル・スクリプト

JSONでRecTypeフィールドを付けているので、ファイルを分割しなくてもよい。
単純にファイルにAppendしていくだけ。

```sh:sshGetDaSysJson_eplexa.sh
#!/bin/sh

outputDir=/var/log/zos_da_json/
yyyymmdd=$(date '+%Y%m%d')
outputFile=${outputDir}da_eplexa_${yyyymmdd}.txt

ssh CICS004@eplexa "REXX/getDaSysJobJson.sh"  >> ${outputFile}
```

## LogstashによるElasticsearchへのデータ取り込み

RecTypeの値を判断して、Elasticsearchへ投入する際のインデックスを変えています。

```conf:da_sysjob_json_eplexa.conf
input {
	file{
		path => ["/var/log/zos_da_json/*.txt"]
		start_position => "beginning"
		sincedb_path => "/var/log/zos_da_json/temp.sincedb"
		codec => "json"
	}
}

filter{
	date{
		match => ["date_time", "yyyy/MM/dd HH:mm:ss"]
		target =>  "@timestamp"
		remove_field => ["path", "@version", "host", "date_time"]
	}
	
}


output {
  if [RecType] == "SYSTEM" {
  	#stdout { codec => rubydebug }
	elasticsearch {
		hosts => ["http://localhost:9200"]
		index => "da.sys-eplexa-%{+YYYY.MM.dd}"
	}
  } else if [RecType] == "JOB" {
	elasticsearch {
		hosts => ["http://localhost:9200"]
		index => "da.job-eplexa-%{+YYYY.MM.dd}"	
	}
  }


}

```

こちらの方式の方がスマートな感じがしますね。