性能試験について(性能試験とは編)

先日、社内会議で性能試験についての発表がありました。 発表された内容がとても分かりやすくまとまっていたため、ブログの方でも紹介したいと思います。

性能試験とは

  • 非機能要件を満たせているか確認するための試験で、以下のような項目から試験の前提条件や内容を決めます。
    • 同時接続数
      • どれくらいの負荷をかけるべきか
    • データ保持期間
      • どれくらいのデータをあらかじめ用意しておくか
    • レスポンスタイム
      • 単位時間内にどれだけ速く処理できるか
    • スループット
      • 単位時間内にどれだけの量を処理できるか
    • リソース(メモリ、CPU)使用率
      • 単位時間内にどれだけリソースを使っているか
  • 非機能要件に記載のないものについても試験に含めるようにします。評価は一般的な基準で行います。
    • 例:GCの負荷状況、ディスクI/O、ネットワーク利用状況、(クラウド環境の場合)ネットワークの帯域制限など
  • 上記から試験計画書、試験シナリオを作成し、お客様と合意をとります。

試験実施タイミングと実施期間

実施タイミング

  • 試験実施は総合テストフェーズで実施します。
    • 事前準備(試験計画など)は、前倒しで結合テスト以前から進めたりもします。

実施期間

  • 試験実施の期間はおおよそ1か月~2か月程度です。
    • チューニングや再試験を考慮して余裕をもった日程にすることが大切です。

性能試験のポイント

最後に性能試験で抑えておくべき重要なポイントをまとめます。

数年後想定での試験計画

  • 非機能要件定義書等で記載されているデータの保持期間(ワークフローの完了案件データ等)やユーザ数の増加見込み等を元にデータの積み込み件数やユーザの同時接続等を検討しましょう。

評価項目の確定

  • 試験計画書作成時に、非機能要件定義書等をベースにお客様とどこを評価対象とし、どこまでをゴールとするか確約を取っておきましょう。

試験の対象範囲

  • 試験計画書作成時に、対象となるアプリケーション(シナリオ)、サーバ台数(縮小試験込みか)、同時接続数等について工数と期間を考慮した上で、お客様と検討しましょう。

ログや各種情報の取得

  • 評価項目の他に、性能問題発生時にどこがボトルネックとなっているか切り分けが出来るように各サーバにおいてログや情報出力の内容を把握した上で取得出来るようにしておきましょう。

バックアップリストア

  • 検証環境や稼働前の本番環境での実施の他に、再試験を行う場合もあるので任意のタイミングで環境を戻せるように、バックアップ/リストアの手順の確立と動作確認した上でバックアップ取得を行っておきましょう(依頼を出すのか自分たちで対応するのかも確認は必要)。

利用するサービス及び環境の事前確認

  • 外部WEBサービス連携やクラウド環境を利用する場合、事前申請手続きが必要であったり、そもそも性能試験NG(短時間の高負荷) の場合もあるので、必ず事前に確認しておきましょう。

次回予定

  • 性能試験の実施について紹介します。

WeatherAPIを使ってintra-martの週間スケジュール画面に都道府県ごとの天気と気温を表示する

2021年度に入社したY.Kです。

今回はintra-martのCollaborationのスケジュール画面に、都道府県ごとの天気予報を表示する機能を実装しましたので紹介します。

事前準備

天気予報の取得にはWeather APIを使います。

https://openweathermap.org/api

このAPIは無料で使うことができますが、アカウントの登録が必要なのでその点は注意しましょう。

APIキーの確認方法

登録が完了すると天気予報を取得するためのAPIキーが発行されます。 このAPIキーはメニュー画面の『MyAPI keys』から確認できます。

f:id:sanok-gsol:20220307184821p:plain
WeatherAPIキーの確認画面

実装内容

APIキーを確認できたら早速実装に入りたいと思います。

都道府県選択用セレクトボックス作成

都道府県のデータは下記からjson形式で取得できます。

https://www.javadrive.jp/google-maps-javascript/data/data/pref.json

取得したデータの都道府県名を使って都道府県を選択できるセレクトボックスを作成します。

 $.ajax({
        url: 'pref_coordinate.json',
        dataType: "json",
        type: 'GET',
    })
    .done(function (data){
        //地域セレクトボックスを作成
        var prefectures = '<option class ="not-select" value="">未選択</option>';
        for (var i = 0; i < data.marker.length; i++){
            prefectures += '<option value=' + i + '>' + data.marker[i].pref + '</option>';
        }
        $('.imui-list-toolbar').append('<li><select class="weather-select" style="margin:5px">' + prefectures + '</select></li>');

日付横に天気アイコンを表示

続いて、セレクトボックスで選択された都道府県の天気を日付横にアイコンで表示させる処理を行います。 この天気アイコンを表示するために、WeatherAPIが提供するOneCallAPIを使います。

OneCallAPIドキュメント

One Call API: weather data for any geographical coordinate - OpenWeatherMap

呼び出しAPI

https://api.openweathermap.org/data/2.5/onecall?lat={lat}&lon={lon}&exclude={part}&appid={API key}

こちらのAPIで必要なデータは以下の3つです。

  • 緯度
  • 経度
  • APIキー

緯度と経度は、セレクトボックス作成時のjsonファイルに記載されていますのでそちらから取得します。APIキーは各自のマイページから確認してそちらをコピーして使いましょう。

天気アイコンにカーソルを合わせると気温が表示される

気温の表示に関してもOneCallAPIを使います。

天気予報を表示する

都道府県で取得した緯度と経度、そしてweatherAPIキーを入れて通信を行いデータを取得します。

'https://api.openweathermap.org/data/2.5/onecall?lat=' + 緯度 + '&lon=' + 経度 + '&exclude=current,minutely,houly,alerts&units=metric&appid=' + weatherAPIキー;

取得した天気予報には天気のアイコンや気温などもあるのでそちらも使って都道府県が選択されている場合にのみ日付横に天気を表示させます。

完成画面

実装をした際の完成画面です。マウスが映っていませんがマウスを天気アイコンに合わせてある状態です。

f:id:yamatotox:20210521221554p:plain
気温表示

まとめ

今回は簡易的ではありますが、天気予報をスケジュール画面に表示させる方法を紹介しました。WeatehrAPIを使えば難しい処理をせずに実装できるので、ぜひ試してみてください。

intra-martスクリプト開発APIで詰まったこと

はじめに

こんにちは。開発部のS.Rです。本年もよろしくお願いいたします。

私の今年の目標は、長らくのテレワーク生活で増えてしまった体重を元に戻すことです。

さっそくですが今回は業務中に詰まってしまったことと、その所感について述べようと思います。

発生したこと

スクリプト開発APIのFormaFileUploadItemManagerオブジェクト、registerFileTempメソッド使用でエラーが発生するが、原因が分からない。

出力されるエラーログは以下のみ。

[ERROR] FR_LOG - [E.FORMA.COMMON.00402] 引数が不正です。

原因

いろいろと調査をしていると、Java版で同等のメソッド(registerFileTemp)の引数がスクリプト版とは、少し異なっていることに気づきました(2022年3月時点)。そこでJava版と同じように、第一引数の ファイル情報(fileInfoParam)を配列にして渡したところ上手く動作しました。

ドキュメントに記載の引数の型に誤りがあったことが、今回のエラーの原因でした。

JavaAPI公式ドキュメント

https://api.intra-mart.jp/forma/apilist-forma-javadoc/doc/jp/co/intra_mart/foundation/forma/fileupload/FormaFileUploadItemManager.html#registerFileTemp-java.util.List-java.lang.String-java.lang.String-

JavaScriptAPI公式ドキュメント

https://api.intra-mart.jp/forma/apilist-forma-ssjs/doc/forma/FormaFileUploadItemManager/index.html#method-registerFileTemp_3

おわりに

今回のエラーに限らず、エラーの調査と対応では「視野を広く持つこと」を忘れないようにしたいです。

私自身の話になりますが「○○に気を付けよう」などの、自分がつまずきやすい点に応じたエラー対応時の自分ルールを複数持つことで、開発の効率が上がったと感じています。あらかじめ対応方針を決めておくことで、根拠の無い調査をして無為に時間を費やすことも少なくなりました。

自己分析を行い、「どのような対応の際に時間が掛かってしまったのか」を明らかにして開発の効率を上げていくことを、今年の目標に加えようと思います。

Amazon Transcribeを利用した文字起こしの利用

AWSでは様々なサービスがリリースされていますが、今回はその中からAmazon Transcribeに関して少し調べてみました。 Amazon Transcribeは、音声をテキストに起こすサービスです。 音声ファイルをバッチ処理してテキストを出力することもできますし、リアルタイムで音声読み取り文字起こしすることも可能です。 また、同じようなサービスとしてAmazon Transcribe Medicalというのもあり、こちらは医療関連の会話に特化したサービスとして提供されています。

音声の文字起こしサービスはいろいろな企業が提供していますが、Amazonのサービスを使う利点の一つとして、AWSの他サービスとの連携が容易な点が挙げられます。 例えばビデオチャットサービスのAmazon Chimeには、Amazon Transcribeと連携する専用のAPIが用意されており、簡単にビデオチャットの音声を文字起こしできるようになっています。 一方で注意点としては、これらの連携機能がかなり最近実装されたものであり、AWS-SDKのバージョンによってはまだ未実装でエラーになるケースがあります。 具体的には、AWS Lambdaで標準利用できるAWS-SDKのバージョンが最新より少しだけ古く、APIが実装されていないということがありました(2021/10/29現在)。 最新バージョンのAWS-SDKを別途組み込むことで解決できましたが、今後も機能追加や仕様変更などには注意する必要がありそうです。

WSL2で使うファイルの配置場所について

WSL2を使ってWindows上にLinux環境を作ることができ非常に便利ですが、注意しなければいけない仕様が色々あったりします。 今回はWSL2で使うファイルの配置場所について触れてみたいと思います。

WSL2のLinux環境とホストのWindows環境の相互ファイル参照について

WSL2では構築したLinux環境のファイルと、ホストのWindowsのファイルを相互に参照することが可能になっています。

WindowsからはLinuxのストレージがネットワークドライブとして参照できるようになっています。 ファイルエクスプローラ\\wsl$を表示すると、WLS2で構築済みの各Linux環境のストレージを見ることができます。 Linuxからは/mntの下にWindowsのCドライブなどがマウントされているので、そこから参照することができます。

異なる環境のファイルにアクセスした場合はパフォーマンスが低下する

上記のように相互に参照可能なので、Windows上とLinux上のどちらにファイルを置いても一応利用できます。 ただし、WSL2の仕組み上、異なる環境のファイルをアクセスする場合、本来の環境のファイルにアクセスするのに比べてパフォーマンスが低下するという仕様があります。 なのでLinux環境で頻繁にWindows上のファイルを参照するようにしていると、処理が重くなったりするので注意が必要です。 Linuxで使うファイルは、原則としてLinux上のファイルシステムに配置するのが妥当となります。

それらを踏まえた適切な構成は?

パフォーマンスは低下するとはいえ、前述の通り参照そのものは可能です。 なので、それを踏まえていればLinuxWindowsの両方からファイルを参照する構成も可能です。 例えばGitのGUIツールはWindowsで動かして、参照先はLinux上のファイル、のような構成も構築できます。 Gitの操作程度では多少ファイルアクセスのパフォーマンスが落ちても大きな影響はないので問題ありません。

またVisualStudioCodeなどのエディタには、WSL2用の拡張機能があります。 それを使えばLinux側のディレクトリやコンソールを開けるので、非常に便利です。

これらのパフォーマンスに関する特性や、別環境のファイルアクセス方法を把握しておくと、WSL2での開発がより良く進められると思います。

共通マスタ インポート/エクスポート ジョブ その2

はじめに

こんにちは。開発部のS.Rです。気がついたら一年の半分が過ぎていました。
今後は有意義な時間が過ごせるように、振り返りのスパンを短くしようと思っています。

今回は前回の『共通マスタ インポート/エクスポート ジョブ』の続きとなります。
共通マスタをインポート/エクスポートする際のデータファイルについて解説します。

データファイル

インポート/エクスポートする際には二種類のデータファイル形式が利用可能です。

CSV フォーマット
XML フォーマット

以下ではそれぞれの違いについてまとめます。

インポートにおける違い

CSV XML
文字コード 設定ファイル内の文字コード必須 設定ファイル内の文字コード無視(XMLファイルのヘッダで判断される)
設定ファイル エンティティの指定が必須 エンティティの指定が不要
インポートするデータファイル エンティティ単位 データ領域単位

Tips
データ領域とエンティティの関係はこちらを参照ください。

3. データ領域 — IM-共通マスタ インポート・エクスポート仕様書   第11版 2020-08-01   intra-mart Accel Platform

以下に例を出します。

データ領域 エンティティ
会社グループ 会社グループ、会社グループ内包、会社グループ所属
ユーザ ユーザ、ユーザ分類所属
通貨 通貨、通貨精度、通貨換算コード、通貨レート

エクスポートにおける違い

CSV XML
ファイルの出力単位 エンティティ エンティティ、データ領域
扱える期間情報 指定した日付上の1期間のみ 存在するすべての期間

おわりに

intra-mart初学者の方にとって、情報の"期間化"は馴染み無いものと思いますが、使いこなせばとても便利なものです。
インポート/エクスポート等、使用する際の制約や注意事項を一つずつ抑えていきましょう。

公式ドキュメント document.intra-mart.jp

intra-mart 共通マスタ インポート/エクスポート ジョブ

はじめに

こんにちは。開発部のS.Rです。 時が経つのも早いもので、4月で入社2年目になりました。 これからもどうぞよろしくお願いします!

今回は共通マスタのインポート/エクスポート ジョブを取り上げます。 初歩的な内容のみですので詳細は公式ドキュメントをご確認ください。

必要なもの

インポートに必要なものは2点

1.インポート実行の設定ファイル

2.インポートするファイル

エクスポートでは1点

1.エクスポート実行の設定ファイル

初学者には馴染みがあまり無いかと思いますが、 インポート/エクスポートどちらにも、それぞれ「設定ファイル」が必要です。 ジョブを流したら、よしなにインポートなどを行ってくれるわけではないのですね。。

設定ファイルについて

配置するパスは以下

■インポート設定ファイル: <パブリックストレージ>/im_master/config/import_config.xml

■エクスポート設定ファイル: <パブリックストレージ>/im_master/config/export_config.xml

インポート設定ファイルの例です。

<app-master-import>
   <company-group-import>
       <name>sample_import</name>
       <format>XML</format>
       <file>im_master/import/data.xml</file>
       <directory>im_master/import/data.</directory>
       <start-date>2000-01-01</start-date>
       <end-date>2999-12-31</end-date>
       <encoding>UTF-8</encoding>
       <extension-param name="replace-pattern">true</extension-param>
       <extension-param name="sub-dirs">false</extension-param>
   </company-group-import>
</app-master-import>

インポートがうまくいかない場合は上記のformatで指定されるファイルの種類がインポートの資材と合っているか、 またはファイル名や文字コードなどを確認してみるとよいかもしれません。

詳細はまた次の機会に。

公式ドキュメント

document.intra-mart.jp

AWS Serverless Application Modelの概要とローカルでの動作確認における注意点

AWSが提供しているServerless Application Modelサービスの概要と、これを使って構築したアプリケーションの動作確認時の注意点を紹介したいと思います。

AWS Serverless Application Model(SAM)とは?

AWS Serverless Application Model(以下SAM)とは、サーバレスな構成、すなわち常時稼働する固有のサーバマシンを持たないようなシステムをAWS上に構築するためのサービスです。 固有のマシンを持たないというのは、具体的にはAWS Lambdaなどのサービスを使って、処理が呼び出されたときだけ一時的にCPUやメモリを割り当てられて動作するような仕組みになります。 SAMはAPI GatewayやLambdaなどの内容を設定ファイルに記述してシステム構成を定義し、それをコマンドひとつでAWSに実際のシステムとして組み建てることができます。 これによりシステムの内容をすべてソースコードや設定ファイルに落とし込んで管理することが可能になります。 インフラをコードで定義して管理する構成はInfrastructure as Code(IaC)と呼ばれており、SAMはこれを実現するためのサービスのひとつです。

ちなみに単にAWS上のリソースをコードで管理するなら、AWS CloudFormationというサービスがSANより以前から存在します。 CloudFormationは、EC2のインスタンスなどサーバレスなリソース以外も全て管理できるサービスです。 しかしその分記述する内容が細かく、管理が煩雑になる場合があります。 SAMはCloudFormationをサーバレスなシステム向けに拡張・特化させたもので、CloudFormationより自由度が下がる分、記述が簡潔で済む利点があります。

また、SAMは限定的ですがローカル環境で動作確認を実行できるという機能もあります。 簡単なデバッグなどを行いたいときは、いちいちAWSに反映させずとも済むため便利です。

インストール

SAM CLIをインストールして、各種コマンドを利用できるようにする必要があります。 AWS公式サイトに十分説明があるので、詳細な説明は割愛します。

docs.aws.amazon.com

Windows、Max、Linuxそれぞれインストール可能です。 ただしローカルでの動作確認にはDockerが必須なので、それは別途インストールしておく必要があります。

また、AWSにデプロイするにはAWS認証情報の設定が行われている必要があります。 こちらも公式ドキュメントに手順が記載されているので、そちらを参照ください。

ローカルでの動作確認手順

以下のコマンドを実行することで、必要な初期設定やサンプルファイルを構築することができます。

sam init

こちらも詳細はチュートリアルに記載されています。

docs.aws.amazon.com

チュートリルにも記載されてますが、以下のコマンドでローカル上でアプリケーションを動作させることができます。

sam local start-api

localhost:3000にアクセスすれば、起動したアプリケーションにアクセスできます。

ローカルで動作確認する場合の注意点(落とし穴?)

チュートリアルでは使われていませんが、SAMはWebSocket用のAPI Gatewayや、DynamoDBなども定義できます。 しかし、これらはAWSにデプロイすると動作しますが、ローカルでは動作しません(2021/03/25 現在)。 そのため、これらを使う前提の処理は、ローカルでそのまま動作確認できません。 動作するのはREST APIAPI Gatewayや、Lambdaだけのようです。

DynamoDBについては別途ローカルに構築して接続させることである程度対処可能ですが、WebSocketは代わりの方法も無く難しそうです。 ローカルでもWebSocketを動作できるように機能追加する動きがあるようですが、残念ながらまだ未実装のままとなっています。 なのでこれらを使う場合、デバッグはある程度AWS上で行う必要がありそうです。 SAMのメリットを十分に行かせないのは残念ですが・・・まだ新しいサービスなので、今後のアップデートに期待したいと思います。

まとめ

AWS SAMについて概要と注意点を紹介いたしました。非常に有用ではありますが、開発時にトラブルが起きないよう、十分に検証しておく必要がありそうです。 もう少し機能が成熟してくれるとより安定して使えると思いますので、今後期待したいところです。

WSL2上でのDocker環境構築について

はじめに

Windows Subsystem Linux 2(以下WSL2)が正式リリースされてだいぶ経ちます。 簡単にLinux環境を利用できるので、とても便利です。 WindowsLinux環境を使える利点はいろいろありますが、今回はDockerをインストールして使う手順と注意点を紹介したいと思います。

WSL2のセットアップ

WSL2のインストール手順は、Microsoft公式サイトで公開されているので、それを参考にインストールしましょう。

docs.microsoft.com

ディストリビューションは任意のものを選びましょう。シェアで考えるとUbuntuあたりが無難でしょうか。 WSL2とディストリビューションをインストールしたら、起動して環境内部のセットアップに移ります。

systemdの起動構築

一般的なLinux環境では、systemd(システム管理デーモン)が起動していますが、WSL2の環境ではこれが存在していません。 そのため、Dockerをインストールしてもサービスが動作しません。 これを解決する手段として、genieというものが開発されてます。

github.com

これをインストールして実行することで、systemdを動作させることが出来ます。

genie -i

ただしWSL2が一度終了して再度起動した場合は、またsystemdが存在しない状態になります。 起動のたびに上記コマンドを実行すれば解決・・・はするのですが、手作業では面倒くさい。 自動的に実行したいですが、systemdが動いてないのでcronで定義しても動作しません。rc.localファイルに書いてもダメです。 ではどうするのかというと・・・いくつか方法はありますが、私はWindows側からコマンドを実行させる方法を使っています。 以下のコマンドをWindowsコマンドプロンプトで実行すると、WSL2環境にコマンドを実行させることが出来ます。

wsl /bin/bash -l -c "genie -i"

batファイルにして実行するとコマンドプロンプトが一瞬表示されてちょっと気になるので、VBScriptを使い非表示のまま実行するように手を加えます。

CreateObject("Wscript.Shell").run "wsl /bin/bash -l -c 'genie -i'", vbHide

これをWindowsのスタートアップ時に実行させれば、自動的にWSL2環境のsystemdを動作させるようにできます。 コマンドプロンプトを管理者権限で開き、以下のコマンドを実行すると、スタートアップ時に上記の処理を書いたスクリプトファイルを実行するようにできます。

echo CreateObject("Wscript.Shell").run "wsl /bin/bash -l -c 'genie -i'", vbHide > "%ALLUSERSPROFILE%\Microsoft\Windows\Start Menu\Programs\StartUp\wsl2_genie.vbs"

Docker Engineのインストール

公式サイトの手順に従ってインストールできます。 OSごとに手順が記載されているので、自分の環境にあった手順を見てインストールしましょう。

docs.docker.com

前述したsystemdの起動ができていれば、それ以上特に特殊な手順は必要ありません。 Dockerサービスが起動していれば、以下のコマンドがエラーにならず正常に動作するはずです。

docker ps

Docker Composeのインストール

こちらはDocker Engineだけで十分な場合は、インストールしなくても問題ありません。 インストールする場合は、こちらも公式サイトの手順に従って行いましょう。

docs.docker.com

日本語でインストール手順を紹介した記事も多数ありますが、注意しないと古いバージョン番号を指定して書かれていることがあります。 公式サイトを参照したほうが無難かと思います。

まとめ

WSL2のセットアップと、その環境にDockerを構築する手順を説明しました。 Docker自体には別に特殊な手順は必要ありませんでした。systemdなどWSL2独自の特殊な仕様は注意が必要です。 WSL2は便利ですが、事前に知っておかないと困る仕様などもあります。利用する場合はそれを承知したうえで使っていきましょう。

intra-martのIM-BISで文字列アイテムを数値アイテムのように使用する際の注意点

2020年入社したM.Sと申します。
本年度からプログラミングを始めたため 若輩者ですが、私の記事が皆さんのお役にたてればと思います。

今回は私が業務でミスしてしまった箇所を紹介します。

なぜ文字列アイテムを使うのか

IM-BISで数値アイテムを使用すると.00が表示されずに省略されてしまいます。 .00を表示させる仕様であったため、数値アイテムを使用せず、文字列アイテムを使って数値表示をすることになりました。

何が起きたか

IM-BISのアクション設定において、文字列アイテムの入力イベントで小数点の付与と指定以下の小数点の切り捨てを行う以下のスクリプトを実装しました。

let comma = (Math.floor(100 * str) / 100).toFixed(2);

ところが、例として72445.40と入力すると72445.39になってしまい、入力内容が変わってしてしまう問題が発生しました。

デバッグしてみたところ、Math.floor(100 * str)の計算結果が7244539.999999999になってしまうことが分かりました。

何故こうなるかを調べたところ以下の記事で挙げられている通り、浮動小数点数の計算時に丸め誤差が発生していたことが原因でした。
https://qiita.com/Chinats/items/e2647ca7900dfe7835a8

どう対処したか

intra-martのクライアントサイドAPI(ImDecimalFormatter)を使用してフォーマット処理を行うこととしました。

前準備としてFormaアイテムの入力値を取得する関数と反映する関数を実装しておきます。 アイテムの値取得・値反映については以下に記載があります。
https://www.intra-mart.jp/document/library/bis/public/bis_specification/texts/spec/csjs_script.html

// 文字列 入力値を取得する関数
function getItemValue(type, name) {
    let data = {}, _type = type || 'product_72_textbox', item = formaItems[_type];
    return item.getItemData[name]();
};

// 文字列 入力値を反映する関数
function setItemValue(type, name, value) {
    let data = {}, _type = type || 'product_72_textbox', item = formaItems[_type];
    data[name] = value;
    item.setItemData[name]({
        data : data
    });
};

次に、ImDecimalFormatterを用いて数値文字列のフォーマットする処理を実装します。
以下はImDecimalFormatterのAPIドキュメントです。
https://www.intra-mart.jp/apidoc/iap/jsdoc/symbols/ImDecimalFormatter.html

function format(name){
    let formatter = ImDecimalFormatter.getAccountInstance();
    // 小数部分の最大桁数を設定
    formatter.setMaximumFractionDigits(2);
    // 小数部分の最小桁数を設定
    formatter.setMinimumFractionDigits(2);

     let str = getItemValue('product_72_textbox', name);

    // ImBigDecimal(任意精度のスケールなしの整数値)に変換
    formatter.parseToBigDecimal(str,function(data, textStatus, jqXHR){
        if(data.error) {
            console.log(data);
            setItemValue('product_72_textbox', name, '');  
            return;
        }    

        // 小数点以下の桁数を丸める
        let bd = data.data;
        // 一つ目めの引数は、求める小数点以下の桁数を指定
        // 二つ目の引数は、列挙型定数のRoundingMode.DOWNを指定し切り捨て
        bd = bd.setScale(2,ImRoundingMode.DOWN);

        // 区切り文字付きでフォーマット
        formatter.format(bd, function(data, textStatus, jqXHR){
            // フォーマットエラーの場合はブランクを設定
            if(data.error) {
                console.log(data);
                setItemValue('product_72_textbox', name, '');
                return;
            }
            setItemValue('product_72_textbox', name, data.data);
        });
    }); 
}

これで小数を入力した場合でも、誤差なく入力した通りの値を表示することができました。

おわりに

JavaScriptを学んでいく中で計算と日付周りは罠が多いとよく目にしましたが、 実際に望まない挙動が起こるとその意味を実感しました。 まだまだ理解が浅いため学習を続け、罠に注意して開発を行っていきたいと思います。

slackで予約投稿する方法

今年度に入社したY.Kです。

slackで予約投稿をするには、現状API拡張機能を利用するしかありません。 今回はAPIを使ってslackに予約投稿をする方法をご紹介します。

使用するAPI

今回使用するAPIはこちらになります。

chat.scheduleMessage method | Slack

投稿準備

このAPIを使うには『OAuthAccessToken』を取得する必要があります。 このOAuthAccessTokenを取得するためにはslackAppを作成しなければならないので、これよりslackAppを作成する手順を紹介します。

1.『slackAPI』にアクセス

こちらのリンクからslackAPIのサイトにアクセスします。

Slack API: Applications | Slack

アクセスできたら『Create New App』を選択します。 f:id:yamatotox:20201222132904p:plain

2.名前とワークスペースを決める

作成するslackAppの名前と、Appを作成するワークスペースを決めます。

f:id:yamatotox:20201222133021p:plain

3.『Permissions』を選択

Permissionsを選択します。

f:id:yamatotox:20201222133108p:plain

4.『Add an OAuth Scope』からスコープを選択

『Add an OAuth Scope』の中から、『chat:write』を選択します。

f:id:yamatotox:20201222133246p:plain

f:id:yamatotox:20201222133148p:plain

5.ワークスペースにインストール

『Install to Workspace』を選択します。

確認画面が出てくるので問題なければ許可をします。

許可されれば選択したワークスペースにslackAppが作成されます。

f:id:yamatotox:20201222133341p:plain

f:id:yamatotox:20201222133410p:plain

OAuthAccessTokenを取得

ワークスペースにslackAppが作成されるとこの画面に飛ぶので、ここからOAuthAccessTokenを取得します。

f:id:yamatotox:20201222133441p:plain

以上が、OAuthAccessTokenの取得手順になります。

chat.scheduleMessage APIを使う

それではこのOAuthAccessTokenを用いてchat.scheduleMessage APIを使っていきます。 こちらが投稿ページになります。

https://api.slack.com/methods/chat.scheduleMessage/test

投稿する上で以下4つの項目が必要になります。

  • token・・・・取得したOAuthAccessToken
  • channel・・・・投稿したいチャンネル名
  • post_at・・・・投稿日時
  • text・・・・本文

その他の引数について気になる場合は、Testerタブの左側にあるDocumentationを参考にしましょう。 注意点として投稿日時はUnixTime型なので、こちらから送信したい時刻をUnixTimeに変換しなければなりません。

UnixTime相互変換ツール

設定したら『Test Method』ボタンを押します。

f:id:yamatotox:20201222133558p:plain

成功すると設定した投稿日時にメッセージが予約されます。

f:id:yamatotox:20201222133622p:plain

そして予約投稿時刻にslackを見ると、

f:id:yamatotox:20201222133650p:plain

このように投稿されます。

以上、APIを使ったslackの予約投稿方法でした。

拡張機能を使った方法

googlechrome拡張機能を使った方法もあるので簡単にご紹介します。

Slack Send Later

こちらの拡張機能を入れた後、オプションに飛びます。

f:id:yamatotox:20201222133729p:plain

tokenを設定する場所があるので、そこにOAuthAccessTokenを設定します。 後はSlack Send Laterを起動してチャンネル名とメッセージ、送信予定日時を入れて送信予約ボタンを押せば予約投稿ができます。

f:id:yamatotox:20201222133818p:plain

まとめ

slackで予約投稿する方法を紹介しました。使いどころはそこまでないとは思いますが、APIを使う良い練習にはなると思うので是非使ってみてください。

IEでアロー関数が使えなかった話

はじめまして。2020年新卒入社しました。S.Rです。
まだまだ勉強中の身ですが、私の記事が皆さんの
お役にたてれば幸いです。

今回は初学者の私が少しつまずいたトピックを取り上げます。

何が起きたか

タイトル通りではありますが、ブラウザInternet Explorer(以下IE)での動作テスト中にエラーが起こる。Google Chromeでの動作は問題が無い。。。
→調べると、クライアント側のJavaScriptにおいてアロー関数がIEでは使えないことが判明しました。

アロー関数とは

下記がES5以前の書き方です。(ESについては後述します。)

var normal = function arrow() {  
    return '一般的な関数';  
};  

一方こちらがアロー関数です。
【 => 】・・・矢印(アロー)

var arrow = () => {  
    return 'アロー関数';  
};  

同じ内容を少ない文字数で書くことができるだけでなく、その他にもメリットはありますがここでは割愛します。 詳細は下記を参照ください。 https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Functions/Arrow_functions

なぜ

なぜ同じプログラミング言語でもブラウザが異なることで一部の関数だけが使えなくなるのでしょうか。

理由は大きく2点
JavaScriptにバージョンがある
②ブラウザ側がバージョンに対応する必要がある

JavaScriptにはES〇〇(数字または西暦)の名前でバージョンが複数存在します。
2015年以降は毎年新しいバージョンがリリースされています。

それに対し、IEは最新バージョンに対応していないため、
過去にリリースされたJavaScriptの機能には対応できても、
一部の新しい機能には対応できないという状況になっています。

IEではES5(2009年リリース)までが使用可能ですが、アロー関数はES6(2015年リリース)から実装された関数であるため、IEではアロー関数が使えないのです。

以下ES6の各ブラウザ対応表です。(IEは真っ赤ですね。。。)
http://kangax.github.io/compat-table/es6/

どうする?

私は今回アロー関数を使わない方針で、書き直しました。
書き直す際には以下のサイトが便利です。
https://babeljs.io/

おわりに

初学者の私にとってプログラミング言語が日々進化し、バージョン毎に違いがあることは新鮮な感覚でした。願わくば私自身も日々成長を続けられたらな、といったところです。

今年も押し迫り、体調管理には気を抜けない日々が続いておりますが、どうぞよいお年をお迎えください。

JavaScriptでAmazon Chime SDKを動かしてみる

Amazon Chime SDKの動作を確認するクライアントサイドをJavaScriptで実装してみたいと思います。

事前準備

サーバサイド

サーバサイドにて、ミーティング(meeting)と参加者(attendee)を作成して、クライアントサイドへ渡してあげる必要があります。 以下などを参考に、サーバサイドを実装してください。

gsol.hatenablog.com

Amazon Chime SDKダウンロード

以下READMEを参考に、Amazon Chime SDKをダウンロードしてください。

https://github.com/aws/amazon-chime-sdk-js/tree/master/demos/singlejs

実装内容

<html lang="ja">
<head>
  <script src="aws/amazon-chime-sdk.min.js"></script>
  <script type="text/javascript">
    var logger = '';
    var deviceController = '';
    var configuration = '';
    var meetingSession = '';
    
    // Meeting参加
    function addMeeting() {
      // サーバサイドにてミーティングと参加者を作成し、クライアントサイドで受け取る
      var meeting = {XXXX};
      var attendee = {XXXX};
      (async () => {
        logger = new ChimeSDK.ConsoleLogger('MyLogger', ChimeSDK.LogLevel.ERROR);
        deviceController = new ChimeSDK.DefaultDeviceController(logger);
        console.log('deviceController', deviceController);
             
        // ミーティングセッション作成
        configuration = new ChimeSDK.MeetingSessionConfiguration(meeting, attendee);
        console.log('configuration', configuration);
        meetingSession = new ChimeSDK.DefaultMeetingSession(configuration, logger, deviceController);
        console.log('meetingSession', meetingSession);

        // 入出力デバイス取得(ブラウザはマイクとカメラの許可を求める)
        const audioInputDevices = await meetingSession.audioVideo.listAudioInputDevices();
        console.log('audioInputDevices', audioInputDevices);
        const audioOutputDevices = await meetingSession.audioVideo.listAudioOutputDevices();
        console.log('audioOutputDevices', audioOutputDevices);
        const videoInputDevices = await meetingSession.audioVideo.listVideoInputDevices();
        console.log('videoInputDevices', videoInputDevices);

        meetingSession.audioVideo.chooseVideoInputQuality(1280,720,3,1000);
        await meetingSession.audioVideo.chooseVideoInputDevice(videoInputDevices[0].deviceId);
        await meetingSession.audioVideo.chooseAudioInputDevice(audioInputDevices[0].deviceId);
        if (audioOutputDevices[0]) {
          await meetingSession.audioVideo.chooseAudioOutputDevice(audioOutputDevices[0].deviceId);
        }

        // オーディオ要素取得・バインド
        const audioElement = document.getElementById('audio-preview');
        meetingSession.audioVideo.bindAudioElement(audioElement);

        const videoElements = {}; // ビデオタイル要素
        for (let i = 0; i < 16; i++) {
          videoElements[i] = document.getElementById(`video-preview` + i);
        }
        const indexMap = {};

        const acquireVideoElement = tileId => {
          // 既にバインドされている場合、同要素を返却
          for (let i = 0; i < 16; i++) {
            if (indexMap[i] === tileId) {
              return videoElements[i];
            }
          }
          // バインド可能な要素を返却
          for (let i = 0; i < 16; i++) {
            if (!indexMap.hasOwnProperty(i)) {
              indexMap[i] = tileId;
              return videoElements[i];
            }
          }
          return;
        }
        if (!acquireVideoElement) {
          alert('利用可能なビデオ要素がありません。');
        };

        // observer設定
        const observer = {
          audioVideoDidStart: () => {
            console.log('Started');
          },
          // 映像要素取得・バインド
          videoTileDidUpdate: tileState => {
            if (!tileState.boundAttendeeId) {
              return;
            }
            console.log('Start video');
            meetingSession.audioVideo.bindVideoElement(tileState.tileId, acquireVideoElement(tileState.tileId));
          }
        };

        // 出力開始
        meetingSession.audioVideo.addObserver(observer);
        meetingSession.audioVideo.start();
        meetingSession.audioVideo.startLocalVideoTile();

        document.getElementById('meetingId').value = meetingInfo.Meeting.MeetingId;
        document.getElementById('attendeeId').value = attendeeInfo.Attendee.AttendeeId;
       }) ();
    },
    error : function() {
      console.log('通信エラーです');
    }

    // Meeting退室
    function leaveMeeting() {
      // observer設定
      const observer = {
        audioVideoDidStop: sessionStatus => {
          const sessionStatusCode = sessionStatus.statusCode();
          if (sessionStatusCode === ChimeSDK.MeetingSessionStatusCode.Left) {
            alert('退室しました。');
          } else {
            alert('セッションが切れました。ステータスコード: ' + sessionStatusCode);
          }
        }
      };
    }
  </script>
</head>
<body>
  <div>
    video test<br>
    MeetingId : <input id="meetingId" value="" style="width:300px"/><br>
    AttendeeId : <input id="attendeeId" value="" style="width:300px"/>
    <br>
    <button type="button" value="" onclick="addMeeting()">参加</button>
    <button type="button" value="" onclick="leaveMeeting()">退室</button>
  </div>
  <video id="video-preview0" style="width:100%; height: 200px"></video>
  <video id="video-preview1" style="width:100%; height: 200px"></video>
  <video id="video-preview2" style="width:100%; height: 200px"></video>
  <video id="video-preview3" style="width:100%; height: 200px"></video>
  <video id="video-preview4" style="width:100%; height: 200px"></video>
  <video id="video-preview5" style="width:100%; height: 200px"></video>
  <video id="video-preview6" style="width:100%; height: 200px"></video>
  <video id="video-preview7" style="width:100%; height: 200px"></video>
  <video id="video-preview8" style="width:100%; height: 200px"></video>
  <video id="video-preview9" style="width:100%; height: 200px"></video>
  <video id="video-preview10" style="width:100%; height: 200px"></video>
  <video id="video-preview11" style="width:100%; height: 200px"></video>
  <video id="video-preview12" style="width:100%; height: 200px"></video>
  <video id="video-preview13" style="width:100%; height: 200px"></video>
  <video id="video-preview14" style="width:100%; height: 200px"></video>
  <video id="video-preview15" style="width:100%; height: 200px"></video>
  <audio id="audio-preview"></audio>
</body>
</html>

各メソッドなどは以下を参照してください。

https://aws.github.io/amazon-chime-sdk-js/

動作確認

ミーティング参加

f:id:sanok-gsol:20201130173256p:plain

ミーティング退出

f:id:sanok-gsol:20201130173311p:plain

注意点

ミーティングセッションの自動終了

生成したミーティングは、音声接続が無いまま5分経過すると自動的にセッションが終了します。 その他ミーティングのセッションが自動的に終了する場合がありますので、以下をご参照ください。

https://docs.aws.amazon.com/chime/latest/dg/mtgs-sdk-mtgs.html

ブラウザの制約

Chromiumをベースにしているブラウザ(Chrome、Edgeなど)では、「https」もしくは「localhost」にてアクセスしないと、「getUserMedia」メソッドが正常に動作しないため、 ビデオ映像や音声などが出力されません。 以下をご参照ください。

https://sites.google.com/a/chromium.org/dev/Home/chromium-security/deprecating-powerful-features-on-insecure-origins

Amazon Chime のミーティングと参加者をJavaで作成する

AWS SDK for Java(以下SDK)を用いて、Amazon Chime(以下Chime)のミーティングと参加者を作成してみたいと思います。

事前準備

credentialsの設定

まず、IAMユーザを作成して「AmazonChimeFullAccess」、「 AmazonChimeSDK」の権限を付与したcredentialsを作成してください。 credentialsをサーバに設定する方法は以下をご参照ください。

https://docs.aws.amazon.com/sdk-for-java/v1/developer-guide/setup-credentials.html

AWS SDK for Javaダウンロード

以下を参照し、SDKをダウンロードして展開してください。

https://docs.aws.amazon.com/ja_jp/sdk-for-java/v1/developer-guide/setup-install.html

実装内容

ディレクトリ構成例

{project}
│  .classpath
│  .project
│
├─.settings
│      org.eclipse.jdt.core.prefs
│
├─bin
│  └─chime
│          createMeeting.class
│
├─lib
│      aws-java-sdk-1.11.884.jar
│
├─src
│  └─chime
│          createMeeting.java
│
└─third-party
    └─lib
          aspectjrt-1.8.2.jar
          aspectjweaver.jar
          aws-swf-build-tools-1.1.jar
          commons-codec-1.11.jar
          commons-logging-1.1.3.jar
          freemarker-2.3.9.jar
          httpclient-4.5.9.jar
          httpcore-4.4.11.jar
          ion-java-1.0.2.jar
          jackson-annotations-2.6.0.jar
          jackson-core-2.6.7.jar
          jackson-databind-2.6.7.3.jar
          jackson-dataformat-cbor-2.6.7.jar
          javax.mail-api-1.4.6.jar
          jmespath-java-1.11.884.jar
          joda-time-2.8.1.jar
          netty-buffer-4.1.48.Final.jar
          netty-codec-4.1.48.Final.jar
          netty-codec-http-4.1.48.Final.jar
          netty-common-4.1.48.Final.jar
          netty-handler-4.1.48.Final.jar
          netty-resolver-4.1.48.Final.jar
          netty-transport-4.1.48.Final.jar
          spring-beans-3.0.7.RELEASE.jar
          spring-context-3.0.7.RELEASE.jar
          spring-core-3.0.7.RELEASE.jar
          spring-test-3.0.7.RELEASE.jar

ダウンロードした「AWS SDK for Java」から持ってきた「{project}/lib」配下と「{project}/third-party/lib」配下のライブラリにはビルド・パスを通してください。

ソース(createMeeting.java)

package chime;

import com.amazonaws.AmazonClientException;
import com.amazonaws.auth.profile.ProfileCredentialsProvider;
import com.amazonaws.services.chime.AmazonChime;
import com.amazonaws.services.chime.AmazonChimeClientBuilder;
import com.amazonaws.services.chime.model.Attendee;
import com.amazonaws.services.chime.model.CreateAttendeeRequest;
import com.amazonaws.services.chime.model.CreateAttendeeResult;
import com.amazonaws.services.chime.model.Meeting;
import com.amazonaws.services.chime.model.CreateMeetingRequest;
import com.amazonaws.services.chime.model.CreateMeetingResult;

public class createMeeting {

    public static void main(String[] args) {
    
        // credentials確認
        ProfileCredentialsProvider credentialsProvider = new ProfileCredentialsProvider();
        try {
            credentialsProvider.getCredentials();
        } catch (Exception e) {
            throw new AmazonClientException(
                    "Cannot load the credentials from the credential profiles file. " +
                    "Please make sure that your credentials file is at the correct " +
                    "location (~/.aws/credentials), and is in valid format.",
                    e
            );
        }
        
        // Chime リージョン設定
        AmazonChime chime = AmazonChimeClientBuilder.standard().withRegion("us-east-1").build();
        
        // Meetingリクエスト作成
        CreateMeetingRequest meetingReq = new CreateMeetingRequest();
        meetingReq.setClientRequestToken("test");
        meetingReq.setMediaRegion("ap-northeast-1");
        // Meeting作成
        CreateMeetingResult meetingRes = chime.createMeeting(meetingReq);
        Meeting meeting = meetingRes.getMeeting();
        // MeetingId取得
        String meetingId = meeting.getMeetingId();
        
        // Attendeeリクエスト作成
        CreateAttendeeRequest attendeeReq = new CreateAttendeeRequest();
        attendeeReq.setExternalUserId("test_user");
        attendeeReq.setMeetingId(meetingId);
        // Attendee作成
        CreateAttendeeResult attendeeRes = chime.createAttendee(attendeeReq);
        Attendee attendee = attendeeRes.getAttendee();
        
        System.out.println(meeting);
        System.out.println(attendee);
    }
}

各クラスやメソッドなどは以下を参照してください。

https://sdk.amazonaws.com/java/api/latest/

動作確認

以下の通り、meeting情報とattendee情報が生成されていることが確認できます。 ※一部マスクしてます

・meeting

{
  MediaPlacement:{
    AudioFallbackUrl: "wss://haxrp.m3.an1.app.chime.aws:443/calls/f02353d3-122c-4c21-9cbb-076e56e2XXXX",
    AudioHostUrl: "XXXXc6f84f16ea0ec5ddbd5bbab101a5.k.m3.an1.app.chime.aws:3478",
    ScreenDataUrl: "wss://bitpw.m3.an1.app.chime.aws:443/v2/screen/f02353d3-122c-4c21-9cbb-076e56e2XXXX",
    ScreenSharingUrl: "wss://bitpw.m3.an1.app.chime.aws:443/v2/screen/f02353d3-122c-4c21-9cbb-076e56e2XXXX",
    ScreenViewingUrl: "wss://bitpw.m3.an1.app.chime.aws:443/ws/connect?passcode=null&viewer_uuid=null&X-BitHub-Call-Id=f02353d3-122c-4c21-9cbb-076e56e2XXXX",
    SignalingUrl: "wss://signal.m3.an1.app.chime.aws/control/f02353d3-122c-4c21-9cbb-076e56e2XXXX",
    TurnControlUrl: "https://ccp.cp.ue1.app.chime.aws/v2/turn_sessions"
  },
  MediaRegion: "ap-northeast-1",
  MeetingId: "f02353d3-122c-4c21-9cbb-076e56e2XXXX"
}

・attendee

{
  Attendee:{
    AttendeeId: "efca3edd-1973-3801-517f-69cc2df3XXXX",
    ExternalUserId: "test_user",
    JoinToken: "ZWZjYTNlZGQtMTk3My0zODAxLTUxN2YtNjljYzJkZjM1YTAyOjA0ODBiN2VmLTcwNTctNGJiNS05NmM0LWI2NmMwOWQ5NDXXXX"
  }
}

サーバサイドの処理については以上となります。ここで生成した情報を利用することで、ミーティングに参加してビデオチャットを行えるようになります。画面の実装については、続きの記事に記載していますので、そちらもご参考ください。

gsol.hatenablog.com

Apache Airflowの紹介(kintoneとの連携編)

Airflowとkintoneアプリを連携して、期日が近づくとメール送信・Slack通知する処理を作成したので紹介します。

処理の概要

次のようなイメージで処理を作成していきます。

  • kintoneの契約書管理アプリから各レコードの契約満了日を取得。
  • 契約自動更新が無しで直近(一週間以内)で契約満了となるのものについて、契約更新のお知らせメール・Slackメッセージを送信。

kintoneアプリ

デフォルトで用意されている契約書管理アプリを利用します。
f:id:toheih:20201012154335p:plain

  • 連絡先にはメールアドレスを指定することとします。

AirflowのDAG作成

以下のタスクを実行するDAGを作成します。

  • kintoneからREST APIでデータ取得。
    • レコードを一括取得するカーソルを作成。(SimpleHttpOperator)
    • カーソルIDをXComから取得。(PythonOperator)
    • カーソルからデータを取得。(SimpleHttpOperator)
  • 取得結果からメール本文を作成し、メール送信処理用のDAGを実行(PythonOperator)
  • メール送信処理を実行(EmailOperator)

XComとは、タスク間で変数のやり取りをするための仕組みです。
また、メール送信処理については複数回実行可能とするためデータ取得処理のDAGとは別のDAGを作成し、データ取得処理DAGから呼び出すようにします。

kintoneのデータ取得処理

複数レコードを取得する必要があるため、カーソルを利用して取得します。

# REST API実行結果判定処理
def response_check(response):
    result=response.json()
    return response.status_code == requests.codes.ok

# カーソル作成タスク
create_cursor = SimpleHttpOperator(
    task_id='create_cursor',
    http_conn_id='',
    method='POST',
    endpoint='https://(サブドメイン名).cybozu.com/k/v1/records/cursor.json',
    data=json.dumps(
        {
            'app': '(アプリID)',
            'fields' : ['company_name', 'tanto_name', 'tanto_address', 'keiyaku_manryo_date'],
            'query' : 'keiyaku_manryo_date <= THIS_WEEK() and keiyaku_auto_update not in ("有")'
        }
    ),
    headers={'X-Cybozu-API-Token': '(kinoneのAPIトークン)', 'Content-Type' : 'application/json'},
    response_check=response_check,
    log_response=True,
    xcom_push=True,
    dag=dag,
)
  • response_checkで指定した関数によって、タスクを正常終了とするかエラーとするかを決定できます。今回はHTTPステータスコードで判定しています。
  • xcom_push=Trueにすることで、タスクの実行結果をXComに保存することが出来ます。
    • 保存した値はタスクのViewLogから確認することが出来ます。

f:id:toheih:20201015160247p:plain

次に、カーソルからレコードデータを取得します。
取得にはカーソルIDが必要で、カーソル作成APIの取得結果から取得します。

# カーソルID取得処理
def get_id_from_xcom(**context):
    jsonvalue = context['ti'].xcom_pull(task_ids='create_cursor')
    value = json.loads(jsonvalue)
    return value['id']

# カーソルID取得タスク
get_id = PythonOperator(
    task_id='get_id',
    dag=dag,
    provide_context=True,
    xcom_push=True,
    python_callable=get_id_from_xcom,
)

# レコードデータ取得タスク
open_cursor = SimpleHttpOperator(
    task_id='open_cursor',
    http_conn_id='',
    method='GET',
    endpoint='https://(サブドメイン名).cybozu.com/k/v1/records/cursor.json',
    data={"id" : "{{ ti.xcom_pull(task_ids='get_id') }}"},
    headers={'X-Cybozu-API-Token': '(kinoneのAPIトークン)'},
    response_check=response_check,
    log_response=True,
    xcom_push=True,
    dag=dag,
)
  • context['ti'].xcom_pull(task_ids='xxxx')でXComに保存された値を取得することが出来ます。
    • また"{{ ti.xcom_pull(task_ids='xxxx') }}"と記載することで文字列に埋め込むことも出来ます。
  • カーソルから取得したレコードデータは以下の通りXComに保存されます。

f:id:toheih:20201015160553p:plain

メール・メッセージ送信処理

レコードデータからメール・メッセージ本文を作成し、別DAGに渡します。

# 本文作成処理
def create_mail_content_from_xcom(**context):
    jsonvalue = context['ti'].xcom_pull(task_ids='open_cursor')
    value = json.loads(jsonvalue)
    for keiyaku_info in value['records']:
        k = {'mail_content' : '', 'slack_content' : '', 'mail_address' : keiyaku_info['tanto_address']['value'] }
        mail_content = keiyaku_info['company_name']['value'] + '<br/>'
        mail_content += keiyaku_info['tanto_name']['value']+ '様<br/><br/>'
        mail_content += '平素よりサービスをご利用いただき、誠にありがとうございます。<br/>'
        mail_content += 'お客様の契約有効期限が間近となっておりますのでお知らせいたします。<br/>'
        mail_content += 'このメールは ' + keiyaku_info['keiyaku_manryo_date']['value'] +' に契約満了を迎える方へ送信しています。<br/>'
        mail_content += '更新のお手続きをお願い致します。'
        k['mail_content'] = mail_content
        k['slack_content'] = mail_content.replace('<br/>','\n')
        trigger_dag(dag_id='sub_notice',
                    conf=json.dumps(k),
                    execution_date=None,
                    replace_microseconds=False)
        pass
    return

# 本文作成処理実行タスク
create_mail_content = PythonOperator(
    task_id='create_mail_content',
    dag=dag,
    provide_context=True,
    xcom_push=False,
    python_callable=create_mail_content_from_xcom,
)
  • trigger_dagで別DAGを実行することが出来ます。またconfに指定した値は別DAGで参照することが可能です。

別DAGではメール送信処理・Slackメッセージ送信処理を実装します。

# メール送信処理タスク
send_email = EmailOperator(
    task_id='send_email',
    dag=sub_dag,
    to="{{ dag_run.conf['mail_address']}}",
    subject='契約満了のお知らせ',
    html_content="{{ dag_run.conf['mail_content']}}",
    mime_charset='utf-8',
)

# Slackメッセージ送信処理タスク
send_slack = SlackWebhookOperator(
    task_id='send_slack',
    dag=sub_dag,
    http_conn_id='slack_test',
    message="{{ dag_run.conf['slack_content']}}",
)

send_email >> send_slack
  • Slack送信を行う場合はあらかじめConn IdとWebhook URLをAirflow側に登録しておく必要があります。
    • Admin > Connection から登録することが出来ます。
    • Conn IdはSlackWebhookOperatorのhttp_conn_idに指定します。

f:id:toheih:20201015165504p:plain

動作確認

実際にkintoneにデータを登録します。
f:id:toheih:20201015182719p:plain

AirflowのDAGを実行し、エラーなく完了しました。
f:id:toheih:20201015182956p:plain

メールが届くことが確認できました。
f:id:toheih:20201015183059p:plain

Slackのほうにもちゃんとメッセージが送られています。
f:id:toheih:20201016131639p:plain

今回はDAGを手動で実行しましたが、スケジュール実行も可能なので定期的にデータをチェックして通知することも可能です。

以上、Airflowとkintoneの連携について紹介しました。