18 Commits
v0.0.1 ... main

Author SHA1 Message Date
67890d936c docs(readme): update help output example 2023-12-21 00:26:43 +00:00
github-actions[bot]
a1d4f70d98 chore(main): release 0.0.6 (#7)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-12-21 00:14:21 +00:00
7b2e98e150 fix(output): write log output to stderr by default instead of stdout
Also refactor flag parsing and main package in general a bit.
2023-12-21 00:11:27 +00:00
3c272d6d3d ci(goreleaser): update homebrew tap repo name 2023-12-20 23:22:11 +00:00
dc62db2ace docs(readme): update homebrew installation instructions 2023-12-20 23:20:55 +00:00
github-actions[bot]
39e3b69c82 chore(main): release 0.0.5 (#6)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-12-16 04:43:57 +00:00
148994ca50 docs(readme): add link to go docs 2023-12-16 04:42:02 +00:00
436e4a4b01 feat(go): rename prom package to suitable prombat 2023-12-16 04:38:30 +00:00
24bd7d6af3 docs(readme): update example output 2023-12-16 03:23:36 +00:00
github-actions[bot]
3791f9772c chore(main): release 0.0.4 (#5)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-12-16 02:36:42 +00:00
bf0da03a3f ci(lint): suppress warnings from golangci-lint action 2023-12-16 02:35:09 +00:00
4a0aa2daba ci(lint): use correct GOOS 2023-12-16 02:31:01 +00:00
2ac3ecb555 ci(lint): setup golangci-lint
Release-As: 0.0.4
2023-12-16 02:27:49 +00:00
597779d04f docs(readme): add Installation section 2023-12-16 02:27:49 +00:00
github-actions[bot]
329a991bb7 chore(main): release 0.0.3 (#4)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-12-16 01:55:10 +00:00
9a168f9ff9 fix(package): resolve issue with running as a homebrew service 2023-12-16 01:53:29 +00:00
github-actions[bot]
cdac9396fc chore(main): release 0.0.2 (#3)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-12-16 01:39:26 +00:00
993b036d99 fix(battery): find ioreg executable more reliably 2023-12-16 01:36:04 +00:00
11 changed files with 344 additions and 87 deletions

View File

@@ -1,3 +1,3 @@
{
".": "0.0.1"
".": "0.0.6"
}

View File

@@ -16,6 +16,24 @@ jobs:
- name: Print version
run: |
./bin/macos-battery-exporter -v
./bin/macos-battery-exporter
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: "1.21"
cache: false
- name: golangci-lint
uses: golangci/golangci-lint-action@v3
with:
version: v1.55
env:
VERBOSE: "true"
GOOS: "darwin"
release-please:
runs-on: ubuntu-latest
@@ -35,7 +53,7 @@ jobs:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-go@v4
- uses: actions/setup-go@v5
with:
go-version: "1.21"
- name: Run GoReleaser

87
.golangci.yml Normal file
View File

@@ -0,0 +1,87 @@
linters-settings:
funlen:
lines: 300
statements: 450
golint:
min-confidence: 0
govet:
enable-all: true
disable:
- fieldalignment
- shadow
lll:
line-length: 80
tab-width: 4
maligned:
suggest-new: true
misspell:
locale: US
linters:
disable-all: true
enable:
- asciicheck
- bodyclose
- durationcheck
- errcheck
- errorlint
- exhaustive
- exportloopref
- funlen
- gochecknoinits
- goconst
- gocritic
- godot
- goimports
- gomodguard
- goprintffuncname
- gosec
- gosimple
- govet
- importas
- ineffassign
- lll
- misspell
- nakedret
- nilerr
- noctx
- nolintlint
- prealloc
- predeclared
- revive
- staticcheck
- typecheck
- unconvert
- unparam
- unused
- whitespace
issues:
exclude:
- Using the variable on range scope `tt` in function literal
- Using the variable on range scope `tc` in function literal
exclude-rules:
- path: "_test\\.go"
linters:
- funlen
- dupl
- source: "^//go:generate "
linters:
- lll
- source: "`env:"
linters:
- lll
- source: "`json:"
linters:
- lll
- source: "`xml:"
linters:
- lll
- source: "`yaml:"
linters:
- lll
run:
timeout: 2m
allow-parallel-runners: true
modules-download-mode: readonly

View File

@@ -39,11 +39,11 @@ brews:
license: "MIT"
skip_upload: auto
service: |
run [bin/"macos-battery-exporter"]
run [bin/"macos-battery-exporter", "-s"]
test: |
system "#{bin}/macos-battery-exporter -v"
repository:
owner: jimeh
name: homebrew-macos-battery-exporter
name: homebrew-tap
branch: main
token: "{{ .Env.BREW_TAP_TOKEN }}"

View File

@@ -1,5 +1,40 @@
# Changelog
## [0.0.6](https://github.com/jimeh/macos-battery-exporter/compare/v0.0.5...v0.0.6) (2023-12-21)
### Bug Fixes
* **output:** write log output to stderr by default instead of stdout ([7b2e98e](https://github.com/jimeh/macos-battery-exporter/commit/7b2e98e150f9ee6d630023879395df0912047667))
## [0.0.5](https://github.com/jimeh/macos-battery-exporter/compare/v0.0.4...v0.0.5) (2023-12-16)
### Features
* **go:** rename prom package to suitable prombat ([436e4a4](https://github.com/jimeh/macos-battery-exporter/commit/436e4a4b01d96654b7012f795f2d305ca4084681))
## [0.0.4](https://github.com/jimeh/macos-battery-exporter/compare/v0.0.3...v0.0.4) (2023-12-16)
### Continuous Integration
* **lint:** setup golangci-lint ([2ac3ecb](https://github.com/jimeh/macos-battery-exporter/commit/2ac3ecb555e0f6eea369516328f1f03da7d61251))
## [0.0.3](https://github.com/jimeh/macos-battery-exporter/compare/v0.0.2...v0.0.3) (2023-12-16)
### Bug Fixes
* **package:** resolve issue with running as a homebrew service ([9a168f9](https://github.com/jimeh/macos-battery-exporter/commit/9a168f9ff918f6539ca85d43202759197ed952b3))
## [0.0.2](https://github.com/jimeh/macos-battery-exporter/compare/v0.0.1...v0.0.2) (2023-12-16)
### Bug Fixes
* **battery:** find ioreg executable more reliably ([993b036](https://github.com/jimeh/macos-battery-exporter/commit/993b036d99362b6bebd36545fc34d325863421d5))
## 0.0.1 (2023-12-16)

View File

@@ -10,14 +10,48 @@
<p align="center">
<a href="https://github.com/jimeh/macos-battery-exporter/releases"><img src="https://img.shields.io/github/v/tag/jimeh/macos-battery-exporter?label=release" alt="GitHub tag (latest SemVer)"></a>
<a href="https://pkg.go.dev/github.com/jimeh/macos-battery-exporter"><img src="https://img.shields.io/badge/%E2%80%8B-reference-387b97.svg?logo=go&logoColor=white" alt="Go Reference"></a>
<a href="https://github.com/jimeh/macos-battery-exporter/issues"><img src="https://img.shields.io/github/issues-raw/jimeh/macos-battery-exporter.svg?style=flat&logo=github&logoColor=white" alt="GitHub issues"></a>
<a href="https://github.com/jimeh/macos-battery-exporter/pulls"><img src="https://img.shields.io/github/issues-pr-raw/jimeh/macos-battery-exporter.svg?style=flat&logo=github&logoColor=white" alt="GitHub pull requests"></a>
<a href="https://github.com/jimeh/macos-battery-exporter/blob/main/LICENSE"><img src="https://img.shields.io/github/license/jimeh/macos-battery-exporter.svg?style=flat" alt="License Status"></a>
</p>
A Prometheus exporter for macOS which exposes most useful details available from
`ioreg`. Includes a lot more details than what `node_exporter` supports via it's
`node_power_supply_*` metrics.
A Prometheus exporter for macOS battery information, exposing wide range of
details available from `ioreg` about batteries. Includes a lot more details than
what `node_exporter` supports via its `node_power_supply_*` metrics.
## Installation
### Manually
Binary releases are available on the
[Releases](https://github.com/jimeh/macos-battery-exporter/releases) page.
### Homebrew
You can install it from the [`jimeh/tap`](https://github.com/jimeh/homebrew-tap)
Tap:
```bash
brew install jimeh/tap/macos-battery-exporter
```
#### Service
The homebrew formula has a service configuration, which can be started with:
```bash
brew services start macos-battery-exporter
```
After which battery metrics are available on
[`http://localhost:9108/metrics`](http://localhost:9108/metrics).
### Go
```bash
go install github.com/jimeh/macos-battery-exporter@latest
```
## Usage
@@ -28,19 +62,22 @@ macos-battery-exporter -h
```
```
Usage of bin/macos-battery-exporter:
usage: macos-battery-exporter [<options>]
-b string
Bind address to run server on (default "127.0.0.1")
Bind address to run server on (default "127.0.0.1")
-d string
Log output device (stderr or stdout) (default "stderr")
-l string
Log level (default "info")
Log level (default "info")
-n string
Namespace for metrics (default "macos")
Namespace for metrics (default "macos")
-o string
Output file to write to in Prometheus format
Output file to write to in Prometheus format
-p int
Port to run server on (default 9108)
-s Run as a Prometheus metrics server
-v Print version and exit
Port to run server on (default 9108)
-s Run as a Prometheus metrics server
-v Print version and exit
```
### Print to STDOUT
@@ -55,19 +92,22 @@ macos-battery-exporter
macos_battery_cell_disconnect_count{serial="ZTMDHJEZ8JKMYVAJKU"} 0
# HELP macos_battery_charge_rate_amps Current charge rate in Ah.
# TYPE macos_battery_charge_rate_amps gauge
macos_battery_charge_rate_amps{serial="ZTMDHJEZ8JKMYVAJKU"} -0.927
macos_battery_charge_rate_amps{serial="ZTMDHJEZ8JKMYVAJKU"} -1.004
# HELP macos_battery_charge_rate_watts Current charge rate in Wh.
# TYPE macos_battery_charge_rate_watts gauge
macos_battery_charge_rate_watts{serial="ZTMDHJEZ8JKMYVAJKU"} -10.297116
macos_battery_charge_rate_watts{serial="ZTMDHJEZ8JKMYVAJKU"} -12.712648
# HELP macos_battery_count Total number of batteries.
# TYPE macos_battery_count gauge
macos_battery_count 1
# HELP macos_battery_current_capacity_amps Current charge capacity in Ah.
# TYPE macos_battery_current_capacity_amps gauge
macos_battery_current_capacity_amps{serial="ZTMDHJEZ8JKMYVAJKU"} 1.127
macos_battery_current_capacity_amps{serial="ZTMDHJEZ8JKMYVAJKU"} 5.39
# HELP macos_battery_current_capacity_watts Current charge capacity in Wh.
# TYPE macos_battery_current_capacity_watts gauge
macos_battery_current_capacity_watts{serial="ZTMDHJEZ8JKMYVAJKU"} 12.518716
macos_battery_current_capacity_watts{serial="ZTMDHJEZ8JKMYVAJKU"} 68.24818
# HELP macos_battery_current_percentage Current battery charge percentage.
# TYPE macos_battery_current_percentage gauge
macos_battery_current_percentage{serial="ZTMDHJEZ8JKMYVAJKU"} 18
macos_battery_current_percentage{serial="ZTMDHJEZ8JKMYVAJKU"} 91
# HELP macos_battery_cycle_count Current battery cycle count.
# TYPE macos_battery_cycle_count counter
macos_battery_cycle_count{serial="ZTMDHJEZ8JKMYVAJKU"} 15
@@ -76,7 +116,7 @@ macos_battery_cycle_count{serial="ZTMDHJEZ8JKMYVAJKU"} 15
macos_battery_design_capacity_amps{serial="ZTMDHJEZ8JKMYVAJKU"} 6.249
# HELP macos_battery_design_capacity_watts Design capacity in Wh.
# TYPE macos_battery_design_capacity_watts gauge
macos_battery_design_capacity_watts{serial="ZTMDHJEZ8JKMYVAJKU"} 69.413892
macos_battery_design_capacity_watts{serial="ZTMDHJEZ8JKMYVAJKU"} 79.124838
# HELP macos_battery_fully_charged Indicates if the battery is fully charged.
# TYPE macos_battery_fully_charged gauge
macos_battery_fully_charged{serial="ZTMDHJEZ8JKMYVAJKU"} 0
@@ -91,19 +131,19 @@ macos_battery_info{built_in="true",device_name="ayzo3hgs",serial="ZTMDHJEZ8JKMYV
macos_battery_is_charging{serial="ZTMDHJEZ8JKMYVAJKU"} 0
# HELP macos_battery_max_capacity_amps Design capacity in Ah.
# TYPE macos_battery_max_capacity_amps gauge
macos_battery_max_capacity_amps{serial="ZTMDHJEZ8JKMYVAJKU"} 6.262
macos_battery_max_capacity_amps{serial="ZTMDHJEZ8JKMYVAJKU"} 6.271
# HELP macos_battery_max_capacity_watts Design capacity in Wh.
# TYPE macos_battery_max_capacity_watts gauge
macos_battery_max_capacity_watts{serial="ZTMDHJEZ8JKMYVAJKU"} 69.558296
macos_battery_max_capacity_watts{serial="ZTMDHJEZ8JKMYVAJKU"} 79.403402
# HELP macos_battery_temperature_celsius Current battery temperature in °C.
# TYPE macos_battery_temperature_celsius gauge
macos_battery_temperature_celsius{serial="ZTMDHJEZ8JKMYVAJKU"} 30.47
macos_battery_temperature_celsius{serial="ZTMDHJEZ8JKMYVAJKU"} 30.53
# HELP macos_battery_time_remaining_seconds Estimated time remaining until battery is fully charged or discharged.
# TYPE macos_battery_time_remaining_seconds gauge
macos_battery_time_remaining_seconds{serial="ZTMDHJEZ8JKMYVAJKU"} 3540
macos_battery_time_remaining_seconds{serial="ZTMDHJEZ8JKMYVAJKU"} 14040
# HELP macos_battery_voltage_volts Current battery voltage in V.
# TYPE macos_battery_voltage_volts gauge
macos_battery_voltage_volts{serial="ZTMDHJEZ8JKMYVAJKU"} 11.108
macos_battery_voltage_volts{serial="ZTMDHJEZ8JKMYVAJKU"} 12.662
```
### Write to File
@@ -121,3 +161,7 @@ macos-battery-exporter -s
```bash
curl http://localhost:9108/metrics
```
## License
[MIT](https://github.com/jimeh/macos-battery-exporter/blob/main/LICENSE)

View File

@@ -15,12 +15,12 @@ type Battery struct {
// BuiltIn indicates if the battery is built-in or not.
BuiltIn bool
// ChargeRateAmps is the current charge rate in mAh. Negative values indicate
// discharge, positive values indicate charging.
// ChargeRateAmps is the current charge rate in mAh. Negative values
// indicate discharge, positive values indicate charging.
ChargeRateAmps int64
// ChargeRateWatts is the current charge rate in mWh. Negative values indicate
// discharge, positive values indicate charging.
// ChargeRateWatts is the current charge rate in mWh. Negative values
// indicate discharge, positive values indicate charging.
ChargeRateWatts float64
// CurrentCapacityAmps is the current battery capacity in mAh.
@@ -76,6 +76,7 @@ type Battery struct {
func newBattery(b *batteryRaw) *Battery {
volts := float64(b.Voltage) / 1000
//nolint:lll
return &Battery{
BatteryCellDisconnectCount: b.BatteryCellDisconnectCount,
BuiltIn: b.BuiltIn,
@@ -101,29 +102,31 @@ func newBattery(b *batteryRaw) *Battery {
}
func Get() (*Battery, error) {
batteriesRaw, err := getAllRaw()
raw, err := getAllRaw()
if err != nil {
return nil, err
}
return newBattery(batteriesRaw[0]), nil
return newBattery(raw[0]), nil
}
func GetAll() ([]*Battery, error) {
batteriesRaw, err := getAllRaw()
raw, err := getAllRaw()
if err != nil {
return nil, err
}
batteries := []*Battery{}
for _, b := range batteriesRaw {
for _, b := range raw {
batteries = append(batteries, newBattery(b))
}
return batteries, nil
}
// roundTo rounds a float64 to 'places' decimal places
// roundTo rounds a float64 to 'places' decimal places.
//
//nolint:unparam
func roundTo(value float64, places int) float64 {
shift := math.Pow(10, float64(places))
return math.Round(value*shift) / shift

View File

@@ -34,7 +34,15 @@ type batteryRaw struct {
}
func getAllRaw() ([]*batteryRaw, error) {
b, err := exec.Command("ioreg", "-ra", "-c", "AppleSmartBattery").Output()
ioreg, err := exec.LookPath("ioreg")
if err != nil {
ioreg, err = exec.LookPath("/usr/sbin/ioreg")
if err != nil {
return nil, err
}
}
b, err := exec.Command(ioreg, "-ra", "-c", "AppleSmartBattery").Output()
if err != nil {
return nil, err
}

148
main.go
View File

@@ -5,104 +5,170 @@ package main
import (
"flag"
"fmt"
"log"
"io"
"log/slog"
"os"
"strings"
"github.com/jimeh/macos-battery-exporter/prom"
"github.com/jimeh/macos-battery-exporter/prombat"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/common/expfmt"
)
var (
name = "macos-battery-exporter"
version = "0.0.0-dev"
commit = "unknown"
)
outputFlag = flag.String(
type configuration struct {
Output string
Server bool
Bind string
Port int
Namespace string
LogLevel string
LogDevice string
PrintVersion bool
}
func configure() (*configuration, *flag.FlagSet, error) {
fs := flag.NewFlagSet(name, flag.ExitOnError)
fs.Usage = func() {
fmt.Fprintf(fs.Output(),
"usage: %s [<options>]\n\n", fs.Name(),
)
fs.PrintDefaults()
}
config := &configuration{}
fs.StringVar(&config.Output,
"o", "", "Output file to write to in Prometheus format",
)
serverFlag = flag.Bool("s", false, "Run as a Prometheus metrics server")
bindFlag = flag.String(
fs.BoolVar(&config.Server,
"s", false, "Run as a Prometheus metrics server",
)
fs.StringVar(&config.Bind,
"b", "127.0.0.1", "Bind address to run server on",
)
portFlag = flag.Int("p", 9108, "Port to run server on")
namespaceFlag = flag.String(
"n", prom.DefaultNamespace, "Namespace for metrics",
fs.IntVar(&config.Port, "p", 9108, "Port to run server on")
fs.StringVar(&config.Namespace,
"n", prombat.DefaultNamespace, "Namespace for metrics",
)
logLevelFlag = flag.String("l", "info", "Log level")
versionFlag = flag.Bool("v", false, "Print version and exit")
)
fs.StringVar(&config.LogLevel,
"l", "info", "Log level",
)
fs.StringVar(&config.LogDevice,
"d", "stderr", "Log output device (stderr or stdout)",
)
fs.BoolVar(&config.PrintVersion,
"v", false, "Print version and exit",
)
err := fs.Parse(os.Args[1:])
if err != nil {
return nil, nil, err
}
return config, fs, nil
}
func main() {
if err := mainE(); err != nil {
log.Fatal(err)
slog.Error(err.Error())
os.Exit(1)
}
}
func mainE() error {
flag.Parse()
err := setupSLog(*logLevelFlag)
config, _, err := configure()
if err != nil {
return err
}
if *versionFlag {
fmt.Printf("macos-battery-exporter %s (%s)\n", version, commit)
err = setupSLog(config.LogDevice, config.LogLevel)
if err != nil {
return err
}
if config.PrintVersion {
fmt.Printf("%s %s (%s)\n", name, version, commit)
return nil
}
if *serverFlag {
opts := prom.ServerOptions{
Bind: *bindFlag,
Port: *portFlag,
}
return prom.RunServer(
*namespaceFlag,
prometheus.DefaultRegisterer.(*prometheus.Registry),
opts,
)
if config.Server {
return runServer(config)
}
registry := prometheus.NewRegistry()
err = registry.Register(prom.NewCollector(*namespaceFlag))
metrics, err := renderMetrics(config)
if err != nil {
return err
}
if config.Output != "" {
return writeToFile(metrics, config.Output)
}
fmt.Print(metrics)
return nil
}
func runServer(config *configuration) error {
opts := prombat.ServerOptions{
Bind: config.Bind,
Port: config.Port,
}
return prombat.RunServer(
config.Namespace,
prometheus.DefaultRegisterer.(*prometheus.Registry),
opts,
)
}
func renderMetrics(config *configuration) (string, error) {
registry := prometheus.NewRegistry()
err := registry.Register(prombat.NewCollector(config.Namespace))
if err != nil {
return "", err
}
gatherers := prometheus.Gatherers{registry}
metricFamilies, err := gatherers.Gather()
if err != nil {
return err
return "", err
}
var sb strings.Builder
for _, mf := range metricFamilies {
_, err := expfmt.MetricFamilyToText(&sb, mf)
if err != nil {
return err
return "", err
}
}
if *outputFlag != "" {
return writeToFile(sb.String(), *outputFlag)
} else {
fmt.Print(sb.String())
}
return nil
return sb.String(), nil
}
func setupSLog(levelStr string) error {
func setupSLog(device string, levelStr string) error {
var w io.Writer
switch device {
case "stderr":
w = os.Stderr
case "stdout":
w = os.Stdout
default:
return fmt.Errorf("invalid log device: %s", device)
}
var level slog.Level
err := level.UnmarshalText([]byte(levelStr))
if err != nil {
return err
}
handler := slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
handler := slog.NewTextHandler(w, &slog.HandlerOptions{
Level: level,
})
logger := slog.New(handler)

View File

@@ -1,6 +1,6 @@
//go:build darwin
package prom
package prombat
import (
"log/slog"
@@ -367,7 +367,9 @@ func boolToFloat64(b bool) float64 {
return 0
}
// roundTo rounds a float64 to 'places' decimal places
// roundTo rounds a float64 to 'places' decimal places.
//
//nolint:unparam
func roundTo(value float64, places int) float64 {
shift := math.Pow(10, float64(places))
return math.Round(value*shift) / shift

View File

@@ -1,6 +1,6 @@
//go:build darwin
package prom
package prombat
import (
"fmt"
@@ -14,23 +14,17 @@ import (
const DefaultNamespace = "macos"
type Registry interface {
prometheus.Registerer
prometheus.Gatherer
}
type ServerOptions struct {
Bind string
Port int
ReadTimeout time.Duration
WriteTimeout time.Duration
IdleTimeout time.Duration
Logger *slog.Logger
}
type Server struct {
*http.Server
registry Registry
registry *prometheus.Registry
mux *http.ServeMux
}