Looker Studio(旧データポータル)のコネクタを自作し、REST APIからデータを取得する

ステータス 有用度 日時 備考
公開 ★★★ 2022/08/31
更新 ★★★ 2022/11/20 Looker Studio に対応

こんにちは 田村 です。

Google Looker Studio (旧データポータル) は、Google が提供するサービスを中心に様々なデータソースを利用して、ダッシュボードの構築ができます。

データソースが用意されていないものでも自分でコネクタを作成して、 Google Looker Studio にデータを取り込めます。コネクタは Google Apps Script で記述し、 Web API、 CSV 、 JSON 、 XML 、Apps Script Services 、 JDBC API などからデータを取得できます。

今回は、 REST API からデータを取り込むコネクタを作りたいと思います。 REST API のサービスとして、 Redmine をベースに説明していきます。 基本的にどんな REST API でも構築の仕方は同様にできると思いますので、適宜ご自身のご利用したいサービスに置き換えてお読みください。

はじめに Looker Studio の完成形のイメージをお見せすると、このような形になります。

file

WESEEK の開発手法

弊社では、アジャイル開発の管理ツールとして、この Redmine を使用しています。 sprint ごとにどれくらい速度(ベロシティ)が出せているかや、プロジェクトによってはある期間においてどれくらいストーリーポイントを消化しているかは気になるところです。 Redmine の REST API を利用して実績情報を取得し、 Looker Studio で閲覧できるようになったら便利だなというのが、このコネクター作成のきっかけです。

Redmine の API を使う

API の有効化

管理 > 認証 > RESTによるWebサービスを有効にする にチェックを入れて、 Redmine で REST API を利用できるようにします。

file

API アクセスキーの確認

Redmine REST API を呼び出すときは、 API アクセスキーが必要です。 個人設定 > APIアクセスキー から確認しておきます。

file

チケット一覧の取得

試しに REST API を利用して、チケット一覧を取得してみます。
HTTP Header に X-Redmine-API-Key を追加し、 API アクセスキーをセットします。
curl で呼び出してみると、下記のようにチケット一覧が取得できました。

$ curl -s --request GET 'https://[REDMINE HOSTNAME]/issues.json?project_id=1&limit=2' --header 'X-Redmine-API-Key: [REDMINE API TOKEN]' | jq .
{
    "issues": [
    {
      "id": 3503,
      "project": {
        "id": 1,
        "name": "[FILTERED]"
      },
      "tracker": {
        "id": 5,
        "name": "タスク"
      },
      "status": {
        "id": 1,
        "name": "新規"
      },
      "priority": {
        "id": 2,
        "name": "通常"
      },
      "author": {
        "id": 141,
        "name": "[FILTERED]"
      },
      "fixed_version": {
        "id": 943,
        "name": "sprint-25"
      },
      "parent": {
        "id": 103421
      },
      "subject": "動作確認",
      "description": "",
      "start_date": "2022-05-19",
      "done_ratio": 0,
      "created_on": "2022-08-29T07:36:06Z",
      "updated_on": "2022-08-29T07:36:06Z"
    },
    {
      "id": 3502,
      "project": {
        "id": 1,
        "name": "[FILTERED]"
      },
      "tracker": {
        "id": 5,
        "name": "タスク"
      },
      "status": {
        "id": 1,
        "name": "新規"
      },
      "priority": {
        "id": 2,
        "name": "通常"
      },
      "author": {
        "id": 141,
        "name": "[FILTERED]"
      },
      "fixed_version": {
        "id": 943,
        "name": "sprint-25"
      },
      "parent": {
        "id": 103421
      },
      "subject": "PR & Merge #3240",
      "description": "PR\r\nタスク\r\n#3240",
      "start_date": "2022-05-19",
      "done_ratio": 0,
      "created_on": "2022-08-29T07:35:58Z",
      "updated_on": "2022-08-29T07:35:58Z"
    }
  ],
  "total_count": 82,
  "offset": 0,
  "limit": 2
}

Redmine REST API の詳細は こちら を参照してください。

Looker Studio コミュニティ コネクタを作る

Google Looker Studio の Codelab を参考に構築していきます。

Apps Script のプロジェクトを作り、下記の4つの関数を定義して、コネクタを作成していきます。

  • getAuthType()
  • getConfig()
  • getSchema()
  • getData()

Apps Script プロジェクトを作る

Google Apps Script にアクセスします。

新しいプロジェクト をクリックします。

file

最上部 無題のプロジェクト名 をクリックして、このプロジェクトに名前をつけます。 今回は RedmineDataStudioConnector としました。

file

これから、 コード.gs の中に機能を実装していきます。

file

getAuthType() の定義

コードを先に示します。

var cc = DataStudioApp.createCommunityConnector();

function getAuthType() {
  var AuthTypes = cc.AuthType;
  return cc
    .newAuthTypeResponse()
    .setAuthType(AuthTypes.NONE)
    .build();
}

getAuthType() は、 Looker Studio がコネクタが使用する認証方法を知る必要があるときに呼び出されます。

認証方法は下記があります。

列挙値 説明
NONE コネクタに認証が必要ないことを示します。
OAUTH2 コネクタが認証に OAuth 2.0 を使用することを示します。
KEY コネクタが認証に API キーを使用することを示します。
USER_PASS コネクタが認証にユーザー名とパスワードを使用することを示します。
USER_TOKEN コネクタが認証にユーザー名とトークンを使用することを示します。

作成したコネクタを一般にも公開する場合には、適切な認証方法を選択することが必要です。今回は Codelab でも説明している NONE を利用します。

認証方法の詳細は こちら を参照してください。

getConfig() の定義

Looker Studio で作成したコネクタをデータソースとして追加する際に、コネクタに任意のパラメータを渡すようにできます。

このようなものです。

file

コードを下記に示します。 getAuthType() の後に続けて記載してください。

function getConfig(request) {
  var config = cc.getConfig();

  config.newInfo()
    .setId('instructions')
    .setText('Enter the Redmine project ID to get a list of Issues for the project.');

  config.newTextInput()
    .setId('project')
    .setName('Enter a project id');

  config.setDateRangeRequired(true);

  return config.build();
}

config.newInfo() では、ユーザーに指示や情報を提供するためのテキストを定義しています。

config.newTextInput() では、 1 行のテキストボックスを定義しています。今回は、 Redmine の プロジェクト ID の入力を促しています。

使用できる ConfigType の一覧は こちら を参照してください。

getSchema() の定義

getSchema() は、 Looker Studio にデータを読み込む際のスキーマを定義します。

この関数が返した結果は、 Looker Studio のデータソースの編集で表示されるフィールド一覧に対応します。

file

コードを示します。

function getFields(request) {
  var cc = DataStudioApp.createCommunityConnector();
  var fields = cc.getFields();
  var types = cc.FieldType;
  var aggregations = cc.AggregationType;

  // Looker Studio 上で `id` と一意に設定し、表示名を `ID` に、そのデータ型を `NUMBER` 型に設定しています
  fields.newDimension()
    .setId('id')
    .setName('ID')
    .setType(types.NUMBER);

  fields.newDimension()
    .setId('projectId')
    .setName('プロジェクト ID')
    .setType(types.NUMBER);

  fields.newDimension()
    .setId('project')
    .setName('プロジェクト')
    .setType(types.TEXT);

  fields.newDimension()
    .setId('trackerId')
    .setName('トラッカー ID')
    .setType(types.NUMBER);

  fields.newDimension()
    .setId('tracker')
    .setName('トラッカー')
    .setType(types.TEXT);

  fields.newDimension()
    .setId('statusId')
    .setName('ステータス ID')
    .setType(types.NUMBER);

  fields.newDimension()
    .setId('status')
    .setName('ステータス')
    .setType(types.TEXT);

  fields.newDimension()
    .setId('priorityId')
    .setName('優先度 ID')
    .setType(types.NUMBER);

  fields.newDimension()
    .setId('priority')
    .setName('優先度')
    .setType(types.TEXT);  

  fields.newDimension()
    .setId('authorId')
    .setName('作成者 ID')
    .setType(types.NUMBER);

  fields.newDimension()
    .setId('author')
    .setName('作成者')
    .setType(types.TEXT);

  fields.newDimension()
    .setId('assignedToId')
    .setName('担当者 ID')
    .setType(types.NUMBER);

  fields.newDimension()
    .setId('assignedTo')
    .setName('担当者')
    .setType(types.TEXT);

  fields.newDimension()
    .setId('fixedVersionId')
    .setName('対象バージョン ID')
    .setType(types.NUMBER);

  fields.newDimension()
    .setId('fixedVersion')
    .setName('対象バージョン')
    .setType(types.TEXT);

  fields.newDimension()
    .setId('parent')
    .setName('親チケット ID')
    .setType(types.NUMBER);

  fields.newDimension()
    .setId('subject')
    .setName('題名')
    .setType(types.TEXT);

  fields.newDimension()
    .setId('description')
    .setName('説明')
    .setType(types.TEXT); 

  fields.newDimension()
    .setId('startDate')
    .setName('開始日')
    .setType(types.YEAR_MONTH_DAY); 

  fields.newDimension()
    .setId('createdOn')
    .setName('作成日')
    .setType(types.YEAR_MONTH_DAY_SECOND);

  fields.newMetric()
    .setId('doneRatio')
    .setName('進捗率')
    .setType(types.PERCENT)
    .setAggregation(aggregations.SUM);

  fields.newDimension()
    .setId('updatedOn')
    .setName('更新日')
    .setType(types.YEAR_MONTH_DAY_SECOND);

  fields.newMetric()
    .setId('storyPoints')
    .setName('ストーリーポイント')
    .setType(types.NUMBER)
    .setAggregation(aggregations.SUM);

  return fields;
}

function getSchema(request) {
  var fields = getFields(request).build();
  return { schema: fields };
}

チケット一覧の取得 で取得した内容をもとに、スキーマを定義しています。

Looker Studio 上で、ディメンションとして定義したい場合は newDimension() を、 指標として定義したい場合は newMetric() を定義します。setId() は、一意のキーを定義します。これは後述する getData() で、 REST API から取得した結果をパースするときに必要となります。getName() はフィールド一覧に表示されるフィールド名を定義します。setType() はフィールドのデータ型を指定します。 使用できるデータ型は こちら を参照してください。

getData() の定義

Looker Studio 上で下記のイベントが発生すると、コネクタの getData() が呼び出されます。

  • ユーザーがダッシュボードにグラフを追加するとき
  • ユーザーがグラフを編集するとき
  • ユーザーがダッシュボードを表示するとき
  • ユーザーが関連付けられたフィルタまたはデータ コントロールを編集するとき
  • Looker Studio がデータサンプルを必要としたとき

getData() はその名前の通り、対象とするデータベース(今回は Redmine REST API)から実際にデータを取得し、定義されたスキーマにパースして Looker Studio に返す処理を行います。

先にコードを示します。

function getData(request) {
  // 後述の「要求されたフィールドのスキーマを作成する」で詳しく説明します
  var requestedFieldIds = request.fields.map(function(field) {
    return field.name;
  });
  var requestedFields = getFields().forIds(requestedFieldIds);

  // 後述の 「API からデータを取得して解析する」で詳しく説明します
  // Fetch and parse data from API
  var url = [
    'https://[REDMINE HOSTNAME]/issues.json?',
    'project_id=',
    request.configParams.project,
    '&limit=100'
  ];
  var headers = {
    'X-Redmine-API-Key': PropertiesService.getScriptProperties().getProperty("REDMINE_API_KEY")
  };
  var options = {
    "headers": headers
  };
  var response = UrlFetchApp.fetch(url.join(''), options);
  var parsedResponse = JSON.parse(response).issues;
  var rows = responseToRows(requestedFields, parsedResponse);

  return {
    schema: requestedFields.build(),
    rows: rows
  };
}

// 後述の「解析されたデータを変換し、要求されたフィールドをフィルタする」で詳しく説明します
function responseToRows(requestedFields, response) {
  // Transform parsed data and filter for requested fields
  return response.map(function(issue) {
    var row = [];
    requestedFields.asArray().forEach(function (field) {
      switch (field.getId()) {
        case 'id':
          return row.push(issue.id);
        case 'projectId':
          return row.push(issue.project.id);
        case 'project':
          return row.push(issue.project.name);
        case 'trackerId':
          return row.push(issue.tracker.id);
        case 'tracker':
          return row.push(issue.tracker.name);
        case 'statusId':
          return row.push(issue.status.id);
        case 'status':
          return row.push(issue.status.name);
        case 'priorityId':
          return row.push(issue.priority.id);
        case 'priority':
          return row.push(issue.priority.name);
        case 'authorId':
          return row.push(issue.author.id);
        case 'author':
          return row.push(issue.author.name);
        case 'assignedToId':
          // レスポンスの issue.assigned_to のキーがなければ null を、そうでなければ id を返します
          return row.push(issue.assigned_to && issue.assigned_to.id);
        case 'assignedTo':
          return row.push(issue.assigned_to && issue.assigned_to.name);
        case 'fixedVersionId':
          return row.push(issue.fixed_version && issue.fixed_version.id);
        case 'fixedVersion':
          return row.push(issue.fixed_version && issue.fixed_version.name);
        case 'parentId':
          return row.push(issue.parent && issue.parent.id);
        case 'subject':
          return row.push(issue.subject);
        case 'description':
          return row.push(issue.description);
        case 'startDate':
          // レスポンスの issue.start_date のキーがなければ null を、そうでなければ 2022-08-29 という形式の日付を 20220829 に変換して値を返します
          return row.push(issue.start_date && issue.start_date.replace(/-/g, ''));
        case 'doneRatio':
          return row.push(issue.done_ratio/100);
        case 'createdOn':
          return row.push(issue.created_on && issue.created_on.replace(/-|:|T|Z/g, ''));
        case 'updatedOn':
          return row.push(issue.updated_on && issue.updated_on.replace(/-|:|T|Z/g, ''));
        case 'storyPoints':
          return row.push(issue.story_points);
        default:
          return row.push('');
      }
    });
    return { values: row };
  });
}

request オブジェクト

getData() で参照されている request オブジェクトについて説明します。

Looker Studio からコネクタの getData() が呼び出されると、この request オブジェクトが渡されます。

request オブジェクトは下記のような構造になっています。

{
  configParams: object,
  scriptParams: object,
  dateRange: {
    startDate: string,
    endDate: string
  },
  fields: [
    {
      name: Field.name
    }
  ]
}

例えば、 configParams は、 getConfig() の定義 で説明したフィールドに入力した内容が格納されます。このような構造です。

{
  configParams: {
    project: '1'
  },
  ...
}

コネクタの getData() を実装する際は、 request オブジェクトの中身を適切に処理してデータを返します。 request オブジェクトの詳細は こちら を参照してください。

要求されたフィールドのスキーマを作成する

request オブジェクトの fields から、 Looker Studio からリクエストされたフィールドのスキーマを作成します。コネクタはAPIに問い合わせたデータのうち、これらのフィールドにフィルタして、結果を Looker Studio に返すようにします。

  var requestedFieldIds = request.fields.map(function(field) {
    return field.name;
  });
  var requestedFields = getFields().forIds(requestedFieldIds);

API からデータを取得して解析する

実際に API に問い合わせてデータを取得します。 チケット一覧の取得 で Redmine REST API を呼び出したのと同様のエンドポイントを url に定義します。

project_id には、 request オブジェクトの中にある configParams.project を渡しています。これは Looker Studio のデータソースの設定時に入力されたプロジェクト ID の値です。

Redmine REST API は、 1 回のリクエストで 100 件までのデータしか取得できません。 100 件を超えるデータに対応するためには、ページを再帰的に追いかける実装が必要です。 今回は、シンプルにするためにこの実装を割愛しています。

  // Fetch and parse data from API
  var url = [
    'https://[REDMINE HOSTNAME]/issues.json?',
    'project_id=',
    request.configParams.project,
    '&limit=100'
  ];

Redmine REST API を呼び出す際には、 X-Redmine-API-Key ヘッダーの指定が必要でした。これは秘匿情報なため、コード上に直接記載するのは望ましくありません。 Google Apps Script のプロパティ サービスを使用して、秘匿情報を管理します。

プロジェクトの設定 を開き、 スクリプト プロパティを追加 をクリックします。

file

プロパティ には REDMINE_API_KEY を、 には Redmine API アクセスキー を入力し、 スクリプト プロパティ を保存 をクリックします。

file

下記のように、 PropertiesService.getScriptProperties().getProperty() を使用して、定義したプロパティを参照できます。

  var headers = {
    'X-Redmine-API-Key': PropertiesService.getScriptProperties().getProperty("REDMINE_API_KEY")
  };
  var options = {
    "headers": headers
  };
  var response = UrlFetchApp.fetch(url.join(''), options);
  var parsedResponse = JSON.parse(response).issues;

解析されたデータを変換し、要求されたフィールドをフィルタする

switch case 文を使い、リクエストされたフィールドの結果を返すようにパースしていきます。 case に記載しているキーは、 getSchema()setId() した値を指定します。

function responseToRows(requestedFields, response) {
  // Transform parsed data and filter for requested fields
  return response.map(function(issue) {
    var row = [];
    requestedFields.asArray().forEach(function (field) {
      switch (field.getId()) {
        case 'id':
          return row.push(issue.id);
        case 'projectId':
          return row.push(issue.project.id);
        ...
        default:
          return row.push('');
      }
    });
    return { values: row };
  });
}

REST API の結果にキーが含まれない場合がある場合は、下記のようにして null safe にします。

        case 'assignedToId':
          return row.push(issue.assigned_to && issue.assigned_to.id);

REST API の結果を Looker Studio のデータ型に変換することが必要になる場合があります。

createdOn は、 2022-08-29T07:35:58Z のような形で REST API から返されますが、 Looker Studio 上では 20220829073558 のような形式のデータが必要です。

下記のようにして、データの変換を行います。

        case 'createdOn':
          return row.push(issue.created_on && issue.created_on.replace(/-|:|T|Z/g, ''));

マニフェストの作成

マニフェストを作成して、 Looker Studio からコネクタを追加する際のコネクタの情報を定義します。

プロジェクトの設定 を開き、 「appsscript.json」マニフェスト ファイルをエディタで表示する をクリックします。

file

ファイルに application.json が表示されるようになりました。

application.json を下記のように書き変えます。

{
  "timeZone": "Asia/Tokyo",
  "exceptionLogging": "STACKDRIVER",
  "runtimeVersion": "V8",

  "dataStudio": {
    "name": "Redmine",
    "logoUrl": "https://www.redmine.org/attachments/download/3462/redmine_fluid_icon.png",
    "company": "WESEEK, Inc.",
    "companyUrl": "https://weseek.co.jp/",
    "addonUrl": "https://weseek.co.jp/",
    "supportUrl": "https://weseek.co.jp/",
    "description": "Get a list of Issues in a Redmine project.",
    "sources": ["redmine"]
  }
}

dataStudio object について説明します。これらは、 Looker Studio でデータソースを追加する際に表示される情報です。 name は、コネクタの名前です。 logoUrl はアイコン画像で、ここでは Redmine のロゴを設定しました。 company companyUrl はこのコネクタを製作した組織の情報を入力します。 addonUrl は、このコネクタの専用の詳細ページの URL を記載します。今回は作成していないため、 companyUrl と同じにしています。 supportUrl は、このコネクタのサポートページの URL を記載します。今回は作成していないため、 companyUrl と同じにしています。 description には、コネクタの説明を記載します。 sources には、このコネクタが利用できるデータソースのリストを列挙します。今回は、 redmine とのみ記載しています。

マニフェストの詳細は こちら を参照してください。

コネクタをデプロイする(テスト)

デプロイ > デプロイをテスト をクリックします。

ヘッドデプロイ ID をコピーしておきます。

file

Looker Studio からコネクタを利用する

作成したコネクタに接続する

Looker Studio を開き、 作成 > レポート をクリックして、新しいレポートを作成します。

データのレポートへの追加 で、 独自に作成 をクリックします。

file

Deployment ID に、先ほどコピーした Deployment ID を入力し、検証をクリックします。

file

下部にこのように表示されたら、 Redmine の枠をクリックします。

file

承認をクリックします。

file

Google hasn’t verified this app と表示されたら、 Advanced をクリックし、 Go to RedmineDataStudioConnector (unsafe) をクリックします。その後、 Allow をクリックします。

Looker Studio に表示したい Redmine プロジェクトの ID を入力し、追加をクリックします。

file

グラフを追加してデータを表示してみる

ここでは、例として Redmine から取得したデータを元に表を描画してみます。

グラフを追加 > をクリックし、任意の場所に配置します。

それぞれ下記のように選択します。

  • データソース
    • Redmine
  • ディメンション
    • 題名
    • 対象バージョン
    • ステータス
    • 作成日
  • 指標
    • SUM: ストーリーポイント

file

このような表が構築できます。

file

その他に棒グラフや、ウォーターフォールグラフなどを使用して、このようなダッシュボードを構築できます。

file

最後に

今回作成したコネクタは、テスト(開発)用としてデプロイを行いました。 作成したコネクタを一般に広く公開する場合は、Publish a Community Connector を実施します。