Introduction to Metrics in Server-Side Swift

In this Server-Side Swift tutorial you will learn how to use Vapor built-in metrics and how to create custom ones. The data is pulled and stored by Prometheus and visualized in beautiful graphs using Grafana. By Jari Koopman.

5 (2) · 1 Review

Download materials
Save for later
Share

It’s probably a bit optimistic to believe production applications will never have issues. There are ways to gain insight and help combat issues, errors and crashes in production, with the two main ones logging and metrics.

Logging is pretty self-explanatory. It allows your application to output messages when something happens, good or bad. The messages focus on a single action and offer you fine-grained insight into what exactly went wrong in your application.

On the other hand, metrics don’t look at a single action but can aggregate information about these actions, such as the number of HTTP requests your application is receiving, grouped by the request path and response status. This gives you a quick and easy way to see how your Server-Side Swift application is doing. When the metrics report a rising trend in 5xx response codes or an increase in response times, it’s time to dive into the logging to see what’s going on.

In this tutorial, you’ll learn how to:

  • Enable default built-in Server-Side Swift metrics in Vapor.
  • Create custom metrics.
  • Export metrics using Prometheus.
  • Create beautiful dashboards with your metrics.

Getting Started

Download the starter project by clicking the Download Materials button at the top or bottom of this tutorial.

The sample app, BirdTracker, allows you to track birds spotted in certain sections of a national park. Along with the Vapor app, there are docker and other config files you’ll use to set up a Prometheus server and create dashboards.

Be sure to check out Docker on MacOS: Getting Started before continuing this tutorial.

Monitoring Your App in Production

Before you start adding metrics to your apps, it’s good to understand their purpose, significance and industry standards.

As mentioned before, applications will break, databases will fail, and don’t even start to think about all the weird things your users might try to do. Implementing metrics allows you to get a quick, high-level overview of your application’s state. The most common metrics for web applications are requests per minute, response codes per minute and request duration. In this tutorial, you’ll export these metrics, along with custom ones.

The Database: Prometheus

To aggregate and report all these metrics, you’ll use Prometheus, an open-source monitoring system. Prometheus works using a pull model, meaning it asks your application for metrics, pulls them in and stores them. Prometheus then allows you to query this data using its PromQL querying language, designed for querying metrics.

Along with simple metric aggregation, Prometheus allows you to set up alerting as well. Alerting allows you to set up rules and boundaries for your metrics, such as that http requests with a 500 status code can’t be more than 5 percent of the total requests. If this condition became true, Prometheus would send up a flare, indicating something was wrong. Setting up alerts falls outside of the scope of this tutorial, but if you’d like to learn more, check out the docs.

The pull model works by asking for the current value of every metric. It then stores these instants together as a time series that can be queried, grouped and filtered. In most cases, metrics are served on a /metrics endpoint, describing each metric in the following way:

  1. A line designating the metric’s type and name.
  2. An optional line with help text for the metrics.
  3. Lines describing the current value of the metric, optionally split based on labels.

You can use labels to split your metrics further. For example, in the case of a request counter, useful labels are the HTTP status, request path and request method. When retrieving the metrics from Prometheus, use these labels to split successful and unsuccessful requests.

For example, a simple counter with a single label will look something like this:

# TYPE my_counter counter
# HELP my_counter Counting ever upwards
my_counter{label="yes"} 1500
my_counter{label="no"} 1000

The Front End: Grafana

Of course, storing and querying metrics isn’t enough. You’ll also want to make them visible. In this tutorial, you’ll use Grafana. Grafana and Prometheus have built-in integrations with each other, so graphing your metrics in a dashboard is easier than ever. Grafana lets you create graphs, gauges and other visuals and use them in beautiful dashboards.

Enabling Basic Metrics in Vapor 4

With the theory all covered, it’s time to set up metrics of your own. Open the starter project and navigate to your Package.swift. Add the following package as a new dependency:

.package(url: "https://github.com/MrLotU/SwiftPrometheus.git", from: "1.0.0-alpha.15"),

Also, add it under the dependencies of the App target as follows:

.product(name: "SwiftPrometheus", package: "SwiftPrometheus"),

This will pull in two packages: SwiftPrometheus and its dependency swift-metrics. Swift Metrics is a package provided by the SSWG, containing generic metric APIs to create counters, recorders, gauges and timers. Libraries such as Vapor, Fluent and others can create these to provide metrics without it mattering what you, the end dev, want to use as a metrics back end. This is where SwiftPrometheus comes in. SwiftPrometheus implements the APIs provided by Swift Metrics in a Prometheus-specific way, allowing you to expose metrics in a Prometheus fashion.

Adding the Endpoint

Open routes.swift and add the following inside the routes function, below the controller registration:

// 1
app.get("metrics") { req -> EventLoopFuture<String> in
  // 2
  let promise = req.eventLoop.makePromise(of: String.self)

  // 3
  DispatchQueue.global().async {
    do {
      try MetricsSystem.prometheus().collect(into: promise)
    } catch {
      promise.fail(error)
    }
  }

  // 4
  return promise.futureResult
}

Then, add import Metrics and import Prometheus to the top of the file.

This code:

  1. Adds a new /metrics endpoint that Prometheus can scrape.
  2. Creates a promise the metrics will be written to.
  3. On a background queue, collects the Prometheus metrics into the promise or fails with an error.
  4. Returns the promise’s future result as the route response.

Using the Built-in Metrics

The final step to enable the built-in metrics is to tell Swift Metrics to use SwiftPrometheus as its back end. In configure.swift add the following lines below the line adding the database:

let promClient = PrometheusMetricsFactory(client: PrometheusClient())
MetricsSystem.bootstrap(promClient)

Don’t forget to add import Metrics and import Prometheus to the top of the file.

This code instantiates a new PrometheusMetricsFactory, a wrapper around a PrometheusClient to make it work with Swift Metrics, and feeds it to Swift Metrics’ MetricsSystem as its back end.

Now, build and run your app in Xcode by clicking the Play button at the top left or in Terminal by typing swift run.

In a browser, refresh your API a couple times by navigating to localhost:8080. The browser should look like this.

Expected error-response from Vapor

Vapor error response in Safari

Expected error-response from Vapor

Go to /metrics. You’ll see two metrics reported: http_requests_total, a counter for HTTP requests, and http_requests_duration_seconds, a summary of request durations. The output in the browser should look something like this:

# TYPE http_requests_total counter
http_requests_total{status="404", path="vapor_route_undefined", method="undefined"} 6
# TYPE http_request_duration_seconds summary
http_request_duration_seconds{status="200", quantile="0.01", path="/metrics", method="GET"} 0.001937932
http_request_duration_seconds{status="200", quantile="0.05", path="/metrics", method="GET"} 0.002231176
http_request_duration_seconds{status="200", quantile="0.5", path="/metrics", method="GET"} 0.0030973375
http_request_duration_seconds{status="200", quantile="0.9", path="/metrics", method="GET"} 0.003550826
http_request_duration_seconds{status="200", quantile="0.95", path="/metrics", method="GET"} 0.0036431995
http_request_duration_seconds{status="200", quantile="0.99", path="/metrics", method="GET"} 0.003878795
http_request_duration_seconds{status="200", quantile="0.999", path="/metrics", method="GET"} 0.004442741
http_request_duration_seconds_count{status="200", path="/metrics", method="GET"} 1072
http_request_duration_seconds_sum{status="200", path="/metrics", method="GET"} 3.148946925