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

表題にある通りMackerelにあるメトリクスのデータをcsvでダウンロードして使いたかったのでやってみたときの備忘録です。
前提
- Node.js(TypeScript)を使ってやります。
- Node.jsのインストール手順は省略します。
手順
1. Node.jsのプロジェクト作成
適当なフォルダ(今回はmackerel-csv-converterとします)を作って以下の内容のpackage.jsonファイルを作成します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"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ファイルも作成して以下の内容をコピペします。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"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」を確認して控えておく
管理画面にアクセスし左上の「オーガニゼーション詳細ページへ」をクリック。

「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
3
4
5
6
7
8
{
"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ファイルを作成し、以下のコードをコピペして保存します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
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の中身は、ダウンロードしたいメトリクスの範囲の開始日時を任意で変更してください。
1
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