-
Notifications
You must be signed in to change notification settings - Fork 41
add cpuexporter example #327
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
endersonmaia
wants to merge
3
commits into
master
Choose a base branch
from
endersonmaia_cpuexporter
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,170 @@ | ||
| -- | ||
| -- SPDX-FileCopyrightText: (c) 2025 Enderson Maia <[email protected]> | ||
| -- SPDX-License-Identifier: MIT OR GPL-2.0-only | ||
| -- | ||
|
|
||
| local lunatik = require("lunatik") | ||
| local thread = require("thread") | ||
| local unix = require("socket.unix") | ||
| local linux = require("linux") | ||
| local cpu = require("cpu") | ||
|
|
||
| local shouldstop = thread.shouldstop | ||
|
|
||
| local server = unix.bind("/tmp/cpuexporter.sock", "STREAM") | ||
| server:listen() | ||
|
|
||
| local last_stats = {} | ||
| local last_total_stats = {} | ||
|
|
||
| -- Scale integer to decimal with high precision | ||
| -- Converts a ratio (metric/total) to percentage format with 16 decimal places | ||
| local function format_percentage(metric, total) | ||
| if total == 0 then | ||
| return "0.0000000000000000" | ||
| end | ||
|
|
||
| local int_part = (metric * 100) // total | ||
| local remainder = (metric * 100) % total | ||
| local frac_high = (remainder * 100000000) // total | ||
| local remainder2 = (remainder * 100000000) % total | ||
| local frac_low = (remainder2 * 100000000) // total | ||
|
Comment on lines
+29
to
+31
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. btw, it's not clear to me why we need to do this twice.. I would move the common pattern to a function anyway.. |
||
|
|
||
| return string.format("%d.%08d%08d", int_part, frac_high, frac_low) | ||
| end | ||
|
|
||
| -- Helper function to sum all stats values in a table | ||
| -- Note: guest and guest_nice are already included in user and nice respectively | ||
| -- according to the Linux kernel documentation, so we must exclude them from the total | ||
| local function sum_stats(stats) | ||
| local sum = 0 | ||
| stats["guest"] = 0 | ||
| stats["guest_nice"] = 0 | ||
| for key, value in pairs(stats) do | ||
| sum = sum + value | ||
| end | ||
| return sum | ||
| end | ||
|
|
||
| -- returns per cpu stats, total_stats tables | ||
| local function cpu_stats() | ||
| --TODO: add cpu-total with accumulated values for all cpus | ||
| local stats = {} | ||
| local total_stats = {} | ||
| cpu.foreach_online(function(id) | ||
| stats[id] = {} | ||
| stats[id] = cpu.stats(id) | ||
| total_stats[id] = sum_stats(stats[id]) | ||
| end) | ||
| return stats, total_stats | ||
| end | ||
|
|
||
| -- returns cpu_usage (%) table | ||
| local function cpu_usage() | ||
| local usage = {} | ||
| local current_stats, current_total_stats = cpu_stats() | ||
|
|
||
| for cpu_id, _ in pairs(current_stats) do | ||
| local total_delta = current_total_stats[cpu_id] - (last_total_stats[cpu_id] or 0) | ||
| local guest_delta = current_stats[cpu_id].guest - (last_stats[cpu_id].guest or 0) | ||
| local guest_nice_delta = current_stats[cpu_id].guest_nice - (last_stats[cpu_id].guest_nice or 0) | ||
|
|
||
| usage[cpu_id] = {} | ||
| for metric, value in pairs(current_stats[cpu_id]) do | ||
| local metric_delta = value - (last_stats[cpu_id][metric] or 0) | ||
|
|
||
| if metric == "user" then | ||
| metric_delta = metric_delta - guest_delta | ||
| elseif metric == "nice" then | ||
| metric_delta = metric_delta - guest_nice_delta | ||
| end | ||
| usage[cpu_id][metric] = format_percentage(metric_delta, total_delta) | ||
| end | ||
| end | ||
|
|
||
| last_stats = current_stats | ||
| last_total_stats = current_total_stats | ||
| return usage | ||
| end | ||
|
|
||
| local function cpu_metrics() | ||
| local metrics = "" | ||
| local ts_ms = linux.time() // 1000 -- Convert to milliseconds (FIXME: note sure if this conversion is necessary) | ||
| local usage_data = cpu_usage() -- Call once and store the result | ||
|
|
||
| -- Collect all unique metric names from the first available CPU | ||
| local cpu_metric_names = {} | ||
| for _, cpu_metrics in pairs(usage_data) do | ||
| for key, _ in pairs(cpu_metrics) do | ||
| cpu_metric_names[key] = true | ||
| end | ||
| break -- Only need one CPU to get all metric names | ||
| end | ||
|
|
||
| -- Output grouped by metric name | ||
| for metric, _ in pairs(cpu_metric_names) do | ||
| metrics = metrics .. string.format('# TYPE cpu_usage_%s gauge\n', metric) | ||
| for cpu_id, cpu_metrics in pairs(usage_data) do | ||
| local value = cpu_metrics[metric] or "0" | ||
| metrics = metrics .. string.format('cpu_usage_%s{cpu="cpu%d"} %s %d\n', | ||
| metric, cpu_id, value, ts_ms) | ||
| end | ||
| end | ||
|
|
||
| return metrics | ||
| end | ||
|
|
||
| local function handle_client(session) | ||
| -- Read the request | ||
| local request, err = session:receive(1024) | ||
| if not request then | ||
| error(err) | ||
| end | ||
|
|
||
| -- Check if this is an HTTP request | ||
| local method, path, http_version = string.match(request, "^(%w+)%s+([^%s]+)%s+(HTTP/%d%.%d)") | ||
|
|
||
| if http_version then | ||
| -- This is an HTTP request, validate it | ||
| if method ~= "GET" then | ||
| session:send("HTTP/1.1 405 Method Not Allowed\r\n\r\n") | ||
| error("Method not allowed: " .. tostring(method)) | ||
| end | ||
|
|
||
| if path ~= "/metrics" then | ||
| session:send("HTTP/1.1 404 Not Found\r\n\r\n") | ||
| error("Path not found: " .. tostring(path)) | ||
| end | ||
|
|
||
| -- Send HTTP response headers | ||
| session:send("HTTP/1.1 200 OK\r\n") | ||
| session:send("Content-Type: text/plain; version=0.0.4\r\n") | ||
| session:send("\r\n") | ||
| end | ||
|
|
||
| -- Send metrics (works for both HTTP and plain connections like socat) | ||
| session:send(cpu_metrics()) | ||
| end | ||
|
|
||
| -- Initial sample | ||
| last_stats = cpu_stats() | ||
|
|
||
| local function daemon() | ||
| print("cpud [daemon]: started") | ||
| while (not shouldstop()) do | ||
| local ok, session = pcall(server.accept, server, unix.NONBLOCK) | ||
| if ok then | ||
| local ok, err = pcall(handle_client, session) | ||
| if not ok then | ||
| print("cpud [daemon]: error handling client: " .. tostring(err)) | ||
| end | ||
| session:close() | ||
| elseif session == "EAGAIN" then | ||
| linux.schedule(100) | ||
| end | ||
| end | ||
| print("cpud [daemon]: stopped") | ||
| end | ||
|
|
||
| return daemon | ||
|
|
||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
remainder_{high, low}?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So I don't overflow 64bit integer and get a 16 decimals precision, I split the operation in two parts (high, low).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
wouldn't you have enough precision using difftime?