Skip to content

Commit

Permalink
Merge pull request #1 from waytohealth/channel-status
Browse files Browse the repository at this point in the history
Add channel status metric
  • Loading branch information
mkopinsky authored Jul 21, 2021
2 parents e23edf7 + e67cb37 commit f149eda
Show file tree
Hide file tree
Showing 4 changed files with 277 additions and 239 deletions.
50 changes: 48 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand All @@ -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

Expand Down
244 changes: 7 additions & 237 deletions main.go
Original file line number Diff line number Diff line change
@@ -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"
)

/*
<map>
<entry>
<string>101af57f-f26c-40d3-86a3-309e74b93512</string>
<string>Send-Email-Notification</string>
</entry>
</map>
*/
type ChannelIdNameMap struct {
XMLName xml.Name `xml:"map"`
Entries []ChannelEntry `xml:"entry"`
}
type ChannelEntry struct {
XMLName xml.Name `xml:"entry"`
Values []string `xml:"string"`
}

/*
<list>
<channelStatistics>
<serverId>c5e6a736-0e88-46a7-bf32-5b4908c4d859</serverId>
<channelId>101af57f-f26c-40d3-86a3-309e74b93512</channelId>
<received>0</received>
<sent>0</sent>
<error>0</error>
<filtered>0</filtered>
<queued>0</queued>
</channelStatistics>
</list>
*/
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 {
Expand All @@ -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) {
Expand All @@ -274,5 +43,6 @@ func main() {
</body>
</html>`))
})
log.Println("Listening on ", *listenAddress)
log.Fatal(http.ListenAndServe(*listenAddress, nil))
}
Loading

0 comments on commit f149eda

Please sign in to comment.