2017-02-17 14:29:37 +00:00
// Copyright 2017 Kumina, https://kumina.nl/
// 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 main
import (
"bufio"
2017-03-17 15:42:54 +00:00
"bytes"
"errors"
"fmt"
2018-12-17 16:01:01 +00:00
"github.com/alecthomas/kingpin"
2017-02-17 14:29:37 +00:00
"io"
"log"
"net"
"net/http"
"os"
"regexp"
"strconv"
2017-05-02 13:07:19 +00:00
"strings"
2017-02-17 14:29:37 +00:00
"time"
2018-09-30 22:16:10 +00:00
"github.com/hpcloud/tail"
2017-02-17 14:29:37 +00:00
"github.com/prometheus/client_golang/prometheus"
)
var (
postfixUpDesc = prometheus . NewDesc (
2017-02-17 14:50:47 +00:00
prometheus . BuildFQName ( "postfix" , "" , "up" ) ,
2017-02-17 14:29:37 +00:00
"Whether scraping Postfix's metrics was successful." ,
2017-05-02 13:07:19 +00:00
[ ] string { "path" } , nil )
2017-02-17 14:29:37 +00:00
)
2017-05-02 13:07:19 +00:00
// PostfixExporter holds the state that should be preserved by the
// Postfix Prometheus metrics exporter across scrapes.
type PostfixExporter struct {
2018-09-30 22:16:10 +00:00
showqPath string
journal * Journal
tailer * tail . Tail
2017-05-02 13:07:19 +00:00
// Metrics that should persist after refreshes, based on logs.
cleanupProcesses prometheus . Counter
cleanupRejects prometheus . Counter
lmtpDelays * prometheus . HistogramVec
pipeDelays * prometheus . HistogramVec
qmgrInsertsNrcpt prometheus . Histogram
qmgrInsertsSize prometheus . Histogram
qmgrRemoves prometheus . Counter
smtpDelays * prometheus . HistogramVec
smtpTLSConnects * prometheus . CounterVec
smtpdConnects prometheus . Counter
smtpdDisconnects prometheus . Counter
smtpdFCrDNSErrors prometheus . Counter
smtpdLostConnections * prometheus . CounterVec
smtpdProcesses * prometheus . CounterVec
smtpdRejects * prometheus . CounterVec
smtpdSASLAuthenticationFailures prometheus . Counter
smtpdTLSConnects * prometheus . CounterVec
unsupportedLogEntries * prometheus . CounterVec
}
// CollectShowqFromReader parses the output of Postfix's 'showq' command
// and turns it into metrics.
2017-02-17 14:31:04 +00:00
//
2017-03-17 15:42:54 +00:00
// The output format of this command depends on the version of Postfix
// used. Postfix 2.x uses a textual format, identical to the output of
// the 'mailq' command. Postfix 3.x uses a binary format, where entries
// are terminated using null bytes. Auto-detect the format by scanning
// for null bytes in the first 128 bytes of output.
2017-02-17 14:29:37 +00:00
func CollectShowqFromReader ( file io . Reader , ch chan <- prometheus . Metric ) error {
2017-03-17 15:42:54 +00:00
reader := bufio . NewReader ( file )
postfix_exporter.go: Fix some gosec issues.
See,
$ gometalinter --vendor ./...
postfix_exporter.go:249::warning: Potential file inclusion via variable,MEDIUM,HIGH (gosec)
postfix_exporter.go:80::warning: Errors unhandled.,LOW,HIGH (gosec)
postfix_exporter.go:121::warning: Errors unhandled.,LOW,HIGH (gosec)
postfix_exporter.go:296::warning: Errors unhandled.,LOW,HIGH (gosec)
postfix_exporter.go:298::warning: Errors unhandled.,LOW,HIGH (gosec)
postfix_exporter.go:300::warning: Errors unhandled.,LOW,HIGH (gosec)
postfix_exporter.go:302::warning: Errors unhandled.,LOW,HIGH (gosec)
postfix_exporter.go:309::warning: Errors unhandled.,LOW,HIGH (gosec)
postfix_exporter.go:311::warning: Errors unhandled.,LOW,HIGH (gosec)
postfix_exporter.go:313::warning: Errors unhandled.,LOW,HIGH (gosec)
postfix_exporter.go:315::warning: Errors unhandled.,LOW,HIGH (gosec)
postfix_exporter.go:322::warning: Errors unhandled.,LOW,HIGH (gosec)
postfix_exporter.go:324::warning: Errors unhandled.,LOW,HIGH (gosec)
postfix_exporter.go:333::warning: Errors unhandled.,LOW,HIGH (gosec)
postfix_exporter.go:335::warning: Errors unhandled.,LOW,HIGH (gosec)
postfix_exporter.go:337::warning: Errors unhandled.,LOW,HIGH (gosec)
postfix_exporter.go:339::warning: Errors unhandled.,LOW,HIGH (gosec)
postfix_exporter.go:650::warning: Errors unhandled.,LOW,HIGH (gosec)
2018-12-04 15:10:28 +00:00
buf , err := reader . Peek ( 128 )
2018-12-17 16:01:01 +00:00
if err != nil && err != io . EOF {
postfix_exporter.go: Fix some gosec issues.
See,
$ gometalinter --vendor ./...
postfix_exporter.go:249::warning: Potential file inclusion via variable,MEDIUM,HIGH (gosec)
postfix_exporter.go:80::warning: Errors unhandled.,LOW,HIGH (gosec)
postfix_exporter.go:121::warning: Errors unhandled.,LOW,HIGH (gosec)
postfix_exporter.go:296::warning: Errors unhandled.,LOW,HIGH (gosec)
postfix_exporter.go:298::warning: Errors unhandled.,LOW,HIGH (gosec)
postfix_exporter.go:300::warning: Errors unhandled.,LOW,HIGH (gosec)
postfix_exporter.go:302::warning: Errors unhandled.,LOW,HIGH (gosec)
postfix_exporter.go:309::warning: Errors unhandled.,LOW,HIGH (gosec)
postfix_exporter.go:311::warning: Errors unhandled.,LOW,HIGH (gosec)
postfix_exporter.go:313::warning: Errors unhandled.,LOW,HIGH (gosec)
postfix_exporter.go:315::warning: Errors unhandled.,LOW,HIGH (gosec)
postfix_exporter.go:322::warning: Errors unhandled.,LOW,HIGH (gosec)
postfix_exporter.go:324::warning: Errors unhandled.,LOW,HIGH (gosec)
postfix_exporter.go:333::warning: Errors unhandled.,LOW,HIGH (gosec)
postfix_exporter.go:335::warning: Errors unhandled.,LOW,HIGH (gosec)
postfix_exporter.go:337::warning: Errors unhandled.,LOW,HIGH (gosec)
postfix_exporter.go:339::warning: Errors unhandled.,LOW,HIGH (gosec)
postfix_exporter.go:650::warning: Errors unhandled.,LOW,HIGH (gosec)
2018-12-04 15:10:28 +00:00
log . Printf ( "Could not read postfix output, %v" , err )
}
2017-03-17 15:42:54 +00:00
if bytes . IndexByte ( buf , 0 ) >= 0 {
return CollectBinaryShowqFromReader ( reader , ch )
}
2017-05-02 13:07:19 +00:00
return CollectTextualShowqFromReader ( reader , ch )
2017-03-17 15:42:54 +00:00
}
2017-05-02 13:07:19 +00:00
// CollectTextualShowqFromReader parses Postfix's textual showq output.
2017-03-17 15:42:54 +00:00
func CollectTextualShowqFromReader ( file io . Reader , ch chan <- prometheus . Metric ) error {
2017-02-17 14:29:37 +00:00
scanner := bufio . NewScanner ( file )
scanner . Split ( bufio . ScanLines )
// Regular expression for matching postqueue's output. Example:
// "A07A81514 5156 Tue Feb 14 13:13:54 MAILER-DAEMON"
2018-12-16 09:55:34 +00:00
messageLine := regexp . MustCompile ( ` ^[0-9A-F]+([\*!]?) +(\d+) (\w { 3} \w { 3} +\d+ +\d+:\d { 2}:\d { 2}) + ` )
2017-02-17 14:29:37 +00:00
// Histograms tracking the messages by size and age.
2017-04-18 14:03:53 +00:00
sizeHistogram := prometheus . NewHistogramVec (
2017-02-17 14:29:37 +00:00
prometheus . HistogramOpts {
2017-02-17 14:50:47 +00:00
Namespace : "postfix" ,
2017-06-01 12:26:00 +00:00
Name : "showq_message_size_bytes" ,
2017-02-17 14:29:37 +00:00
Help : "Size of messages in Postfix's message queue, in bytes" ,
Buckets : [ ] float64 { 1e3 , 1e4 , 1e5 , 1e6 , 1e7 , 1e8 , 1e9 } ,
2017-04-18 14:03:53 +00:00
} ,
[ ] string { "queue" } )
ageHistogram := prometheus . NewHistogramVec (
2017-02-17 14:29:37 +00:00
prometheus . HistogramOpts {
2017-02-17 14:50:47 +00:00
Namespace : "postfix" ,
2017-06-01 12:26:00 +00:00
Name : "showq_message_age_seconds" ,
2017-02-17 14:29:37 +00:00
Help : "Age of messages in Postfix's message queue, in seconds" ,
Buckets : [ ] float64 { 1e1 , 1e2 , 1e3 , 1e4 , 1e5 , 1e6 , 1e7 , 1e8 } ,
2017-04-18 14:03:53 +00:00
} ,
[ ] string { "queue" } )
2017-02-17 14:29:37 +00:00
2018-02-07 14:12:35 +00:00
// Initialize all queue buckets to zero.
2018-02-07 13:24:08 +00:00
for _ , q := range [ ] string { "active" , "hold" , "other" } {
sizeHistogram . WithLabelValues ( q )
ageHistogram . WithLabelValues ( q )
}
2017-02-17 14:29:37 +00:00
now := time . Now ( )
postfix_exporter.go: Fix some gosec issues.
See,
$ gometalinter --vendor ./...
postfix_exporter.go:249::warning: Potential file inclusion via variable,MEDIUM,HIGH (gosec)
postfix_exporter.go:80::warning: Errors unhandled.,LOW,HIGH (gosec)
postfix_exporter.go:121::warning: Errors unhandled.,LOW,HIGH (gosec)
postfix_exporter.go:296::warning: Errors unhandled.,LOW,HIGH (gosec)
postfix_exporter.go:298::warning: Errors unhandled.,LOW,HIGH (gosec)
postfix_exporter.go:300::warning: Errors unhandled.,LOW,HIGH (gosec)
postfix_exporter.go:302::warning: Errors unhandled.,LOW,HIGH (gosec)
postfix_exporter.go:309::warning: Errors unhandled.,LOW,HIGH (gosec)
postfix_exporter.go:311::warning: Errors unhandled.,LOW,HIGH (gosec)
postfix_exporter.go:313::warning: Errors unhandled.,LOW,HIGH (gosec)
postfix_exporter.go:315::warning: Errors unhandled.,LOW,HIGH (gosec)
postfix_exporter.go:322::warning: Errors unhandled.,LOW,HIGH (gosec)
postfix_exporter.go:324::warning: Errors unhandled.,LOW,HIGH (gosec)
postfix_exporter.go:333::warning: Errors unhandled.,LOW,HIGH (gosec)
postfix_exporter.go:335::warning: Errors unhandled.,LOW,HIGH (gosec)
postfix_exporter.go:337::warning: Errors unhandled.,LOW,HIGH (gosec)
postfix_exporter.go:339::warning: Errors unhandled.,LOW,HIGH (gosec)
postfix_exporter.go:650::warning: Errors unhandled.,LOW,HIGH (gosec)
2018-12-04 15:10:28 +00:00
location , err := time . LoadLocation ( "Local" )
if err != nil {
log . Println ( err )
}
2017-02-17 14:29:37 +00:00
for scanner . Scan ( ) {
matches := messageLine . FindStringSubmatch ( scanner . Text ( ) )
if matches != nil {
2019-01-28 19:40:52 +00:00
continue
}
2017-04-18 14:03:53 +00:00
2019-01-28 19:40:52 +00:00
// Derive the name of the message queue.
queue := "other"
if matches [ 1 ] == "*" {
queue = "active"
} else if matches [ 1 ] == "!" {
queue = "hold"
}
2017-02-17 14:29:37 +00:00
2019-01-28 19:40:52 +00:00
// Parse the message size.
size , err := strconv . ParseFloat ( matches [ 2 ] , 64 )
if err != nil {
return err
}
2017-02-17 14:29:37 +00:00
2019-01-28 19:40:52 +00:00
// Parse the message date. Unfortunately, the
// output contains no year number. Assume it
// applies to the last year for which the
// message date doesn't exceed time.Now().
date , err := time . ParseInLocation ( "Mon Jan 2 15:04:05" ,
matches [ 3 ] , location )
if err != nil {
return err
}
date = date . AddDate ( now . Year ( ) , 0 , 0 )
if date . After ( now ) {
date = date . AddDate ( - 1 , 0 , 0 )
2017-02-17 14:29:37 +00:00
}
2019-01-28 19:40:52 +00:00
sizeHistogram . WithLabelValues ( queue ) . Observe ( size )
ageHistogram . WithLabelValues ( queue ) . Observe ( now . Sub ( date ) . Seconds ( ) )
2017-02-17 14:29:37 +00:00
}
2017-04-18 14:03:53 +00:00
sizeHistogram . Collect ( ch )
ageHistogram . Collect ( ch )
2017-02-17 14:29:37 +00:00
return scanner . Err ( )
}
2017-05-02 13:07:19 +00:00
// ScanNullTerminatedEntries is a splitting function for bufio.Scanner
// to split entries by null bytes.
2017-03-17 15:42:54 +00:00
func ScanNullTerminatedEntries ( data [ ] byte , atEOF bool ) ( advance int , token [ ] byte , err error ) {
if i := bytes . IndexByte ( data , 0 ) ; i >= 0 {
// Valid record found.
return i + 1 , data [ 0 : i ] , nil
} else if atEOF && len ( data ) != 0 {
// Data at the end of the file without a null terminator.
return 0 , nil , errors . New ( "Expected null byte terminator" )
} else {
// Request more data.
return 0 , nil , nil
}
}
2017-05-02 13:07:19 +00:00
// CollectBinaryShowqFromReader parses Postfix's binary showq format.
2017-03-17 15:42:54 +00:00
func CollectBinaryShowqFromReader ( file io . Reader , ch chan <- prometheus . Metric ) error {
scanner := bufio . NewScanner ( file )
scanner . Split ( ScanNullTerminatedEntries )
// Histograms tracking the messages by size and age.
2017-04-18 14:03:53 +00:00
sizeHistogram := prometheus . NewHistogramVec (
2017-03-17 15:42:54 +00:00
prometheus . HistogramOpts {
Namespace : "postfix" ,
2017-06-01 12:26:00 +00:00
Name : "showq_message_size_bytes" ,
2017-03-17 15:42:54 +00:00
Help : "Size of messages in Postfix's message queue, in bytes" ,
Buckets : [ ] float64 { 1e3 , 1e4 , 1e5 , 1e6 , 1e7 , 1e8 , 1e9 } ,
2017-04-18 14:03:53 +00:00
} ,
[ ] string { "queue" } )
ageHistogram := prometheus . NewHistogramVec (
2017-03-17 15:42:54 +00:00
prometheus . HistogramOpts {
Namespace : "postfix" ,
2017-06-01 12:26:00 +00:00
Name : "showq_message_age_seconds" ,
2017-03-17 15:42:54 +00:00
Help : "Age of messages in Postfix's message queue, in seconds" ,
Buckets : [ ] float64 { 1e1 , 1e2 , 1e3 , 1e4 , 1e5 , 1e6 , 1e7 , 1e8 } ,
2017-04-18 14:03:53 +00:00
} ,
[ ] string { "queue" } )
2017-03-17 15:42:54 +00:00
2018-02-07 14:12:35 +00:00
// Initialize all queue buckets to zero.
2018-02-07 13:24:08 +00:00
for _ , q := range [ ] string { "active" , "deferred" , "hold" , "incoming" , "maildrop" } {
sizeHistogram . WithLabelValues ( q )
ageHistogram . WithLabelValues ( q )
}
2017-03-17 15:42:54 +00:00
now := float64 ( time . Now ( ) . UnixNano ( ) ) / 1e9
2017-04-18 14:03:53 +00:00
queue := "unknown"
2017-03-17 15:42:54 +00:00
for scanner . Scan ( ) {
// Parse a key/value entry.
key := scanner . Text ( )
if len ( key ) == 0 {
2017-04-18 14:03:53 +00:00
// Empty key means a record separator.
2017-04-18 14:09:22 +00:00
queue = "unknown"
2017-03-17 15:42:54 +00:00
continue
}
if ! scanner . Scan ( ) {
2017-05-02 13:07:19 +00:00
return fmt . Errorf ( "key %q does not have a value" , key )
2017-03-17 15:42:54 +00:00
}
value := scanner . Text ( )
2017-04-18 14:03:53 +00:00
if key == "queue_name" {
// The name of the message queue.
queue = value
} else if key == "size" {
2017-03-17 15:42:54 +00:00
// Message size in bytes.
size , err := strconv . ParseFloat ( value , 64 )
if err != nil {
return err
}
2017-04-18 14:03:53 +00:00
sizeHistogram . WithLabelValues ( queue ) . Observe ( size )
2017-03-17 15:42:54 +00:00
} else if key == "time" {
// Message time as a UNIX timestamp.
2019-02-15 09:41:53 +00:00
utime , err := strconv . ParseFloat ( value , 64 )
2017-03-17 15:42:54 +00:00
if err != nil {
return err
}
2019-02-15 09:41:53 +00:00
ageHistogram . WithLabelValues ( queue ) . Observe ( now - utime )
2017-03-17 15:42:54 +00:00
}
}
2017-04-18 14:03:53 +00:00
sizeHistogram . Collect ( ch )
ageHistogram . Collect ( ch )
2017-03-17 15:42:54 +00:00
return scanner . Err ( )
}
2017-05-02 13:07:19 +00:00
// CollectShowqFromFile collects Postfix queue statistics from a file.
2018-12-04 15:50:01 +00:00
//func CollectShowqFromFile(path string, ch chan<- prometheus.Metric) error {
// fd, err := os.Open(path)
// if err != nil {
// return err
// }
// defer fd.Close()
// return CollectShowqFromReader(fd, ch)
//}
2017-02-17 14:29:37 +00:00
2017-05-02 13:07:19 +00:00
// CollectShowqFromSocket collects Postfix queue statistics from a socket.
2017-02-17 14:29:37 +00:00
func CollectShowqFromSocket ( path string , ch chan <- prometheus . Metric ) error {
2017-05-02 13:07:19 +00:00
fd , err := net . Dial ( "unix" , path )
2017-02-17 14:29:37 +00:00
if err != nil {
return err
}
2017-05-02 13:07:19 +00:00
defer fd . Close ( )
return CollectShowqFromReader ( fd , ch )
2017-02-17 14:29:37 +00:00
}
2018-01-28 13:13:49 +00:00
// Patterns for parsing log messages.
var (
2018-12-16 09:55:34 +00:00
logLine = regexp . MustCompile ( ` ?postfix/(\w+)\[\d+\]: (.*) ` )
lmtpPipeSMTPLine = regexp . MustCompile ( ` , relay=(\S+), .*, delays=([0-9\.]+)/([0-9\.]+)/([0-9\.]+)/([0-9\.]+), ` )
qmgrInsertLine = regexp . MustCompile ( ` :.*, size=(\d+), nrcpt=(\d+) ` )
smtpTLSLine = regexp . MustCompile ( ` ^(\S+) TLS connection established to \S+: (\S+) with cipher (\S+) \((\d+)/(\d+) bits\)$ ` )
smtpdFCrDNSErrorsLine = regexp . MustCompile ( ` ^warning: hostname \S+ does not resolve to address ` )
smtpdProcessesSASLLine = regexp . MustCompile ( ` : client=.*, sasl_username=(\S+) ` )
smtpdRejectsLine = regexp . MustCompile ( ` ^NOQUEUE: reject: RCPT from \S+: ([0-9]+) ` )
smtpdLostConnectionLine = regexp . MustCompile ( ` ^lost connection after (\w+) from ` )
smtpdSASLAuthenticationFailuresLine = regexp . MustCompile ( ` ^warning: \S+: SASL \S+ authentication failed: ` )
smtpdTLSLine = regexp . MustCompile ( ` ^(\S+) TLS connection established from \S+: (\S+) with cipher (\S+) \((\d+)/(\d+) bits\)$ ` )
2018-01-28 13:13:49 +00:00
)
// CollectFromLogline collects metrict from a Postfix log line.
func ( e * PostfixExporter ) CollectFromLogline ( line string ) {
// Strip off timestamp, hostname, etc.
2019-01-28 19:40:52 +00:00
logMatches := logLine . FindStringSubmatch ( line )
if logMatches == nil {
// Unknown log entry format.
e . unsupportedLogEntries . WithLabelValues ( "" ) . Inc ( )
return
}
// Group patterns to check by Postfix service.
switch logMatches [ 1 ] {
case "cleanup" :
if strings . Contains ( logMatches [ 2 ] , ": message-id=<" ) {
e . cleanupProcesses . Inc ( )
} else if strings . Contains ( logMatches [ 2 ] , ": reject: " ) {
e . cleanupRejects . Inc ( )
} else {
e . unsupportedLogEntries . WithLabelValues ( logMatches [ 1 ] ) . Inc ( )
}
case "lmtp" :
if lmtpMatches := lmtpPipeSMTPLine . FindStringSubmatch ( logMatches [ 2 ] ) ; lmtpMatches != nil {
pdelay , err := strconv . ParseFloat ( lmtpMatches [ 2 ] , 64 )
if err != nil {
log . Printf ( "Couldn't convert LMTP pdelay: %v" , err )
}
e . lmtpDelays . WithLabelValues ( "before_queue_manager" ) . Observe ( pdelay )
adelay , err := strconv . ParseFloat ( lmtpMatches [ 3 ] , 64 )
if err != nil {
log . Printf ( "Couldn't convert LMTP adelay: %v" , err )
}
e . lmtpDelays . WithLabelValues ( "queue_manager" ) . Observe ( adelay )
sdelay , err := strconv . ParseFloat ( lmtpMatches [ 4 ] , 64 )
if err != nil {
log . Printf ( "Couldn't convert LMTP adelay: %v" , err )
}
e . lmtpDelays . WithLabelValues ( "connection_setup" ) . Observe ( sdelay )
xdelay , err := strconv . ParseFloat ( lmtpMatches [ 5 ] , 64 )
if err != nil {
log . Printf ( "Couldn't convert LMTP xdelay: %v" , err )
2018-01-28 13:13:49 +00:00
}
2019-01-28 19:40:52 +00:00
e . lmtpDelays . WithLabelValues ( "transmission" ) . Observe ( xdelay )
} else {
e . unsupportedLogEntries . WithLabelValues ( logMatches [ 1 ] ) . Inc ( )
}
case "pipe" :
if pipeMatches := lmtpPipeSMTPLine . FindStringSubmatch ( logMatches [ 2 ] ) ; pipeMatches != nil {
pdelay , err := strconv . ParseFloat ( pipeMatches [ 2 ] , 64 )
if err != nil {
log . Printf ( "Couldn't convert PIPE pdelay: %v" , err )
2018-01-28 13:13:49 +00:00
}
2019-01-28 19:40:52 +00:00
e . pipeDelays . WithLabelValues ( pipeMatches [ 1 ] , "before_queue_manager" ) . Observe ( pdelay )
adelay , err := strconv . ParseFloat ( pipeMatches [ 3 ] , 64 )
if err != nil {
log . Printf ( "Couldn't convert PIPE adelay: %v" , err )
2018-01-28 13:13:49 +00:00
}
2019-01-28 19:40:52 +00:00
e . pipeDelays . WithLabelValues ( pipeMatches [ 1 ] , "queue_manager" ) . Observe ( adelay )
sdelay , err := strconv . ParseFloat ( pipeMatches [ 4 ] , 64 )
if err != nil {
log . Printf ( "Couldn't convert PIPE sdelay: %v" , err )
2018-01-28 13:13:49 +00:00
}
2019-01-28 19:40:52 +00:00
e . pipeDelays . WithLabelValues ( pipeMatches [ 1 ] , "connection_setup" ) . Observe ( sdelay )
xdelay , err := strconv . ParseFloat ( pipeMatches [ 5 ] , 64 )
if err != nil {
log . Printf ( "Couldn't convert PIPE xdelay: %v" , err )
2018-01-28 13:13:49 +00:00
}
2019-01-28 19:40:52 +00:00
e . pipeDelays . WithLabelValues ( pipeMatches [ 1 ] , "transmission" ) . Observe ( xdelay )
} else {
e . unsupportedLogEntries . WithLabelValues ( logMatches [ 1 ] ) . Inc ( )
}
case "qmgr" :
if qmgrInsertMatches := qmgrInsertLine . FindStringSubmatch ( logMatches [ 2 ] ) ; qmgrInsertMatches != nil {
size , err := strconv . ParseFloat ( qmgrInsertMatches [ 1 ] , 64 )
if err != nil {
log . Printf ( "Couldn't convert QMGR size: %v" , err )
2018-01-28 13:13:49 +00:00
}
2019-01-28 19:40:52 +00:00
e . qmgrInsertsSize . Observe ( size )
nrcpt , err := strconv . ParseFloat ( qmgrInsertMatches [ 2 ] , 64 )
if err != nil {
log . Printf ( "Couldn't convert QMGR nrcpt: %v" , err )
}
e . qmgrInsertsNrcpt . Observe ( nrcpt )
} else if strings . HasSuffix ( logMatches [ 2 ] , ": removed" ) {
e . qmgrRemoves . Inc ( )
2018-01-28 13:13:49 +00:00
} else {
e . unsupportedLogEntries . WithLabelValues ( logMatches [ 1 ] ) . Inc ( )
}
2019-01-28 19:40:52 +00:00
case "smtp" :
if smtpMatches := lmtpPipeSMTPLine . FindStringSubmatch ( logMatches [ 2 ] ) ; smtpMatches != nil {
pdelay , err := strconv . ParseFloat ( smtpMatches [ 2 ] , 64 )
if err != nil {
log . Printf ( "Couldn't convert SMTP pdelay: %v" , err )
}
e . smtpDelays . WithLabelValues ( "before_queue_manager" ) . Observe ( pdelay )
adelay , err := strconv . ParseFloat ( smtpMatches [ 3 ] , 64 )
if err != nil {
log . Printf ( "Couldn't convert SMTP adelay: %v" , err )
}
e . smtpDelays . WithLabelValues ( "queue_manager" ) . Observe ( adelay )
sdelay , err := strconv . ParseFloat ( smtpMatches [ 4 ] , 64 )
if err != nil {
log . Printf ( "Couldn't convert SMTP sdelay: %v" , err )
}
e . smtpDelays . WithLabelValues ( "connection_setup" ) . Observe ( sdelay )
xdelay , err := strconv . ParseFloat ( smtpMatches [ 5 ] , 64 )
if err != nil {
log . Printf ( "Couldn't convert SMTP xdelay: %v" , err )
}
e . smtpDelays . WithLabelValues ( "transmission" ) . Observe ( xdelay )
} else if smtpTLSMatches := smtpTLSLine . FindStringSubmatch ( logMatches [ 2 ] ) ; smtpTLSMatches != nil {
e . smtpTLSConnects . WithLabelValues ( smtpTLSMatches [ 1 : ] ... ) . Inc ( )
} else {
e . unsupportedLogEntries . WithLabelValues ( logMatches [ 1 ] ) . Inc ( )
}
case "smtpd" :
if strings . HasPrefix ( logMatches [ 2 ] , "connect from " ) {
e . smtpdConnects . Inc ( )
} else if strings . HasPrefix ( logMatches [ 2 ] , "disconnect from " ) {
e . smtpdDisconnects . Inc ( )
} else if smtpdFCrDNSErrorsLine . MatchString ( logMatches [ 2 ] ) {
e . smtpdFCrDNSErrors . Inc ( )
} else if smtpdLostConnectionMatches := smtpdLostConnectionLine . FindStringSubmatch ( logMatches [ 2 ] ) ; smtpdLostConnectionMatches != nil {
e . smtpdLostConnections . WithLabelValues ( smtpdLostConnectionMatches [ 1 ] ) . Inc ( )
} else if smtpdProcessesSASLMatches := smtpdProcessesSASLLine . FindStringSubmatch ( logMatches [ 2 ] ) ; smtpdProcessesSASLMatches != nil {
e . smtpdProcesses . WithLabelValues ( smtpdProcessesSASLMatches [ 1 ] ) . Inc ( )
} else if strings . Contains ( logMatches [ 2 ] , ": client=" ) {
e . smtpdProcesses . WithLabelValues ( "" ) . Inc ( )
} else if smtpdRejectsMatches := smtpdRejectsLine . FindStringSubmatch ( logMatches [ 2 ] ) ; smtpdRejectsMatches != nil {
e . smtpdRejects . WithLabelValues ( smtpdRejectsMatches [ 1 ] ) . Inc ( )
} else if smtpdSASLAuthenticationFailuresLine . MatchString ( logMatches [ 2 ] ) {
e . smtpdSASLAuthenticationFailures . Inc ( )
} else if smtpdTLSMatches := smtpdTLSLine . FindStringSubmatch ( logMatches [ 2 ] ) ; smtpdTLSMatches != nil {
e . smtpdTLSConnects . WithLabelValues ( smtpdTLSMatches [ 1 : ] ... ) . Inc ( )
} else {
e . unsupportedLogEntries . WithLabelValues ( logMatches [ 1 ] ) . Inc ( )
}
default :
// Unknown Postfix service.
e . unsupportedLogEntries . WithLabelValues ( logMatches [ 1 ] ) . Inc ( )
2018-01-28 13:13:49 +00:00
}
}
2018-09-30 22:16:10 +00:00
// CollectLogfileFromFile tails a Postfix log file and collects entries from it.
func ( e * PostfixExporter ) CollectLogfileFromFile ( ) error {
for {
select {
case line := <- e . tailer . Lines :
e . CollectFromLogline ( line . Text )
default :
return nil
}
2017-05-02 13:07:19 +00:00
}
2017-02-17 14:29:37 +00:00
}
2017-05-02 13:07:19 +00:00
// NewPostfixExporter creates a new Postfix exporter instance.
2018-01-28 13:13:49 +00:00
func NewPostfixExporter ( showqPath string , logfilePath string , journal * Journal ) ( * PostfixExporter , error ) {
2018-09-30 22:16:10 +00:00
var tailer * tail . Tail
if logfilePath != "" {
var err error
tailer , err = tail . TailFile ( logfilePath , tail . Config {
ReOpen : true , // reopen the file if it's rotated
MustExist : true , // fail immediately if the file is missing or has incorrect permissions
Follow : true , // run in follow mode
} )
if err != nil {
return nil , err
}
}
2017-02-17 14:29:37 +00:00
return & PostfixExporter {
2018-09-30 22:16:10 +00:00
showqPath : showqPath ,
tailer : tailer ,
journal : journal ,
2017-05-02 13:07:19 +00:00
cleanupProcesses : prometheus . NewCounter ( prometheus . CounterOpts {
Namespace : "postfix" ,
Name : "cleanup_messages_processed_total" ,
Help : "Total number of messages processed by cleanup." ,
} ) ,
cleanupRejects : prometheus . NewCounter ( prometheus . CounterOpts {
Namespace : "postfix" ,
Name : "cleanup_messages_rejected_total" ,
Help : "Total number of messages rejected by cleanup." ,
} ) ,
lmtpDelays : prometheus . NewHistogramVec (
prometheus . HistogramOpts {
Namespace : "postfix" ,
Name : "lmtp_delivery_delay_seconds" ,
Help : "LMTP message processing time in seconds." ,
Buckets : [ ] float64 { 1e-3 , 1e-2 , 1e-1 , 1e0 , 1e1 , 1e2 , 1e3 } ,
} ,
[ ] string { "stage" } ) ,
pipeDelays : prometheus . NewHistogramVec (
prometheus . HistogramOpts {
Namespace : "postfix" ,
Name : "pipe_delivery_delay_seconds" ,
Help : "Pipe message processing time in seconds." ,
Buckets : [ ] float64 { 1e-3 , 1e-2 , 1e-1 , 1e0 , 1e1 , 1e2 , 1e3 } ,
} ,
[ ] string { "relay" , "stage" } ) ,
qmgrInsertsNrcpt : prometheus . NewHistogram ( prometheus . HistogramOpts {
Namespace : "postfix" ,
Name : "qmgr_messages_inserted_receipients" ,
Help : "Number of receipients per message inserted into the mail queues." ,
Buckets : [ ] float64 { 1 , 2 , 4 , 8 , 16 , 32 , 64 , 128 } ,
} ) ,
qmgrInsertsSize : prometheus . NewHistogram ( prometheus . HistogramOpts {
Namespace : "postfix" ,
Name : "qmgr_messages_inserted_size_bytes" ,
Help : "Size of messages inserted into the mail queues in bytes." ,
Buckets : [ ] float64 { 1e3 , 1e4 , 1e5 , 1e6 , 1e7 , 1e8 , 1e9 } ,
} ) ,
qmgrRemoves : prometheus . NewCounter ( prometheus . CounterOpts {
Namespace : "postfix" ,
Name : "qmgr_messages_removed_total" ,
Help : "Total number of messages removed from mail queues." ,
} ) ,
smtpDelays : prometheus . NewHistogramVec (
prometheus . HistogramOpts {
Namespace : "postfix" ,
Name : "smtp_delivery_delay_seconds" ,
Help : "SMTP message processing time in seconds." ,
Buckets : [ ] float64 { 1e-3 , 1e-2 , 1e-1 , 1e0 , 1e1 , 1e2 , 1e3 } ,
} ,
[ ] string { "stage" } ) ,
smtpTLSConnects : prometheus . NewCounterVec (
prometheus . CounterOpts {
Namespace : "postfix" ,
Name : "smtp_tls_connections_total" ,
Help : "Total number of outgoing TLS connections." ,
} ,
[ ] string { "trust" , "protocol" , "cipher" , "secret_bits" , "algorithm_bits" } ) ,
smtpdConnects : prometheus . NewCounter ( prometheus . CounterOpts {
Namespace : "postfix" ,
Name : "smtpd_connects_total" ,
Help : "Total number of incoming connections." ,
} ) ,
smtpdDisconnects : prometheus . NewCounter ( prometheus . CounterOpts {
Namespace : "postfix" ,
Name : "smtpd_disconnects_total" ,
Help : "Total number of incoming disconnections." ,
} ) ,
smtpdFCrDNSErrors : prometheus . NewCounter ( prometheus . CounterOpts {
Namespace : "postfix" ,
2017-06-01 12:30:55 +00:00
Name : "smtpd_forward_confirmed_reverse_dns_errors_total" ,
2017-05-02 13:07:19 +00:00
Help : "Total number of connections for which forward-confirmed DNS cannot be resolved." ,
} ) ,
smtpdLostConnections : prometheus . NewCounterVec (
prometheus . CounterOpts {
Namespace : "postfix" ,
Name : "smtpd_connections_lost_total" ,
Help : "Total number of connections lost." ,
} ,
[ ] string { "after_stage" } ) ,
smtpdProcesses : prometheus . NewCounterVec (
prometheus . CounterOpts {
Namespace : "postfix" ,
Name : "smtpd_messages_processed_total" ,
Help : "Total number of messages processed." ,
} ,
[ ] string { "sasl_username" } ) ,
smtpdRejects : prometheus . NewCounterVec (
prometheus . CounterOpts {
Namespace : "postfix" ,
Name : "smtpd_messages_rejected_total" ,
Help : "Total number of NOQUEUE rejects." ,
} ,
[ ] string { "code" } ) ,
smtpdSASLAuthenticationFailures : prometheus . NewCounter ( prometheus . CounterOpts {
Namespace : "postfix" ,
2017-06-01 12:26:00 +00:00
Name : "smtpd_sasl_authentication_failures_total" ,
2017-05-02 13:07:19 +00:00
Help : "Total number of SASL authentication failures." ,
} ) ,
smtpdTLSConnects : prometheus . NewCounterVec (
prometheus . CounterOpts {
Namespace : "postfix" ,
Name : "smtpd_tls_connections_total" ,
Help : "Total number of incoming TLS connections." ,
} ,
[ ] string { "trust" , "protocol" , "cipher" , "secret_bits" , "algorithm_bits" } ) ,
unsupportedLogEntries : prometheus . NewCounterVec (
prometheus . CounterOpts {
Namespace : "postfix" ,
Name : "unsupported_log_entries_total" ,
Help : "Log entries that could not be processed." ,
} ,
[ ] string { "service" } ) ,
2017-02-17 14:29:37 +00:00
} , nil
}
2017-05-02 13:07:19 +00:00
// Describe the Prometheus metrics that are going to be exported.
2017-02-17 14:29:37 +00:00
func ( e * PostfixExporter ) Describe ( ch chan <- * prometheus . Desc ) {
ch <- postfixUpDesc
2017-05-02 13:07:19 +00:00
ch <- e . cleanupProcesses . Desc ( )
ch <- e . cleanupRejects . Desc ( )
e . lmtpDelays . Describe ( ch )
e . pipeDelays . Describe ( ch )
ch <- e . qmgrInsertsNrcpt . Desc ( )
ch <- e . qmgrInsertsSize . Desc ( )
ch <- e . qmgrRemoves . Desc ( )
e . smtpDelays . Describe ( ch )
e . smtpTLSConnects . Describe ( ch )
ch <- e . smtpdConnects . Desc ( )
ch <- e . smtpdDisconnects . Desc ( )
ch <- e . smtpdFCrDNSErrors . Desc ( )
e . smtpdLostConnections . Describe ( ch )
e . smtpdProcesses . Describe ( ch )
e . smtpdRejects . Describe ( ch )
ch <- e . smtpdSASLAuthenticationFailures . Desc ( )
e . smtpdTLSConnects . Describe ( ch )
e . unsupportedLogEntries . Describe ( ch )
2017-02-17 14:29:37 +00:00
}
2017-05-02 13:07:19 +00:00
// Collect metrics from Postfix's showq socket and its log file.
2017-02-17 14:29:37 +00:00
func ( e * PostfixExporter ) Collect ( ch chan <- prometheus . Metric ) {
err := CollectShowqFromSocket ( e . showqPath , ch )
if err == nil {
ch <- prometheus . MustNewConstMetric (
postfixUpDesc ,
prometheus . GaugeValue ,
2017-05-02 13:07:19 +00:00
1.0 ,
e . showqPath )
2017-02-17 14:29:37 +00:00
} else {
log . Printf ( "Failed to scrape showq socket: %s" , err )
ch <- prometheus . MustNewConstMetric (
postfixUpDesc ,
prometheus . GaugeValue ,
2017-05-02 13:07:19 +00:00
0.0 ,
e . showqPath )
2017-02-17 14:29:37 +00:00
}
2017-05-02 13:07:19 +00:00
2018-01-28 13:13:49 +00:00
var src string
if e . journal != nil {
err = e . CollectLogfileFromJournal ( )
src = e . journal . Path
} else {
2018-09-30 22:16:10 +00:00
err = e . CollectLogfileFromFile ( )
src = e . tailer . Filename
2018-01-28 13:13:49 +00:00
}
2017-05-02 13:07:19 +00:00
if err == nil {
ch <- prometheus . MustNewConstMetric (
postfixUpDesc ,
prometheus . GaugeValue ,
1.0 ,
2018-01-28 13:13:49 +00:00
src )
2017-05-02 13:07:19 +00:00
} else {
2018-01-28 13:13:49 +00:00
log . Printf ( "Failed to scrape log: %s" , err )
2017-05-02 13:07:19 +00:00
ch <- prometheus . MustNewConstMetric (
postfixUpDesc ,
prometheus . GaugeValue ,
0.0 ,
2018-01-28 13:13:49 +00:00
src )
2017-05-02 13:07:19 +00:00
}
ch <- e . cleanupProcesses
ch <- e . cleanupRejects
e . lmtpDelays . Collect ( ch )
e . pipeDelays . Collect ( ch )
ch <- e . qmgrInsertsNrcpt
ch <- e . qmgrInsertsSize
ch <- e . qmgrRemoves
e . smtpDelays . Collect ( ch )
e . smtpTLSConnects . Collect ( ch )
ch <- e . smtpdConnects
ch <- e . smtpdDisconnects
ch <- e . smtpdFCrDNSErrors
e . smtpdLostConnections . Collect ( ch )
e . smtpdProcesses . Collect ( ch )
e . smtpdRejects . Collect ( ch )
ch <- e . smtpdSASLAuthenticationFailures
e . smtpdTLSConnects . Collect ( ch )
e . unsupportedLogEntries . Collect ( ch )
2017-02-17 14:29:37 +00:00
}
func main ( ) {
var (
2019-02-15 09:41:53 +00:00
app = kingpin . New ( "postfix_exporter" , "Prometheus metrics exporter for postfix" )
listenAddress = app . Flag ( "web.listen-address" , "Address to listen on for web interface and telemetry." ) . Default ( ":9154" ) . String ( )
metricsPath = app . Flag ( "web.telemetry-path" , "Path under which to expose metrics." ) . Default ( "/metrics" ) . String ( )
postfixShowqPath = app . Flag ( "postfix.showq_path" , "Path at which Postfix places its showq socket." ) . Default ( "/var/spool/postfix/public/showq" ) . String ( )
postfixLogfilePath = app . Flag ( "postfix.logfile_path" , "Path where Postfix writes log entries. This file will be truncated by this exporter." ) . Default ( "/var/log/postfix_exporter_input.log" ) . String ( )
2018-04-15 13:03:36 +00:00
systemdEnable bool
systemdUnit , systemdSlice , systemdJournalPath string
2017-02-17 14:29:37 +00:00
)
2018-12-17 16:01:01 +00:00
systemdFlags ( & systemdEnable , & systemdUnit , & systemdSlice , & systemdJournalPath , app )
kingpin . MustParse ( app . Parse ( os . Args [ 1 : ] ) )
2017-02-17 14:29:37 +00:00
2018-01-28 13:13:49 +00:00
var journal * Journal
2018-04-15 13:03:36 +00:00
if systemdEnable {
2018-01-28 13:13:49 +00:00
var err error
2018-04-15 13:03:36 +00:00
journal , err = NewJournal ( systemdUnit , systemdSlice , systemdJournalPath )
2018-01-28 13:13:49 +00:00
if err != nil {
log . Fatalf ( "Error opening systemd journal: %s" , err )
}
defer journal . Close ( )
}
exporter , err := NewPostfixExporter (
* postfixShowqPath ,
* postfixLogfilePath ,
journal ,
)
2017-02-17 14:29:37 +00:00
if err != nil {
2018-09-30 22:16:10 +00:00
log . Fatalf ( "Failed to create PostfixExporter: %s" , err )
2017-02-17 14:29:37 +00:00
}
prometheus . MustRegister ( exporter )
http . Handle ( * metricsPath , prometheus . Handler ( ) )
http . HandleFunc ( "/" , func ( w http . ResponseWriter , r * http . Request ) {
postfix_exporter.go: Fix some gosec issues.
See,
$ gometalinter --vendor ./...
postfix_exporter.go:249::warning: Potential file inclusion via variable,MEDIUM,HIGH (gosec)
postfix_exporter.go:80::warning: Errors unhandled.,LOW,HIGH (gosec)
postfix_exporter.go:121::warning: Errors unhandled.,LOW,HIGH (gosec)
postfix_exporter.go:296::warning: Errors unhandled.,LOW,HIGH (gosec)
postfix_exporter.go:298::warning: Errors unhandled.,LOW,HIGH (gosec)
postfix_exporter.go:300::warning: Errors unhandled.,LOW,HIGH (gosec)
postfix_exporter.go:302::warning: Errors unhandled.,LOW,HIGH (gosec)
postfix_exporter.go:309::warning: Errors unhandled.,LOW,HIGH (gosec)
postfix_exporter.go:311::warning: Errors unhandled.,LOW,HIGH (gosec)
postfix_exporter.go:313::warning: Errors unhandled.,LOW,HIGH (gosec)
postfix_exporter.go:315::warning: Errors unhandled.,LOW,HIGH (gosec)
postfix_exporter.go:322::warning: Errors unhandled.,LOW,HIGH (gosec)
postfix_exporter.go:324::warning: Errors unhandled.,LOW,HIGH (gosec)
postfix_exporter.go:333::warning: Errors unhandled.,LOW,HIGH (gosec)
postfix_exporter.go:335::warning: Errors unhandled.,LOW,HIGH (gosec)
postfix_exporter.go:337::warning: Errors unhandled.,LOW,HIGH (gosec)
postfix_exporter.go:339::warning: Errors unhandled.,LOW,HIGH (gosec)
postfix_exporter.go:650::warning: Errors unhandled.,LOW,HIGH (gosec)
2018-12-04 15:10:28 +00:00
_ , err = w . Write ( [ ] byte ( `
2017-02-17 14:29:37 +00:00
< html >
< head > < title > Postfix Exporter < / title > < / head >
< body >
< h1 > Postfix Exporter < / h1 >
< p > < a href = ' ` + *metricsPath + ` ' > Metrics < / a > < / p >
< / body >
< / html > ` ) )
postfix_exporter.go: Fix some gosec issues.
See,
$ gometalinter --vendor ./...
postfix_exporter.go:249::warning: Potential file inclusion via variable,MEDIUM,HIGH (gosec)
postfix_exporter.go:80::warning: Errors unhandled.,LOW,HIGH (gosec)
postfix_exporter.go:121::warning: Errors unhandled.,LOW,HIGH (gosec)
postfix_exporter.go:296::warning: Errors unhandled.,LOW,HIGH (gosec)
postfix_exporter.go:298::warning: Errors unhandled.,LOW,HIGH (gosec)
postfix_exporter.go:300::warning: Errors unhandled.,LOW,HIGH (gosec)
postfix_exporter.go:302::warning: Errors unhandled.,LOW,HIGH (gosec)
postfix_exporter.go:309::warning: Errors unhandled.,LOW,HIGH (gosec)
postfix_exporter.go:311::warning: Errors unhandled.,LOW,HIGH (gosec)
postfix_exporter.go:313::warning: Errors unhandled.,LOW,HIGH (gosec)
postfix_exporter.go:315::warning: Errors unhandled.,LOW,HIGH (gosec)
postfix_exporter.go:322::warning: Errors unhandled.,LOW,HIGH (gosec)
postfix_exporter.go:324::warning: Errors unhandled.,LOW,HIGH (gosec)
postfix_exporter.go:333::warning: Errors unhandled.,LOW,HIGH (gosec)
postfix_exporter.go:335::warning: Errors unhandled.,LOW,HIGH (gosec)
postfix_exporter.go:337::warning: Errors unhandled.,LOW,HIGH (gosec)
postfix_exporter.go:339::warning: Errors unhandled.,LOW,HIGH (gosec)
postfix_exporter.go:650::warning: Errors unhandled.,LOW,HIGH (gosec)
2018-12-04 15:10:28 +00:00
if err != nil {
panic ( err )
}
2017-02-17 14:29:37 +00:00
} )
2018-01-28 13:13:49 +00:00
log . Print ( "Listening on " , * listenAddress )
2017-02-17 14:29:37 +00:00
log . Fatal ( http . ListenAndServe ( * listenAddress , nil ) )
}