This document provides comprehensive guidance for AI agents working on the Termux API Exporter project.
Purpose: A Prometheus exporter that collects metrics from Termux API commands on Android devices and exposes them in Prometheus format.
Language: Go 1.21+
Architecture: Clean Architecture with interface-based design
Primary Dependencies: github.com/prometheus/client_golang
The codebase follows clean architecture with three primary layers:
-
Domain Layer (
model/)- Pure domain models with no external dependencies
- Structs map to JSON output from Termux commands
- Tags: Use
json:"field_name"for JSON unmarshaling
-
Business Logic Layer (
collector/,executor/)- Interfaces define contracts
- Implementations provide concrete behavior
- No direct dependencies between implementations
-
Application Layer (
main.go)- Wires up dependencies
- Handles HTTP server lifecycle
- Registers collectors with Prometheus registry
- Dependency Injection: All components receive dependencies via constructors
- Interface Segregation: Small, focused interfaces (Executor, Collector)
- Single Responsibility: Each package has one clear purpose
- Error Handling: Always log errors but don't crash; return early on failures
.
├── model/ # Domain models (pure Go structs)
├── executor/ # Command execution abstraction
├── collector/ # Prometheus metric collectors
└── main.go # Application entry point
- Packages: Lowercase, singular nouns (
model,collector,executor) - Files: Lowercase with underscores if needed (
battery.go,wifi.go) - Interfaces: Describe capability (
Executor,Collector) - Implementations: Describe what/how (
TermuxExecutor,BatteryCollector) - Constructors: Always use
Newprefix (NewBatteryCollector)
Follow this exact pattern for consistency:
Location: model/<feature>.go
package model
// <Feature>Info represents the output from termux-<feature> command
type <Feature>Info struct {
// Add fields that match JSON output from the command
// Use appropriate Go types (int, float64, string, bool)
FieldName Type `json:"json_field_name"`
}Example: For termux-battery-status:
type BatteryStatus struct {
Present bool `json:"present"`
Technology string `json:"technology"`
Health string `json:"health"`
Plugged string `json:"plugged"`
Status string `json:"status"`
Temperature float64 `json:"temperature"`
Voltage int `json:"voltage"`
Current int `json:"current"`
Percentage int `json:"percentage"`
Level int `json:"level"`
Scale int `json:"scale"`
ChargeCounter int `json:"charge_counter"`
Cycle int `json:"cycle"`
}Location: collector/<feature>.go
package collector
import (
"context"
"encoding/json"
"log"
"github.com/anshulpatel25/termux-api-exporter/executor"
"github.com/anshulpatel25/termux-api-exporter/model"
"github.com/prometheus/client_golang/prometheus"
)
// <Feature>Collector collects metrics from termux-<feature> command
type <Feature>Collector struct {
executor executor.Executor
metric1 *prometheus.Desc
metric2 *prometheus.Desc
}
// New<Feature>Collector creates a new <Feature>Collector
func New<Feature>Collector(exec executor.Executor) *<Feature>Collector {
return &<Feature>Collector{
executor: exec,
metric1: prometheus.NewDesc(
prometheus.BuildFQName(namespace, "<subsystem>", "<metric_name>"),
"Metric description",
nil,
nil,
),
}
}
// Describe implements the prometheus.Collector interface
func (c *<Feature>Collector) Describe(ch chan<- *prometheus.Desc) {
ch <- c.metric1
ch <- c.metric2
}
// Collect implements the prometheus.Collector interface
func (c *<Feature>Collector) Collect(ch chan<- prometheus.Metric) {
ctx := context.Background()
// Execute the termux command
output, err := c.executor.Execute(ctx, "termux-<feature>")
if err != nil {
log.Printf("Error executing termux-<feature>: %v", err)
return
}
// Parse JSON output
var info model.<Feature>Info
if err := json.Unmarshal(output, &info); err != nil {
log.Printf("Error parsing <feature> JSON: %v", err)
return
}
// Send metrics to Prometheus
ch <- prometheus.MustNewConstMetric(
c.metric1,
prometheus.GaugeValue,
float64(info.FieldName),
)
}Add these lines in main.go:
// Create the collector (after other collectors)
<feature>Collector := collector.New<Feature>Collector(exec)
// Register the collector (after other registrations)
if err := registry.Register(<feature>Collector); err != nil {
log.Fatalf("Failed to register <feature> collector: %v", err)
}Follow Prometheus naming best practices:
- Namespace: Always
termux - Subsystem: Feature name (e.g.,
battery,wifi,location) - Name: Descriptive metric name with unit suffix
- Units: Include in metric name (
_celsius,_mbps,_dbm,_bytes)
Examples:
termux_battery_temperature_celsiustermux_wifi_rssi_dbmtermux_memory_available_bytes
Always follow this pattern:
- Log errors, don't panic: Use
log.Printf()for errors - Return early: Exit the function on error, don't continue
- Add context: Include command name or operation in error message
- Preserve errors: Wrap errors with
fmt.Errorf("...: %w", err)
// Good ✅
output, err := c.executor.Execute(ctx, "termux-battery-status")
if err != nil {
log.Printf("Error executing termux-battery-status: %v", err)
return
}
// Bad ❌ - Don't panic
if err != nil {
panic(err)
}- Use pointer receivers for structs with methods
- Return errors, don't panic (except in
main.gofor fatal setup errors) - Use
context.Background()for top-level operations - Short variable names in limited scope (
ch,err,ctx) - Descriptive names for package-level variables
- Add doc comments for all exported types, functions, and methods
- Format:
// TypeName description(no blank line before declaration) - Explain "why" not "what" for complex logic
- Use complete sentences with proper punctuation
Group imports in this order:
- Standard library
- External packages
- Internal packages (this project)
import (
"context"
"encoding/json"
"log"
"github.com/prometheus/client_golang/prometheus"
"github.com/anshulpatel25/termux-api-exporter/executor"
"github.com/anshulpatel25/termux-api-exporter/model"
)Create mock implementations for testing:
// executor/mock.go
package executor
import "context"
type MockExecutor struct {
MockOutput []byte
MockError error
}
func (m *MockExecutor) Execute(ctx context.Context, command string, args ...string) ([]byte, error) {
return m.MockOutput, m.MockError
}- Test files:
<filename>_test.go - Place in same package as code under test
- Use table-driven tests for multiple scenarios
❌ Bad: Putting HTTP handling in collector
func (c *BatteryCollector) Collect(w http.ResponseWriter, r *http.Request) {
// NO!
}✅ Good: Keep collectors focused on metrics
func (c *BatteryCollector) Collect(ch chan<- prometheus.Metric) {
// YES!
}❌ Bad: Silent failures
output, _ := c.executor.Execute(ctx, "termux-battery-status")✅ Good: Log and return
output, err := c.executor.Execute(ctx, "termux-battery-status")
if err != nil {
log.Printf("Error: %v", err)
return
}❌ Bad: Magic numbers and strings
time.Sleep(5 * time.Second)✅ Good: Use constants or configuration
const defaultTimeout = 5 * time.SecondAll collectors must implement both methods:
Describe(chan<- *prometheus.Desc)Collect(chan<- prometheus.Metric)
❌ Bad: Panicking on errors
if err != nil {
panic(err)
}✅ Good: Log and continue or return
if err != nil {
log.Printf("Error: %v", err)
return
}Current collectors use:
termux-battery-status- Battery metricstermux-wifi-connectioninfo- WiFi metrics
termux-location- GPS location (lat/long/altitude)termux-sensor- Device sensors (accelerometer, gyroscope, etc.)termux-telephony-deviceinfo- Phone/SIM infotermux-clipboard-get- Clipboard contentstermux-torch- Flashlight controltermux-brightness- Screen brightness
Use the mock scripts in the repository root:
termux-battery-status- Returns mock battery JSONtermux-wifi-connectioninfo- Returns mock WiFi JSON
# Standard build
go build -o termux-api-exporter
# With optimizations
go build -ldflags="-s -w" -o termux-api-exporter
# Cross-compile for Android ARM64
GOOS=linux GOARCH=arm64 go build -o termux-api-exporter- Make changes to code
- Run
go mod tidyif adding/removing dependencies - Build:
go build -o termux-api-exporter - Test:
./termux-api-exporter - Verify metrics:
curl http://localhost:9797/metrics
Use appropriate Prometheus metric types:
- Gauge: Values that go up and down (temperature, percentage, RSSI)
- Counter: Monotonically increasing values (total requests, errors)
- Histogram: Observations (request durations, response sizes)
- Summary: Similar to histogram with quantiles
For Termux metrics, Gauge is most common since we're exposing current state.
Always check registration errors:
if err := registry.Register(collector); err != nil {
log.Fatalf("Failed to register collector: %v", err)
}When adding new features, update:
-
README.md:
- Features list
- Architecture diagram
- Metrics table
- Example output
-
This file (AGENTS.md):
- Add to "Available Commands" if new Termux command
- Update examples if patterns change
-
Comments in code:
- Doc comments for new types
- Inline comments for complex logic
See "Adding a New Termux Command Collector" section above for complete templates.
const (
namespace = "termux" // Prometheus namespace
defaultTimeout = 5 * time.Second
defaultPort = ":9797"
metricsPath = "/metrics"
)// For models
import "encoding/json"
// For collectors
import (
"github.com/prometheus/client_golang/prometheus"
"github.com/anshulpatel25/termux-api-exporter/executor"
"github.com/anshulpatel25/termux-api-exporter/model"
)
// For main
import (
"github.com/prometheus/client_golang/prometheus/promhttp"
)- Check existing collectors (battery, wifi) as reference implementations
- Follow the exact patterns shown in this document
- Prometheus docs: https://prometheus.io/docs/
- Go best practices: https://go.dev/doc/effective_go
Core Principle: Follow the existing patterns exactly. Every new collector should look structurally identical to existing ones, just with different:
- Model fields
- Termux command name
- Metric names and descriptions
This consistency makes the codebase easy to understand and maintain.