Mackerel上にあるメトリックデータをcsvでダウンロード保存してみたときの備忘録

Mackerel上にあるメトリックデータをcsvでダウンロード保存してみたときの備忘録

表題にある通りMackerelにあるメトリクスのデータをcsvでダウンロードして使いたかったのでやってみたときの備忘録です。

前提

  • Node.js(TypeScript)を使ってやります。
  • Node.jsのインストール手順は省略します。

手順

1. Node.jsのプロジェクト作成

適当なフォルダ(今回はmackerel-csv-converterとします)を作って以下の内容のpackage.jsonファイルを作成します。

{
  "name": "mackerel-csv-converter",
  "version": "1.0.0",
  "description": "Convert Mackerel API response to CSV",
  "main": "dist/index.js",
  "scripts": {
    "build": "tsc",
    "start": "node dist/index.js"
  },
  "dependencies": {
    "axios": "^1.6.7",
    "csv-writer": "^1.6.0",
    "dotenv": "^16.4.1"
  },
  "devDependencies": {
    "@types/node": "^20.11.16",
    "typescript": "^5.3.3"
  }
} 

同じディレクトリにtsconfig.jsonファイルも作成して以下の内容をコピペします。

{
  "compilerOptions": {
    "target": "es2020",
    "module": "commonjs",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
} 

2. npm install

package.jsonと同じディレクトリでnpm installを実行。

3. Mackerelの管理画面で「APIキー」と「ホストID」を確認して控えておく

管理画面にアクセスし左上の「オーガニゼーション詳細ページへ」をクリック。
2025-02-24_1.webp
「APIキー」タブがあるのでそこでAPIキーをコピーして控えておく。(なければ作成してください)
2025-02-24_2.webp

左ペインの「ホスト」からメトリクスをダウンロードしたいホストのIDを確認して控えておく。
2025-02-24_3.webp
2025-02-24_4.webp

4. ホストにあるメトリクス一覧をcurlで取得する

控えておいたAPIキーとホストIDで以下のコマンドを実行して、取得可能なメトリクス名一覧を取得します。

curl -H "X-Api-Key: <APIキー>" "https://api.mackerelio.com/api/v0/hosts/<ホストID>/metric-names"

実行するとjson形式でレスポンスが来るはずです。

5. metric-names.json作成

package.jsonと同じディレクトリにmetric-names.jsonというファイルを作成して、先程取得したレスポンスをコピペして保存します。

{
  "names": [
    "cpu.guest.percentage",
    "cpu.idle.percentage",
    "cpu.iowait.percentage",
    ...省略
  ]
}

6. .envファイル作成

package.jsonと同じディレクトリに.envファイルを作成し以下のようにAPIキーとホストIDを書いておきます。

MACKEREL_API_KEY=<APIキー>
MACKEREL_HOST_ID=<ホストID> 

7. index.ts作成

package.jsonと同じディレクトリにsrcフォルダを作成し、その中にindex.tsファイルを作成し、以下のコードをコピペして保存します。

import axios from 'axios';
import { createObjectCsvWriter } from 'csv-writer';
import dotenv from 'dotenv';
import path from 'path';

dotenv.config();

interface MetricValue {
  time: number;
  value: number;
}

interface MetricData {
  metrics: MetricValue[];
  name: string;
}

interface GroupedMetricData {
  [timestamp: number]: {
    time: number;
    [metric: string]: number | null;
  };
}

const MACKEREL_API_KEY = process.env.MACKEREL_API_KEY;
const MACKEREL_HOST_ID = process.env.MACKEREL_HOST_ID;

if (!MACKEREL_API_KEY || !MACKEREL_HOST_ID) {
  console.error('環境変数 MACKEREL_API_KEY と MACKEREL_HOST_ID を設定してください。');
  process.exit(1);
}

const fetchMetrics = async (metricName: string, from: number, to: number): Promise<MetricData> => {
  const url = `https://api.mackerelio.com/api/v0/hosts/${MACKEREL_HOST_ID}/metrics`;
  const response = await axios.get(url, {
    headers: {
      'X-Api-Key': MACKEREL_API_KEY,
    },
    params: {
      name: metricName,
      from,
      to,
    },
  });

  return {
    name: metricName,
    metrics: response.data.metrics,
  };
};

const convertToCsv = async (groupName: string, data: GroupedMetricData, outputDir: string) => {
  // ヘッダーを動的に生成
  const firstRecord = Object.values(data)[0];
  const metrics = Object.keys(firstRecord).filter(key => key !== 'time');
  const header = [
    { id: 'time_jst', title: 'TIMESTAMP_JST' },
    ...metrics.map(metric => {
      // ドットが含まれない場合は元の名前をそのまま使用
      if (!metric.includes('.')) {
        return {
          id: metric,
          title: metric,
        };
      }
      // ドットが含まれる場合はプレフィックスを除去
      return {
        id: metric,
        title: metric.split('.').slice(1).join('.'),
      };
    }),
  ];

  const csvWriter = createObjectCsvWriter({
    path: path.join(outputDir, `${groupName}.csv`),
    header,
  });

  // 時刻でソートされた配列に変換
  const records = Object.entries(data)
    .sort(([timeA], [timeB]) => parseInt(timeA) - parseInt(timeB))
    .map(([_, record]) => ({
      time_jst: new Date(record.time * 1000 + (9 * 60 * 60 * 1000)).toISOString(),
      ...metrics.reduce((acc, metric) => ({
        ...acc,
        [metric]: record[metric],
      }), {}),
    }));

  await csvWriter.writeRecords(records);
  console.log(`${groupName}.csv を作成しました。`);
};

// データ取得期間の定数を定義(メモ:最小は2024-09-24)
const START_DATE = '2025-01-01T00:00:00+09:00'; // 開始日時をJST(日本時間)で指定
const HOURS_PER_REQUEST = 20; // 1リクエストあたりの時間範囲(時間)
const SECONDS_PER_HOUR = 3600;

// 日付文字列からUNIXタイムスタンプを取得する関数(JSTを考慮)
const getUnixTimestamp = (dateStr: string): number => {
  // タイムゾーン付きの日付文字列をパースしてUNIXタイムスタンプを取得
  return Math.floor(new Date(dateStr).getTime() / 1000);
};

// 指定された期間のメトリクスを取得する関数
const fetchMetricsForPeriod = async (metricName: string, from: number, to: number): Promise<MetricValue[]> => {
  const timeRangeInSeconds = to - from;
  const timeRangeInHours = timeRangeInSeconds / SECONDS_PER_HOUR;
  const metrics: MetricValue[] = [];
  const totalRequests = Math.ceil(timeRangeInHours / HOURS_PER_REQUEST);
  let currentRequest = 1;

  console.log(`${metricName}: ${totalRequests}回のリクエストに分割して取得します...`);

  // 20時間ごとにリクエストを分割
  for (let currentFrom = from; currentFrom < to; currentFrom += HOURS_PER_REQUEST * SECONDS_PER_HOUR) {
    const currentTo = Math.min(currentFrom + HOURS_PER_REQUEST * SECONDS_PER_HOUR, to);
    console.log(`${metricName}: ${currentRequest}/${totalRequests} リクエスト実行中...`);
    const data = await fetchMetrics(metricName, currentFrom, currentTo);
    metrics.push(...data.metrics);
    currentRequest++;
  }

  console.log(`${metricName}: 全${metrics.length}件のデータを取得しました。`);
  return metrics;
};

const main = async () => {
  try {
    // 現在時刻(JST)から5分前までのデータを取得
    const FIVE_MINUTES = 5 * 60; // 5分を秒に変換
    const now = new Date();
    // JSTでの現在時刻を取得
    const to = Math.floor(now.getTime() / 1000) - FIVE_MINUTES;
    const from = getUnixTimestamp(START_DATE);

    // メトリクス名のリストを読み込み
    const metricNames = require('../metric-names.json').names;

    // メトリクス名をグループ化
    const groupedMetrics: { [prefix: string]: string[] } = {};
    metricNames.forEach((name: string) => {
      let groupKey: string;
      const parts = name.split('.');
      
      if (parts[0] === 'custom') {
        // custom.xxx.yyy.zzzの形式の場合、xxx-yyyをグループキーとして使用
        groupKey = `${parts[1]}-${parts[2]}`;
      } else if (parts[0].startsWith('loadavg')) {
        // loadavgから始まるメトリクスは全てloadavgグループに
        groupKey = 'loadavg';
      } else {
        // それ以外は最初の部分でグルーピング
        groupKey = parts[0];
      }

      if (!groupedMetrics[groupKey]) {
        groupedMetrics[groupKey] = [];
      }
      groupedMetrics[groupKey].push(name);
    });

    // 出力ディレクトリを作成
    const outputDir = path.join(__dirname, '../output');
    require('fs').mkdirSync(outputDir, { recursive: true });

    // グループごとにデータを取得してCSVに変換
    for (const [groupName, metrics] of Object.entries(groupedMetrics)) {
      try {
        console.log(`${groupName}グループのデータを取得中...`);
        const groupData: GroupedMetricData = {};

        // グループ内の各メトリクスのデータを取得
        for (const metricName of metrics) {
          const metricData = await fetchMetricsForPeriod(metricName, from, to);
          
          // データをグループ化して整理
          metricData.forEach(metric => {
            if (!groupData[metric.time]) {
              const initialMetrics = metrics.reduce<{ [key: string]: number | null }>((acc, name) => {
                acc[name] = null;
                return acc;
              }, {});
              
              groupData[metric.time] = {
                time: metric.time,
                ...initialMetrics,
              };
            }
            groupData[metric.time][metricName] = metric.value;
          });
        }

        await convertToCsv(groupName, groupData, outputDir);
      } catch (error) {
        console.error(`${groupName}グループの処理中にエラーが発生しました:`, error);
      }
    }

    console.log('すべての処理が完了しました。');
  } catch (error) {
    console.error('エラーが発生しました:', error);
    process.exit(1);
  }
};

main(); 

コードのconst START_DATEの中身は、ダウンロードしたいメトリクスの範囲の開始日時を任意で変更してください。

const START_DATE = '2025-01-01T00:00:00+09:00';

※ END_DATEに該当するものはなく、現在時刻になるようになっています。

他はとくに修正しなくてokです。
HOURS_PER_REQUESTの値は大きくするとapiから返却されるデータの粒度が変わるので注意してください。

8. ビルドする

ターミナルでnpm run buildを実行します。
distというディレクトリが作成されます。

9. 実行する

npm run startを実行します。
outputディレクトリが作成され、その中にメトリクスデータをダウンロードして作られたcsvデータが出力されます。

以上。

Share this post