Skip to content

Commit

Permalink
Merge pull request #9 from sergeymakinen/v2
Browse files Browse the repository at this point in the history
Prepare v2
  • Loading branch information
sergeymakinen committed Dec 3, 2023
2 parents 1ad4689 + 8a639f8 commit 91d1f51
Show file tree
Hide file tree
Showing 13 changed files with 443 additions and 116 deletions.
74 changes: 74 additions & 0 deletions CONFIGURATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Postfix exporter configuration

The file is written in [YAML format](http://en.wikipedia.org/wiki/YAML), defined by the scheme described below.
Brackets indicate that a parameter is optional.
For non-list parameters the value is set to the specified default.

Generic placeholders are defined as follows:

* `<string>`: a regular string
* `<regex>`: a regular expression (see https://golang.org/s/re2syntax)

The other placeholders are specified separately.

See [postfix.yml](exporter/testdata/postfix.yml) for configuration examples.

```yml
host_replies:
[ - <host_reply>, ... ]
noqueue_reject_replies:
[ - <noqueue_reject_reply>, ... ]
```

### `<host_reply>`

Example log entry for `queue_status`:

```
Jan 1 00:00:00 hostname postfix/smtp[12345]: 123456789AB: to=<user@example.com>, relay=example.com[123.45.67.89]:25, delay=1.23, delays=1.23/1.23/1.23/1.23, dsn=1.2.3, status=bounced (host example.com[123.45.67.89] said: 123 #1.2.3 Reasons (in reply to end of DATA command))
```

Example log entry for `other`:

```
Jan 1 00:00:00 hostname postfix/smtp[12345]: 123456789AB: host example.com[123.45.67.89] said: 123 1.2.3 Reasons (in reply to RCPT TO command)
```

In both cases:

* `123` is a status code
* `1.2.3` is an enhanced status code
* `Reasons` is the text of the reply

```yml
# The type of the reply. Accepted values: any, queue_status, other.
[ type: <string> | default = "any" ]

# The regular expression matching the reply text.
regexp: <regex>

# The replacement text (may include placeholders supported by Go, see https://pkg.go.dev/regexp#Regexp.Expand).
text: <string>
```

### `<noqueue_reject_replies>`

Example log entry:

```
Jan 1 00:00:00 hostname postfix/smtpd[12345]: NOQUEUE: reject: RCPT from example.com[123.45.67.89]: 123 1.2.3 <user@example.com>: Reasons; from=<user@example.com> to=<user@example.com> proto=ESMTP helo=<example.com>
```

In this case:

* `123` is a status code
* `1.2.3` is an enhanced status code
* `Reasons` is the text of the reply

```yml
# The regular expression matching the reply text.
regexp: <regex>

# The replacement text (may include placeholders supported by Go, see https://pkg.go.dev/regexp#Regexp.Expand).
text: <string>
```
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Postfix Exporter

[![tests](https://github.com/sergeymakinen/postfix_exporter/workflows/tests/badge.svg)](https://github.com/sergeymakinen/postfix_exporter/actions?query=workflow%3Atests)
[![Go Reference](https://pkg.go.dev/badge/github.com/sergeymakinen/postfix_exporter.svg)](https://pkg.go.dev/github.com/sergeymakinen/postfix_exporter)
[![Go Reference](https://pkg.go.dev/badge/github.com/sergeymakinen/postfix_exporter.svg)](https://pkg.go.dev/github.com/sergeymakinen/postfix_exporter/v2)
[![Go Report Card](https://goreportcard.com/badge/github.com/sergeymakinen/postfix_exporter)](https://goreportcard.com/report/github.com/sergeymakinen/postfix_exporter)
[![codecov](https://codecov.io/gh/sergeymakinen/postfix_exporter/branch/main/graph/badge.svg)](https://codecov.io/gh/sergeymakinen/postfix_exporter)

Expand Down Expand Up @@ -29,19 +29,23 @@ make
| postfix_lmtp_statuses_total | Total number of times LMTP server message status change events were collected. | status
| postfix_lmtp_delay_seconds | Delay in seconds for a LMTP server to process a message. | status
| postfix_smtp_statuses_total | Total number of times SMTP server message status change events were collected. | status
| postfix_smtp_status_replies_total | Total number of times SMTP server message status change event replies were collected. Requires [configuration](CONFIGURATION.md) to be present. | status, code, enhanced_code, text
| postfix_smtp_replies_total | Total number of times SMTP server replies were collected. Requires [configuration](CONFIGURATION.md) to be present. | code, enhanced_code, text
| postfix_smtp_delay_seconds | Delay in seconds for a SMTP server to process a message. | status
| postfix_milter_actions_total | Total number of times milter events were collected. | subprogram, action
| postfix_login_failures_total | Total number of times login failure events were collected. | subprogram, method
| postfix_qmgr_statuses_total | Total number of times Postfix queue manager message status change events were collected. | status
| postfix_logs_total | Total number of log records processed. | subprogram, severity
| postfix_noqueue_rejects_total | Total number of times NOQUEUE: reject events were collected. | subprogram, command, message
| postfix_noqueue_reject_replies_total | Total number of times NOQUEUE: reject event replies were collected. Requires [configuration](CONFIGURATION.md) to be present. | subprogram, command, code, enhanced_code, text

## Flags

```bash
./postfix_exporter --help
```

* __`config.file`:__ Postfix exporter [configuration file](CONFIGURATION.md).
* __`config.check`:__ If true validate the config file and then exit.
* __`collector`:__ Collector type to scrape metrics with. `file` or `journald`.
* __`postfix.instance`:__ Postfix instance name. `postfix` by default.
* __`file.log`:__ Path to a file containing Postfix logs. Example: `/var/log/mail.log`.
Expand Down
26 changes: 23 additions & 3 deletions cmd/postfix_exporter/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,22 @@ import (
"os"

"github.com/alecthomas/kingpin/v2"
"github.com/go-kit/kit/log/level"
"github.com/go-kit/log/level"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/prometheus/common/promlog"
"github.com/prometheus/common/promlog/flag"
"github.com/prometheus/common/version"
"github.com/prometheus/exporter-toolkit/web"
webflag "github.com/prometheus/exporter-toolkit/web/kingpinflag"
"github.com/sergeymakinen/postfix_exporter/exporter"
"github.com/sergeymakinen/postfix_exporter/v2/config"
"github.com/sergeymakinen/postfix_exporter/v2/exporter"
)

func main() {
var (
configFile = kingpin.Flag("config.file", "Postfix exporter configuration file.").String()
configCheck = kingpin.Flag("config.check", "If true validate the config file and then exit.").Default().Bool()
collector = kingpin.Flag("collector", "Collector type to scrape metrics with. One of: [file, journald]").Default("file").Enum("file", "journald")
instance = kingpin.Flag("postfix.instance", "Postfix instance name.").Default("postfix").String()
logPath = kingpin.Flag("file.log", "Path to a file containing Postfix logs.").Default("/var/log/mail.log").String()
Expand All @@ -36,12 +39,29 @@ func main() {
level.Info(logger).Log("msg", "Starting postfix_exporter", "version", version.Info())
level.Info(logger).Log("msg", "Build context", "context", version.BuildContext())

var (
cfg *config.Config
err error
)
if *configFile != "" {
cfg, err = config.Load((*configFile))
if err != nil {
level.Error(logger).Log("msg", "Error loading config", "err", err)
os.Exit(1)
}
if *configCheck {
level.Info(logger).Log("msg", "Config file is ok, exiting...")
return
}
level.Info(logger).Log("msg", "Loaded config file")
}

prometheus.MustRegister(version.NewCollector("postfix_exporter"))
collectorType := exporter.CollectorFile
if *collector == "journald" {
collectorType = exporter.CollectorJournald
}
exporter, err := exporter.New(collectorType, *instance, *logPath, *journaldPath, *journaldUnit, logger)
exporter, err := exporter.New(collectorType, *instance, *logPath, *journaldPath, *journaldUnit, cfg, logger)
if err != nil {
level.Error(logger).Log("msg", "Error creating the exporter", "err", err)
os.Exit(1)
Expand Down
107 changes: 107 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package config

import (
"errors"
"fmt"
"os"
"regexp"

"gopkg.in/yaml.v3"
)

type Config struct {
HostReplies []HostReplyConfig `yaml:"host_replies,omitempty"`
NoqueueRejectReplies []NoqueueRejectReplyConfig `yaml:"noqueue_reject_replies,omitempty"`
}

func Load(name string) (*Config, error) {
f, err := os.Open(name)
if err != nil {
return nil, fmt.Errorf("error reading config file: %v", err)
}
defer f.Close()
d := yaml.NewDecoder(f)
d.KnownFields(true)
var cfg Config
if err = d.Decode(&cfg); err != nil {
return nil, fmt.Errorf("error parsing config file: %v", err)
}
return &cfg, nil
}

type HostReplyConfig struct {
Type HostReplyType `yaml:"type,omitempty"`
Regexp *Regexp `yaml:"regexp"`
Text string `yaml:"text"`
}

func (cfg *HostReplyConfig) UnmarshalYAML(value *yaml.Node) error {
type plain HostReplyConfig
if err := value.Decode((*plain)(cfg)); err != nil {
return err
}
if cfg.Text == "" {
return errors.New("empty text replacement")
}
return nil
}

type HostReplyType int

func (t *HostReplyType) UnmarshalYAML(value *yaml.Node) error {
var s string
if err := value.Decode(&s); err != nil {
return err
}
switch s {
case "", "any":
*t = HostReplyAny
case "queue_status":
*t = HostReplyQueueStatus
case "other":
*t = HostReplyOther
default:
return fmt.Errorf("unsupported host reply type %q", s)
}
return nil
}

// HostReplyType types.
const (
HostReplyAny HostReplyType = iota
HostReplyQueueStatus
HostReplyOther
)

type NoqueueRejectReplyConfig struct {
Regexp *Regexp `yaml:"regexp"`
Text string `yaml:"text"`
}

func (cfg *NoqueueRejectReplyConfig) UnmarshalYAML(value *yaml.Node) error {
type plain NoqueueRejectReplyConfig
if err := value.Decode((*plain)(cfg)); err != nil {
return err
}
if cfg.Text == "" {
return errors.New("empty text replacement")
}
return nil
}

type Regexp struct {
*regexp.Regexp
}

func (r *Regexp) UnmarshalYAML(value *yaml.Node) error {
var s string
if err := value.Decode(&s); err != nil {
return err
}
re, err := regexp.Compile(s)
if err != nil {
return err
}
*r = Regexp{re}
return nil
}
24 changes: 19 additions & 5 deletions exporter/collector.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const (
severityPanic severity = "panic"
)

const timeFormat = "Jan 2 15:04:05"
const bsdFormat = "Jan 2 15:04:05"

type record struct {
Time time.Time
Expand Down Expand Up @@ -51,7 +51,7 @@ func parseRecord(line string) (record, error) {
}
return ss[:i-len(substr)], nil
}
ss, err := readUntil(" ", 3)
ss, err := readUntil(" ", 1)
if err != nil {
return record{}, err
}
Expand All @@ -60,9 +60,23 @@ func parseRecord(line string) (record, error) {

Severity: severityInfo,
}
r.Time, err = time.Parse(timeFormat, ss)
if err != nil {
return record{}, err
if strings.Contains(ss, ":") {
// RFC3339 timestamp.
r.Time, err = time.Parse(time.RFC3339Nano, ss)
if err != nil {
return record{}, err
}
} else {
// Classic BSD timestamp.
ss2, err := readUntil(" ", 2)
if err != nil {
return record{}, err
}
ss += " " + ss2
r.Time, err = time.Parse(bsdFormat, ss)
if err != nil {
return record{}, err
}
}
r.Hostname, err = readUntil(" ", 1)
if err != nil {
Expand Down
Loading

0 comments on commit 91d1f51

Please sign in to comment.