diff --git a/README.md b/README.md index 69827d6..5c49573 100644 --- a/README.md +++ b/README.md @@ -3,8 +3,7 @@ Export [Mirth Connect](https://en.wikipedia.org/wiki/Mirth_Connect) channel statistics to [Prometheus](https://prometheus.io). -Metrics are retrieved using the Mirth Connect REST API. This has only been tested -with Mirth Connect 3.7.1, and it should work with version after 3.7.1. +Metrics are retrieved using the Mirth Connect REST API. This was tested in versions 3.7.1 and newer. It is generally expected to work in any MC release 3.4.0 and greater but has not been explicitly tested in all releases. Test cases are welcome. To run it: @@ -15,12 +14,59 @@ To run it: | Metric | Description | Labels | | ------ | ------- | ------ | | mirth_up | Was the last Mirth CLI query successful | | +| mirth_request_duration | Histogram for the runtime of the metric pull from Mirth | | +| mirth_channel_status | Status of all deployed channels | channel, status | | mirth_messages_received_total | How many messages have been received | channel | | mirth_messages_filtered_total | How many messages have been filtered | channel | | mirth_messages_queued | How many messages are currently queued | channel | | mirth_messages_sent_total | How many messages have been sent | channel | | mirth_messages_errored_total | How many messages have errored | channel | +``` +# HELP mirth_channel_status +# TYPE mirth_channel_status gauge +mirth_channel_status{channel="foo", status="STARTED"} 1 +mirth_channel_status{channel="bar", status="PAUSED"} 1 + +# HELP mirth_request_duration Histogram for the runtime of the metric pull from Mirth. +# TYPE mirth_request_duration histogram +mirth_request_duration_bucket{le="0.1"} 0 +mirth_request_duration_bucket{le="0.2"} 0 +mirth_request_duration_bucket{le="0.30000000000000004"} 1 +... +mirth_request_duration_bucket{le="2.0000000000000004"} 5 +mirth_request_duration_bucket{le="+Inf"} 5 + +# HELP mirth_messages_errored_total How many messages have errored (per channel). +# TYPE mirth_messages_errored_total gauge +mirth_messages_errored_total{channel="foo"} 0 +mirth_messages_errored_total{channel="bar"} 2 + +# HELP mirth_messages_filtered_total How many messages have been filtered (per channel). +# TYPE mirth_messages_filtered_total gauge +mirth_messages_filtered_total{channel="foo"} 0 +mirth_messages_filtered_total{channel="bar"} 193 + +# HELP mirth_messages_queued How many messages are currently queued (per channel). +# TYPE mirth_messages_queued gauge +mirth_messages_queued{channel="foo"} 0 +mirth_messages_queued{channel="bar"} 0 + +# HELP mirth_messages_received_total How many messages have been received (per channel). +# TYPE mirth_messages_received_total gauge +mirth_messages_received_total{channel="foo"} 6.3965406e+07 +mirth_messages_received_total{channel="bar"} 387 + +# HELP mirth_messages_sent_total How many messages have been sent (per channel). +# TYPE mirth_messages_sent_total gauge +mirth_messages_sent_total{channel="foo"} 1.21855264e+08 +mirth_messages_sent_total{channel="bar"} 964 + +# HELP mirth_up Was the last Mirth query successful. +# TYPE mirth_up gauge +mirth_up 1 +``` + ## Flags ./mirth_channel_exporter --help diff --git a/main.go b/main.go index 5cdd644..7fabb39 100644 --- a/main.go +++ b/main.go @@ -1,254 +1,22 @@ -// A minimal example of how to include Prometheus instrumentation. package main import ( - "crypto/tls" - "encoding/xml" "flag" - "io/ioutil" - "log" - "net/http" - "os" - "strconv" - "github.com/joho/godotenv" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" + "log" + "net/http" + "os" ) -/* - - - 101af57f-f26c-40d3-86a3-309e74b93512 - Send-Email-Notification - - -*/ -type ChannelIdNameMap struct { - XMLName xml.Name `xml:"map"` - Entries []ChannelEntry `xml:"entry"` -} -type ChannelEntry struct { - XMLName xml.Name `xml:"entry"` - Values []string `xml:"string"` -} - -/* - - - c5e6a736-0e88-46a7-bf32-5b4908c4d859 - 101af57f-f26c-40d3-86a3-309e74b93512 - 0 - 0 - 0 - 0 - 0 - - -*/ -type ChannelStatsList struct { - XMLName xml.Name `xml:"list"` - Channels []ChannelStats `xml:"channelStatistics"` -} -type ChannelStats struct { - XMLName xml.Name `xml:"channelStatistics"` - ServerId string `xml:"serverId"` - ChannelId string `xml:"channelId"` - Received string `xml:"received"` - Sent string `xml:"sent"` - Error string `xml:"error"` - Filtered string `xml:"filtered"` - Queued string `xml:"queued"` -} - -const namespace = "mirth" -const channelIdNameApi = "/api/channels/idsAndNames" -const channelStatsApi = "/api/channels/statistics" - var ( - tr = &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, - } - client = &http.Client{Transport: tr} - listenAddress = flag.String("web.listen-address", ":9141", "Address to listen on for telemetry") metricsPath = flag.String("web.telemetry-path", "/metrics", "Path under which to expose metrics") - - // Metrics - up = prometheus.NewDesc( - prometheus.BuildFQName(namespace, "", "up"), - "Was the last Mirth query successful.", - nil, nil, - ) - messagesReceived = prometheus.NewDesc( - prometheus.BuildFQName(namespace, "", "messages_received_total"), - "How many messages have been received (per channel).", - []string{"channel"}, nil, - ) - messagesFiltered = prometheus.NewDesc( - prometheus.BuildFQName(namespace, "", "messages_filtered_total"), - "How many messages have been filtered (per channel).", - []string{"channel"}, nil, - ) - messagesQueued = prometheus.NewDesc( - prometheus.BuildFQName(namespace, "", "messages_queued"), - "How many messages are currently queued (per channel).", - []string{"channel"}, nil, - ) - messagesSent = prometheus.NewDesc( - prometheus.BuildFQName(namespace, "", "messages_sent_total"), - "How many messages have been sent (per channel).", - []string{"channel"}, nil, - ) - messagesErrored = prometheus.NewDesc( - prometheus.BuildFQName(namespace, "", "messages_errored_total"), - "How many messages have errored (per channel).", - []string{"channel"}, nil, - ) ) -type Exporter struct { - mirthEndpoint, mirthUsername, mirthPassword string -} - -func NewExporter(mirthEndpoint string, mirthUsername string, mirthPassword string) *Exporter { - return &Exporter{ - mirthEndpoint: mirthEndpoint, - mirthUsername: mirthUsername, - mirthPassword: mirthPassword, - } -} - -func (e *Exporter) Describe(ch chan<- *prometheus.Desc) { - ch <- up - ch <- messagesReceived - ch <- messagesFiltered - ch <- messagesQueued - ch <- messagesSent - ch <- messagesErrored -} - -func (e *Exporter) Collect(ch chan<- prometheus.Metric) { - channelIdNameMap, err := e.LoadChannelIdNameMap() - if err != nil { - ch <- prometheus.MustNewConstMetric( - up, prometheus.GaugeValue, 0, - ) - log.Println(err) - return - } - ch <- prometheus.MustNewConstMetric( - up, prometheus.GaugeValue, 1, - ) - - e.HitMirthRestApisAndUpdateMetrics(channelIdNameMap, ch) -} - -func (e *Exporter) LoadChannelIdNameMap() (map[string]string, error) { - // Create the map of channel id to names - channelIdNameMap := make(map[string]string) - - req, err := http.NewRequest("GET", e.mirthEndpoint+channelIdNameApi, nil) - if err != nil { - return nil, err - } - - // This one line implements the authentication required for the task. - req.SetBasicAuth(e.mirthUsername, e.mirthPassword) - // Make request and show output. - resp, err := client.Do(req) - if err != nil { - return nil, err - } - - body, err := ioutil.ReadAll(resp.Body) - resp.Body.Close() - if err != nil { - return nil, err - } - // fmt.Println(string(body)) - - // we initialize our array - var channelIdNameMapXML ChannelIdNameMap - // we unmarshal our byteArray which contains our - // xmlFiles content into 'users' which we defined above - err = xml.Unmarshal(body, &channelIdNameMapXML) - if err != nil { - return nil, err - } - - for i := 0; i < len(channelIdNameMapXML.Entries); i++ { - channelIdNameMap[channelIdNameMapXML.Entries[i].Values[0]] = channelIdNameMapXML.Entries[i].Values[1] - } - - return channelIdNameMap, nil -} - -func (e *Exporter) HitMirthRestApisAndUpdateMetrics(channelIdNameMap map[string]string, ch chan<- prometheus.Metric) { - // Load channel stats - req, err := http.NewRequest("GET", e.mirthEndpoint+channelStatsApi, nil) - if err != nil { - log.Fatal(err) - } - - // This one line implements the authentication required for the task. - req.SetBasicAuth(e.mirthUsername, e.mirthPassword) - // Make request and show output. - resp, err := client.Do(req) - if err != nil { - log.Fatal(err) - } - - body, err := ioutil.ReadAll(resp.Body) - resp.Body.Close() - if err != nil { - log.Fatal(err) - } - // fmt.Println(string(body)) - - // we initialize our array - var channelStatsList ChannelStatsList - // we unmarshal our byteArray which contains our - // xmlFiles content into 'users' which we defined above - err = xml.Unmarshal(body, &channelStatsList) - if err != nil { - log.Fatal(err) - } - - for i := 0; i < len(channelStatsList.Channels); i++ { - channelName := channelIdNameMap[channelStatsList.Channels[i].ChannelId] - - channelReceived, _ := strconv.ParseFloat(channelStatsList.Channels[i].Received, 64) - ch <- prometheus.MustNewConstMetric( - messagesReceived, prometheus.GaugeValue, channelReceived, channelName, - ) - - channelSent, _ := strconv.ParseFloat(channelStatsList.Channels[i].Sent, 64) - ch <- prometheus.MustNewConstMetric( - messagesSent, prometheus.GaugeValue, channelSent, channelName, - ) - - channelError, _ := strconv.ParseFloat(channelStatsList.Channels[i].Error, 64) - ch <- prometheus.MustNewConstMetric( - messagesErrored, prometheus.GaugeValue, channelError, channelName, - ) - - channelFiltered, _ := strconv.ParseFloat(channelStatsList.Channels[i].Filtered, 64) - ch <- prometheus.MustNewConstMetric( - messagesFiltered, prometheus.GaugeValue, channelFiltered, channelName, - ) - - channelQueued, _ := strconv.ParseFloat(channelStatsList.Channels[i].Queued, 64) - ch <- prometheus.MustNewConstMetric( - messagesQueued, prometheus.GaugeValue, channelQueued, channelName, - ) - } - - log.Println("Endpoint scraped") -} - func main() { err := godotenv.Load() if err != nil { @@ -261,8 +29,9 @@ func main() { mirthUsername := os.Getenv("MIRTH_USERNAME") mirthPassword := os.Getenv("MIRTH_PASSWORD") - exporter := NewExporter(mirthEndpoint, mirthUsername, mirthPassword) - prometheus.MustRegister(exporter) + prometheus.MustRegister( + NewExporter(mirthEndpoint, mirthUsername, mirthPassword), + ) http.Handle(*metricsPath, promhttp.Handler()) http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { @@ -274,5 +43,6 @@ func main() { `)) }) + log.Println("Listening on ", *listenAddress) log.Fatal(http.ListenAndServe(*listenAddress, nil)) } diff --git a/metrics.go b/metrics.go new file mode 100644 index 0000000..d818a7d --- /dev/null +++ b/metrics.go @@ -0,0 +1,178 @@ +package main + +import ( + "crypto/tls" + "encoding/xml" + "github.com/prometheus/client_golang/prometheus" + "io/ioutil" + "log" + "net/http" +) + +const namespace = "mirth" +const channelStatusesApi = "/api/channels/statuses" + +var ( + tr = &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + } + + client = &http.Client{Transport: tr} + + up = prometheus.NewDesc( + prometheus.BuildFQName(namespace, "", "up"), + "Was the last Mirth query successful.", + nil, nil, + ) + + channelStatuses = prometheus.NewDesc( + prometheus.BuildFQName(namespace, "", "channel_status"), + "Status of all deployed channels", + []string{"channel", "status"}, nil, + ) + + messagesReceived = prometheus.NewDesc( + prometheus.BuildFQName(namespace, "", "messages_received_total"), + "How many messages have been received (per channel).", + []string{"channel"}, nil, + ) + + messagesFiltered = prometheus.NewDesc( + prometheus.BuildFQName(namespace, "", "messages_filtered_total"), + "How many messages have been filtered (per channel).", + []string{"channel"}, nil, + ) + + messagesQueued = prometheus.NewDesc( + prometheus.BuildFQName(namespace, "", "messages_queued"), + "How many messages are currently queued (per channel).", + []string{"channel"}, nil, + ) + + messagesSent = prometheus.NewDesc( + prometheus.BuildFQName(namespace, "", "messages_sent_total"), + "How many messages have been sent (per channel).", + []string{"channel"}, nil, + ) + + messagesErrored = prometheus.NewDesc( + prometheus.BuildFQName(namespace, "", "messages_errored_total"), + "How many messages have errored (per channel).", + []string{"channel"}, nil, + ) + + requestDuration = prometheus.NewHistogram(prometheus.HistogramOpts{ + Name: prometheus.BuildFQName(namespace, "", "request_duration"), + Help: "Histogram for the runtime of the metric pull from Mirth.", + Buckets: prometheus.LinearBuckets(0.1, 0.1, 20), + }) +) + +func (e *Exporter) LoadChannelStatuses() (*ChannelStatusMap, error) { + timer := prometheus.NewTimer(requestDuration) + defer timer.ObserveDuration() + req, err := http.NewRequest("GET", e.mirthEndpoint+channelStatusesApi, nil) + if err != nil { + return nil, err + } + + // This one line implements the authentication required for the task. + req.SetBasicAuth(e.mirthUsername, e.mirthPassword) + // Make request and show output. + resp, err := client.Do(req) + if err != nil { + return nil, err + } + + body, err := ioutil.ReadAll(resp.Body) + resp.Body.Close() + if err != nil { + return nil, err + } + // fmt.Println(string(body)) + + // initialize map variable + var channelStatusMap ChannelStatusMap + // unmarshal body byteArray into the ChannelStatusMap struct + err = xml.Unmarshal(body, &channelStatusMap) + if err != nil { + return nil, err + } + + return &channelStatusMap, nil +} + +func pickMetric(status string) *prometheus.Desc { + switch status { + case "RECEIVED": + return messagesReceived + case "FILTERED": + return messagesFiltered + case "SENT": + return messagesSent + case "QUEUED": + return messagesQueued + case "ERROR": + return messagesErrored + } + return nil +} + +func (e *Exporter) AssembleMetrics(channelStatusMap *ChannelStatusMap, ch chan<- prometheus.Metric) { + ch <- requestDuration + + for _, channel := range channelStatusMap.Channels { + ch <- prometheus.MustNewConstMetric( + channelStatuses, prometheus.GaugeValue, 1, channel.Name, channel.State, + ) + + for _, entry := range channel.CurrentStatistics { + metric := pickMetric(entry.Status) + if metric != nil { + ch <- prometheus.MustNewConstMetric( + metric, prometheus.GaugeValue, entry.MessageCount, channel.Name, + ) + } + } + } + + log.Println("Endpoint scraped") +} + +type Exporter struct { + mirthEndpoint, mirthUsername, mirthPassword string +} + +func NewExporter(mirthEndpoint string, mirthUsername string, mirthPassword string) *Exporter { + return &Exporter{ + mirthEndpoint: mirthEndpoint, + mirthUsername: mirthUsername, + mirthPassword: mirthPassword, + } +} + +func (e *Exporter) Describe(ch chan<- *prometheus.Desc) { + ch <- up + ch <- channelStatuses + ch <- messagesReceived + ch <- messagesFiltered + ch <- messagesQueued + ch <- messagesSent + ch <- messagesErrored +} + +func (e *Exporter) Collect(ch chan<- prometheus.Metric) { + channelIdStatusMap, err := e.LoadChannelStatuses() + if err != nil { + ch <- prometheus.MustNewConstMetric( + up, prometheus.GaugeValue, 0, + ) + log.Println(err) + return + } + ch <- prometheus.MustNewConstMetric( + up, prometheus.GaugeValue, 1, + ) + + e.AssembleMetrics(channelIdStatusMap, ch) +} diff --git a/types.go b/types.go new file mode 100644 index 0000000..0d7e5f0 --- /dev/null +++ b/types.go @@ -0,0 +1,44 @@ +package main + +import "encoding/xml" + +type ChannelStatus struct { + XMLName xml.Name `xml:"dashboardStatus"` + ChannelId string `xml:"channelId"` + Name string `xml:"name"` + State string `xml:"state"` + CurrentStatistics []ChannelStatusStatisticsEntry `xml:"statistics>entry"` + LifetimeStatistics []ChannelStatusStatisticsEntry `xml:"lifetimeStatistics>entry"` + + /* + + + RECEIVED + 70681 + + + FILTERED + 0 + + + SENT + 67139 + + + ERROR + 3542 + + + */ +} + +type ChannelStatusMap struct { + XMLName xml.Name `xml:"list"` + Channels []ChannelStatus `xml:"dashboardStatus"` +} + +type ChannelStatusStatisticsEntry struct { + Status string `xml:"com.mirth.connect.donkey.model.message.Status"` + MessageCount float64 `xml:"long"` +} +