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

表題にある通りMackerelにあるメトリクスのデータをcsvでダウンロードして使いたかったのでやってみたときの備忘録です。
前提
- Node.js(TypeScript)を使ってやります。
- Node.jsのインストール手順は省略します。
手順
1. Node.jsのプロジェクト作成
適当なフォルダ(今回はmackerel-csv-converter
とします)を作って以下の内容のpackage.jsonファイルを作成します。
1{
2 "name": "mackerel-csv-converter",
3 "version": "1.0.0",
4 "description": "Convert Mackerel API response to CSV",
5 "main": "dist/index.js",
6 "scripts": {
7 "build": "tsc",
8 "start": "node dist/index.js"
9 },
10 "dependencies": {
11 "axios": "^1.6.7",
12 "csv-writer": "^1.6.0",
13 "dotenv": "^16.4.1"
14 },
15 "devDependencies": {
16 "@types/node": "^20.11.16",
17 "typescript": "^5.3.3"
18 }
19}
同じディレクトリにtsconfig.json
ファイルも作成して以下の内容をコピペします。
1{
2 "compilerOptions": {
3 "target": "es2020",
4 "module": "commonjs",
5 "outDir": "./dist",
6 "rootDir": "./src",
7 "strict": true,
8 "esModuleInterop": true,
9 "skipLibCheck": true,
10 "forceConsistentCasingInFileNames": true
11 },
12 "include": ["src/**/*"],
13 "exclude": ["node_modules"]
14}
2. npm install
package.jsonと同じディレクトリでnpm install
を実行。
3. Mackerelの管理画面で「APIキー」と「ホストID」を確認して控えておく
管理画面にアクセスし左上の「オーガニゼーション詳細ページへ」をクリック。
「APIキー」タブがあるのでそこでAPIキーをコピーして控えておく。(なければ作成してください)
左ペインの「ホスト」からメトリクスをダウンロードしたいホストのIDを確認して控えておく。
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
というファイルを作成して、先程取得したレスポンスをコピペして保存します。
1{
2 "names": [
3 "cpu.guest.percentage",
4 "cpu.idle.percentage",
5 "cpu.iowait.percentage",
6 ...省略
7 ]
8}
6. .env
ファイル作成
package.jsonと同じディレクトリに.env
ファイルを作成し以下のようにAPIキーとホストIDを書いておきます。
MACKEREL_API_KEY=<APIキー>
MACKEREL_HOST_ID=<ホストID>
7. index.ts作成
package.jsonと同じディレクトリにsrc
フォルダを作成し、その中にindex.ts
ファイルを作成し、以下のコードをコピペして保存します。
1import axios from 'axios';
2import { createObjectCsvWriter } from 'csv-writer';
3import dotenv from 'dotenv';
4import path from 'path';
5
6dotenv.config();
7
8interface MetricValue {
9 time: number;
10 value: number;
11}
12
13interface MetricData {
14 metrics: MetricValue[];
15 name: string;
16}
17
18interface GroupedMetricData {
19 [timestamp: number]: {
20 time: number;
21 [metric: string]: number | null;
22 };
23}
24
25const MACKEREL_API_KEY = process.env.MACKEREL_API_KEY;
26const MACKEREL_HOST_ID = process.env.MACKEREL_HOST_ID;
27
28if (!MACKEREL_API_KEY || !MACKEREL_HOST_ID) {
29 console.error('環境変数 MACKEREL_API_KEY と MACKEREL_HOST_ID を設定してください。');
30 process.exit(1);
31}
32
33const fetchMetrics = async (metricName: string, from: number, to: number): Promise<MetricData> => {
34 const url = `https://api.mackerelio.com/api/v0/hosts/${MACKEREL_HOST_ID}/metrics`;
35 const response = await axios.get(url, {
36 headers: {
37 'X-Api-Key': MACKEREL_API_KEY,
38 },
39 params: {
40 name: metricName,
41 from,
42 to,
43 },
44 });
45
46 return {
47 name: metricName,
48 metrics: response.data.metrics,
49 };
50};
51
52const convertToCsv = async (groupName: string, data: GroupedMetricData, outputDir: string) => {
53 // ヘッダーを動的に生成
54 const firstRecord = Object.values(data)[0];
55 const metrics = Object.keys(firstRecord).filter(key => key !== 'time');
56 const header = [
57 { id: 'time_jst', title: 'TIMESTAMP_JST' },
58 ...metrics.map(metric => {
59 // ドットが含まれない場合は元の名前をそのまま使用
60 if (!metric.includes('.')) {
61 return {
62 id: metric,
63 title: metric,
64 };
65 }
66 // ドットが含まれる場合はプレフィックスを除去
67 return {
68 id: metric,
69 title: metric.split('.').slice(1).join('.'),
70 };
71 }),
72 ];
73
74 const csvWriter = createObjectCsvWriter({
75 path: path.join(outputDir, `${groupName}.csv`),
76 header,
77 });
78
79 // 時刻でソートされた配列に変換
80 const records = Object.entries(data)
81 .sort(([timeA], [timeB]) => parseInt(timeA) - parseInt(timeB))
82 .map(([_, record]) => ({
83 time_jst: new Date(record.time * 1000 + (9 * 60 * 60 * 1000)).toISOString(),
84 ...metrics.reduce((acc, metric) => ({
85 ...acc,
86 [metric]: record[metric],
87 }), {}),
88 }));
89
90 await csvWriter.writeRecords(records);
91 console.log(`${groupName}.csv を作成しました。`);
92};
93
94// データ取得期間の定数を定義(メモ:最小は2024-09-24)
95const START_DATE = '2025-01-01T00:00:00+09:00'; // 開始日時をJST(日本時間)で指定
96const HOURS_PER_REQUEST = 20; // 1リクエストあたりの時間範囲(時間)
97const SECONDS_PER_HOUR = 3600;
98
99// 日付文字列からUNIXタイムスタンプを取得する関数(JSTを考慮)
100const getUnixTimestamp = (dateStr: string): number => {
101 // タイムゾーン付きの日付文字列をパースしてUNIXタイムスタンプを取得
102 return Math.floor(new Date(dateStr).getTime() / 1000);
103};
104
105// 指定された期間のメトリクスを取得する関数
106const fetchMetricsForPeriod = async (metricName: string, from: number, to: number): Promise<MetricValue[]> => {
107 const timeRangeInSeconds = to - from;
108 const timeRangeInHours = timeRangeInSeconds / SECONDS_PER_HOUR;
109 const metrics: MetricValue[] = [];
110 const totalRequests = Math.ceil(timeRangeInHours / HOURS_PER_REQUEST);
111 let currentRequest = 1;
112
113 console.log(`${metricName}: ${totalRequests}回のリクエストに分割して取得します...`);
114
115 // 20時間ごとにリクエストを分割
116 for (let currentFrom = from; currentFrom < to; currentFrom += HOURS_PER_REQUEST * SECONDS_PER_HOUR) {
117 const currentTo = Math.min(currentFrom + HOURS_PER_REQUEST * SECONDS_PER_HOUR, to);
118 console.log(`${metricName}: ${currentRequest}/${totalRequests} リクエスト実行中...`);
119 const data = await fetchMetrics(metricName, currentFrom, currentTo);
120 metrics.push(...data.metrics);
121 currentRequest++;
122 }
123
124 console.log(`${metricName}: 全${metrics.length}件のデータを取得しました。`);
125 return metrics;
126};
127
128const main = async () => {
129 try {
130 // 現在時刻(JST)から5分前までのデータを取得
131 const FIVE_MINUTES = 5 * 60; // 5分を秒に変換
132 const now = new Date();
133 // JSTでの現在時刻を取得
134 const to = Math.floor(now.getTime() / 1000) - FIVE_MINUTES;
135 const from = getUnixTimestamp(START_DATE);
136
137 // メトリクス名のリストを読み込み
138 const metricNames = require('../metric-names.json').names;
139
140 // メトリクス名をグループ化
141 const groupedMetrics: { [prefix: string]: string[] } = {};
142 metricNames.forEach((name: string) => {
143 let groupKey: string;
144 const parts = name.split('.');
145
146 if (parts[0] === 'custom') {
147 // custom.xxx.yyy.zzzの形式の場合、xxx-yyyをグループキーとして使用
148 groupKey = `${parts[1]}-${parts[2]}`;
149 } else if (parts[0].startsWith('loadavg')) {
150 // loadavgから始まるメトリクスは全てloadavgグループに
151 groupKey = 'loadavg';
152 } else {
153 // それ以外は最初の部分でグルーピング
154 groupKey = parts[0];
155 }
156
157 if (!groupedMetrics[groupKey]) {
158 groupedMetrics[groupKey] = [];
159 }
160 groupedMetrics[groupKey].push(name);
161 });
162
163 // 出力ディレクトリを作成
164 const outputDir = path.join(__dirname, '../output');
165 require('fs').mkdirSync(outputDir, { recursive: true });
166
167 // グループごとにデータを取得してCSVに変換
168 for (const [groupName, metrics] of Object.entries(groupedMetrics)) {
169 try {
170 console.log(`${groupName}グループのデータを取得中...`);
171 const groupData: GroupedMetricData = {};
172
173 // グループ内の各メトリクスのデータを取得
174 for (const metricName of metrics) {
175 const metricData = await fetchMetricsForPeriod(metricName, from, to);
176
177 // データをグループ化して整理
178 metricData.forEach(metric => {
179 if (!groupData[metric.time]) {
180 const initialMetrics = metrics.reduce<{ [key: string]: number | null }>((acc, name) => {
181 acc[name] = null;
182 return acc;
183 }, {});
184
185 groupData[metric.time] = {
186 time: metric.time,
187 ...initialMetrics,
188 };
189 }
190 groupData[metric.time][metricName] = metric.value;
191 });
192 }
193
194 await convertToCsv(groupName, groupData, outputDir);
195 } catch (error) {
196 console.error(`${groupName}グループの処理中にエラーが発生しました:`, error);
197 }
198 }
199
200 console.log('すべての処理が完了しました。');
201 } catch (error) {
202 console.error('エラーが発生しました:', error);
203 process.exit(1);
204 }
205};
206
207main();
コードのconst START_DATE
の中身は、ダウンロードしたいメトリクスの範囲の開始日時を任意で変更してください。
1const 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