Skip to content

Commit

Permalink
Merge pull request #83 from grafana/dannykopping/last-used
Browse files Browse the repository at this point in the history
Add metric for disk info & last update
  • Loading branch information
inkel authored Jan 22, 2024
2 parents 19f45d2 + 582d6e3 commit 54bcedb
Show file tree
Hide file tree
Showing 6 changed files with 219 additions and 11 deletions.
11 changes: 10 additions & 1 deletion aws/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"github.com/aws/aws-sdk-go-v2/service/ec2"
"github.com/aws/aws-sdk-go-v2/service/ec2/types"
"github.com/grafana/unused"
"github.com/inkel/logfmt"
)

var _ unused.Provider = &Provider{}
Expand All @@ -16,6 +17,7 @@ var _ unused.Provider = &Provider{}
type Provider struct {
client *ec2.Client
meta unused.Meta
logger *logfmt.Logger
}

// Name returns AWS.
Expand All @@ -29,14 +31,15 @@ func (p *Provider) Meta() unused.Meta { return p.meta }
// A valid EC2 client must be supplied in order to list the unused
// resources. The metadata passed will be used to identify the
// provider.
func NewProvider(client *ec2.Client, meta unused.Meta) (*Provider, error) {
func NewProvider(logger *logfmt.Logger, client *ec2.Client, meta unused.Meta) (*Provider, error) {
if meta == nil {
meta = make(unused.Meta)
}

return &Provider{
client: client,
meta: meta,
logger: logger,
}, nil
}

Expand All @@ -45,10 +48,16 @@ func NewProvider(client *ec2.Client, meta unused.Meta) (*Provider, error) {
func (p *Provider) ListUnusedDisks(ctx context.Context) (unused.Disks, error) {
params := &ec2.DescribeVolumesInput{
Filters: []types.Filter{
// only show available (i.e. not "in-use") volumes
{
Name: aws.String("status"),
Values: []string{string(types.VolumeStateAvailable)},
},
// exclude snapshots
{
Name: aws.String("snapshot-id"),
Values: []string{""},
},
},
}

Expand Down
6 changes: 3 additions & 3 deletions aws/provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ func TestNewProvider(t *testing.T) {
t.Fatalf("cannot load AWS config: %v", err)
}

p, err := aws.NewProvider(ec2.NewFromConfig(cfg), nil)
p, err := aws.NewProvider(nil, ec2.NewFromConfig(cfg), nil)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
Expand All @@ -40,7 +40,7 @@ func TestProviderMeta(t *testing.T) {
t.Fatalf("cannot load AWS config: %v", err)
}

return aws.NewProvider(ec2.NewFromConfig(cfg), meta)
return aws.NewProvider(nil, ec2.NewFromConfig(cfg), meta)
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
Expand Down Expand Up @@ -121,7 +121,7 @@ func TestListUnusedDisks(t *testing.T) {
t.Fatalf("cannot load AWS config: %v", err)
}

p, err := aws.NewProvider(ec2.NewFromConfig(cfg), nil)
p, err := aws.NewProvider(nil, ec2.NewFromConfig(cfg), nil)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/internal/providers.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ func CreateProviders(ctx context.Context, logger *logfmt.Logger, gcpProjects, aw
return nil, fmt.Errorf("cannot load AWS config for profile %s: %w", profile, err)
}

p, err := aws.NewProvider(ec2.NewFromConfig(cfg), map[string]string{"profile": profile})
p, err := aws.NewProvider(logger, ec2.NewFromConfig(cfg), map[string]string{"profile": profile})
if err != nil {
return nil, fmt.Errorf("creating AWS provider for profile %s: %w", profile, err)
}
Expand Down
31 changes: 30 additions & 1 deletion cmd/unused-exporter/exporter.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ type exporter struct {
count *prometheus.Desc
dur *prometheus.Desc
suc *prometheus.Desc
dlu *prometheus.Desc
}

func registerExporter(ctx context.Context, providers []unused.Provider, cfg config) error {
Expand Down Expand Up @@ -59,6 +60,12 @@ func registerExporter(ctx context.Context, providers []unused.Provider, cfg conf
"Static metric indicating if collecting the metrics succeeded or not",
labels,
nil),

dlu: prometheus.NewDesc(
prometheus.BuildFQName(namespace, "disks", "last_used_at"),
"Kubernetes metadata associated with each unused disk, with the value as the last time the disk was used (if available)",
append(labels, []string{"disk", "created_for_pv", "created_for_pvc", "zone"}...),
nil),
}

return prometheus.Register(e)
Expand All @@ -68,6 +75,7 @@ func (e *exporter) Describe(ch chan<- *prometheus.Desc) {
ch <- e.info
ch <- e.count
ch <- e.dur
ch <- e.dlu
}

func (e *exporter) Collect(ch chan<- prometheus.Metric) {
Expand Down Expand Up @@ -133,11 +141,32 @@ func (e *exporter) Collect(ch chan<- prometheus.Metric) {
labels[k] = meta[k]
}
e.logger.Log("unused disk found", labels)
countByNamespace[meta["kubernetes.io/created-for/pvc/namespace"]] += 1
countByNamespace[meta.CreatedForNamespace()] += 1
}
for ns, c := range countByNamespace {
ch <- prometheus.MustNewConstMetric(e.count, prometheus.GaugeValue, float64(c), name, pid, ns)
}

for _, d := range disks {
m := d.Meta()

var ts float64
lastUsed := d.LastUsedAt()
if !lastUsed.IsZero() {
ts = float64(lastUsed.UnixMilli())
}

if m.CreatedForPV() == "" {
continue
}

ch <- prometheus.MustNewConstMetric(e.dlu, prometheus.GaugeValue, ts, name, pid,
d.ID(),
m.CreatedForPV(),
m.CreatedForPVC(),
m.Zone(),
)
}
}(p)
}

Expand Down
48 changes: 48 additions & 0 deletions meta.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,56 @@ func (m Meta) String() string {
return s.String()
}

func (m Meta) Equals(b Meta) bool {
if len(m) != len(b) {
return false
}

for ak, av := range m {
bv, ok := b[ak]
if !ok || av != bv {
return false
}
}

return true
}

// Matches returns true when the given key exists in the map with the
// given value.
func (m Meta) Matches(key, val string) bool {
return m[key] == val
}

func (m Meta) CreatedForPV() string {
return m.coalesce("kubernetes.io/created-for/pv/name", "kubernetes.io-created-for-pv-name")
}

func (m Meta) CreatedForPVC() string {
return m.coalesce("kubernetes.io/created-for/pvc/name", "kubernetes.io-created-for-pvc-name")
}

func (m Meta) CreatedForNamespace() string {
return m.coalesce("kubernetes.io/created-for/pvc/namespace", "kubernetes.io-created-for-pvc-namespace")
}

func (m Meta) CreatedBy() string {
return m.coalesce("storage.gke.io/created-by", "created-by")
}

func (m Meta) Zone() string {
return m.coalesce("zone", "location")
}

func (m Meta) coalesce(keys ...string) string {
for _, k := range keys {
v, ok := m[k]
if !ok {
continue
}

return v
}

return ""
}
132 changes: 127 additions & 5 deletions meta_test.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
package unused_test
package unused

import (
"sort"
"testing"

"github.com/grafana/unused"
)

func TestMeta(t *testing.T) {
m := &unused.Meta{
m := &Meta{
"def": "123",
"ghi": "456",
"abc": "789",
Expand All @@ -35,7 +33,7 @@ func TestMeta(t *testing.T) {
}

func TestMetaMatches(t *testing.T) {
m := &unused.Meta{
m := &Meta{
"def": "123",
"ghi": "456",
"abc": "789",
Expand All @@ -51,3 +49,127 @@ func TestMetaMatches(t *testing.T) {
t.Error("expecting no match for different value")
}
}

func TestCoalesce(t *testing.T) {
tests := []struct {
name string
m Meta
input []string
expected string
}{
{
name: "single key returns self",
m: Meta{
"foo": "bar",
},
input: []string{"foo"},
expected: "bar",
},
{
name: "multiple keys returns first non-nil, single match",
m: Meta{
"foo": "bar",
},
input: []string{"buz", "foo"},
expected: "bar",
},
{
name: "multiple keys returns first non-nil, many possible matches",
m: Meta{
"foo": "bar",
"buz": "qux",
},
input: []string{"buz", "foo"},
expected: "qux",
},
{
name: "any value is returned if key is present",
m: Meta{
"foo": "",
"buz": "qux",
},
input: []string{"foo", "buz"},
expected: "",
},
{
name: "no given keys returns zero value",
m: Meta{
"foo": "bar",
"buz": "qux",
},
input: []string{},
expected: "",
},
{
name: "no matching keys returns zero value",
m: Meta{
"foo": "bar",
"buz": "qux",
},
input: []string{"nope"},
expected: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
actual := tt.m.coalesce(tt.input...)
if tt.expected != actual {
t.Fatalf("expected %v but got %v", tt.expected, actual)
}
})
}
}

func TestEquals(t *testing.T) {
tests := []struct {
name string
m Meta
input Meta
expected bool
}{
{
name: "nil values are equal",
m: Meta{},
input: Meta{},
expected: true,
},
{
name: "nil & non-nil values are not equal",
m: Meta{"not": "nil"},
input: Meta{},
expected: false,
},
{
name: "same keys but different values are not equal",
m: Meta{"a": "b"},
input: Meta{"a": "c"},
expected: false,
},
{
name: "same values but different keys are not equal",
m: Meta{"a": "b"},
input: Meta{"c": "b"},
expected: false,
},
{
name: "same keys & values are equal",
m: Meta{"a": "b", "c": "d"},
input: Meta{"a": "b", "c": "d"},
expected: true,
},
{
name: "order is irrelevant",
m: Meta{"a": "b", "c": "d"},
input: Meta{"c": "d", "a": "b"},
expected: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
actual := tt.m.Equals(tt.input)
if tt.expected != actual {
t.Fatalf("expected %v but got %v", tt.expected, actual)
}
})
}
}

0 comments on commit 54bcedb

Please sign in to comment.