SplunkのMissile Mapに文字表示できるようにした話

# はじめに

* Splunk の[Missile Map](https://splunkbase.splunk.com/app/3511/) は、地図上に始点、終点の緯度経度を指定して線分（矢印）を表示する無償Appsの一つです。
* 今回、線分（矢印）だけでなくその矢印の付加情報を表示したいというリクエストがあり、始点、終点付近に、文字列を表示する機能追加を行い、開発者に連絡を取りマージを行っていただきました。 
* Missile Mapのバージョン v1.5.0から、この機能が使用可能になる予定です。
    * 追記: April 11, 2022 Version 1.5.0がリリースされました！

* 本書では、新機能の紹介や活用方法の紹介、さらに、どのようにAppに機能追加したかについてご紹介致します。

---

# 新機能紹介

## 矢印の始点と終点に文字列表示ができるようになります

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/589834/eb3a4e65-edde-b25e-66ae-de8c24b786fa.png)

---

## 使い方

下記のように、start_labelと、end_labelフィールドを追加することで、矢印の始点、終点にそれぞれ文字列を表示することができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/589834/7e5a96ce-695d-545c-f095-4b8a6f25b15e.png)

:::note info
（上記に記載が漏れていますが）start_label, end_label はそれぞれ (optional) です。
フィールドがなくても今まで通り文字列表示なしで動作します。
片方だけフィールドを追加することもできます。
:::

---

## サーチ例

始点と終点に都市名を表示するようにしてみました。

```sql
| inputlookup missilemap_testdata
| lookup geolocate local=t lat as start_lat lon as start_lon output city as start_label
| lookup geolocate local=t lat as end_lat lon as end_lon output city as end_label
| table start_lon,start_lat,end_lon,end_lat,color,animate,pulse_at_start,weight,start_label,end_label
```

* 上記 `missilemap_testdata` のlookupは、 [Missile Map](https://splunkbase.splunk.com/app/3511/) をインストールすると使用できます。
* 上記 `lookup geolocate local=t` は、 [Geolocation Lookup for Splunk](https://splunkbase.splunk.com/app/4102/) をインストールすると使用できます。
    * [Geolocation Lookup for Splunk](https://splunkbase.splunk.com/app/4102/) は、緯度経度から country, region, city, timezone等の情報を得ること（逆ジオコーディング）ができる無償Appです。通常のcsvファイルを使ったlookupとは異なり、Custom lookup という仕組みで、緯度経度から近い都市名の情報を見つけるためのアルゴリズムで効率的に情報を検索するようです。国名や都市名から緯度経度を得ること（ジオコーディング）もできます。

---

### 上記サーチのサーチ結果

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/589834/d088abc5-487a-6da5-249e-f16b03414da7.png)

* 右端2列に加えた、start_label, end_labelフィールドで、矢印の始点、終点の文字列を指定できます。

---

# 前提知識

* Missile Mapは、Splunk社がオフィシャルにサポートしているApp（Splunk Supported）ではありませんが、Developer Support（開発者によるサポート）に区分されるAppで、お客様が開発者と直接連絡を取ることでサポートを受けられることがあります。
    * サポートの区分については [こちらの記事](https://qiita.com/odorusatoshi/items/52123ce3351b0343ef7a) で紹介されています。

* Splunkは、Apps(Add-on)をインストールすることで、Splunkに、諸設定、ダッシュボード等のUI、保存されたサーチ、API、サーチコマンド（Custom Command）、可視化（Custom Visualization）などを追加することができます。
    * Missile Mapの地図表示機能は、Custom Visualizationの一つです。

* Custom Visualization のAppの作成方法については、 [こちらの公式docページ](https://docs.splunk.com/Documentation/SplunkCloud/latest/AdvancedDev/CustomVizTutorial) に記載があります。

---

# どうやってAppに機能追加したのか？

Missile Mapの v1.3.0をベースに変更を行いました。

## 対象ファイル

missile mapのAppをインストール後、インストールディレクトリを確認すると以下のようなファイルが存在することがわかります。

```sh
$ cd /opt/splunk/etc/apps/
$ find missile_map -type f

missile_map/lookups/missilemap_testdata.csv
missile_map/default/app.conf
missile_map/default/savedsearches.conf
missile_map/default/transforms.conf
missile_map/default/visualizations.conf
missile_map/README/savedsearches.conf.spec
missile_map/README.md
missile_map/static/screenshot.png
missile_map/static/appIcon.png
missile_map/static/appIcon_2x.png
missile_map/static/appIconAlt.png
missile_map/static/appIconAlt_2x.png
missile_map/license.txt
missile_map/appserver/static/visualizations/missile_map/preview.png
missile_map/appserver/static/visualizations/missile_map/visualization.js
missile_map/appserver/static/visualizations/missile_map/webpack.config.js
missile_map/appserver/static/visualizations/missile_map/contrib/leaflet.migrationLayer.js
missile_map/appserver/static/visualizations/missile_map/README.md
missile_map/appserver/static/visualizations/missile_map/visualization.css
missile_map/appserver/static/visualizations/missile_map/package.json
missile_map/appserver/static/visualizations/missile_map/formatter.html
missile_map/appserver/static/visualizations/missile_map/src/missile_map.js
missile_map/metadata/local.meta
missile_map/metadata/default.meta
```

---

[公式docページ](https://docs.splunk.com/Documentation/SplunkCloud/latest/AdvancedDev/CustomVizTutorial#App_directory_and_file_structure) で確認できるとおり、
Custom Visualizationのソースコードは、
missile_map/appserver/static/visualizations/missile_map/src/missile_map.js
と
missile_map/appserver/static/visualizations/missile_map/visualization.js
であることがわかります。

---

昨今のJavaScript開発では、`npm run build`というコマンドで、
元のソースコードと、必要なライブラリの複数のソースコードをマージして1つのソースコードにまとめ、
そちらのソースコードをブラウザに読み込ませることで効率化※を図っています。

::: note info
※npm run buildで効率化されることの具体例：
ブラウザ毎の固有の仕様など互換性の問題を吸収して、大半のブラウザで同じように動作させることができるようにしたり、複数のソースコードを個別にダウンロードする時間をなくし読み込み速度を早くする効果など。
:::

src/missile_map.js が元のソースコードで、
visualization.js がビルドされたソースコードです。
ブラウザで開いた際に使われるのは、visualization.jsの方です。

---

## まずは、元のソースコード（src/missile_map.js） の中身を確認

ソースコードを開いてみると、なんと、高々250行前後しかソースコードがありません。
地図のタイルを表示したり、ズームしたりスクロールさせたりしている割には、随分簡潔ですね。
どうなっているのか確かめたところ、どうやら、leaflet という地図表示用のライブラリを内部で使用しているようです。

leafletについては、 [Missle Mapのdetailsページ](https://splunkbase.splunk.com/app/3511/#/details) でも確認できます。 

> Software credits
> LeafletJS: http://leafletjs.com/
> Used under BSD license (https://github.com/Leaflet/Leaflet/blob/master/LICENSE)
>
> leaflet.migrationLayer: https://github.com/react-map/leaflet.migrationLayer
> Used under MIT license (https://github.com/react-map/leaflet.migrationLayer/blob/master/LICENSE)

---

## leaflet のAPIを確認

次に、 [leaflet.migrationLayerのAPI](https://github.com/lit-forest/leaflet.migrationLayer#apioptions) を確認してみると、

| option | Description | Default Value |	Possible values | Required |
| -- | -- | -- | -- | -- |
| arcLabel | show from and to label | true | Bool | no |
| arcLabelFont | label font and size | '15px sans-serif' | 'size font' | no |

のように ラベル表示の有無や、フォント指定できるオプションがあるため、このライブラリには元々 文字表示する機能が実装されていそうです。

---

さらに詳しく、表示方法を探していくと、

> data format
> `data = [{"from":[-118.2705,33.9984],"to":[-122.789336,37.920458],"labels":["Los Angeles","San Francisco"],"color":"#ff3a31","value":15}];`


のように、 `"labels":["始点のラベル", "終点のラベル"]` という情報を、地図上の各線分（矢印）を表示するための data に含めるとラベルを表示することができるようです。

---

## ソースコード修正箇所の特定

### ①ライブラリに渡す data の作成箇所

ソースコード src/missile_map.js 内で dataを作成している箇所をまず見つけます。

下記の部分で、サーチ結果の複数行のデータを示す変数 `dataRows` から、leafletライブラリに渡す 線分（矢印） の情報（data）として、`formatted`という変数を作成しているようです。

```js
var formatted = dataRows.map(function(d) {
...
return {
    "from":[start_lon, start_lat], "to":[end_lon, end_lat], "color": color, "animate": animate, "pulse_at_start": pulse_at_start, "weight": weight
}
```

---

### ②data 作成に使用している変数の設定箇所

上記の `return { ...（略）... }` の中で使用している `start_lon`, `start_lat`, `end_lon`, `end_lat`, ... 等を設定している箇所を、ソースコードをさかのぼって探してみると、以下のあたりにあります。

```js
var formatted = dataRows.map(function(d) {
    var end_lat = parseFloat(+d[end_lat_idx]);
    var end_lon = parseFloat(+d[end_lon_idx]);
    var start_lat = parseFloat(+d[start_lat_idx]);
    var start_lon = parseFloat(+d[start_lon_idx]);
    var color = vizUtils.escapeHtml(d[color_idx]);
    var animate = vizUtils.normalizeBoolean(d[animate_idx]);
    var pulse_at_start = vizUtils.normalizeBoolean(d[pulse_idx]);
    var weight = +d[weight_idx];
```

この中で、`start_lat_idx`, `start_lon_idx` などの変数を使用しています。
これらは、サーチ結果`dataRows`の中の何列目に、missile mapで使用するフィールド名の列があるかを示す数値のようです。

---

### ③列番号「〜_idx」変数の設定箇所

これらの変数を設定している箇所もソースコードをさかのぼって探します。
以下のところで設定していました。

```js
var dataFields = data.fields;
for (i = 0; i < dataFields.length; i++) {

    switch (dataFields[i].name) {
        case "end_lat":
            var end_lat_idx = i;
            break;
        case "end_lon":
            var end_lon_idx = i;
            break;
        case "start_lat":
            var start_lat_idx = i;
            break;
        case "start_lon":
            var start_lon_idx = i;
            break;
        case "color":
            var color_idx = i;
            break;
        case "animate":
            var animate_idx = i;
            break;
        case "pulse_at_start":
            var pulse_idx = i;
            break;
        case "weight":
            var weight_idx = i;
            break;
```

---

## ソースコード変更

処理の順序的には、③→②→① なので、以下、この順で変更内容を記載します。

### 【③列番号「〜_idx」変数の設定箇所】の変更

以下のcaseを追加。

```js
case "start_label":
    var start_label_idx = i;
    break;
case "end_label":
    var end_label_idx = i;
    break;
```

---

### 【②data 作成に使用している変数の設定箇所】の変更

以下の変数設定を追加。

```js
var start_label = d[start_label_idx];
var end_label = d[end_label_idx];
```

---

### 【①ライブラリに渡す data の作成箇所】の変更

サーチ結果に、

1. start_label, end_label の両方のフィールドが含まれている場合
1. start_labelフィールドのみ含まれている（end_labelフィールドはない）場合
1. end_labelフィールドのみ含まれている（start_labelフィールドはない）場合
1. start_label, end_label のどちらのフィールドも含まれていない場合（従来どおりの場合）

の4パターンに場合分けして、`if else if ... else` で、以下の情報を追加したdataを作成するよう処理を追加します。

1. `"labels":[start_label, end_label],`
1. `"labels":[start_label, ""],`
1. `"labels":["", end_label],`
1. 追加なし（従来どおり）

---

```js
if( start_label_idx && end_label_idx ){
    return {
        "from":[start_lon, start_lat], "to":[end_lon, end_lat], "labels":[start_label, end_label], "color": color, "animate": animate, "pulse_at_start": pulse_at_start, "weight": weight
    }
}
else if( start_label_idx ){
    return {
        "from":[start_lon, start_lat], "to":[end_lon, end_lat], "labels":[start_label, ""], "color": color, "animate": animate, "pulse_at_start": pulse_at_start, "weight": weight
    }
}
else if( end_label_idx ){
    return {
        "from":[start_lon, start_lat], "to":[end_lon, end_lat], "labels":["", end_label], "color": color, "animate": animate, "pulse_at_start": pulse_at_start, "weight": weight
    }
}
else{
    return {
        "from":[start_lon, start_lat], "to":[end_lon, end_lat], "color": color, "animate": animate, "pulse_at_start": pulse_at_start, "weight": weight
    }
}
```

---

## visualization.js を更新

本来は、ここで、srcの変更後に、`npm run build`コマンドで、visualization.jsをリビルド（再生成）します。
しかし、今回はちょっとした変更で済み、開発環境を作るまでもないので、
npm環境を用意する作業をスキップして、直接 visualization.js を編集して書き換えて、動作確認することにしました。

* 正規の手順は、 以下の箇所に記載があります。
    * ですが、その前に、`npm install` コマンドなどで、必要なライブラリをインターネット上からダウンロードして集めてきたり、そもそも npmコマンドが未インストールの場合はインストールしたりする必要があります。
[参考箇所：Set up the visualization source code](https://docs.splunk.com/Documentation/SplunkCloud/latest/AdvancedDev/CustomVizTutorial#Set_up_the_visualization_source_code)

https://docs.splunk.com/Documentation/SplunkCloud/latest/AdvancedDev/CustomVizTutorial#Rebuild_the_visualization

---

### visualization.js の当該部分を直接書き換え

visualization.js をエディタで開き、「start_lat」などで該当箇所を検索して、先程、src/missile_map.js に対して加えた変更と同じ変更を追記します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/589834/ecb8782b-e2d5-ce79-e771-0aab4882e76c.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/589834/133c95ed-5981-97d5-3482-ab6f412a6df9.png)

---

# 動作確認

## splunk再起動で、変更を反映

`/opt/splunk/bin/splunk restart` を実行し、設定変更を反映します。

その後、ブラウザでSplunkのWebUI画面を開きます。
Splunkのデフォルト設定では、
ブラウザが古いソースコードのままの visualization.js をキャッシュしつづけていることがありますので、
ブラウザのキャッシュのクリア等を実施して、新しい visualization.js を読み込ませます。

---

## サーチして表示を確認

テストデータを生成するようなサーチを適当に作成。
Alice, Bob, Carolの3者が、アメリカ上空を移動した軌跡？みたいな模擬データです。

```sh
| makeresults count=3
| streamstats count 
| eval id=case(count=1,"Alice",count=2,"Bob",count=3,"Carol"), end_lon=-120+(random()%100)/10, end_lat=30+(random()%100)/10, speed=0.2+random()%50/100
| map search=" | makeresults count=6 | streamstats count | eval _time=_time+count*3600, id=\"$id$\", speed=$speed$, end_lon=$end_lon$, end_lon=end_lon+sin(count*speed)*speed*count, end_lat=$end_lat$, end_lat=end_lat+cos(count*speed)*speed*count, color=\"#\".printf(\"%02x%02x%02x\",tonumber(substr(md5(id),0,2),16)/2,tonumber(substr(md5(id),2,2),16)/2,tonumber(substr(md5(id),4,2),16)/2) | autoregress end_lon as start_lon | autoregress end_lat as start_lat | autoregress _time as start_time | where isnotnull(start_lon) "
| eventstats earliest(_time) as earliest latest(_time) as latest by id
| eval start_label=if(_time=earliest,id,""), end_label=strftime(_time,"%H:%M")
| fieldformat earliest=strftime(earliest,"%F %T") 
| fieldformat latest=strftime(latest,"%F %T")
| table color start_label end_label start_lat start_lon end_lat end_lon
```

---

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/589834/53cf1584-64f0-0539-e50d-521b218ae662.png)

このように、各者の最初の矢印の始点にのみ、start_label として名前（Alice, Bob, Carol）を表示し、矢印の終点に、end_label として時刻情報を表示することができました。

---

# Missile Mapの作者に変更内容の取り込みを依頼

Missile MapはDeveloper Support（開発者によるサポート）に区分されるAppですので、
開発者にお願いすれば今回の変更を取り入れてくれるかもしれません。
[Missle Mapのdetailsページ](https://splunkbase.splunk.com/app/3511/#/details)に、
以下のように、Supportの連絡先が記載されていました。
こちらの連絡先にソースコードのdiff（変更箇所）とREADMEの変更案など記載（英語）してマージをお願いしたところ、すぐに対応してくれて次回リリース(v1.5.0)で機能追加されることとなりました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/589834/e1111b84-618e-1a64-74f9-21ed7b42a680.png)

:::note info
無償Appは、他のオープンソースソフトウェア（OSS）と同様に、サポート体制はさまざまですので、常に今回のように迅速に対応してくれるとはかぎりません。
今回は、作者である Luke が快く対応してくれました。 Thanks, Luke!
:::

---

# まとめ

* Missile Mapの新機能である、始点、終点の文字列表示についてご紹介しました。（v1.5.0から使用可能になる予定）
* 活用方法として、[都市名を表示](#矢印の始点と終点に文字列表示ができるようになります)したり、[移動対象のIDや時刻情報などを表示](#サーチして表示を確認)する例をご紹介しました。
* さらに、App（Custom Visualization）の機能追加方法についても簡単にご紹介しました。

Missile Map v1.5.0がリリースされたらぜひ新機能をご活用下さい！

https://splunkbase.splunk.com/app/3511/

---

# 謝辞

* リクエストを上げてくださった H様 ありがとうございました。
    * Missile Mapは多くのSplunkユーザに愛用されているAppであるため、今回の機能追加は多くの皆様のお役に立てるものだと思います。
* 突然のマージリクエストにも関わらず快く対応してくださった Luke 、及び Rivium の皆様 ありがとうございました。