Initial release
Signed-off-by: Marcus Noble <github@marcusnoble.co.uk>
This commit is contained in:
commit
c0163bd9af
71
.github/workflows/docker.yaml
vendored
Normal file
71
.github/workflows/docker.yaml
vendored
Normal file
@ -0,0 +1,71 @@
|
||||
name: Docker Image CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
tags:
|
||||
- v*
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Prepare
|
||||
id: prepare
|
||||
run: |
|
||||
DOCKER_IMAGE=averagemarcus/tailscale-exporter
|
||||
DOCKER_PLATFORMS=linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64,linux/386,linux/ppc64le,linux/s390x
|
||||
|
||||
VERSION=latest
|
||||
if [[ $GITHUB_REF == refs/tags/* ]]; then
|
||||
VERSION=${GITHUB_REF#refs/tags/}
|
||||
fi
|
||||
|
||||
TAGS="--tag ${DOCKER_IMAGE}:${VERSION}"
|
||||
if [[ $VERSION =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then
|
||||
TAGS="$TAGS --tag ${DOCKER_IMAGE}:latest"
|
||||
fi
|
||||
|
||||
echo ::set-output name=tags::${TAGS}
|
||||
echo ::set-output name=platforms::${DOCKER_PLATFORMS}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: crazy-max/ghaction-docker-buildx@v3
|
||||
|
||||
- name: Cache Docker layers
|
||||
uses: actions/cache@v2
|
||||
id: cache
|
||||
with:
|
||||
path: /tmp/.buildx-cache
|
||||
key: ${{ runner.os }}-buildx-${{ github.sha }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-buildx-
|
||||
|
||||
- name: Docker Buildx (build)
|
||||
run: |
|
||||
docker buildx build \
|
||||
--cache-from "type=local,src=/tmp/.buildx-cache" \
|
||||
--cache-to "type=local,dest=/tmp/.buildx-cache" \
|
||||
--platform ${{ steps.prepare.outputs.platforms }} \
|
||||
--output "type=image,push=false" \
|
||||
${{ steps.prepare.outputs.tags }} \
|
||||
.
|
||||
|
||||
- name: Docker Login
|
||||
env:
|
||||
DOCKER_USERNAME: averagemarcus
|
||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_TOKEN }}
|
||||
run: |
|
||||
echo "${DOCKER_PASSWORD}" | docker login --username "${DOCKER_USERNAME}" --password-stdin
|
||||
|
||||
- name: Docker Buildx (push)
|
||||
run: |
|
||||
docker buildx build \
|
||||
--cache-from "type=local,src=/tmp/.buildx-cache" \
|
||||
--cache-to "type=local,dest=/tmp/.buildx-cache" \
|
||||
--platform ${{ steps.prepare.outputs.platforms }} \
|
||||
--output "type=image,push=true" \
|
||||
${{ steps.prepare.outputs.tags }} \
|
||||
.
|
21
Dockerfile
Normal file
21
Dockerfile
Normal file
@ -0,0 +1,21 @@
|
||||
FROM --platform=${BUILDPLATFORM:-linux/amd64} golang:1.20-alpine as builder
|
||||
|
||||
ARG TARGETPLATFORM
|
||||
ARG BUILDPLATFORM
|
||||
ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
|
||||
RUN apk update && apk add -U --no-cache ca-certificates
|
||||
|
||||
WORKDIR /app/
|
||||
ADD go.mod go.sum ./
|
||||
RUN go mod download
|
||||
ADD main.go .
|
||||
ADD pkg/ ./pkg
|
||||
RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -ldflags="-w -s" -o tailscale-exporter main.go
|
||||
|
||||
FROM --platform=${TARGETPLATFORM:-linux/amd64} scratch
|
||||
WORKDIR /app/
|
||||
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
||||
COPY --from=builder /app/tailscale-exporter /app/tailscale-exporter
|
||||
ENTRYPOINT ["/app/tailscale-exporter"]
|
88
README.md
Normal file
88
README.md
Normal file
@ -0,0 +1,88 @@
|
||||
# tailscale-exporter
|
||||
|
||||
Exports Prometheus metrics for Tailscale devices and keys.
|
||||
|
||||
## Metrics
|
||||
|
||||
* `tailscale_devices_expiry_seconds_remaining` - The number of seconds remaining until the device authentication expires
|
||||
* `tailscale_devices_expiry_time` - The timestamp (as Unix timestamp) that the device expires
|
||||
* `tailscale_devices_update_available` - Whether the device can be updated to a newer version of Tailscale or not
|
||||
* `tailscale_keys_expiry_seconds_remaining` - The number of seconds remaining until the key expires
|
||||
* `tailscale_keys_expiry_time` - The timestamp (as Unix timestamp) that the key expires
|
||||
|
||||
## Configuration
|
||||
|
||||
The following environment variable can be used to configure the exporter:
|
||||
|
||||
* `TAILSCALE_API_KEY` - A valid Tailscale API key [Required]
|
||||
* `TAILSCALE_TAILNET` - The Tailnet to export metrics for [Required]
|
||||
* `PORT` - The port to run the exporter on (Defaults to `8080`)
|
||||
|
||||
## Running with Docker
|
||||
|
||||
```shell
|
||||
export TAILSCALE_API_KEY="my-tailscale-api-key"
|
||||
export TAILSCALE_TAILNET="my-tailnet.github"
|
||||
docker run --rm -it -p 8080:8080 -e TAILSCALE_API_KEY -e TAILSCALE_TAILNET averagemarcus/tailscale-exporter:latest
|
||||
```
|
||||
|
||||
Then visit: [http://localhost:8080/metrics](http://localhost:8080/metrics)
|
||||
|
||||
## Deploying to Kubernetes
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: tailscale-exporter
|
||||
labels:
|
||||
app.kubernetes.io/name: tailscale-exporter
|
||||
stringData:
|
||||
TAILSCALE_API_KEY: xxxx
|
||||
TAILSCALE_TAILNET: xxxx
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: tailscale-exporter
|
||||
labels:
|
||||
app.kubernetes.io/name: tailscale-exporter
|
||||
annotations:
|
||||
prometheus.io/scrape: "true"
|
||||
prometheus.io/port: "8080"
|
||||
spec:
|
||||
type: ClusterIP
|
||||
ports:
|
||||
- port: 8080
|
||||
targetPort: 8080
|
||||
selector:
|
||||
app: tailscale-exporter
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: tailscale-exporter
|
||||
labels:
|
||||
app.kubernetes.io/name: tailscale-exporter
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: tailscale-exporter
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: tailscale-exporter
|
||||
spec:
|
||||
containers:
|
||||
- name: tailscale-exporter
|
||||
image: averagemarcus/tailscale-exporter:latest
|
||||
imagePullPolicy: Always
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
name: metrics
|
||||
envFrom:
|
||||
- secretRef:
|
||||
name: tailscale-exporter
|
||||
|
||||
```
|
26
go.mod
Normal file
26
go.mod
Normal file
@ -0,0 +1,26 @@
|
||||
module tailscale-exporter
|
||||
|
||||
go 1.20
|
||||
|
||||
replace github.com/tailscale/tailscale-client-go => github.com/AverageMarcus/tailscale-client-go v0.0.0-20230729201523-e4a8f131596d
|
||||
|
||||
require (
|
||||
github.com/prometheus/client_golang v1.16.0
|
||||
github.com/tailscale/tailscale-client-go v1.9.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
|
||||
github.com/prometheus/client_model v0.3.0 // indirect
|
||||
github.com/prometheus/common v0.42.0 // indirect
|
||||
github.com/prometheus/procfs v0.10.1 // indirect
|
||||
github.com/tailscale/hujson v0.0.0-20220506213045-af5ed07155e5 // indirect
|
||||
golang.org/x/net v0.10.0 // indirect
|
||||
golang.org/x/oauth2 v0.8.0 // indirect
|
||||
golang.org/x/sys v0.8.0 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/protobuf v1.30.0 // indirect
|
||||
)
|
50
go.sum
Normal file
50
go.sum
Normal file
@ -0,0 +1,50 @@
|
||||
github.com/AverageMarcus/tailscale-client-go v0.0.0-20230729201523-e4a8f131596d h1:D+4y8aN+WvqPW+5hbAB0rioM6rL3+A19aoXwKP3/6Wk=
|
||||
github.com/AverageMarcus/tailscale-client-go v0.0.0-20230729201523-e4a8f131596d/go.mod h1:Sipqv1kTdwxy38walczihMQQm5uWIHeizasQ3Z3tHNU=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8=
|
||||
github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc=
|
||||
github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4=
|
||||
github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w=
|
||||
github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM=
|
||||
github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc=
|
||||
github.com/prometheus/procfs v0.10.1 h1:kYK1Va/YMlutzCGazswoHKo//tZVlFpKYh+PymziUAg=
|
||||
github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM=
|
||||
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
|
||||
github.com/tailscale/hujson v0.0.0-20220506213045-af5ed07155e5 h1:erxeiTyq+nw4Cz5+hLDkOwNF5/9IQWCQPv0gpb3+QHU=
|
||||
github.com/tailscale/hujson v0.0.0-20220506213045-af5ed07155e5/go.mod h1:DFSS3NAGHthKo1gTlmEcSBiZrRJXi28rLNd/1udP1c8=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/oauth2 v0.8.0 h1:6dkIjl3j3LtZ/O3sTgZTMsLKSftL/B8Zgq4huOIIUu8=
|
||||
golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
|
||||
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
|
||||
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
38
main.go
Normal file
38
main.go
Normal file
@ -0,0 +1,38 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"tailscale-exporter/pkg/metrics"
|
||||
"tailscale-exporter/pkg/tailscale"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
)
|
||||
|
||||
var addr string
|
||||
|
||||
func init() {
|
||||
port := os.Getenv("PORT")
|
||||
if port != "" {
|
||||
addr = ":" + port
|
||||
} else {
|
||||
addr = ":8080"
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
client, err := tailscale.New()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
reg := prometheus.NewRegistry()
|
||||
metrics.Collect(client, reg)
|
||||
|
||||
http.Handle("/metrics", promhttp.HandlerFor(reg, promhttp.HandlerOpts{Registry: reg}))
|
||||
log.Printf("tailscale-exporter")
|
||||
log.Printf("Listening on %s", addr)
|
||||
log.Fatal(http.ListenAndServe(addr, nil))
|
||||
}
|
72
pkg/metrics/devices.go
Normal file
72
pkg/metrics/devices.go
Normal 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
56
pkg/metrics/keys.go
Normal 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
19
pkg/metrics/metrics.go
Normal 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
64
pkg/tailscale/client.go
Normal 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)
|
||||
}
|
Loading…
Reference in New Issue
Block a user