Line data Source code
1 : // dtcPrometheusExporter.cc
2 : // Prometheus metrics HTTP exporter for Mu2e DTC/CFO register monitoring.
3 :
4 : #include <arpa/inet.h>
5 : #include <dirent.h>
6 : #include <netinet/in.h>
7 : #include <sys/select.h>
8 : #include <sys/socket.h>
9 : #include <unistd.h>
10 :
11 : #include <algorithm>
12 : #include <cerrno>
13 : #include <csignal>
14 : #include <cctype>
15 : #include <cstring>
16 : #include <iostream>
17 : #include <optional>
18 : #include <set>
19 : #include <sstream>
20 : #include <string>
21 : #include <vector>
22 :
23 : #include "cfoInterfaceLib/CFO_Registers.h"
24 : #include "dtcInterfaceLib/DTC_Registers.h"
25 :
26 : namespace {
27 : volatile sig_atomic_t g_running = 1;
28 :
29 : struct ExporterConfig
30 : {
31 : std::optional<int> dtcFilter;
32 : std::string memFileName;
33 : };
34 :
35 0 : void signalHandler(int /*sig*/) { g_running = 0; }
36 :
37 0 : std::string escapeLabelValue(const std::string& in)
38 : {
39 0 : std::string out;
40 0 : out.reserve(in.size());
41 0 : for (char c : in)
42 : {
43 0 : if (c == '\\' || c == '"') out.push_back('\\');
44 0 : out.push_back(c);
45 : }
46 0 : return out;
47 0 : }
48 :
49 0 : std::vector<int> discoverMu2eDeviceIndices()
50 : {
51 0 : std::vector<int> ids;
52 0 : DIR* dir = opendir("/dev");
53 0 : if (!dir) return ids;
54 :
55 0 : while (auto* entry = readdir(dir))
56 : {
57 0 : const std::string name(entry->d_name);
58 0 : if (name.rfind("mu2e", 0) != 0 || name.size() <= 4) continue;
59 0 : const std::string suffix = name.substr(4);
60 0 : if (!std::all_of(suffix.begin(), suffix.end(), [](unsigned char ch) { return std::isdigit(ch) != 0; })) continue;
61 0 : ids.push_back(std::stoi(suffix));
62 0 : }
63 0 : closedir(dir);
64 0 : std::sort(ids.begin(), ids.end());
65 0 : ids.erase(std::unique(ids.begin(), ids.end()), ids.end());
66 0 : return ids;
67 0 : }
68 :
69 0 : void writeGaugeSample(std::ostringstream& oss, const std::string& labels, uint32_t value)
70 : {
71 0 : oss << "dtc_register_value{" << labels << "} " << value << "\n";
72 0 : }
73 :
74 0 : void appendRegisterMetrics(std::ostringstream& oss, int deviceIndex, const std::string& deviceType,
75 : const std::vector<std::function<DTCLib::RegisterFormatter()>>& functions,
76 : std::set<uint16_t>& seenAddresses)
77 : {
78 0 : for (const auto& fn : functions)
79 : {
80 : try
81 : {
82 0 : auto reg = fn();
83 0 : if (!seenAddresses.insert(reg.address).second) continue;
84 :
85 0 : std::ostringstream addr;
86 0 : addr << "0x" << std::hex << reg.address;
87 0 : const std::string regName = reg.description.empty() ? addr.str() : reg.description;
88 : const std::string labels =
89 0 : "dtc=\"" + std::to_string(deviceIndex) + "\",device_type=\"" + deviceType +
90 0 : "\",address=\"" + addr.str() + "\",register=\"" + escapeLabelValue(regName) + "\"";
91 0 : writeGaugeSample(oss, labels, reg.value);
92 0 : }
93 0 : catch (...)
94 0 : {}
95 : }
96 0 : }
97 :
98 0 : std::string collectMetrics(const ExporterConfig& config)
99 : {
100 0 : std::ostringstream oss;
101 0 : oss << "# HELP dtc_register_value Raw register value for Mu2e DTC/CFO devices\n";
102 0 : oss << "# TYPE dtc_register_value gauge\n";
103 :
104 0 : std::vector<int> deviceIds;
105 0 : if (config.dtcFilter.has_value())
106 : {
107 0 : deviceIds.push_back(*config.dtcFilter);
108 : }
109 : else
110 : {
111 0 : deviceIds = discoverMu2eDeviceIndices();
112 : }
113 :
114 0 : for (int deviceIndex : deviceIds)
115 : {
116 : try
117 : {
118 : {
119 0 : DTCLib::DTC_Registers regs(DTCLib::DTC_SimMode_Disabled, deviceIndex, config.memFileName, 0x1, "", true);
120 0 : if (!regs.isCFODesignFlavour())
121 : {
122 0 : std::set<uint16_t> seenAddresses;
123 0 : appendRegisterMetrics(oss, deviceIndex, "dtc", regs.getFormattedSimpleDumpFunctions(), seenAddresses);
124 0 : appendRegisterMetrics(oss, deviceIndex, "dtc", regs.getFormattedDumpFunctions(), seenAddresses);
125 0 : appendRegisterMetrics(oss, deviceIndex, "dtc", regs.formattedPerformanceCounterFunctions_, seenAddresses);
126 0 : appendRegisterMetrics(oss, deviceIndex, "dtc", regs.formattedSERDESErrorFunctions_, seenAddresses);
127 0 : appendRegisterMetrics(oss, deviceIndex, "dtc", regs.formattedPacketCounterFunctions_, seenAddresses);
128 0 : continue;
129 0 : }
130 0 : }
131 :
132 0 : CFOLib::CFO_Registers cfo(DTCLib::DTC_SimMode_Disabled, deviceIndex, "", true);
133 0 : std::set<uint16_t> seenAddresses;
134 0 : appendRegisterMetrics(oss, deviceIndex, "cfo", cfo.getFormattedSimpleDumpFunctions(), seenAddresses);
135 0 : appendRegisterMetrics(oss, deviceIndex, "cfo", cfo.getFormattedDumpFunctions(), seenAddresses);
136 0 : appendRegisterMetrics(oss, deviceIndex, "cfo", cfo.formattedCounterFunctions_, seenAddresses);
137 0 : }
138 0 : catch (...)
139 0 : {}
140 : }
141 :
142 0 : return oss.str();
143 0 : }
144 :
145 0 : void writeAll(int fd, const char* buf, size_t len)
146 : {
147 0 : while (len > 0)
148 : {
149 0 : const ssize_t n = send(fd, buf, len, MSG_NOSIGNAL);
150 0 : if (n < 0)
151 : {
152 0 : if (errno == EINTR) continue;
153 0 : break;
154 : }
155 0 : if (n == 0) break;
156 0 : buf += n;
157 0 : len -= static_cast<size_t>(n);
158 : }
159 0 : }
160 :
161 0 : void handleRequest(int clientFd, const ExporterConfig& config)
162 : {
163 0 : char buf[4096] = {};
164 0 : ssize_t nread = -1;
165 : do
166 : {
167 0 : nread = read(clientFd, buf, sizeof(buf) - 1);
168 0 : } while (nread < 0 && errno == EINTR);
169 0 : if (nread <= 0)
170 : {
171 0 : close(clientFd);
172 0 : return;
173 : }
174 :
175 0 : const std::string request(buf);
176 0 : const auto firstLineEnd = request.find("\r\n");
177 0 : if (firstLineEnd == std::string::npos)
178 : {
179 0 : close(clientFd);
180 0 : return;
181 : }
182 0 : const std::string requestLine = request.substr(0, firstLineEnd);
183 0 : const bool isMetrics = requestLine == "GET /metrics HTTP/1.1" ||
184 0 : requestLine == "GET /metrics HTTP/1.0" ||
185 0 : requestLine == "GET /metrics";
186 :
187 0 : std::string body;
188 0 : std::string statusLine;
189 0 : std::string contentType;
190 :
191 0 : if (isMetrics)
192 : {
193 0 : body = collectMetrics(config);
194 0 : statusLine = "HTTP/1.1 200 OK\r\n";
195 0 : contentType = "text/plain; version=0.0.4; charset=utf-8";
196 : }
197 : else
198 : {
199 0 : body = "Not Found. Use /metrics\n";
200 0 : statusLine = "HTTP/1.1 404 Not Found\r\n";
201 0 : contentType = "text/plain; charset=utf-8";
202 : }
203 :
204 0 : const std::string response = statusLine +
205 0 : "Content-Type: " + contentType +
206 : "\r\n"
207 0 : "Content-Length: " +
208 0 : std::to_string(body.size()) +
209 : "\r\n"
210 : "Connection: close\r\n\r\n" +
211 0 : body;
212 :
213 0 : writeAll(clientFd, response.c_str(), response.size());
214 0 : close(clientFd);
215 0 : }
216 : } // namespace
217 :
218 0 : void printHelpMsg()
219 : {
220 : std::cout << "Usage: dtcPrometheusExporter [options]\n"
221 : << "Options:\n"
222 : << " -h: This message.\n"
223 : << " -d: Restrict to one DTC/CFO instance (default: scrape all /dev/mu2e* devices)\n"
224 : << " -m: Use <file> as the emulated DTC memory area (default: mu2esim.bin)\n"
225 0 : << " -p: TCP port to listen on (default: 9100)\n";
226 0 : exit(0);
227 : }
228 :
229 0 : int main(int argc, char* argv[])
230 : {
231 0 : ExporterConfig config;
232 0 : uint16_t port = 9100;
233 0 : config.memFileName = "mu2esim.bin";
234 :
235 0 : for (int optind = 1; optind < argc; ++optind)
236 : {
237 0 : if (argv[optind][0] == '-')
238 : {
239 0 : switch (argv[optind][1])
240 : {
241 0 : case 'h':
242 0 : printHelpMsg();
243 0 : break;
244 0 : case 'd':
245 0 : config.dtcFilter = DTCLib::Utilities::getOptionValue(&optind, &argv);
246 0 : break;
247 0 : case 'm':
248 0 : config.memFileName = DTCLib::Utilities::getOptionString(&optind, &argv);
249 0 : break;
250 0 : case 'p':
251 0 : port = static_cast<uint16_t>(DTCLib::Utilities::getOptionValue(&optind, &argv));
252 0 : break;
253 0 : default:
254 0 : std::cerr << "Unknown option: " << argv[optind] << "\n";
255 0 : printHelpMsg();
256 0 : break;
257 : }
258 : }
259 : }
260 :
261 0 : signal(SIGINT, signalHandler);
262 0 : signal(SIGTERM, signalHandler);
263 :
264 0 : const int serverFd = socket(AF_INET, SOCK_STREAM, 0);
265 0 : if (serverFd < 0)
266 : {
267 0 : std::cerr << "Failed to create socket: " << strerror(errno) << "\n";
268 0 : return 1;
269 : }
270 :
271 0 : const int opt = 1;
272 0 : if (setsockopt(serverFd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) < 0)
273 : {
274 0 : std::cerr << "Warning: setsockopt SO_REUSEADDR failed: " << strerror(errno) << "\n";
275 : }
276 :
277 0 : sockaddr_in addr{};
278 0 : addr.sin_family = AF_INET;
279 0 : addr.sin_addr.s_addr = INADDR_ANY;
280 0 : addr.sin_port = htons(port);
281 :
282 0 : if (bind(serverFd, reinterpret_cast<sockaddr*>(&addr), sizeof(addr)) < 0)
283 : {
284 0 : std::cerr << "Failed to bind to port " << port << ": " << strerror(errno) << "\n";
285 0 : close(serverFd);
286 0 : return 1;
287 : }
288 :
289 0 : if (listen(serverFd, 10) < 0)
290 : {
291 0 : std::cerr << "Failed to listen: " << strerror(errno) << "\n";
292 0 : close(serverFd);
293 0 : return 1;
294 : }
295 :
296 0 : std::cout << "DTC/CFO Prometheus exporter listening on :" << port << "/metrics";
297 0 : if (config.dtcFilter.has_value()) std::cout << " (device " << *config.dtcFilter << ")";
298 0 : std::cout << "\n";
299 :
300 0 : while (g_running)
301 : {
302 : fd_set fds;
303 0 : struct timeval tv = {1, 0};
304 0 : FD_ZERO(&fds);
305 0 : FD_SET(serverFd, &fds);
306 :
307 0 : if (select(serverFd + 1, &fds, nullptr, nullptr, &tv) <= 0)
308 : {
309 0 : continue;
310 : }
311 :
312 0 : const int clientFd = accept(serverFd, nullptr, nullptr);
313 0 : if (clientFd >= 0)
314 : {
315 0 : handleRequest(clientFd, config);
316 : }
317 : }
318 :
319 0 : close(serverFd);
320 0 : std::cout << "DTC/CFO Prometheus exporter stopped.\n";
321 0 : return 0;
322 0 : }
|