Libertyの性能情報をPythonのSidecarで取得する（restConnector）

Libertyの性能情報をREST APIで取得するPythonのスクリプトを作成し、サイドカーコンテナとして動かしてみたメモ。

なお、Libertyの性能情報をREST APIで取得するには、JMXのRest Connectorを使う方法とMicroProfile Metricを使う方法があり、今回試しているのは前者。

- [monitor-1.0 を使用したモニタリング](https://www.ibm.com/support/knowledgecenter/ja/SSEQTP_liberty/com.ibm.websphere.wlp.doc/ae/twlp_monitor10.html)
- [MicroProfile メトリックを使用したモニタリング](https://www.ibm.com/support/knowledgecenter/ja/SSEQTP_liberty/com.ibm.websphere.wlp.doc/ae/twlp_mp_metrics_monitor.html)

mpMetricを使う方法は以下。

- [Libertyの性能情報をPythonのSidecarで取得する（mpMetrics）](https://qiita.com/sotoiwa/items/113258e8ac52b676b791)

## 参考リンク

- [Open Libertyの性能値をREST APIで取得する](https://qiita.com/rinaxsumomo/items/9d0776f8a3e506fdebc4)
- [IBM WAS LibertyのJMX restConnectorを試してみる](https://qiita.com/Pnk_Peng/items/1aef87a8168289f7f8ab)

## ローカルLibertyの準備

はじめにローカルでLibertyを動かして確認する。以下のような`server.xml`でLibertyを起動する。

`monitor-1.0`フィーチャーを追加することで`WebSphere:type=JvmStats`や`WebSphere:type=ThreadPoolStats`などのパフォーマンスデータが追加で取得できるようになる。ローカルJMX接続には`localConnector-1.0`が必要であり、REST APIでの接続には`restConnector-2.0`が必要。また、RESTでは認証のための管理者ロールを持つユーザーが必要。

- [Liberty での管理者ロールのマップ](https://www.ibm.com/support/knowledgecenter/ja/SSEQTP_liberty/com.ibm.websphere.wlp.doc/ae/twlp_sec_mapadmin.html)

```xml:server.xml
<server description="new server">

    <!-- Enable features -->
    <featureManager>
        <feature>webProfile-8.0</feature>
        <feature>monitor-1.0</feature>
        <feature>localConnector-1.0</feature>
        <feature>restConnector-2.0</feature>
    </featureManager>

    <!-- To access this server from a remote client add a host attribute to the following element, e.g. host="*" -->
    <httpEndpoint id="defaultHttpEndpoint"
                  host="*"
                  httpPort="9080"
                  httpsPort="9443" />

    <keyStore id="defaultKeyStore" password="password"/>

    <basicRegistry>
        <user name="jmxadmin" password="password" />
    </basicRegistry>

    <administrator-role>
        <user>jmxadmin</user>
    </administrator-role>

    <!-- Automatically expand WAR files and EAR files -->
    <applicationManager autoExpand="true"/>

    <applicationMonitor updateTrigger="mbean"/>

    <webApplication id="sample" location="sample.war" name="sample"/>

</server>
```

## jconsole

Libertyの性能情報としてどのような情報がとれるのかをjconsoleで確認してみる。

### ローカルJMX接続

- [Liberty へのローカル JMX 接続の構成](https://www.ibm.com/support/knowledgecenter/ja/SSEQTP_liberty/com.ibm.websphere.wlp.doc/ae/twlp_admin_localconnector.html)

jconsoleを起動する。

```shell
jconsole
```

「ローカル・プロセス」で`ws-server.jar defaultServer`を選択する。

<img width="900" alt="image.png" src="https://qiita-image-store.s3.amazonaws.com/0/86449/bb42e8a0-6ffd-344e-3fa3-f1fff08f14f3.png">

あるいは、`${server.output.dir}/logs/state/com.ibm.ws.jmx.local.address` の内容を「リモート・プロセス」に入力する。こちらの方がより高い信頼性があると[KnowledgeCenterには書いてある](https://www.ibm.com/support/knowledgecenter/ja/SSEQTP_liberty/com.ibm.websphere.wlp.doc/ae/twlp_admin_localconnector.html)。どちらの場合も、セキュアな接続は失敗するので非セキュアで接続する。

<img width="900" alt="image.png" src="https://qiita-image-store.s3.amazonaws.com/0/86449/fe0e2ce2-8a43-9b7c-ff45-270f1608e11a.png">

MBeansタブでどのような情報が取得できるのか確認できる。

<img width="900" alt="image.png" src="https://qiita-image-store.s3.amazonaws.com/0/86449/7b8db3c3-d3bf-c508-ecba-66718b333b90.png">

<img width="900" alt="image.png" src="https://qiita-image-store.s3.amazonaws.com/0/86449/6361c066-4a15-0895-3355-02e1ef9672cf.png">


### REST接続

jconsoleからREST接続するのは少々ややこしい。

- [Liberty へのセキュア JMX 接続の構成](https://www.ibm.com/support/knowledgecenter/ja/SSEQTP_liberty/com.ibm.websphere.wlp.doc/ae/twlp_admin_restconnector.html)

jconsoleにいろいろオプションをつけて起動する必要がある。

```shell
jconsole \
  -J-Djava.class.path=${JAVA_HOME}/lib/jconsole.jar:${JAVA_HOME}/lib/tools.jar:${WLP_HOME}/clients/restConnector.jar \
  -J-Djavax.net.ssl.trustStore=${WLP_HOME}/usr/servers/defaultServer/resources/security/key.jks \
  -J-Djavax.net.ssl.trustStorePassword=password \
  -J-Djavax.net.ssl.trustStoreType=jks
```

`${server.output.dir}/logs/state/com.ibm.ws.jmx.rest.address` の内容を「リモート・プロセス」に入力する。このときの接続先のホスト名は証明書のCNと一致する必要があるので、`192.168.99.1`とかになっていたら`localhost`にする必要があるかもしれない。ブラウザで証明書を確認してそれに合わせること。

<img width="612" alt="image.png" src="https://qiita-image-store.s3.amazonaws.com/0/86449/67ce82c9-97c8-270c-a1de-092ddb0e0ea7.png">

<img width="900" alt="image.png" src="https://qiita-image-store.s3.amazonaws.com/0/86449/70fcaa96-2bdb-32ea-68f5-0232e2addece.png">

<img width="900" alt="image.png" src="https://qiita-image-store.s3.amazonaws.com/0/86449/400d7841-b9eb-51cd-c748-208e85650caa.png">

## curl

`curl`コマンドでREST APIから情報がとれるのかを確認する。`https://localhost:9443/IBMJMXConnectorREST/mbeans/`でどのようなMBeanがあるのか確認できる。以下では`WebSphere`を含むもののみを抽出。ここで返されているURLにアクセスすることで性能情報を取得できる。

```shell-session
$ curl -s -k --basic -u jmxadmin:password https://localhost:9443/IBMJMXConnectorREST/mbeans/ | jq '.[] | select( .objectName | test("WebSphere") )'
{
  "objectName": "WebSphere:type=ServletStats,name=com.ibm.ws.jmx.connector.server.rest.JMXRESTProxyServlet",
  "className": "com.ibm.ws.webcontainer.monitor.ServletStats",
  "URL": "/IBMJMXConnectorREST/mbeans/WebSphere%3Aname%3Dcom.ibm.ws.jmx.connector.server.rest.JMXRESTProxyServlet%2Ctype%3DServletStats"
}
{
  "objectName": "WebSphere:name=com.ibm.websphere.config.mbeans.FeatureListMBean",
  "className": "com.ibm.ws.config.featuregen.internal.FeatureListMBeanImpl",
  "URL": "/IBMJMXConnectorREST/mbeans/WebSphere%3Aname%3Dcom.ibm.websphere.config.mbeans.FeatureListMBean"
}
{
  "objectName": "WebSphere:name=com.ibm.websphere.runtime.update.RuntimeUpdateNotificationMBean",
  "className": "com.ibm.ws.runtime.update.internal.RuntimeUpdateNotificationMBeanImpl",
  "URL": "/IBMJMXConnectorREST/mbeans/WebSphere%3Aname%3Dcom.ibm.websphere.runtime.update.RuntimeUpdateNotificationMBean"
}
{
  "objectName": "WebSphere:name=com.ibm.ws.jmx.mbeans.sessionManagerMBean",
  "className": "com.ibm.ws.session.SessionManagerMBeanImpl",
  "URL": "/IBMJMXConnectorREST/mbeans/WebSphere%3Aname%3Dcom.ibm.ws.jmx.mbeans.sessionManagerMBean"
}
{
  "objectName": "WebSphere:name=com.ibm.ws.config.serverSchemaGenerator",
  "className": "com.ibm.ws.config.schemagen.internal.ServerSchemaGeneratorImpl",
  "URL": "/IBMJMXConnectorREST/mbeans/WebSphere%3Aname%3Dcom.ibm.ws.config.serverSchemaGenerator"
}
{
  "objectName": "WebSphere:feature=kernel,name=ServerInfo",
  "className": "com.ibm.ws.kernel.server.internal.ServerInfoMBeanImpl",
  "URL": "/IBMJMXConnectorREST/mbeans/WebSphere%3Afeature%3Dkernel%2Cname%3DServerInfo"
}
{
  "objectName": "WebSphere:type=ThreadPoolStats,name=Default Executor",
  "className": "com.ibm.ws.monitors.helper.ThreadPoolStats",
  "URL": "/IBMJMXConnectorREST/mbeans/WebSphere%3Aname%3DDefault+Executor%2Ctype%3DThreadPoolStats"
}
{
  "objectName": "WebSphere:name=com.ibm.ws.jmx.mbeans.generatePluginConfig",
  "className": "com.ibm.ws.webcontainer.osgi.mbeans.GeneratePluginConfigMBean",
  "URL": "/IBMJMXConnectorREST/mbeans/WebSphere%3Aname%3Dcom.ibm.ws.jmx.mbeans.generatePluginConfig"
}
{
  "objectName": "WebSphere:feature=PluginUtility,name=PluginConfigRequester",
  "className": "com.ibm.ws.webserver.plugin.runtime.requester.PluginConfigRequesterImpl",
  "URL": "/IBMJMXConnectorREST/mbeans/WebSphere%3Afeature%3DPluginUtility%2Cname%3DPluginConfigRequester"
}
{
  "objectName": "WebSphere:feature=CacheAdmin,type=DynaCache,name=DistributedMap",
  "className": "com.ibm.ws.cache.MBeans",
  "URL": "/IBMJMXConnectorREST/mbeans/WebSphere%3Afeature%3DCacheAdmin%2Cname%3DDistributedMap%2Ctype%3DDynaCache"
}
{
  "objectName": "WebSphere:feature=channelfw,type=endpoint,name=defaultHttpEndpoint",
  "className": "com.ibm.ws.channelfw.internal.chains.EndPointInfoImpl",
  "URL": "/IBMJMXConnectorREST/mbeans/WebSphere%3Afeature%3Dchannelfw%2Cname%3DdefaultHttpEndpoint%2Ctype%3Dendpoint"
}
{
  "objectName": "WebSphere:type=SessionStats,name=default_host/IBMJMXConnectorREST",
  "className": "com.ibm.ws.session.monitor.SessionStats",
  "URL": "/IBMJMXConnectorREST/mbeans/WebSphere%3Aname%3Ddefault_host%2FIBMJMXConnectorREST%2Ctype%3DSessionStats"
}
{
  "objectName": "WebSphere:name=com.ibm.websphere.config.mbeans.ServerXMLConfigurationMBean",
  "className": "com.ibm.ws.config.xml.internal.ServerXMLConfigurationMBeanImpl",
  "URL": "/IBMJMXConnectorREST/mbeans/WebSphere%3Aname%3Dcom.ibm.websphere.config.mbeans.ServerXMLConfigurationMBean"
}
{
  "objectName": "WebSphere:type=JvmStats",
  "className": "com.ibm.ws.monitors.helper.JvmStats",
  "URL": "/IBMJMXConnectorREST/mbeans/WebSphere%3Atype%3DJvmStats"
}
{
  "objectName": "WebSphere:feature=kernel,name=ServerEndpointControl",
  "className": "com.ibm.ws.kernel.server.internal.ServerEndpointControlMBeanImpl",
  "URL": "/IBMJMXConnectorREST/mbeans/WebSphere%3Afeature%3Dkernel%2Cname%3DServerEndpointControl"
}
{
  "objectName": "WebSphere:service=com.ibm.ws.kernel.filemonitor.FileNotificationMBean",
  "className": "com.ibm.ws.kernel.filemonitor.internal.FileNotificationImpl",
  "URL": "/IBMJMXConnectorREST/mbeans/WebSphere%3Aservice%3Dcom.ibm.ws.kernel.filemonitor.FileNotificationMBean"
}
{
  "objectName": "WebSphere:feature=restConnector,type=FileService,name=FileService",
  "className": "com.ibm.ws.filetransfer.internal.mbean.FileService",
  "URL": "/IBMJMXConnectorREST/mbeans/WebSphere%3Afeature%3DrestConnector%2Cname%3DFileService%2Ctype%3DFileService"
}
{
  "objectName": "WebSphere:service=com.ibm.websphere.application.ApplicationMBean,name=sample",
  "className": "com.ibm.ws.app.manager.internal.ApplicationConfigurator$NamedApplication$2",
  "URL": "/IBMJMXConnectorREST/mbeans/WebSphere%3Aname%3Dsample%2Cservice%3Dcom.ibm.websphere.application.ApplicationMBean"
}
{
  "objectName": "WebSphere:feature=channelfw,type=endpoint,name=defaultHttpEndpoint-ssl",
  "className": "com.ibm.ws.channelfw.internal.chains.EndPointInfoImpl",
  "URL": "/IBMJMXConnectorREST/mbeans/WebSphere%3Afeature%3Dchannelfw%2Cname%3DdefaultHttpEndpoint-ssl%2Ctype%3Dendpoint"
}
{
  "objectName": "WebSphere:feature=restConnector,type=FileTransfer,name=FileTransfer",
  "className": "com.ibm.ws.filetransfer.internal.mbean.FileTransfer",
  "URL": "/IBMJMXConnectorREST/mbeans/WebSphere%3Afeature%3DrestConnector%2Cname%3DFileTransfer%2Ctype%3DFileTransfer"
}
$
```

### JvmStats

JvmStatsを取得する。

```shell-session
$ curl -s -k --basic -u jmxadmin:password https://localhost:9443/IBMJMXConnectorREST/mbeans/WebSphere%3Atype%3DJvmStats/attributes | jq .
[
  {
    "name": "ProcessCPU",
    "value": {
      "value": "0.23676892201834862",
      "type": "java.lang.Double"
    }
  },
  {
    "name": "GcCount",
    "value": {
      "value": "7",
      "type": "java.lang.Long"
    }
  },
  {
    "name": "GcTime",
    "value": {
      "value": "78",
      "type": "java.lang.Long"
    }
  },
  {
    "name": "UpTime",
    "value": {
      "value": "1871036",
      "type": "java.lang.Long"
    }
  },
  {
    "name": "UsedMemory",
    "value": {
      "value": "157499176",
      "type": "java.lang.Long"
    }
  },
  {
    "name": "FreeMemory",
    "value": {
      "value": "427606232",
      "type": "java.lang.Long"
    }
  },
  {
    "name": "Heap",
    "value": {
      "value": "585105408",
      "type": "java.lang.Long"
    }
  }
]
$
```

### ThreadPoolStats

Default ExecutorのThreadPoolStatsを取得する。

```shell-session
$ curl -s -k --basic -u jmxadmin:password https://localhost:9443/IBMJMXConnectorREST/mbeans/WebSphere%3Aname%3DDefault+Executor%2Ctype%3DThreadPoolStats/attributes | jq .
[
  {
    "name": "PoolSize",
    "value": {
      "value": "16",
      "type": "java.lang.Integer"
    }
  },
  {
    "name": "PoolName",
    "value": {
      "value": "Default Executor",
      "type": "java.lang.String"
    }
  },
  {
    "name": "ActiveThreads",
    "value": {
      "value": "1",
      "type": "java.lang.Integer"
    }
  }
]
$
```

### SessionStats

SessionStatsを取得する。このMBeanはアプリケーション毎であり、アプリケーションが実際にセッションを使うまで作成されない。本来は自分がセッション情報をとりたいアプリケーション名を指定する必要があるが、今回はIBMJMXConnectorRESTアプリケーションを指定する。

```shell-session
$ curl -s -k --basic -u jmxadmin:password https://localhost:9443/IBMJMXConnectorREST/mbeans/WebSphere%3Aname%3Ddefault_host%2FIBMJMXConnectorREST%2Ctype%3DSessionStats/attributes | jq .
[
  {
    "name": "ActiveCount",
    "value": {
      "value": "0",
      "type": "java.lang.Long"
    }
  },
  {
    "name": "CreateCount",
    "value": {
      "value": "1",
      "type": "java.lang.Long"
    }
  },
  {
    "name": "LiveCount",
    "value": {
      "value": "0",
      "type": "java.lang.Long"
    }
  },
  {
    "name": "InvalidatedCountbyTimeout",
    "value": {
      "value": "1",
      "type": "java.lang.Long"
    }
  },
  {
    "name": "InvalidatedCount",
    "value": {
      "value": "1",
      "type": "java.lang.Long"
    }
  }
]
$
```

### ConnectionPoolStats

他に一般的にモニターしたくなりそうな情報としてはコネクションプールがある。コネクションプールはその定義を作成し、また、実際にコネクションが発生しないとMBeanが作成されないと思われる。今回は省略。

## Python

PythonからローカルLibertyに接続してみる。

### コード作成

Pythonスクリプトは以下の通り作成。→[GitHub](https://github.com/sotoiwa/liberty_monitor)

```python:monitor_restConnector.py
#!/usr/bin/env python3

import argparse
import csv
import datetime
import logging
import os
import sys
import time

import requests
import urllib3


# 警告を非表示にする
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

formatter = '%(asctime)s %(name)-12s %(levelname)-8s %(message)s'
logging.basicConfig(level=logging.WARNING, format=formatter)
logger = logging.getLogger(__name__)


def request_with_retry(url, user, password, timeout, retry):
    """HTTPリクエストを行い、失敗した場合はリトライを行う。

    :param url:
    :param user:
    :param password:
    :param timeout:
    :param retry:
    :return: jsonデータ
    """

    session = requests.Session()
    
    # backoff_factorが1だとリトライの間のスリープ時間は[0s, 2s, 4s, 8s, ...]となる
    retries = urllib3.util.retry.Retry(total=retry, backoff_factor=1)

    session.mount('https://', requests.adapters.HTTPAdapter(max_retries=retries))
    session.mount('http://', requests.adapters.HTTPAdapter(max_retries=retries))

    logger.info('GET {}'.format(url))
    # verify=Falseで証明書のチェックを行わない
    # ただしこれだけだと警告がでるので最初にurllib3.disable_warningsをしている
    response = session.get(url, auth=(user, password), verify=False, timeout=timeout)

    return response.json()


def get_value(name, items):
    """辞書のリストからnameがマッチする辞書を探し、value.valueを返す。

    :param name:
    :param items:
    :return: データの値
    """

    for item in items:
        if item['name'] == name:
            return item['value']['value']

    return None


def write_header(filepath):
    """ヘッダー書き込む。
    filepathが指定され、ファイルが存在しなかった場合はファイルを作成してヘッダーを書く。
    filepathが指定され、ファイルが空の場合もヘッダーを書く。
    filepathが指定されなかった場合は標準出力にヘッダーを書く。

    :param filepath:
    :return:
    """

    fieldnames = [
        'Time',
        'Heap', 'UsedMemory',
        'PoolSize', 'ActiveThreads',
        'LiveCount', 'ActiveCount']

    # filepathが指定された場合
    if filepath:
        # ファイルが存在しない場合はファイルを作成してヘッダーを書く
        if not os.path.isfile(filepath):
            with open(filepath, 'w') as csv_file:
                writer = csv.DictWriter(csv_file, fieldnames=fieldnames)
                writer.writeheader()
        # ファイルが存在する場合
        else:
            with open(filepath, 'r+') as csv_file:
                # ファイルが空の場合はヘッダーを書く
                if not csv_file.read():
                    writer = csv.DictWriter(csv_file, fieldnames=fieldnames)
                    writer.writeheader()
    # filepathが指定されなかった場合は標準出力にヘッダーを書く
    else:
        writer = csv.DictWriter(sys.stdout, fieldnames=fieldnames)
        writer.writeheader()


def append_data(filepath, jvmstats, threadpoolstats, sessionstats):
    """性能情報の値を追記する。
    filepathが渡された場合はcsvに書き込み、渡されなかった場合は標準出力に書き込む。

    :param filepath:
    :param jvmstats:
    :param threadpoolstats:
    :param sessionstats:
    :return:
    """

    fieldnames = [
        'Time',
        'Heap', 'UsedMemory',
        'PoolSize', 'ActiveThreads',
        'LiveCount', 'ActiveCount']

    item = {
        'Time': datetime.datetime.now(),
        'Heap': get_value('Heap', jvmstats),
        'UsedMemory': get_value('UsedMemory', jvmstats),
        'PoolSize': get_value('PoolSize', threadpoolstats),
        'ActiveThreads': get_value('ActiveThreads', threadpoolstats),
        'LiveCount': get_value('LiveCount', sessionstats),
        'ActiveCount': get_value('ActiveCount', sessionstats)
    }

    if filepath:
        # データをファイルに追記する
        with open(filepath, 'a') as csv_file:
            writer = csv.DictWriter(csv_file, fieldnames=fieldnames)
            writer.writerow(item)
    else:
        # 標準出力に出力する
        writer = csv.DictWriter(sys.stdout, fieldnames=fieldnames)
        writer.writerow(item)


def main():

    # コマンド引数の処理
    parser = argparse.ArgumentParser()
    parser.add_argument('--host',
                        action='store',
                        type=str,
                        default='localhost',
                        help='接続先のホスト名を指定します')
    parser.add_argument('--port',
                        action='store',
                        type=int,
                        default=9443,
                        help='接続先のポートを指定します')
    parser.add_argument('--interval',
                        action='store',
                        type=int,
                        default=10,
                        help='データの取得間隔（秒）を指定します')
    parser.add_argument('--delay',
                        action='store',
                        type=int,
                        default=30,
                        help='モニターを開始するまでの待機時間（秒）を指定します')
    parser.add_argument('--timeout',
                        action='store',
                        type=int,
                        default=2,
                        help='モニターリクエストのタイムアウト時間（秒）を指定します')
    parser.add_argument('--retry',
                        action='store',
                        type=int,
                        default=10,
                        help='モニターのリトライ回数を指定します')
    parser.add_argument('-f', '--filename',
                        action='store',
                        type=str,
                        help='出力先のファイル名を指定します')
    args = parser.parse_args()
    host = args.host
    port = args.port
    timeout = args.timeout
    retry = args.retry
    filepath = args.filename

    # 接続ユーザーとパスワードは環境変数から取得
    user = os.getenv('JMX_USER', 'jmxadmin')
    password = os.getenv('JMX_PASSWORD', 'password')

    # 待機時間
    time.sleep(args.delay)

    # ヘッダーを書く
    write_header(filepath)

    # MBeanのURL
    jvmstats_url = 'https://{}:{}/IBMJMXConnectorREST/mbeans/' \
                   'WebSphere%3Atype%3DJvmStats/' \
                   'attributes'.format(host, port)
    threadpoolstats_url = 'https://{}:{}/IBMJMXConnectorREST/mbeans/' \
                          'WebSphere%3Aname%3DDefault+Executor%2Ctype%3DThreadPoolStats/' \
                          'attributes'.format(host, port)
    sessionstats_url = 'https://{}:{}/IBMJMXConnectorREST/mbeans/' \
                       'WebSphere%3Aname%3Ddefault_host%2FIBMJMXConnectorREST%2Ctype%3DSessionStats/' \
                       'attributes'.format(host, port)

    # モニター
    while True:
        jvmstats = request_with_retry(jvmstats_url, user, password, timeout, retry)
        threadpoolstats = request_with_retry(threadpoolstats_url, user, password, timeout, retry)
        sessionstats = request_with_retry(sessionstats_url, user, password, timeout, retry)
        append_data(filepath, jvmstats, threadpoolstats, sessionstats)
        time.sleep(args.interval)


if __name__ == '__main__':
    main()
```

- ファイル出力と標準出力への出力ができるようにした
- Libertyのコンテナの起動に時間がかかる場合を想定し、モニター開始までのdelayを設定できるようにした
- Libertyへの接続に失敗した場合にリトライをするようにした
  - （参考）[Can I set max_retries for requests.request?](https://stackoverflow.com/questions/15431044/can-i-set-max-retries-for-requests-request)
- いくつかのパラメータを引数で設定できるようにしたが、接続ユーザーとパスワードについては環境変数から取得するようにした

### 実行例

```shell-session
$ ./monitor_restConnector.py --help
usage: monitor_restConnector.py [-h] [--host HOST] [--port PORT]
                                [--interval INTERVAL] [--delay DELAY]
                                [--timeout TIMEOUT] [--retry RETRY]
                                [-f FILENAME]

optional arguments:
  -h, --help            show this help message and exit
  --host HOST           接続先のホスト名を指定します default:localhost
  --port PORT           接続先のポートを指定します default:9443
  --interval INTERVAL   データの取得間隔（秒）を指定します default:60
  --delay DELAY         モニターを開始するまでの待機時間（秒）を指定します default:30
  --timeout TIMEOUT     モニターリクエストのタイムアウト時間（秒）を指定します default:2
  --retry RETRY         モニターのリトライ回数を指定します default:10
  -f FILENAME, --filename FILENAME
                        出力先のファイル名を指定します default:未指定（標準出力）
sotoiwa@SotonoMacBook-Pro:~/workspace/liberty_monitor/monitor_restConnector (master)
$ ./monitor_restConnector.py --interval 10 --delay 0
Time,Heap,UsedMemory,PoolSize,ActiveThreads,LiveCount,ActiveCount
2019-01-13 15:43:23.679303,649592832,74171952,16,1,1,0
2019-01-13 15:43:33.753766,649592832,77029800,16,1,1,0
2019-01-13 15:43:43.830487,649592832,82213760,16,1,1,0
2019-01-13 15:43:53.907145,649592832,83140824,16,1,1,0
```

## サイドカーとして動かす

Pythonスクリプトをコンテナ化し、Liberyコンテナのサイドカーとして動かす。

### Libertyコンテナのビルド

上に記載した`server.xml`とアプリを含めてLibertyコンテナをビルドする。

```dockerfile:Dockerfile
FROM websphere-liberty:18.0.0.4-webProfile8
COPY --chown=1001:0 server.xml /config/
COPY --chown=1001:0 sample.war /config/apps/
```

ファイル配置は以下。

```
liberty-restconnector
├── Dockerfile
├── sample.war
└── server.xml
```

ビルドする。

```shell
docker build -t sotoiwa540/liberty-restconnector:1.0 .
docker push sotoiwa540/liberty-restconnector:1.0
```

### Pythonサイドカーコンテナのビルド

Pythonのコンテナをビルドする。`python`コマンドに`-u`オプションをつけるか、環境変数`PYTHONUNBUFFERED=TRUE`を設定しないと、標準出力がバッファされてdockerのログに出力されないことに注意。

- （参考）[Python app does not print anything when running detached in docker](https://stackoverflow.com/questions/29663459/python-app-does-not-print-anything-when-running-detached-in-docker)

```dockerfile:Dockerfile
FROM python:3-alpine

WORKDIR /usr/src/app

COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt

COPY monitor_restConnector.py ./

ENV PYTHONUNBUFFERED TRUE
ENTRYPOINT [ "python", "./monitor_restConnector.py" ]
```

ファイル配置は以下。

```
monitor-restconnector
├── Dockerfile
├── monitor_restConnector.py
└── requirements.txt
```

```shell
docker build -t sotoiwa540/monitor-restconnector:1.0 .
docker push sotoiwa540/monitor-restconnector:1.0
```

### Kubernetesへのデプロイ

パスワードを格納したSecretを作成する。

```shell
kubectl create secret generic jmx-secret --from-literal=JMX_USER=jmxadmin --from-literal=JMX_PASSWORD=password
```

以下のようにLiberty（with サイドカー）のマニフェストを作成。

```yaml:liberty-restconnector-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: liberty-restconnector
spec:
  selector:
    matchLabels:
      app: liberty
  replicas: 1
  template:
    metadata:
      labels:
        app: liberty
    spec:
      containers:
      - name: liberty
        image: sotoiwa540/liberty-restconnector:1.0
        imagePullPolicy: Always
        ports:
        - containerPort: 9080
      - name: monitor
        image: sotoiwa540/monitor-restconnector:1.0
        imagePullPolicy: Always
        command:
        - python
        - ./monitor_restConnector.py
        args:
        - --host
        - localhost
        - --port
        - "9443"
        - --interval
        - "60"
        - --timeout
        - "2"
        - --delay
        - "30"
        env:
        - name: JMX_USER
          valueFrom:
            secretKeyRef:
              name: jmx-secret
              key: JMX_USER
        - name: JMX_PASSWORD
          valueFrom:
            secretKeyRef:
              name: jmx-secret
              key: JMX_PASSWORD
```

デプロイする。

```shell
kubectl apply -f liberty-restconnector-deployment.yaml
```

### 稼働確認

LibertyのPodが稼働していることを確認。コンテナの数が2となっている。

```shell-session
$ kubectl get po
NAME                       READY   STATUS    RESTARTS   AGE
liberty-76c9d556c9-dhmcs   2/2     Running   0          25s
$
```

サイドカーの方のログを確認。csvが出力できた。

```shell-session
$ kubectl logs -f liberty-restconnector-76c9d556c9-j2gb5 -c monitor
Time,Heap,UsedMemory,PoolSize,ActiveThreads,LiveCount,ActiveCount
2019-01-13 07:20:04.737811,72351744,68460832,6,1,1,0
2019-01-13 07:21:04.963036,72351744,61504136,4,1,1,0
```

## TODO

SessionStatsやConnectionPoolStatsは、アプリケーションやデータソース毎にMBeanができるので、どのアプリケーションあるいはどのデータソースなのか、MBeanのURLを引数か環境変数で指定できるようにする必要がある。
また、このMBeanができるのは、実際にセッションやコネクションプールが使われてからなので、MBeanができる前にアクセスした場合に上手いこと処理が必要。

<img width="900" alt="image.png" src="https://qiita-image-store.s3.amazonaws.com/0/86449/d561868c-d056-0262-2d14-db4c975c786b.png">