2019年9月10日火曜日

V11 の DQL はさらに進化!全文索引の検索も可能に!

Notes/Domino の次期バージョンであるV11のベータプログラムが始まりました。

https://hcljapan.co.jp/software/blog/hcl-notes-domino-v11-beta-started

私もさっそくWindows版のNotesとDominoをダウンロードして、試しているところです。

このベータで私が最も注目している機能のひとつに「DQLの拡張」があります。

DQL は V10 の新機能として追加された、データベースの文書へアクセスするための新たな枠組みです。V10 では NSFサーチ(文書のサマリーフィールド)とビューを検索できるのみで、サマリーフィールドでもないしビューにも表示できない「リッチテキストフィールド」の内容は、検索できませんでした。

V11 ではこれが改善され、全文索引を検索するためのクエリーを書くことができようになりました。全文索引にはリッチテキストフィールドのテキストも含むので、リッチテキストの中も検索できるようになるようです。添付ファイルを全文索引に含める設定をしていれば、サポートされているファイル形式のファイルにあるテキストも検索できそうです。


DQL では、検索する際に使うもの(サマリーフィールド or ビュー or 全文索引)がクエリーの書き方で変わります。


クエリーの例をいくつか挙げます。


たとえば、件名が"Request"のメールを探したい場合、

Subject = 'request'

とすると、データベース内の Subject という名前のサマリーフィールドから値が一致するもの探します。※このとき Subject でソートできるビューがDQLで使える仕様になっていればビューから探す場合もあります

検索に「All」ビューを使いたい場合、明示的にビュー名を指定することも可能です。

'All'.Subject = 'request'

このようにビューを検索で使う場合、Subject 列(Subjectは列のプログラム名)が最も左にあるソートされた列である、または Subject 列にクリックソートが設定されていることが必要です。

テクてくで発表した資料で示したとおり、NSFサーチ(サマリーフィールド)よりもビューを使った検索のほうがパフォーマンスは良好でした。


ここまで(これはほんの一部です)が V10 の DQL で使えた構文です。


V10 までは、フィールドまたはビュー列と、指定した値とが完全に一致する場合のみヒットしましたが、V11 では全文索引を使ってこれまでより柔軟な検索ができるようになりそうです。

V11 以降の DQL で全文索引を使うには、クエリーに Contains を用います。

たとえば先の例を、全文索引を使うよう書き換える場合、

Subject Contains ('request')

となります。ただし V10 では Subject の値が Complicated request の場合はヒットしませんでしたが、Contains を使えばヒットします。検索バーから全文検索する場合と違い、DQL では = と Contains は同じではないので注意が必要です。

それから、全文索引を使う場合には、アスタリスク * やクエスチョンマーク ? といったワイルドカードが使えます。

先の例では、件名が [Request] や REQUEST: といった文字列はヒットしますが、 Req: や はヒットしません。
もし Request 以外にも Req: や といった文言も検索したい場合、

Subject Contains ('req*')

とします。

クエスチョンマークは任意の1文字を表す場合に使います。例えば、'r??t' とすると root や REST はヒットしますが request はヒットしません。

また、件名だけでなく、本文など他のフィールドも含めて検索させたい場合、

Contains ('req*')

のように、フィールド名を指定せずに Contains を用いることで、全文索引にあるすべてのフィールドを対象として探すことができます。


さらに、複数の文言のどれかを含む文書を探したい場合は

Contains ('req*', 'server', 'replica')

のように括弧の中にカンマ区切りで指定できます。
また、

Contains all ('req*', 'server', 'replica')

のように contains の直後に all を加えることで、複数の文言のすべてを含む文書を探すことができます。


なお、前回のエントリで話題にしました「読者フィールドを使って閲覧制限した文書が全文検索にヒットする」事象は、DQL で全文索引を用いる場合にも同様であることを本ベータでは確認しました。※NSF サーチやビューを使った場合はヒットしません


V11 の DQL ではこういった構文の拡張の他、パフォーマンスのさらなる向上や、DQL準拠のビューを自動作成するといったプランもあるようです。次のリンクを参照ください。

https://www.cwpcollaboration.com/uploads/1/0/2/7/102707030/domino_app_dev__cr_.pdf

まだ未検証ですが、パフォーマンスについて書かれた12ページのグラフを見ると、検索スピードが物凄く速くなるようです。

さらに V11 の先には、複数のデータベースを対象にしたクエリーの拡張も計画にあるようですね。

今後の DQL にも注目していきたいと思います。

2019年9月9日月曜日

閲覧制限した文書も検索にヒットするのです

Notesでは「読者フィールド」と呼ばれる特殊なフィールドを用意しておき、閲覧させたい人やグループ、あるいは特定のロール(を付与されたACLエントリ)を読者フィールドへ埋め込むことで、文書の閲覧を制御することができることはご存知のとおりです。

読者フィールドに自分のアカウントや自分が所属するグループ、あるいは自分に付与されたロールが無い場合、その文書を閲覧することができません。

しかしながら、自分が閲覧できる文書がデータベースの中に1件も無い場合でも、データベースのプロパティには、閲覧できない文書を含む文書数が表示されます。エンドユーザーは何か見えない文書があることがわかるわけです。


さて、NotesDatabase クラスの Search メソッドや FTSearch メソッドを使う場合、検索条件に一致すれば、閲覧できない文書もヒットします。

これらのメソッドでは NotesDocumentCollection クラスのインスタンスが返ります。

もし、自分が閲覧できない文書がヒットした場合、それを GetFirstDocument や GetNextDocument を使って NotesDocument クラスのインスタンスが作られるのでしょうか。

次のようなコードを書きました。
Dim ss as New NotesSession
Dim db As NotesDatabase
Dim dc As NotesDocumentCollection
Dim doc As NotesDocument
Dim Query As String

Query = |[Subject] contains "request"|
Set db = ss.CurrentDatabase
Set dc = db.FTSearch( Query, 0 )
Set doc = dc.GetFirstDocument
While not doc is Nothing
    set doc = dc.GetNextDocument( doc )
Wend

このコードを実行して閲覧できない文書が検索にヒットした場合、GetFirstDocument や GetNextDocument で取り出した NotesDocument は Nothing になりませんが、UniversalID プロパティや NoteID プロパティの値は "" となりました。また NotesDocument クラスの Items プロパティの値はヌルになるため、フィールドの値へアクセスすることもできません。

つまり、見えない文書が存在することは分かるけれども、その詳細にはアクセスできないのです。

ここで注意しなければならないのは、読者フィールドを使ったデータベースでは、閲覧できない文書がヒットすることも想定しなければならない、ということです。

実は、私が書いたコードで「結果がなんか変だよ」と指摘があり調べたら、この想定がされてなかった、ということがあったのを思い出しました。

皆様も読者フィールドによる閲覧制限機能を後から追加するような変更を行う場合は、ぜひともご注意ください。

2019年8月13日火曜日

JSON Pointer に翻弄される

前回のエントリ「JSON から目的の値を取り出す」では NotesJSONNavigator クラスの getElementByPointer メソッドを使った例をロクな説明もなしに示しました。

私が翻弄されたのは、JSONのデータに「配列」と「連想配列」が混在することからくるものでした。

この「配列」は、
[ "v1", "v2", "v3" ]
のように「値」を大括弧 [ ] で囲みます。

もうひとつの「連想配列」はというと
{ "k1": "v1", "k2": "v2", "k3": "v3" }
のように「キー」と「値」のペアを中括弧 { } で囲みます。
キーと値のペアで配列を作るところは、LotusScript で例えるなら「リスト」に似ていると思います。


そして getElementByPonter メソッドを使うと、JSON 上の位置を Pointer に指定することで効率よく目的の Element から「値」を取り出すことができます。

実際のコードを示します。
※以下の環境は Community版の Notes 10.0.1 FP2 です

まずは次のような Function を用意しました。
Function getValue( json$, pointer$ ) As Variant
 Dim ss As New NotesSession
 Dim nav As NotesJSONNavigator
 Dim elm As NotesJSONElement
 
 Set nav = ss.Createjsonnavigator( json )
 Set elm = nav.Getelementbypointer( pointer )
 Select Case elm.Type
  Case 1, 2 '1: Object, 2: Array
   MessageBox "Element type is Object or Array"
  Case 3, 4, 5, 64 '3: String, 4: Number, 5: Boolean, 64: Empty
   getValue = elm.Value
  Case Else
   messagebox "undefined type"
 End Select
End Function

配列にある2番目の値を取り出す場合、Pointer に "/1" を指定します。
JSON = |[ "v1", "v2", "v3" ]|
pointer = "/1"
Print getValue( JSON, pointer )

配列の最初は 0、2番目は 1、n番目は n-1 といった数字をスラッシュ"/"の右側に指定します。

連想配列では、キー "k2" の値を取り出す場合、Pointer に "/k2" を指定します。
JSON = |{ "k1": "v1", "k2": "v2", "k3": "v3" }|
pointer = "/k2"
Print getValue( JSON, pointer )

連想配列は、キーを人間が理解しやすい文言にしておくことで、値が取り出しやすくなります。


さて、実際の JSON では、連想配列の「値」の部分が「配列」になっている、といったように、連想配列と配列とが交互に現れたりする場合があります。

これらを区別しながら、連想配列 { } の場合は「キー」を、配列 [] の場合は「位置」を、それぞれ正しく指定しなければなりません。

前回のエントリでも示した次の JSON を見ていきます。
{
  "responses": [
    {
      "textAnnotations": [
        {
          "locale": "ja",
          "description": "取り出したいテキストは\nここにあります!\n",
          "boundingPoly": {
            "vertices": [・・・
            ]
          }
        }
      ]
    }
  ]
}

ここから "description" の「値」を取り出したいので、

  1. 連想配列にあるキー"responses" の値(配列)の、
  2. その配列の最初 "0" の、
  3. 連想配列にあるキー "textAnnotations" の値(配列)の、
  4. その配列の最初 "0" の、
  5. 連想配列にあるキー "description"

という構造になるよう Pointer を組み立てると、次のようになります。
/responses/0/textAnnotations/0/description


ところで Pointer では、連想配列の「キー」や配列の「位置」をスラッシュ"/"で区切っていますが、「キー」にスラッシュ"/"を含む場合はどうしたら?と疑問がわきました。

「きっとバックスラッシュでエスケープするんだろうな」などと安易に試したら見事に外れてしまい...

実はこれ RFC 6901 で仕様化されていました。

スラッシュ "/" とチルダ "~" は特別な意味があることから、
"/" は "~1"
"~" は "~0"
に符号化するのだそうです。

ということで、次のように キーが "Key/0" の場合、Pointer は "/key~10" とするのが正解でした。
JSON = |{ "key/0": false, "Key/1", true }|
pointer = "/key~10"
Print getValue( JSON, pointer )



以下におまけとして、(私にとって?)イレギュラーだったケースを以下に示します。

JSON文字列が(指定できることが正しいかどうかはさておき)配列や連想配列ではない場合、Pointer を "" とすると取れました。
JSON = |"v1"|
pointer = ""
Print getValue( JSON, pointer )

連想配列の「キー」が日本語の場合、Pointer として日本語文字列を指定しても not found (コード 4843)となりました。
JSON = |["キー1", "値1", "キー2", "値2"]|
pointer = "キー2"
Print getValue( JSON, pointer )


「値」が真偽値( True / False )の場合、True / False の最初の文字が小文字になっていないと、 JSON を JSONNavigator へロードしたところでパースできないことを示すエラー(コード 4842)となりました。
JSON = |["1", 2, True]| 'true でないとエラーになる
pointer = "/2"
Print getValue( JSON, pointer )


それから、連想配列や配列の最後に余計なカンマがある場合もパースできないと叱られてしまいました...
文法は厳しくチェックされるようです。

2019年8月10日土曜日

JSON から目的の値を取り出す

画像に含まれるテキストをOCRで抽出するサービスが Google にあることを IBM Champion の小野様に教えていただきました。 


ものは試しにと LotusScript のエージェントでチャレンジしてみたのですが、不慣れな JSON に手間取ってしまったので、ここにメモとして残しておこうと思った次第です。


作成したエージェントの概要ですが、NotesStream に画像ファイルを読み込み Base64 エンコードしたものを、リクエストのボディとして渡す JSON の一部に埋め込み、NotesHTTPRequest を使って指定のエンドポイント(URL)へ JSON を POST します。返ってきた JSON から OCR で取得できたテキストを取り出して表示します。

このエージェントの中で OCR の結果が JSON 形式で戻るのですが、これが私にとってなかなか分かりづらいものでした。 戻ってきた JSON は(実際には2121行もあったのでほぼ省略しますが)次のようになっていました。
{
  "responses": [
    {
      "textAnnotations": [
        {
          "locale": "ja",
          "description": "取り出したいテキストは\nここにあります!\n",
          "boundingPoly": {
            "vertices": [・・・
            ]
          }
        },・・・
      ]
    }
  ]
}

実際に戻ってきた JSON データには、OCR で抽出できた「テキストの全体」を示すものと、「テキストの一部」を示すものがありました。今回取り出したいのはテキストの全体を示すほうです。

そして「テキスト全体」がJSONデータの中に、どうやら2か所にあることがわかりました。どちらを採用するのが正しいのかはさておき、今回はJSONデータの冒頭に見つけた、次の位置の文字列を取り出すことにしました。

「配列 "responses" の最初のエレメントである配列 "textAnnotations" の最初のエレメントにある "description"」


まずは戻ってきた JSON を NotesJSONNavigator などの V10 で追加されたJSONを処理するためのクラス群を使って目的の値を取り出します。

まず私が一つ目の値の取り込みに使用したのは次のようなコードです。変数"JSON"に上記JOSN文字列がセットされてます。

Dim ss As New NotesSession
Dim nav As NotesJSONNavigator
Dim elm As NotesJSONElement
Dim obj As NotesJSONObject
Dim arr As NotesJSONArray

Set nav = ss.CreateJSONNavigator(JSON)
Set elm = nav.Getelementbyname("responses")
Set arr = elm.Value
Set elm = arr.Getfirstelement()
Set obj = elm.Value
Set elm = obj.Getelementbyname("textAnnotations")
Set arr = elm.Value
Set elm = arr.Getfirstelement()
Set obj = elm.Value
Set elm = obj.Getelementbyname("description")
Print elm.Value


取り込みたい JSONObject の名前や配列上の位置が決まっていることが前提なので JSONElement の Type を確認することなくこのようなコードにしています。

位置が「決まっている」とはいえ、少々面倒くさいというか冗長なコードだなという印象です。


ところが、先ほどこちらの資料を読んでいたら、もっとシンプルなコードで実現できることがわかりました。

Dim ss As New NotesSession
Dim nav As NotesJSONNavigator

Set nav = ss.CreateJSONNavigator(JSON)
Set elm = nav.Getelementbypointer( "/responses/0/textAnnotations/0/description" )
Print elm.Value


NotesJSONNavigator クラスの GetElementByPointer メソッドを使うことで、ずいぶんとシンプルになりました。
このとき「配列の最初」を示す数値は「0」となります。

2019年8月9日金曜日

ビューの「索引」

昨晩は「のの会」に参加しました。

実は先月の「のの会」でお話したネタが時間切れのため約半分を次回持越しとしました。
それで持ち越した残り半分を、昨晩お話しました。

そのネタとは「ビュー索引」です。

Notes/Domino の世界で索引と言えば、「全文索引」と「ビュー索引」の2つありますが、今回はビュー索引にスポットをあててみました。

ビュー索引は、パフォーマンスに影響することがしばしばあります。そのため、ビュー索引を維持管理する仕組みには様々な工夫があり、機能改善が続けられています。

お話しながら見ていただいた資料は(のの会でのお話にあわせて)前半/後半に分けて SlideShare へアップしました。

前半は、

  • ビューの構造
  • 索引の維持に関連するビューの設定
  • インデクサ

後半は、

  • 索引を維持するタスクと調整するオプション
  • バージョン9,10における新機能
  • パフォーマンスに係る状況の確認の方法

といった内容となっています。

基本的には、メーカー様のWebサイトの情報やヘルプを読んだ上で、私の理解を書き出したものであり、決してメーカー様の校閲を受けたわけではありません。そのため、もしかしたら内容に誤りがあるかもしれません。内容への自身の無さの表れとしてネタ元へのリンク(IBM様のサイトためそのうちリンク切れするかもですが...)を付けていますので、ネタ元もあわせてご確認ください。





2019年7月26日金曜日

Domino の CORS 対応

CORS (Cross-Origin Resource Sharing)とは、ブラウザがHTMLを読み込んだサーバー(オリジン)以外のサーバーのデータへのアクセスを可能にするための仕組みのことです。

たとえば、node.js アプリを実行中のサーバーへブラウザでアクセスすると、アプリが HTML を返します。そのHTMLに書かれた JavaScript が(オリジンが異なる)Domino サーバー上のデータへ REST API でアクセスしようとします。この時 Domino 側で node.js アプリからのアクセスを許可する設定を行なっていない場合、ブラウザ側でブロックする、というものです。

実際このような構成で、何も対策していない Domino を使ってテストしてみたところ 401 Unauthorized となりました。
※上の画像でメソッドが GET でなく OPTIONS になっていますが、これはプリフライト(サーバーから対応するメソッドの一覧を収集すること)によるものと思います

今回テスト環境として用意した node.js 側のアプリ「app.js」と、そこから呼び出す「index.html」の内容は次のとおりです。

「app.js」の内容
const app = require('express')();
const http = require('http').Server(app);

http.listen(3000, () => console.log('Server started'))

app.get('/', function(req, res) {
    res.sendFile(__dirname + '/index.html')
})


「index.html」の内容
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>10.0.1 FP2 CORS Test</title>
    <script type="text/javascript">
 function firstscript() {
      var url = 'http://localhost/statpub.nsf/api/data/collections';
      var req = new XMLHttpRequest();
      req.open( 'GET', url);
      req.onreadystatechange = function() {
        if(req.readyState === 4 && req.status === 200) {
          var element = document.getElementById('corstest');
          element.innerHTML = '<pre>' + req.responseText + '</pre>';
        }
      };
      var authBasic = window.btoa('testuser:password');
      req.setRequestHeader('Authorization', 'Basic ' + authBasic);
      req.send();
    }
    </script>
  </head>
  <body onload="firstscript();">
    <div id="corstest">test</div>
  </body>
</html>


node.js のアプリは ポート 3000番を指定しています。Domino の HTTP タスクはデフォルトのままですので 80 番が使用されます。
どちらも同じPC上で稼働しているため、ホストは localhost です。
しかしながらオリジンは「スキーム(http://)」、「ホスト(localhost)」、「ポート」の組み合わせですので node.js の http://localhost:3000 と Domino の http://localhost は同一オリジンとはなりません。


そこで、ブラウザが CORS によりブロックしないよう、Domino 側で CORS への対応を実施することが必要になります。

CORS への対応とは、許可する「オリジン」や「メソッド」等を設定することです。

Domino へアクセスした時、Domino はオリジンやメソッド等が許可されたものであるかを確認します。それらが許可しているものであれば、許可していることを示すヘッダー情報をブラウザへ返します。


さて、Domino のオンラインヘルプによれば、Domino 10.0.1 Fix Pack 2 以降で CORS に対応する、とあります。


実はそれまでのバージョンでも、Webサイトルール文書を作成することによって対応可能だったようです。
smaconne の利用マニュアルには「複数の IBM Domino サーバーを利用する場合のサーバー設定」として記載があります。※光安様、情報ありがとうございます。
また smaconne ではありませんが、Webサイトルール文書を使って設定する際の参考となりそうな情報はこちらにもありました。10.0.1 FP2 以前の Domino をご利用の場合は参考になるのではないでしょうか。

では FP2 ではどのようにして許可の設定を行うのでしょうか。

設定方法はこちらにありますが、次の3つの手順を踏みます。

  1. サーバー文書で CORS を有効にする
  2. CORS のルールを記述したファイル(ファイル名: cors-rules.json)を作成し、Domino\Data\Domino\CORS フォルダへ保存する
  3. HTTP タスクを再起動する

サーバー文書の設定は、Internet Protocols タブ - HTTP タブ - DSAPI のくくりにある 「DSAPI filter file name」に ncorsext を追加します。

手順の2番目で作成する json ファイルのサンプルはこちらにあります。

今回テスト環境用に作成した、CORS のルールを記述したファイル「cors-rules.json」の内容は次のとおりです。
{
  "version": "1.0",
  "rules": [
    {
      "resource": {
        "path": "/api/data/collections"
      },
      "allowOrigins": [ "http://localhost:3000" ],
      "allowMethods": [ "GET" ],
      "allowCredentials": true
    }
  ]
}


node.js アプリのオリジン「http://localhost:3000」から Domino の 「/api/data/collections」への GET 要求を許可しています。


CORS のルールが HTTP タスク起動時に正常にロードされると、コンソールに次のように表示されました。
2019/07/26 15:07:04   HTTP Server: DSAPI CORS Filter Loaded successfully

参考までに json ファイルに誤りがあった際にコンソールに表示されたメッセージがこちらです。
2019/07/25 20:45:11.36 cors::InitConfig> Unable to parse configuration C:\IBM\Domino\data\domino\cors\cors-rules.json
2019/07/25 20:45:11   HTTP Server: Failed to load DSAPI module ncorsext
※上で紹介しました「json ファイルのサンプル」のページに記載されている例には、不要なカンマが含まれており、そのままコピーするとロードに失敗します...

以上の設定を行い、http://localhost:3000 へアクセスしたところ、オリジンが異なる Domino (http://localhost)のデータにアクセスすることができ、取得したデータが表示されました。
左側が取得できた json を表示したもの。右側は取得した際のデバッグ情報

CORS に対応できたことが確認できました。


試しに CORS のルールで、allowOrigins のポート番号を変えたり、allowMethods を POST に変えたりしてみたところ、一致するルールが無いため 401 Unauthorized となりました。

なお allowMethods に "OPTIONS" も追加したほうが良いのかもしれませんが、今回の環境ではあってもなくても変化が見られませんでした。

2019年7月21日日曜日

おれおれ APM で Domino の統計情報をグラフ化

以前のエントリでも紹介しましたが、V10 では統計情報を New Relic というSaaSの監視サービスと連携する新機能があります。

実は、New Relic だけでなく、他の監視サービスとも連携できそうだということはオンラインヘルプにも書かれていたのでなんとなく知識としてはありましたが、これまで試したことはありませんでした。

ところで "New Relic" をグーグル先生に聞くと「APM」という単語が頻繁にヒットします。
この APM とは「アプリケーション性能管理」を指す一般的な用語らしいのですが、世の中には APM を謳うサービスが New Relic 以外にも多く存在することを最近知りました。

参考までに、グーグル先生が教えてくれた APM 関連製品/サービスを羅列してみます。
Datadog, Nagios, Stackify, Sensu, Pingdom, LogicMonitor, Splunk, Grafana, Scout, SteelCentral, Applications Manager, AppDynamics, dynaTrace, CA APM, JENNIFER, Hosted Graphite
※これらが Domino と連携できるかどうかは未確認です

この中のいくつかを試してみたくなったのですが、New Relic のようにグラフ化されない統計情報があるのだろうし、その場合にどうすればグラフ化できるのかを調べる手間よりも、自前でグラフ化できるのならそっちの手間のほうが楽しそうだと考えました。


さしずめ「おれおれAPM」です。


まずは、New Relic 等への統計の連携機能が REST API を使って POST していることを確認するため、POST 先として Domino の nsf ファイルを試してみることにしました。

まずは、POST を受けるために HTTP タスクを起動します。

サーバーコンソールから次のコマンドを投入しました。
load http


ドミノディレクトリのサーバー文書で、Domino Access Service (DAS) を有効にします。
また、統計情報を保存する NSF ファイル「statpub.nsf」とビューにおいても DAS を有効にしておきます。

※DASを有効にするための設定については「IBM Domino REST API 利用ガイド」が詳しいです


私の環境では、現在の統計の連携先が New Relic なので、それをやめるために notes.ini から NEWRELIC_LICENSE_KEY= で始まる行を削除しています。

そして Domino の NSF ファイルへ統計情報を POST するよう、notes.ini へ次のように設定しました。
STATPUB_ENABLE=1
STATPUB_URI=http://localhost/statpub.nsf/api/data/documents
STATPUB_DATA_HEAD={"host":"xsp13/v10","stattime":$Timestamp$000,
STATPUB_DATA_TAIL=}
STATPUB_METRIC_FORMAT="metric.$Name$":$Value$
STATPUB_DELTA_METRIC_FORMAT="metric.delta.$Name$":$Value$
STATPUB_HEADERS=Content-Type: application/json$Newline$Accept: application/json$Newline$StatTimeStamp: $Timestamp$Newline$

New Relic 以外へ連携する場合は STATPUB_ で始まるパラメータを必要に応じていくつか指定します。

1行目の STATPUB_ENABLE= は統計の公開を有効にする場合は 1 を指定します。無効は 0 です。

2行目の URI は、REST API を使用して NSF に文書を作成する場合に指定するものと同じです。

3,4行目は、統計情報を JSON 形式とするために最初と最後に{}を付加したり、後から使うであろうサーバー名などを追加しています。stattime にはタイムスタンプとしてUNIXエポックの日時を付加できるのですが、ミリ秒の部分が欠落しており、後述の Chart.js で使う場合に都合が悪いので、3桁のゼロを追加しています。

5,6行目は統計情報のフォーマットを定義します。統計情報にある Update.DefferdList といった名前の前後にダブルコーテーションを加え、名前と値の区切り文字としてコロンを指定しています。なお、6行目の delta で始まる統計情報には、前回の値との差を示す値がセットされるようです。

7行目には POST する際にヘッダとして指定するべき Content-Type といったパラメータなどを定義しています。複数のパラメータを改行で区切るために $Newline$ を挟んでいます。
また、このままでは NSFファイル内に作成される文書の作成者は Anonymous となりますが、もし、作成者を特定のアカウントにしたい場合は「Authorization: Basic <アカウント:パスワード>」を追加すると Basic 認証できます(<>には BASE64でエンコードした文字列を設定します)。


さて、これを設定して数分待っていると statpub.nsf に文書が登録されていました。


こうしてこの連携機能が REST API を使って POST していることが確認できました。


ところで、APM はリアルタイムにグラフ化できることがメリットのひとつだと思うのですが、NSFファイルにあるデータをリアルタイムにグラフ化するには、NSFにデータが作成された瞬間に値をグラフへ反映する仕組みが必要です。また、そもそもグラフをどうやって表現するかということにも関係します。

実は現在参加しているノーツコンソーシアムの「アプリ開発研究会」で、リアルタイムにグラフ化するほうのネタを提供してしまったため、このエントリでは「リアルタイム」をちょっと犠牲にして実現する方法をご紹介します。

POST 先のデータベース「statpub.nsf」に、次のようなビューを追加しています。
1列目)列の値:statpub、最新のデータを最も上に表示するため降順でソートします
2列目)列の値:metric.Update.DeferredList
3列目)列の値:metric.Update2.DeferredList

グラフには2つの統計値を表示したいので、2列目と3列目で値を表示しています。
今回は「Updateタスクのキューに入っている要求の数」を見ることを想定して 「metric.Update.DefferdList」を指定しました。なお、私の環境は2つのUpdateタスクを起動しているので Update2 の統計情報が存在します


さらにページ「index」を追加しています。データベースをWebブラウザで開く際にこのページが開くようデータベースのプロパティを変更しています。

ページには次のコードを記述してあり、このコード全体をパススルーHTMLとしています。
</form>
<canvas id="myChart"></canvas>
<script type="text/javascript">
    var ctx = document.getElementById('myChart').getContext('2d');
    var chart = new Chart(ctx, {
        type: 'line',
        data: {
            datasets: [{
                data: [],
                label: 'Update.DeferredList'
            },{
                data: [],
                label: 'Update2.DeferredList'
            }]
        },
        options: {
            elements: {
                line: {
                    tension: 0
                }
            },
            scales: {
                xAxes: [{
                    type: 'realtime'
                }]
            },
            plugins: {
                streaming: {
                    duration: 600000, //保持する時間幅=画面の横幅(ミリ秒)
                    refresh: 15000, //次にデータを読み込むまでのインターバル(ミリ秒)
                    delay: 0, //最新のデータを表示する間隔(tension 0 の場合、関係なし)
                    frameRate: 30, //数字を大きくするとスクロールがスムーズになる
                    pause: false,

                    onRefresh: function(chart) {
                        var json = '';
                        var xhr = new XMLHttpRequest();
                        xhr.onreadystatechange = function() {
                            if (xhr.readyState === 4) {
                                if (xhr.status === 200) {
                                    json = JSON.parse(xhr.responseText);
                                    chart.data.datasets[0].data.push({
                                        x: json[0]["stattime"],
                                        y: json[0]["metric.Update.DeferredList"]
                                    });
                                    chart.data.datasets[1].data.push({
                                        x: json[0]["stattime"],
                                        y: json[0]["metric.Update2.DeferredList"]
                                    });
                                } else {
                                }
                            }
                        }
                        xhr.open( 'GET', 'http://localhost/statpub.nsf/api/data/collections/name/Update.DeferredList?count=1&systemcolumns=0x80a');
                        xhr.send(null);
                    }
                }
            }
        }
    })
</script>
<form>

また、このページの HTML Head Content には以下のように指定しています。
"<meta charset=\"UTF-8\">
<title>リアルタイム統計情報</title>
<script src=\"https://cdn.jsdelivr.net/npm/moment@2.24.0/min/moment.min.js\"></script>
<script src=\"https://cdn.jsdelivr.net/npm/chart.js@2.8.0\"></script>
<script src=\"https://cdn.jsdelivr.net/npm/chartjs-plugin-streaming@1.8.0\"></script>
<style>* { margin: 0px; padding: 0px; }</style>"


このページに書いたコードを簡単に説明します。

統計情報の折れ線グラフを時系列に表示したかったので、今回は Chart.js に chartjs-plugin-streaming プラグインを組み合わせています。グラフは canvas タグの場所に最新の10分間(単位をミリ秒で指定するためコード上は600000)だけ表示する設定です。

ここでは、15秒間隔(単位をミリ秒で指定するためコード上は15000)でリフレッシュしていますが、そのタイミングで XMLHTTPRequest を使ってビューの1行目(最新の統計値)だけを取得します。取得できた JSON データをパースして、日時値と表示したい統計情報の値をグラフに反映します。

グラフを表示するにはブラウザから http://localhost/statpub.nsf へアクセスします。

こうして表示されたグラフがこちらです。※実際はもたもたと右から左へ動きます。


ひとまず「おれおれAPM」ができました。