<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>grafana &amp;mdash; musicmatzes blog</title>
    <link>https://beyermatthias.de/tag:grafana</link>
    <description></description>
    <pubDate>Fri, 17 Apr 2026 06:03:42 +0200</pubDate>
    <item>
      <title>Writing a Prometheus MPD exporter</title>
      <link>https://beyermatthias.de/writing-a-prometheus-mpd-exporter</link>
      <description>&lt;![CDATA[Today, I challenged myself to write a prometheus exporter for&#xA;MPD&#xA;in Rust.&#xA;&#xA;  Shut up and show me the code!&#xA;&#xA;Here you go&#xA;and&#xA;here you go for submitting patches.&#xA;&#xA;The challenge&#xA;&#xA;I recently started monitoring my server with prometheus and grafana.&#xA;I am no-way a professional user of these pieces of software, but I slowly got&#xA;everything up and running.&#xA;I learned about timeseries databases at university, so the basic concept of&#xA;prometheus was not new to me.&#xA;Grafana was, though.&#xA;I then started learning about prometheus exporters and how they are working and&#xA;managed to setup node exporters for all my devices and imported their metrics&#xA;into a nice grafana dashboard I downloaded from the official website.&#xA;&#xA;I figured, that writing an exporter would make me understand the whole thing&#xA;even better.&#xA;So what would be better than exporting music data to my prometheus and plotting&#xA;it with grafana?&#xA;Especially because my nickname online is &#34;musicmatze&#34;, right?&#xA;&#xA;So I started writing a prometheus exporter for MPD.&#xA;And because my language of choice is Rust, I wrote it in Rust.&#xA;Rust has good libraries available for everything I needed to do to export basic&#xA;MPD metrics to prometheus and even a prometheus exporter library exists!&#xA;&#xA;The libraries I decided to use&#xA;&#xA;Note that this article was written using&#xA;prometheus-mpd-exporter v0.1.0&#xA;of the prometheus-mpd-exporter code.&#xA;The current codebase might differ, but this was the first working&#xA;implementation.&#xA;&#xA;So, the scope of my idea was set.&#xA;Of course, I needed a library to talk to my music player daemon.&#xA;And because async libraries would be better, since I would essentially write a&#xA;kind of a web-server, it should be async.&#xA;Thankfully, asyncmpd exists.&#xA;&#xA;Next, I needed a&#xA;prometheus helper library.&#xA;The examples in this library work with hyper.&#xA;I was not able to implement my idea with hyper though (because of some weird&#xA;borrowing error), but thankfully, actix-web worked just&#xA;fine.&#xA;&#xA;Besides that I used a bunch of convenience libraries:&#xA;&#xA;anyhow and thiserror for error handling&#xA;envlogger and log for logging&#xA;structopt for CLI parsing&#xA;getset, parse-display and itertools to be able to write less code&#xA;&#xA;The first implementation&#xA;&#xA;The first implementation took me about four hours to write, because I had to&#xA;understand the actix-web infrastructure first (and because I tried it with&#xA;hyper in the first place, which did not work for about three of that four&#xA;hours).&#xA;&#xA;The boilerplate of the program includes&#xA;&#xA;Defining an ApplicationError type for easy passing-around of errors that&#xA;  happen during the runtime of the program&#xA;Defining an Opt as a commandline interface definition using structopt&#xA;&#xA;[actixweb::main]&#xA;async fn main() -  Result(), ApplicationError {&#xA;    let  = envlogger::init();&#xA;    log::info!(&#34;Starting...&#34;);&#xA;    let opt = Opt::fromargs();&#xA;&#xA;    let prometheusbindaddr = format!(&#34;{}:{}&#34;, opt.bindaddr, opt.bindport);&#xA;    let mpdconnectstring = format!(&#34;{}:{}&#34;, opt.mpdserveraddr, opt.mpdserverport);&#xA;&#xA;The main() function then sets up the logging and parses the commandline&#xA;arguments. Thanks to envlogger and structopt, that&#39;s easy.&#xA;The main() function also acts as the actixweb::main function and is async&#xA;because of that.&#xA;It also returns a Result(), ApplicationError, so I can easily fail during&#xA;the setup phase of the program.&#xA;&#xA;Next, I needed to setup the connection to MPD and wrap that in a Mutex, so it&#xA;can be shared between request handlers.&#xA;&#xA;    log::debug!(&#34;Connecting to MPD = {}&#34;, mpdconnectstring);&#xA;    let mpd = asyncmpd::MpdClient::new(&amp;*mpdconnectstring)&#xA;        .await&#xA;        .map(Mutex::new)?;&#xA;&#xA;    let mpd = web::Data::new(mpd);&#xA;&#xA;And then setup the HttpServer instance for actix-web, and run it.&#xA;&#xA;    HttpServer::new(move || {&#xA;        App::new()&#xA;            .appdata(mpd.clone()) // add shared state&#xA;            .wrap(middleware::Logger::default())&#xA;            .route(&#34;/&#34;, web::get().to(index))&#xA;            .route(&#34;/metrics&#34;, web::get().to(metrics))&#xA;    })&#xA;    .bind(prometheusbindaddr)?&#xA;    .run()&#xA;    .await&#xA;    .maperr(ApplicationError::from)&#xA;} // end of main()&#xA;&#xA;Now comes the fun part, tho.&#xA;First of all, I have setup the connection to MPD.&#xA;In the above snippet, I add routes to the HttpServer for a basic index endpoint&#xA;as well as for the /metrics endpoint prometheus fetches the metrics from.&#xA;&#xA;Lets have a look at the index handler first, to get a basic understanding of&#xA;how it works:&#xA;&#xA;async fn index(: web::DataMutex&lt;MpdClient  , : HttpRequest) -  impl Responder {&#xA;    HttpResponse::build(StatusCode::OK)&#xA;        .contenttype(&#34;text/text; charset=utf-8&#34;)&#xA;        .body(String::from(&#34;Running&#34;))&#xA;}&#xA;&#xA;This function gets called every time someone accesses the service without&#xA;specifying an endpoint, for example curl localhost:9123 would result in this&#xA;function being called.&#xA;&#xA;Here, I can get the web::DataMutex&lt;MpdClient   object instance that&#xA;actix-web handles for us as well as a HttpRequest object to get information&#xA;about the request itself.&#xA;Because I don&#39;t need this data here, the variables are not bound (``).&#xA;I added them to be able to extend this function later on easily.&#xA;&#xA;I return a simple 200 (that&#39;s the StatusCode::OK here) with a simple&#xA;Running body.&#xA;curling would result in a simple response:&#xA;&#xA;$ curl 127.0.0.1:9123&#xA;Running&#xA;&#xA;Now, lets have a look at the /metrics endpoint.&#xA;First of all, the signature of the function is the same:&#xA;&#xA;async fn metrics(mpddata: web::DataMutex&lt;MpdClient  , : HttpRequest) -  impl Responder {&#xA;    match metricshandler(mpddata).await {&#xA;        Ok(text) =  {&#xA;            HttpResponse::build(StatusCode::OK)&#xA;                .contenttype(&#34;text/text; charset=utf-8&#34;)&#xA;                .body(text)&#xA;        }&#xA;&#xA;        Err(e) =  {&#xA;            HttpResponse::build(StatusCode::INTERNALSERVERERROR)&#xA;                .contenttype(&#34;text/text; charset=utf-8&#34;)&#xA;                .body(format!(&#34;{}&#34;, e))&#xA;        }&#xA;    }&#xA;}&#xA;&#xA;but here, we bind the mpd client object to mpddata, because we want to&#xA;actually use that object.&#xA;We then call a function metricshandler() with that object, wait for the&#xA;result (because that function itself is async, too), and match the result.&#xA;If the result is Ok(), we get the result text and return a 200 with the&#xA;text as the body.&#xA;If the result is an error, which means that fetching the data from MPD somehow&#xA;resulted in an error, we return an internal server error (500) and the error&#xA;message as body of the response.&#xA;&#xA;Now, to the metricshandler() function, which is where the real work happens.&#xA;&#xA;async fn metricshandler(mpddata: web::DataMutex&lt;MpdClient  ) -  ResultString, ApplicationError {&#xA;    let mut mpd = mpddata.lock().unwrap();&#xA;    let stats = mpd.stats().await?;&#xA;&#xA;    let instance = String::new(); // TODO&#xA;&#xA;First of all, we extract the actual MpdClient object from the&#xA;web::DataMutex&lt;   wrapper.&#xA;Them, we ask MPD to get some stats() and wait for the result.&#xA;&#xA;After that, we create a variable we don&#39;t fill yet, which we later push in the&#xA;release without solving the &#34;TODO&#34; marker and when we blog about what we did, we&#xA;feel ashamed about it.&#xA;&#xA;Next, we create Metric objects for each metric we record from MPD and render&#xA;all of them into one big String object.&#xA;&#xA;    let res = vec![&#xA;        Metric::new(&#34;mpduptime&#34;      , stats.uptime      , &#34;The uptime of mpd&#34;, &amp;instance).intometric()?,&#xA;        Metric::new(&#34;mpdplaytime&#34;    , stats.playtime    , &#34;The playtime of the current playlist&#34;, &amp;instance).intometric()?,&#xA;        Metric::new(&#34;mpdartists&#34;     , stats.artists     , &#34;The number of artists&#34;, &amp;instance).intometric()?,&#xA;        Metric::new(&#34;mpdalbums&#34;      , stats.albums      , &#34;The number of albums&#34;, &amp;instance).intometric()?,&#xA;        Metric::new(&#34;mpdsongs&#34;       , stats.songs       , &#34;The number of songs&#34;, &amp;instance).intometric()?,&#xA;        Metric::new(&#34;mpddbplaytime&#34; , stats.dbplaytime , &#34;The database playtime&#34;, &amp;instance).intometric()?,&#xA;        Metric::new(&#34;mpddbupdate&#34;   , stats.dbupdate   , &#34;The updates of the database&#34;, &amp;instance).intometric()?,&#xA;    ]&#xA;    .intoiter()&#xA;    .map(|m| {&#xA;        m.render()&#xA;    })&#xA;    .join(&#34;\n&#34;);&#xA;&#xA;    log::debug!(&#34;res = {}&#34;, res);&#xA;    Ok(res)&#xA;}&#xA;&#xA;Lastly, we return that String object from our handler implementation.&#xA;&#xA;The Metric object implementation my own, we&#39;ll focus on that now.&#xA;It will help a bit with the interface of the prometheusexporterbase API&#xA;interface.&#xA;&#xA;But first, I need to explain the Metric type:&#xA;&#xA;pub struct Metric&#39;a, T: IntoNumMetric {&#xA;    name: &amp;&#39;static str,&#xA;    value: T,&#xA;    description: &amp;&#39;static str,&#xA;    instance: &amp;&#39;a str,&#xA;}&#xA;&#xA;The Metric type is a type that holds a name for a metric, its value and some&#xA;description (and the aforementioned irrelevant instance).&#xA;But because the metrics we collect can be of different types (for example a&#xA;8-bit unsigned integer u8 or a 32-bit unsigned integer u32), I made that&#xA;type abstract over it.&#xA;The type of the metric value must implement a IntoNumMetric trait, though.&#xA;That trait is a simple helper trait:&#xA;&#xA;use numtraits::Num;&#xA;pub trait IntoNumMetric {&#xA;    type Output: Num + Display + Debug;&#xA;&#xA;    fn intonummetric(self) -  Self::Output;&#xA;}&#xA;&#xA;And I implemented it for std::time::Duration, u8, u32 and i32 - the&#xA;implementation itself is trivial and I won&#39;t show it here.&#xA;&#xA;Now, I was able to implement the Metric::intometric() function shown above:&#xA;&#xA;impl&#39;a, T: IntoNumMetric + Debug Metric&#39;a, T {&#xA;    // Metric::new() implementation, hidden here&#xA;&#xA;    pub fn intometric&#39;b(self) -  ResultPrometheusMetric&lt;&#39;b  {&#xA;        let instance = PrometheusInstance::new()&#xA;            .withlabel(&#34;instance&#34;, self.instance)&#xA;            .withvalue(self.value.intonummetric())&#xA;            .withcurrenttimestamp()&#xA;            .maperr(Error::from)?;&#xA;&#xA;        let mut m = PrometheusMetric::new(self.name, MetricType::Counter, self.description);&#xA;        m.renderandappendinstance(&amp;instance);&#xA;        Ok(m)&#xA;    }&#xA;}&#xA;&#xA;This function is used for converting a Metric object into the appropriate&#xA;PrometheusMetric object from prometheusexporterbase.&#xA;&#xA;The implementation is, of course, also generic over the type the Metric object&#xA;holds.&#xA;A PrometheusInstance is created, a label &#34;instance&#34; is added (empty, you know&#xA;why... :-( ).&#xA;Then, the value is added to that instance using the conversion from the&#xA;IntoNumMetric trait.&#xA;The current timestamp is added as well, or an error is returned if that fails.&#xA;&#xA;Last but not least, a new PrometheusMetric object is created with the&#xA;appropriate name and description, and the instance is rendered to it.&#xA;&#xA;And that&#39;s it!&#xA;&#xA;Deploying&#xA;&#xA;The code is there now.&#xA;But of course, I still needed to deploy this to my hosts and make it available&#xA;in my prometheus and grafana instances.&#xA;&#xA;Because I use NixOS, I wrote a nix package definition and a&#xA;nix service defintion for it,&#xA;making the endpoint available to my prometheus instance via my wireguard&#xA;network.&#xA;&#xA;After that, I was able to add queries to my grafana instance, for example:&#xA;&#xA;mpddbplaytime / 60 / 60 / 24&#xA;&#xA;to display the DB playtime of an instance of my MPD in days.&#xA;&#xA;I&#39;m not yet very proficient in grafana and the query language, and also the&#xA;service implementation is rather minimal, so there cannot be that much metrics&#xA;yet.&#xA;&#xA;Either way, it works!&#xA;&#xA;A basic dashboard for MPD stats&#xA;&#xA;Next steps and closing words&#xA;&#xA;The next steps are quite simple.&#xA;First of all, I want to make more stats available to prometheus. Right now, only&#xA;the basic statistics of the database are exported.&#xA;&#xA;The asyncmpd crate makes a lot of&#xA;other status information&#xA;available.&#xA;&#xA;Also, I want to get better with grafana queries and make some nice-looking&#xA;graphs for my dashboard.&#xA;&#xA;Either way, that challenge took me longer than I anticipated in the first place&#xA;(&#34;I can hack this in 15 minutes&#34; - famous last words)!&#xA;But it was fun nonetheless!&#xA;&#xA;The outcome of this little journey is on&#xA;crates.io&#xA;and I will also submit a PR to nixpkgs to make it available there, too.&#xA;&#xA;If you want to contribute patches to the sourcecode, which I encourage you to&#xA;do, feel free to send me patches!&#xA;&#xA;tags: #prometheus #grafana #rust #mpd #music&#xA;]]&gt;</description>
      <content:encoded><![CDATA[<p>Today, I challenged myself to write a prometheus exporter for
<a href="https://www.musicpd.org/">MPD</a>
in Rust.</p>

<blockquote><p>Shut up and show me the code!</p></blockquote>

<p><a href="https://git.beyermatthi.as/prometheus-mpd-exporter/">Here you go</a>
and
<a href="https://sr.ht/~matthiasbeyer/prometheus-mpd-exporter/">here you go for submitting patches</a>.</p>

<h1 id="the-challenge" id="the-challenge">The challenge</h1>

<p>I recently started monitoring my server with prometheus and grafana.
I am no-way a professional user of these pieces of software, but I slowly got
everything up and running.
I learned about timeseries databases at university, so the basic concept of
prometheus was not new to me.
Grafana was, though.
I then started learning about prometheus exporters and how they are working and
managed to setup node exporters for all my devices and imported their metrics
into a nice grafana dashboard I downloaded from the official website.</p>

<p>I figured, that writing an exporter would make me understand the whole thing
even better.
So what would be better than exporting music data to my prometheus and plotting
it with grafana?
Especially because my nickname online is “musicmatze”, right?</p>

<p>So I started writing a prometheus exporter for MPD.
And because my language of choice is Rust, I wrote it in Rust.
Rust has good libraries available for everything I needed to do to export basic
MPD metrics to prometheus and even a prometheus exporter library exists!</p>

<h1 id="the-libraries-i-decided-to-use" id="the-libraries-i-decided-to-use">The libraries I decided to use</h1>

<p>Note that this article was written using
<a href="https://git.beyermatthi.as/prometheus-mpd-exporter/tag/?h=v0.1.0">prometheus-mpd-exporter v0.1.0</a>
of the prometheus-mpd-exporter code.
The current codebase might differ, but this was the first working
implementation.</p>

<p>So, the scope of my idea was set.
Of course, I needed a library to talk to my music player daemon.
And because async libraries would be better, since I would essentially write a
kind of a web-server, it should be async.
Thankfully, <a href="https://docs.rs/async-mpd/">async_mpd</a> exists.</p>

<p>Next, I needed a
<a href="https://github.com/MindFlavor/prometheus_exporter_base">prometheus helper library</a>.
The examples in this library work with hyper.
I was not able to implement my idea with hyper though (because of some weird
borrowing error), but thankfully, <a href="https://actix.rs/">actix-web</a> worked just
fine.</p>

<p>Besides that I used a bunch of convenience libraries:</p>
<ul><li><code>anyhow</code> and <code>thiserror</code> for error handling</li>
<li><code>env_logger</code> and <code>log</code> for logging</li>
<li><code>structopt</code> for CLI parsing</li>
<li><code>getset</code>, <code>parse-display</code> and <code>itertools</code> to be able to write less code</li></ul>

<h1 id="the-first-implementation" id="the-first-implementation">The first implementation</h1>

<p>The first implementation took me about four hours to write, because I had to
understand the <code>actix-web</code> infrastructure first (and because I tried it with
<code>hyper</code> in the first place, which did not work for about three of that four
hours).</p>

<p>The boilerplate of the program includes</p>
<ul><li>Defining an <code>ApplicationError</code> type for easy passing-around of errors that
happen during the runtime of the program</li>
<li>Defining an <code>Opt</code> as a commandline interface definition using <code>structopt</code></li></ul>

<pre><code class="language-rust">#[actix_web::main]
async fn main() -&gt; Result&lt;(), ApplicationError&gt; {
    let _ = env_logger::init();
    log::info!(&#34;Starting...&#34;);
    let opt = Opt::from_args();

    let prometheus_bind_addr = format!(&#34;{}:{}&#34;, opt.bind_addr, opt.bind_port);
    let mpd_connect_string = format!(&#34;{}:{}&#34;, opt.mpd_server_addr, opt.mpd_server_port);
</code></pre>

<p>The <code>main()</code> function then sets up the logging and parses the commandline
arguments. Thanks to <code>env_logger</code> and <code>structopt</code>, that&#39;s easy.
The <code>main()</code> function also acts as the <code>actix_web::main</code> function and is <code>async</code>
because of that.
It also returns a <code>Result&lt;(), ApplicationError&gt;</code>, so I can easily fail during
the setup phase of the program.</p>

<p>Next, I needed to setup the connection to MPD and wrap that in a Mutex, so it
can be shared between request handlers.</p>

<pre><code class="language-rust">    log::debug!(&#34;Connecting to MPD = {}&#34;, mpd_connect_string);
    let mpd = async_mpd::MpdClient::new(&amp;*mpd_connect_string)
        .await
        .map(Mutex::new)?;

    let mpd = web::Data::new(mpd);
</code></pre>

<p>And then setup the <code>HttpServer</code> instance for actix-web, and run it.</p>

<pre><code class="language-rust">    HttpServer::new(move || {
        App::new()
            .app_data(mpd.clone()) // add shared state
            .wrap(middleware::Logger::default())
            .route(&#34;/&#34;, web::get().to(index))
            .route(&#34;/metrics&#34;, web::get().to(metrics))
    })
    .bind(prometheus_bind_addr)?
    .run()
    .await
    .map_err(ApplicationError::from)
} // end of main()
</code></pre>

<p>Now comes the fun part, tho.
First of all, I have setup the connection to MPD.
In the above snippet, I add routes to the HttpServer for a basic index endpoint
as well as for the <code>/metrics</code> endpoint prometheus fetches the metrics from.</p>

<p>Lets have a look at the <code>index</code> handler first, to get a basic understanding of
how it works:</p>

<pre><code class="language-rust">async fn index(_: web::Data&lt;Mutex&lt;MpdClient&gt;&gt;, _: HttpRequest) -&gt; impl Responder {
    HttpResponse::build(StatusCode::OK)
        .content_type(&#34;text/text; charset=utf-8&#34;)
        .body(String::from(&#34;Running&#34;))
}
</code></pre>

<p>This function gets called every time someone accesses the service without
specifying an endpoint, for example <code>curl localhost:9123</code> would result in this
function being called.</p>

<p>Here, I can get the <code>web::Data&lt;Mutex&lt;MpdClient&gt;&gt;</code> object instance that
actix-web handles for us as well as a <code>HttpRequest</code> object to get information
about the request itself.
Because I don&#39;t need this data here, the variables are not bound (<code>_</code>).
I added them to be able to extend this function later on easily.</p>

<p>I return a simple <code>200</code> (that&#39;s the <code>StatusCode::OK</code> here) with a simple
<code>Running</code> body.
<code>curl</code>ing would result in a simple response:</p>

<pre><code>$ curl 127.0.0.1:9123
Running
</code></pre>

<p>Now, lets have a look at the <code>/metrics</code> endpoint.
First of all, the signature of the function is the same:</p>

<pre><code class="language-rust">async fn metrics(mpd_data: web::Data&lt;Mutex&lt;MpdClient&gt;&gt;, _: HttpRequest) -&gt; impl Responder {
    match metrics_handler(mpd_data).await {
        Ok(text) =&gt; {
            HttpResponse::build(StatusCode::OK)
                .content_type(&#34;text/text; charset=utf-8&#34;)
                .body(text)
        }

        Err(e) =&gt; {
            HttpResponse::build(StatusCode::INTERNAL_SERVER_ERROR)
                .content_type(&#34;text/text; charset=utf-8&#34;)
                .body(format!(&#34;{}&#34;, e))
        }
    }
}
</code></pre>

<p>but here, we bind the mpd client object to <code>mpd_data</code>, because we want to
actually use that object.
We then call a function <code>metrics_handler()</code> with that object, wait for the
result (because that function itself is <code>async</code>, too), and match the result.
If the result is <code>Ok(_)</code>, we get the result text and return a <code>200</code> with the
text as the body.
If the result is an error, which means that fetching the data from MPD somehow
resulted in an error, we return an internal server error (<code>500</code>) and the error
message as body of the response.</p>

<p>Now, to the <code>metrics_handler()</code> function, which is where the real work happens.</p>

<pre><code class="language-rust">async fn metrics_handler(mpd_data: web::Data&lt;Mutex&lt;MpdClient&gt;&gt;) -&gt; Result&lt;String, ApplicationError&gt; {
    let mut mpd = mpd_data.lock().unwrap();
    let stats = mpd.stats().await?;

    let instance = String::new(); // TODO
</code></pre>

<p>First of all, we extract the actual <code>MpdClient</code> object from the
<code>web::Data&lt;Mutex&lt;_&gt;&gt;</code> wrapper.
Them, we ask MPD to get some <code>stats()</code> and wait for the result.</p>

<p>After that, we create a variable we don&#39;t fill yet, which we later push in the
release without solving the “TODO” marker and when we blog about what we did, we
feel ashamed about it.</p>

<p>Next, we create <code>Metric</code> objects for each metric we record from MPD and render
all of them into one big <code>String</code> object.</p>

<pre><code class="language-rust">    let res = vec![
        Metric::new(&#34;mpd_uptime&#34;      , stats.uptime      , &#34;The uptime of mpd&#34;, &amp;instance).into_metric()?,
        Metric::new(&#34;mpd_playtime&#34;    , stats.playtime    , &#34;The playtime of the current playlist&#34;, &amp;instance).into_metric()?,
        Metric::new(&#34;mpd_artists&#34;     , stats.artists     , &#34;The number of artists&#34;, &amp;instance).into_metric()?,
        Metric::new(&#34;mpd_albums&#34;      , stats.albums      , &#34;The number of albums&#34;, &amp;instance).into_metric()?,
        Metric::new(&#34;mpd_songs&#34;       , stats.songs       , &#34;The number of songs&#34;, &amp;instance).into_metric()?,
        Metric::new(&#34;mpd_db_playtime&#34; , stats.db_playtime , &#34;The database playtime&#34;, &amp;instance).into_metric()?,
        Metric::new(&#34;mpd_db_update&#34;   , stats.db_update   , &#34;The updates of the database&#34;, &amp;instance).into_metric()?,
    ]
    .into_iter()
    .map(|m| {
        m.render()
    })
    .join(&#34;\n&#34;);

    log::debug!(&#34;res = {}&#34;, res);
    Ok(res)
}
</code></pre>

<p>Lastly, we return that <code>String</code> object from our handler implementation.</p>

<p>The <code>Metric</code> object implementation my own, we&#39;ll focus on that now.
It will help a bit with the interface of the <code>prometheus_exporter_base</code> API
interface.</p>

<p>But first, I need to explain the <code>Metric</code> type:</p>

<pre><code class="language-rust">pub struct Metric&lt;&#39;a, T: IntoNumMetric&gt; {
    name: &amp;&#39;static str,
    value: T,
    description: &amp;&#39;static str,
    instance: &amp;&#39;a str,
}
</code></pre>

<p>The <code>Metric</code> type is a type that holds a name for a metric, its value and some
description (and the aforementioned irrelevant <code>instance</code>).
But because the metrics we collect can be of different types (for example a
8-bit unsigned integer <code>u8</code> or a 32-bit unsigned integer <code>u32</code>), I made that
type abstract over it.
The type of the metric value must implement a <code>IntoNumMetric</code> trait, though.
That trait is a simple helper trait:</p>

<pre><code class="language-rust">use num_traits::Num;
pub trait IntoNumMetric {
    type Output: Num + Display + Debug;

    fn into_num_metric(self) -&gt; Self::Output;
}
</code></pre>

<p>And I implemented it for <code>std::time::Duration</code>, <code>u8</code>, <code>u32</code> and <code>i32</code> – the
implementation itself is trivial and I won&#39;t show it here.</p>

<p>Now, I was able to implement the <code>Metric::into_metric()</code> function shown above:</p>

<pre><code class="language-rust">impl&lt;&#39;a, T: IntoNumMetric + Debug&gt; Metric&lt;&#39;a, T&gt; {
    // Metric::new() implementation, hidden here

    pub fn into_metric&lt;&#39;b&gt;(self) -&gt; Result&lt;PrometheusMetric&lt;&#39;b&gt;&gt; {
        let instance = PrometheusInstance::new()
            .with_label(&#34;instance&#34;, self.instance)
            .with_value(self.value.into_num_metric())
            .with_current_timestamp()
            .map_err(Error::from)?;

        let mut m = PrometheusMetric::new(self.name, MetricType::Counter, self.description);
        m.render_and_append_instance(&amp;instance);
        Ok(m)
    }
}
</code></pre>

<p>This function is used for converting a <code>Metric</code> object into the appropriate
<code>PrometheusMetric</code> object from <code>prometheus_exporter_base</code>.</p>

<p>The implementation is, of course, also generic over the type the <code>Metric</code> object
holds.
A <code>PrometheusInstance</code> is created, a label “instance” is added (empty, you know
why... :–( ).
Then, the value is added to that instance using the conversion from the
<code>IntoNumMetric</code> trait.
The current timestamp is added as well, or an error is returned if that fails.</p>

<p>Last but not least, a new <code>PrometheusMetric</code> object is created with the
appropriate name and description, and the instance is rendered to it.</p>

<p>And that&#39;s it!</p>

<h1 id="deploying" id="deploying">Deploying</h1>

<p>The code is there now.
But of course, I still needed to deploy this to my hosts and make it available
in my prometheus and grafana instances.</p>

<p>Because I use <a href="https://nixos.org">NixOS</a>, I wrote a nix package definition and a
nix service defintion for it,
making the endpoint available to my prometheus instance via my wireguard
network.</p>

<p>After that, I was able to add queries to my grafana instance, for example:</p>

<pre><code>mpd_db_playtime / 60 / 60 / 24
</code></pre>

<p>to display the DB playtime of an instance of my MPD in days.</p>

<p>I&#39;m not yet very proficient in grafana and the query language, and also the
service implementation is rather minimal, so there cannot be that much metrics
yet.</p>

<p>Either way, it works!</p>

<p><img src="/prometheus-mpd-exporter-grafana-dashboard.png" alt="A basic dashboard for MPD stats"></p>

<h1 id="next-steps-and-closing-words" id="next-steps-and-closing-words">Next steps and closing words</h1>

<p>The next steps are quite simple.
First of all, I want to make more stats available to prometheus. Right now, only
the basic statistics of the database are exported.</p>

<p>The <code>async_mpd</code> crate makes a lot of
<a href="https://docs.rs/async-mpd/0.4.0/async_mpd/struct.Status.html">other status information</a>
available.</p>

<p>Also, I want to get better with grafana queries and make some nice-looking
graphs for my dashboard.</p>

<p>Either way, that challenge took me longer than I anticipated in the first place
(“I can hack this in 15 minutes” – famous last words)!
But it was fun nonetheless!</p>

<p>The outcome of this little journey is on
<a href="https://crates.io/crates/prometheus-mpd-exporter">crates.io</a>
and I will also submit a PR to nixpkgs to make it available there, too.</p>

<p>If you want to contribute patches to the sourcecode, which I encourage you to
do, feel free to <a href="https://git-send-email.io">send me patches</a>!</p>

<p>tags: <a href="https://beyermatthias.de/tag:prometheus" class="hashtag"><span>#</span><span class="p-category">prometheus</span></a> <a href="https://beyermatthias.de/tag:grafana" class="hashtag"><span>#</span><span class="p-category">grafana</span></a> <a href="https://beyermatthias.de/tag:rust" class="hashtag"><span>#</span><span class="p-category">rust</span></a> <a href="https://beyermatthias.de/tag:mpd" class="hashtag"><span>#</span><span class="p-category">mpd</span></a> <a href="https://beyermatthias.de/tag:music" class="hashtag"><span>#</span><span class="p-category">music</span></a></p>
]]></content:encoded>
      <guid>https://beyermatthias.de/writing-a-prometheus-mpd-exporter</guid>
      <pubDate>Sun, 03 Jan 2021 16:40:15 +0100</pubDate>
    </item>
    <item>
      <title>Monitoring Syncthing with Prometheus and Grafana</title>
      <link>https://beyermatthias.de/monitoring-syncthing-with-prometheus-and-grafana</link>
      <description>&lt;![CDATA[Happy new year!&#xA;&#xA;I just managed to implement syncthing monitoring for my prometheus and grafana&#xA;instance, so I figured to write a short blog post about it.&#xA;&#xA;Note: This post is written for&#xA;prometheus-json-exporter pre-0.1.0&#xA;and the configuration file format changed since.&#xA;&#xA;Now, as you&#39;ve read in the note above, I managed to do this using the&#xA;prometheus-json-exporter.&#xA;Syncthing has a status page that can be accessed with&#xA;&#xA;$ curl localhost:22070/status&#xA;&#xA;if enabled.&#xA;This can then be used to push to prometheus using the prometheus-json-exporter&#xA;mentioned above using the following configuration for mapping the values from&#xA;the JSON to prometheus:&#xA;&#xA;name: syncthingbuildDate&#xA;  path: $.buildDate&#xA;  help: Value of buildDate&#xA;&#xA;name: syncthingbuildHost&#xA;  path: $.buildHost&#xA;  help: Value of buildHost&#xA;&#xA;name: syncthingbuildUser&#xA;  path: $.buildUser&#xA;  help: Value of buildUser&#xA;&#xA;name: syncthingbytesProxied&#xA;  path: $.bytesProxied&#xA;  help: Value of bytesProxied&#xA;&#xA;name: syncthinggoArch&#xA;  path: $.goArch&#xA;  help: Value of goArch&#xA;&#xA;name: syncthinggoMaxProcs&#xA;  path: $.goMaxProcs&#xA;  help: Value of goMaxProcs&#xA;&#xA;name: syncthinggoNumRoutine&#xA;  path: $.goNumRoutine&#xA;  help: Value of goNumRoutine&#xA;&#xA;name: syncthinggoOS&#xA;  path: $.goOS&#xA;  help: Value of goOS&#xA;&#xA;name: syncthinggoVersion&#xA;  path: $.goVersion&#xA;  help: Value of goVersion&#xA;&#xA;name: syncthingkbps10s1m5m15m30m60m&#xA;  path: $.kbps10s1m5m15m30m60m&#xA;  help: Value of kbps10s1m5m15m30m60m&#xA;  type: object&#xA;  values:&#xA;    time10sec: $[0]&#xA;    time1min: $[1]&#xA;    time5min: $[2]&#xA;    time15min: $[3]&#xA;    time30min: $[4]&#xA;    time60min: $[5]&#xA;&#xA;name: syncthingnumActiveSessions&#xA;  path: $.numActiveSessions&#xA;  help: Value of numActiveSessions&#xA;&#xA;name: syncthingnumConnections&#xA;  path: $.numConnections&#xA;  help: Value of numConnections&#xA;&#xA;name: syncthingnumPendingSessionKeys&#xA;  path: $.numPendingSessionKeys&#xA;  help: Value of numPendingSessionKeys&#xA;&#xA;name: syncthingnumProxies&#xA;  path: $.numProxies&#xA;  help: Value of numProxies&#xA;&#xA;name: syncthingglobalrate&#xA;  path: $.options.global-rate&#xA;  help: Value of options.global-rate&#xA;&#xA;name: syncthingmessagetimeout&#xA;  path: $.options.message-timeout&#xA;  help: Value of options.message-timeout&#xA;&#xA;name: syncthingnetworktimeout&#xA;  path: $.options.network-timeout&#xA;  help: Value of options.network-timeout&#xA;&#xA;name: syncthingpersessionrate&#xA;  path: $.options.per-session-rate&#xA;  help: Value of options.session-rate&#xA;&#xA;name: syncthingpinginterval&#xA;  path: $.options.ping-interval&#xA;  help: Value of options.ping-interval&#xA;&#xA;name: syncthingstartTime&#xA;  path: $.startTime&#xA;  help: Value of startTime&#xA;&#xA;name: syncthinguptimeSeconds&#xA;  path: $.uptimeSeconds&#xA;  help: Value of uptimeSeconds&#xA;&#xA;name: syncthing_version&#xA;  path: $.version&#xA;  help: Value of version&#xA;&#xA;When configured properly, one is then able to draw graphs using the&#xA;syncthing-exported data in grafana.&#xA;&#xA;There&#39;s nothing more to it.&#xA;&#xA;tags: #nixos #grafana #prometheus #syncthing&#xA;]]&gt;</description>
      <content:encoded><![CDATA[<p>Happy new year!</p>

<p>I just managed to implement syncthing monitoring for my prometheus and grafana
instance, so I figured to write a short blog post about it.</p>

<p><strong>Note:</strong> This post is written for
<a href="https://github.com/prometheus-community/json_exporter">prometheus-json-exporter pre-0.1.0</a>
and the configuration file format changed since.</p>

<p>Now, as you&#39;ve read in the note above, I managed to do this using the
prometheus-json-exporter.
Syncthing has a status page that can be accessed with</p>

<pre><code>$ curl localhost:22070/status
</code></pre>

<p>if enabled.
This can then be used to push to prometheus using the prometheus-json-exporter
mentioned above using the following configuration for mapping the values from
the JSON to prometheus:</p>

<pre><code class="language-yaml">- name: syncthing_buildDate
  path: $.buildDate
  help: Value of buildDate

- name: syncthing_buildHost
  path: $.buildHost
  help: Value of buildHost

- name: syncthing_buildUser
  path: $.buildUser
  help: Value of buildUser

- name: syncthing_bytesProxied
  path: $.bytesProxied
  help: Value of bytesProxied

- name: syncthing_goArch
  path: $.goArch
  help: Value of goArch

- name: syncthing_goMaxProcs
  path: $.goMaxProcs
  help: Value of goMaxProcs

- name: syncthing_goNumRoutine
  path: $.goNumRoutine
  help: Value of goNumRoutine

- name: syncthing_goOS
  path: $.goOS
  help: Value of goOS

- name: syncthing_goVersion
  path: $.goVersion
  help: Value of goVersion

- name: syncthing_kbps10s1m5m15m30m60m
  path: $.kbps10s1m5m15m30m60m
  help: Value of kbps10s1m5m15m30m60m
  type: object
  values:
    time_10_sec: $[0]
    time_1_min: $[1]
    time_5_min: $[2]
    time_15_min: $[3]
    time_30_min: $[4]
    time_60_min: $[5]

- name: syncthing_numActiveSessions
  path: $.numActiveSessions
  help: Value of numActiveSessions

- name: syncthing_numConnections
  path: $.numConnections
  help: Value of numConnections

- name: syncthing_numPendingSessionKeys
  path: $.numPendingSessionKeys
  help: Value of numPendingSessionKeys

- name: syncthing_numProxies
  path: $.numProxies
  help: Value of numProxies

- name: syncthing_globalrate
  path: $.options.global-rate
  help: Value of options.global-rate

- name: syncthing_messagetimeout
  path: $.options.message-timeout
  help: Value of options.message-timeout

- name: syncthing_networktimeout
  path: $.options.network-timeout
  help: Value of options.network-timeout

- name: syncthing_persessionrate
  path: $.options.per-session-rate
  help: Value of options.session-rate

- name: syncthing_pinginterval
  path: $.options.ping-interval
  help: Value of options.ping-interval

- name: syncthing_startTime
  path: $.startTime
  help: Value of startTime

- name: syncthing_uptimeSeconds
  path: $.uptimeSeconds
  help: Value of uptimeSeconds

- name: syncthing_version
  path: $.version
  help: Value of version
</code></pre>

<p>When configured properly, one is then able to draw graphs using the
syncthing-exported data in grafana.</p>

<p>There&#39;s nothing more to it.</p>

<p>tags: <a href="https://beyermatthias.de/tag:nixos" class="hashtag"><span>#</span><span class="p-category">nixos</span></a> <a href="https://beyermatthias.de/tag:grafana" class="hashtag"><span>#</span><span class="p-category">grafana</span></a> <a href="https://beyermatthias.de/tag:prometheus" class="hashtag"><span>#</span><span class="p-category">prometheus</span></a> <a href="https://beyermatthias.de/tag:syncthing" class="hashtag"><span>#</span><span class="p-category">syncthing</span></a></p>
]]></content:encoded>
      <guid>https://beyermatthias.de/monitoring-syncthing-with-prometheus-and-grafana</guid>
      <pubDate>Fri, 01 Jan 2021 16:40:14 +0100</pubDate>
    </item>
  </channel>
</rss>