Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

collect metrics for the agent lockfile (if present) #105

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ puppet_last_run_duration_seconds 28.023470087
# HELP puppet_last_run_success 1 if the last Puppet run was successful.
# TYPE puppet_last_run_success gauge
puppet_last_run_success 1
# HELP puppet_disabled_since_seconds Unix timestamp since when puppet has been disabled.
# TYPE puppet_disabled_since_seconds gauge
puppet_disabled_since_seconds 1.6863428664914448e+09
```

### Example Alert Rules
Expand Down
21 changes: 21 additions & 0 deletions internal/logging/logger.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Copyright 2023 RetailNext, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package logging

// Logger is a vendor-agnostic interface for event tracking
type Logger interface {
Errorw(msg string, keysAndValues ...interface{})
Panicw(msg string, keysAndValues ...interface{})
}
24 changes: 24 additions & 0 deletions internal/logging/noop.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright 2023 RetailNext, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package logging

type noOpLogger struct{}

// NewNoOpLogger returns a Logger implementation with no side effects
func NewNoOpLogger() Logger { return noOpLogger{} }

func (noOpLogger) Errorw(_msg string, _keysAndValues ...interface{}) {}

func (noOpLogger) Panicw(_msg string, _keysAndValues ...interface{}) {}
30 changes: 30 additions & 0 deletions internal/logging/noop_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Copyright 2023 RetailNext, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package logging

import (
"testing"
)

func TestNoOpLogger(t *testing.T) {
got := NewNoOpLogger()

got.Errorw("error", "key", "value")
got.Panicw("panic", "key", "value")
got.Errorw("error", "key")
got.Panicw("panic", "key")
got.Errorw("error")
got.Panicw("panic")
}
61 changes: 61 additions & 0 deletions internal/puppet/agent_disabled_lockfile.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// Copyright 2023 RetailNext, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package puppet

import (
"encoding/json"
"io"
"os"
"time"
)

// AgentDisabledLockfile holds metadata when the
// agent has been administratively disabled.
type AgentDisabledLockfile struct {
DisabledMessage string `json:"disabled_message"`
DisabledSince time.Time `json:-`
}

// ParseAgentDisabledLockfile reads the file and its filesystem metadata
// at the given path. The returned error can be the result of a filesystem
// operation (including `os.ErrNotExist`) or parsing procedure.
func ParseAgentDisabledLockfile(path string) (*AgentDisabledLockfile, error) {
info, err := os.Stat(path)
if err != nil {
return nil, err
}

file, err := os.Open(path)
if err != nil {
return nil, err
}
defer file.Close()

data, err := io.ReadAll(file)
if err != nil {
return nil, err
}

result := &AgentDisabledLockfile{
DisabledSince: info.ModTime(),
}

err = json.Unmarshal(data, result)
if err != nil {
return nil, err
}

return result, nil
}
51 changes: 51 additions & 0 deletions internal/puppet/agent_disabled_lockfile_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// Copyright 2023 RetailNext, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package puppet

import (
"testing"
)

func TestParseAgentDisabledLockfile(t *testing.T) {
testCases := map[string]struct {
wantDisabledMessage string
wantError bool
}{
"404": {wantError: true},
"empty": {wantError: true},
"malformed": {wantError: true},
"empty_hash": {},
"disabled_message": {wantDisabledMessage: "testing unmarshalling"},
}

for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
got, err := ParseAgentDisabledLockfile("testdata/agent_disabled_lockfile/" + name + ".json")

if tc.wantError {
if err == nil {
t.Errorf("expected fixture %q to produce error but got none", name)
}
} else {
if err != nil {
t.Errorf("expected fixture %q to parse properly but got error\n%s", name, err)
}
if got == nil {
t.Errorf("expected fixture %q to parse properly but got nil as result", name)
}
}
})
}
}
21 changes: 21 additions & 0 deletions internal/puppet/defaults.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Copyright 2023 RetailNext, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package puppet

const (
DefaultConfigFile = "/etc/puppetlabs/puppet/puppet.conf"
DefaultAgentDisabledLockfile = "/opt/puppetlabs/puppet/cache/state/agent_disabled.lock"
DefaultLastRunReportFile = "/opt/puppetlabs/puppet/cache/state/last_run_report.yaml"
)
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"disabled_message":"testing unmarshalling"}
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"disabled_message": "missing bracket"
23 changes: 23 additions & 0 deletions internal/utils/time.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Copyright 2023 RetailNext, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package utils

import (
"time"
)

func UnixSeconds(t time.Time) float64 {
return float64(t.Unix()) + (float64(t.Nanosecond()) / 1e+9)
}
9 changes: 7 additions & 2 deletions puppet-agent-exporter.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,12 @@ import (
"github.com/alecthomas/kingpin/v2"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/retailnext/puppet-agent-exporter/puppetconfig"
"github.com/retailnext/puppet-agent-exporter/puppetreport"
"go.uber.org/zap"
"golang.org/x/term"

"github.com/retailnext/puppet-agent-exporter/puppetconfig"
"github.com/retailnext/puppet-agent-exporter/puppetdisabled"
"github.com/retailnext/puppet-agent-exporter/puppetreport"
)

func setupLogger() func() {
Expand Down Expand Up @@ -87,6 +89,9 @@ func run(ctx context.Context, listenAddress, telemetryPath string) (ok bool) {
prometheus.DefaultRegisterer.MustRegister(puppetreport.Collector{
Logger: lgr,
})
prometheus.DefaultRegisterer.MustRegister(puppetdisabled.Collector{
Logger: lgr,
})

mux := http.NewServeMux()
mux.Handle(telemetryPath, promhttp.Handler())
Expand Down
12 changes: 5 additions & 7 deletions puppetconfig/collector.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ package puppetconfig
import (
"github.com/prometheus/client_golang/prometheus"
"gopkg.in/ini.v1"

"github.com/retailnext/puppet-agent-exporter/internal/logging"
"github.com/retailnext/puppet-agent-exporter/internal/puppet"
)

var configDesc = prometheus.NewDesc(
Expand All @@ -27,7 +30,7 @@ var configDesc = prometheus.NewDesc(
)

type Collector struct {
Logger Logger
Logger logging.Logger
ConfigPath string
}

Expand All @@ -50,10 +53,5 @@ func (c Collector) configPath() string {
if c.ConfigPath != "" {
return c.ConfigPath
}
return "/etc/puppetlabs/puppet/puppet.conf"
}

type Logger interface {
Errorw(msg string, keysAndValues ...interface{})
Panicw(msg string, keysAndValues ...interface{})
return puppet.DefaultConfigFile
}
62 changes: 62 additions & 0 deletions puppetdisabled/collector.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// Copyright 2021 RetailNext, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package puppetdisabled

import (
"errors"
"os"

"github.com/prometheus/client_golang/prometheus"

"github.com/retailnext/puppet-agent-exporter/internal/logging"
"github.com/retailnext/puppet-agent-exporter/internal/puppet"
"github.com/retailnext/puppet-agent-exporter/internal/utils"
)

var disabledSinceDesc = prometheus.NewDesc(
"puppet_disabled_since_seconds",
"Time since when puppet has been disabled.",
nil,
nil,
)

type Collector struct {
Logger logging.Logger
LockfilePath string
}

func (c Collector) Describe(ch chan<- *prometheus.Desc) {
ch <- disabledSinceDesc
}

func (c Collector) Collect(ch chan<- prometheus.Metric) {
dto, err := puppet.ParseAgentDisabledLockfile(c.lockfilePath())
if errors.Is(err, os.ErrNotExist) {
return // nothing to report
} else if err != nil {
c.Logger.Errorw("puppet_open_lockfile_failed", "err", err)
return
}

disabledSince := utils.UnixSeconds(dto.DisabledSince)
ch <- prometheus.MustNewConstMetric(disabledSinceDesc, prometheus.GaugeValue, disabledSince)
}

func (c Collector) lockfilePath() string {
if c.LockfilePath != "" {
return c.LockfilePath
}
return puppet.DefaultAgentDisabledLockfile
}
Loading