Initial release

Signed-off-by: Marcus Noble <github@marcusnoble.co.uk>
This commit is contained in:
2023-07-29 21:48:11 +01:00
commit c0163bd9af
10 changed files with 505 additions and 0 deletions

72
pkg/metrics/devices.go Normal file
View File

@@ -0,0 +1,72 @@
package metrics
import (
"fmt"
"tailscale-exporter/pkg/tailscale"
"time"
"github.com/prometheus/client_golang/prometheus"
)
func collectDevices(client *tailscale.Client) []prometheus.Collector {
deviceExpiry := prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "tailscale_devices_expiry_time",
Help: "The expiry time of devices authentication",
ConstLabels: prometheus.Labels{
"tailnet": client.GetTailnet(),
},
},
[]string{"id", "created", "name"},
)
deviceSecondsRemaining := prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "tailscale_devices_expiry_seconds_remaining",
Help: "The number of seconds remaining until a device expires",
ConstLabels: prometheus.Labels{
"tailnet": client.GetTailnet(),
},
},
[]string{"id", "created", "name"},
)
deviceUpdateAvailable := prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "tailscale_devices_update_available",
Help: "If the device can be updated (1) or is running the latest version of Tailscale (0)",
ConstLabels: prometheus.Labels{
"tailnet": client.GetTailnet(),
},
},
[]string{"id", "created", "name", "version"},
)
go func() {
for {
devices, err := client.GetDevices()
if err != nil {
fmt.Println("Failed to get devices: ", err)
} else {
for _, device := range devices {
if !device.KeyExpiryDisabled {
remainingSeconds := time.Until(device.Expires.Time).Seconds()
deviceExpiry.With(prometheus.Labels{"id": device.ID, "created": device.Created.String(), "name": device.Name}).Set(float64(device.Expires.Unix()))
deviceSecondsRemaining.With(prometheus.Labels{"id": device.ID, "created": device.Created.String(), "name": device.Name}).Set(remainingSeconds)
}
updateAvailable := 0.0
if device.UpdateAvailable {
updateAvailable = 1.0
}
deviceUpdateAvailable.With(prometheus.Labels{"id": device.ID, "created": device.Created.String(), "name": device.Name, "version": device.ClientVersion}).Set(updateAvailable)
}
}
time.Sleep(60 * time.Second)
}
}()
return []prometheus.Collector{deviceExpiry, deviceSecondsRemaining, deviceUpdateAvailable}
}

56
pkg/metrics/keys.go Normal file
View File

@@ -0,0 +1,56 @@
package metrics
import (
"fmt"
"tailscale-exporter/pkg/tailscale"
"time"
"github.com/prometheus/client_golang/prometheus"
)
func collectKeys(client *tailscale.Client) []prometheus.Collector {
keyExpiry := prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "tailscale_keys_expiry_time",
Help: "The expiry time of auth keys",
ConstLabels: prometheus.Labels{
"tailnet": client.GetTailnet(),
},
},
[]string{"id", "created", "description", "type"},
)
keySecondsRemaining := prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "tailscale_keys_expiry_seconds_remaining",
Help: "The number of seconds remaining until a key expires",
ConstLabels: prometheus.Labels{
"tailnet": client.GetTailnet(),
},
},
[]string{"id", "created", "description", "type"},
)
go func() {
for {
keys, err := client.GetKeys()
if err != nil {
fmt.Println("Failed to get keys: ", err)
} else {
for _, key := range keys {
remainingSeconds := time.Until(key.Expires).Seconds()
keyType := "auth_key"
if key.Capabilities == nil {
keyType = "api_access_token"
}
keyExpiry.With(prometheus.Labels{"id": key.ID, "created": key.Created.String(), "description": key.Description, "type": keyType}).Set(float64(key.Expires.Unix()))
keySecondsRemaining.With(prometheus.Labels{"id": key.ID, "created": key.Created.String(), "description": key.Description, "type": keyType}).Set(remainingSeconds)
}
}
time.Sleep(60 * time.Second)
}
}()
return []prometheus.Collector{keyExpiry, keySecondsRemaining}
}

19
pkg/metrics/metrics.go Normal file
View File

@@ -0,0 +1,19 @@
package metrics
import (
"tailscale-exporter/pkg/tailscale"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/collectors"
)
var defaultCollectors = []prometheus.Collector{
collectors.NewGoCollector(),
collectors.NewProcessCollector(collectors.ProcessCollectorOpts{}),
}
func Collect(client *tailscale.Client, reg *prometheus.Registry) {
reg.MustRegister(defaultCollectors...)
reg.MustRegister(collectKeys(client)...)
reg.MustRegister(collectDevices(client)...)
}

64
pkg/tailscale/client.go Normal file
View File

@@ -0,0 +1,64 @@
package tailscale
import (
"context"
"fmt"
"os"
ts "github.com/tailscale/tailscale-client-go/tailscale"
)
type Client struct {
tsClient *ts.Client
tailnet string
ctx context.Context
}
func New() (*Client, error) {
apiKey := os.Getenv("TAILSCALE_API_KEY")
tailnet := os.Getenv("TAILSCALE_TAILNET")
if apiKey == "" {
return nil, fmt.Errorf("TAILSCALE_API_KEY must be set")
}
if tailnet == "" {
return nil, fmt.Errorf("TAILSCALE_TAILNET must be set")
}
client, err := ts.NewClient(apiKey, tailnet)
if err != nil {
return nil, err
}
return &Client{
tsClient: client,
tailnet: tailnet,
ctx: context.Background(),
}, nil
}
func (c *Client) GetTailnet() string {
return c.tailnet
}
func (c *Client) GetKeys() ([]ts.Key, error) {
allKeys := []ts.Key{}
keys, err := c.tsClient.Keys(c.ctx)
if err != nil {
return nil, err
} else {
for _, k := range keys {
key, err := c.tsClient.GetKey(c.ctx, k.ID)
if err != nil {
return nil, err
}
allKeys = append(allKeys, key)
}
}
return allKeys, nil
}
func (c *Client) GetDevices() ([]ts.Device, error) {
return c.tsClient.Devices(c.ctx)
}