MetricsKit Metrics

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

To get started, open the sample project for this lesson.

The lesson has some convenience classes and protocols to reduce the boilerplate code you have to write to submit your MetricsKit data. You can find them all in the “MetricsKit” folder of the project

The 'MetricsKit' Folder hierarchy
The 'MetricsKit' Folder hierarchy

To integrate with MetricsKit, only one thing is required: Register a delegate of type MXMetricManagerSubscriber to receive the payload. It sounds very simple and it really is. Before going through those files, start with the basic steps. Go to MetricsKitService.swift and update the implementation of beginCollection() and endCollection() to the following:

public class func endCollection() {
  MXMetricManager.shared.remove(MetricsKitService.instance)
}

public class func beginCollection() {
  MXMetricManager.shared.add(MetricsKitService.instance)
}

Then at the end of the file add this extension to silence the error:

extension MetricsKitService: MXMetricManagerSubscriber {
}

Go to TheMetApp.swift and add the call to beginCollection() in the app init:

init() {
  MetricsKitService.beginCollection()
}

This initializes the static property in MetricsKitService and registers it as the delegate in MXMetricManager.

Now you’re ready to start collecting the data but you didn’t implement the delegate method yet.

Return back to MetricsKitService.swift and add this method in the extension:

public func didReceive(_ payloads: [MXMetricPayload]) {
  
}

iOS will send your app its metrics payloads at-most once per day. This data is not in real time and each payload represents a session from the user. It doesn’t work in the background so the app needs to be launched.

The app will receive a collection of MXMetricPayload. Each item contains all the metrics data for that session. You’ll first need to look over them one-by-one. Add this in the delegate method:

payloads.forEach { payload in

}

Each payload consists of a group of metrics:

  • CPU Metrics
  • GPU Metrics
  • Cellular Conditions
  • Application Time
  • Location Activity
  • Network Transfer
  • Application Launch
  • Application Responsiveness
  • Disk IO
  • Memory
  • Display
  • Animation
  • App Exit
  • SignPost

Some of them are measured values like Network Transfer’s total downloaded data and total uploaded data, others are histograms like Cellular conditions which represent the signal strength for cellular data over the duration the app was open.

In this lesson segment, you’ll implement one of each type: Network Transfer, and Cellular conditions.

First, go through the files that were already added in the starter project:

  • MXMetricsPayload+Attributes.swift: An extension for MXMetricsPayload to extract some of the data already in the payload to OpenTelemetry Attributes.
  • MetricPayloadSender.swift: A helper class to easily convert MetricKit types to histograms, events or logs.
  • HistogrammedMetric.swift: A protocol to define histogram boundaries, the unit name and the metric name for a histogram
  • ValueMetric.swift: A protocol to extract value metrics to a dictionary.

You’re going to send value based metrics as an event. If you’re thinking about sending value metrics as gauges you’ll end up messing up your data because gauges will maintain the last value only, and the values stays as long as the app is open. It would make sense if this data was in real-time, but MetricsKit provides aggregated values that are related to past sessions. Gauges aren’t the right choice here, events are.

As for histogram based data, you’ll send them as group of logs with one for each bucket describing the value or boundary of the bucket, and the count of elements inside it. OpenTelemetry Histograms are also counter based with the intention to monitor the rate of changes of the values in the histogram. However, the goal for the data from MetricKit is to understand the absolute values directly.

In the Project navigator, add a new swift file named MXNetworkTransferMetric.swift under the folder “MetricTypes”. Set the file contents to the following:

import MetricKit

extension MXNetworkTransferMetric: ValueMetric {
  public func toDictionary() -> [String: String] {  // 1
    return [
      "cumulativeWifiUpload": "\(cumulativeWifiUpload.value)",
      "cumulativeWifiDownload": "\(cumulativeWifiDownload.value)",
      "cumulativeCellularUpload": "\(cumulativeCellularUpload.value)",
      "cumulativeCellularDownload": "\(cumulativeCellularDownload.value)"
    ]
  }
  
  public var name: String { "network_transfer" } // 2
}
  1. MXNetworkTransferMetric has 4 values inside it, you’re creating a dictionary of those values to send them as attributes in one event. You’ll have one event representing network transfers, and all the data related to it will be in its attributes.
  2. Set a constant for the event name to be extracted from the metric when you send it.

MetricPayloadSender already has a method to send a ValueMetric object with OTelLog.sendEvent(). Open MetricsKitService.swift again and inside the didReceive(_:) create a new payload sender for each payload. add this inside the loop:

let payloadSender = MetricPayloadSender(attributes: payload.attributes)

You can modify MetricPayloadSender code to create it once before the loop, but that means you’ll need to send payload.attributes with each metric.

After the creation of the sender, add this line:

payloadSender.sendMetric(metric: payload.networkTransferMetrics)

As for sending cellular conditions, Add a new swift file named MXCellularConditionMetric.swift in the folder “MetricTypes” and set its content with the following:

import MetricKit

extension MXCellularConditionMetric: HistogrammedMetric {
  var unit: String {
    "bars"
  }
  var boundaries: [Double] {
    [1, 2, 3]
  }
  var name: String {
    "cellular_condition_metric"
  }
}

Cellular conditions are measured in bars, so it doesn’t make sense to set the boundaries different than the possible number of bars you can have on your phone.

Return back to MetricKitService.swift and add this at the end of the loop:

payloadSender.sendHistogram(
  name: "Cellular_Condition_Time",
  values: payload.cellularConditionMetrics?.histogrammedCellularConditionTime,
  histogramInfo: payload.cellularConditionMetrics)

This sends a histogram from payload.cellularConditionMetrics?.histogrammedCellularConditionTime and the information for its boundaries and unit are extracted from payload.cellularConditionMetrics since cellularConditionMetrics conforms to HistogrammedMetric.

The reason the API here is different as it specified the data of the histogram directly from inside cellularConditionMetrics is because other histogram based metrics contain more than one histograms inside like MXAppLaunchMetric which contains 4 different histograms. You can modify the code in the payload sender to be more convenient to you if you prefer to make it match the value based metrics.

Before running the app to test what you implemented, be aware that its not possible to test MetricsKit from the simulator. You’ll need to build the app on your device.

Open “Signing & Capabilities” for the project and change the Team to your personal account, and change the bundle identifier to something unique.

The 'Signing & Capabilities' page showing that it requires a development team
The 'Signing & Capabilities' page showing that it requires a development team

Build and run the app on your device. From the “Debug” menu at the top of your screen. Select “Simulate MetricKit Payloads”. This will execute your delegate method with testing data not actual data from your app.

The 'Simulate MetricKit Payloads' option inside the 'Debug' menu
The 'Simulate MetricKit Payloads' option inside the 'Debug' menu

Open your Grafana stack and from “Dashboards” create a new visualization.

Select grafanacloud-[STACK_NAME]-logs as the data source

In the query code enter:

{service_name="TheMet"} | event_name="network_transfer" 

Press run query and this should show the list of logs that match this query.

Next step is to format the data so that the graph is able to understand it and plot it. Go to the transformations tab and add the first of type “Extract fields” and set its configuration as follows:

The 'Extract fields' data for the transformations tab
The 'Extract fields' data for the transformations tab

The next one, of type “Organize fields by name” and rename the fields as follows:

Organize fields by name manually
Organize fields by name manually

This step isn’t necessary but having easy to read names on the graph legend can go a very long way.

Last transformation of type “Convert field type”. Change the four fields you renamed to “Number”:

Convert field type with all fields as 'Number'
Convert field type with all fields as 'Number'

Make sure the switch at the top for “Table view” is disabled and you should see a time series graph for your values.

 Time series graph for the values
Time series graph for the values

You can add more fields to represent “TotalDownload” and “TotalUpload” in the graph using the transformation “Add field from calculation”:

Use the 'Add field from calculation' for 'TotalDownload' and 'TotalUpload'
Use the 'Add field from calculation' for 'TotalDownload' and 'TotalUpload'

Save the new dashboard as “Network Transfers”, then create a new dashboard for the cellular conditions histogram.

In the query code, set it to the following:

sum by (event_data_bucketName) (sum_over_time({service_name="TheMet"} | event_name="Cellular_Condition_Time" | unwrap event_data_bucket_count [$__auto]))

Then expand the small dropdown beside Options and set the Legend value to {{event_data_bucketName}}.

Set 'Legend' of 'Options'
Set 'Legend' of 'Options'

Make sure on the right, the visualization Heatmap is selected.

Choose 'Heatmap' for 'Visualization'
Choose 'Heatmap' for 'Visualization'

This query might seem a little more complicated. This histogram is sent over 3 logs, 1 for each bucket [1, 2, 3]

To break down the query:

sum by (event_data_bucketName)      // 1
(sum_over_time(     // 2
  {service_name="TheMet"} | event_name="Cellular_Condition_Time"    // 3
  | unwrap event_data_bucket_count [$__auto]    // 4
))
  1. Sum of the value in the expression covered by points 2 to 4 while grouping by the field “event_data_bucketName”.
  2. Sum of the logs within the same period of time since the graph will consider time of all logs. This is to ensure that the visualization aggregates all data.
  3. Filter logs with service name of “TheMet” and event name of “Cellular_Condition_Time”
  4. Use the field “event_data_bucket_count” for the sum of point 1 and unwrap that value to cover a duration automatically selected by the graph. You can also use [5m], [15m], [1h] but it might cause confusion. But feel free to try.

Setting the field name as {{event_data_bucketName}} for the legend allows the graph to understand which values to set on the Y-Axis.

The heatmap settings should automatically change Calculate from data to No. If the legend is not correct ensure its not set to Yes as this would ignore the options.

Set the Calculate from data setting to 'No'
Set the Calculate from data setting to 'No'

The rest of the metrics will work the same, either metric based or histogram based. Feel free to add some on your own, or open the final project where all the metrics are added.

In the next segment, you’ll cover the other half of MetricKit: Diagnostics.

See forum comments
Download course materials from Github
Previous: Introduction Next: MetricsKit Diagnostics