Measure Internet Connectivity in number of 9s


AWS S3 offers 99.999999999% (11 9s) of data durability and 99.99% availability.

I wonder how the measurement for my home internet would be.

In this post, I’ll explain how the measurement was performed in detail, the configurations I used, as well as the Grafana dashboard I created.

TL;DR

Here’s the “internet connectivity level” in the sense of the number of 9s, measured for the past 90 days (2022-05-27 to 2022-08-25), by periodically probing a series of well-known servers on the internet from my home lab, with both HTTPS and DNS requests.

The metrics are presented in the number of 9s. In other words, a measure of 3 means 99.9% of the probing requests were successful. Likewise, 4 means 99.99% of requests went through.

TargetConnectivity (in number of 9s)Probing Method
https://1.1.1.13.7HTTP
https://connectivitycheck.gstatic.com3.7HTTP
https://www.gstatic.com3.7HTTP
http://www.msftconnecttest.com3.6HTTP
https://cp.cloudflare.com3.5HTTP
1.0.0.13.1DNS
1.1.1.13.1DNS
8.8.8.83.1DNS
8.8.4.43.1DNS
https://captive.apple.com2.9HTTP
https://www.apple.com2.6HTTP
Internet Connectivity Measured in Number of 9s

Higher than 99.9% (3.7 9s, more on the decimal part later) of uptime is quite good for home internet. HTTP requests are generally more reliable than DNS requests, which correlates to the reliable nature of TCP over UDP.

The Setup

This monitoring is done by using the prometheus/blackbox_exporter and Prometheus, visualization is done via Grafana. Everything is running inside of Docker.

Here’s the docker-compose services to start all the related processes. Remember to update the config to fit your setup.

# docker-compose.yaml
version: "2.4"
services:
  prometheus:
    image: prom/prometheus
    volumes:
      - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
      - ./prometheus/rules:/rules:ro
      - prometheus_data:/prometheus
    command:
      - '--config.file=/etc/prometheus/prometheus.yml'
      - '--storage.tsdb.path=/prometheus'
      - '--storage.tsdb.retention.time=500d'
      - '--web.console.libraries=/usr/share/prometheus/console_libraries'
      - '--web.console.templates=/usr/share/prometheus/consoles'
      - '--web.external-url=[YOUR SERVER URL]'
      - '--web.enable-admin-api'
    networks:
      - monitoring
      - default
    restart: always

  blackbox-exporter:
    image: prom/blackbox-exporter
    volumes:
      - ./blackbox-exporter/conf.yaml:/conf.yaml
    networks:
      - monitoring
    restart: always
    command:
      - --config.file=/conf.yaml

  grafana:
    image: grafana/grafana
    volumes:
      - grafana_data:/var/lib/grafana
    environment:
      - GF_SERVER_PROTOCOL=http
      - GF_INSTALL_PLUGINS=natel-discrete-panel
      - GF_SECURITY_ADMIN_USER=[Your gmail or google workspace email]
      # OAuth with Google
      - GF_SERVER_DOMAIN=[Your Grafana Server Domain]
      - GF_SERVER_ROOT_URL=https://%(domain)s/
      - GF_AUTH_ANONYMOUS_ENABLED=false
      - GF_AUTH_DISABLE_LOGIN_FORM=true
      - GF_AUTH_BASIC_ENABLED=false
      - GF_AUTH_GOOGLE_ENABLED=true
      - GF_AUTH_GOOGLE_SCOPES=https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email
      - GF_AUTH_GOOGLE_CLIENT_ID=[Client ID]
      - GF_AUTH_GOOGLE_CLIENT_SECRET=[Client Secret]
      - GF_AUTH_GOOGLE_AUTH_URL=https://accounts.google.com/o/oauth2/auth
      - GF_AUTH_GOOGLE_TOKEN_URL=https://accounts.google.com/o/oauth2/token
      - GF_AUTH_GOOGLE_ALLOWED_DOMAINS=[email domain]
      - GF_AUTH_GOOGLE_ALLOW_SIGN_UP=true
    restart: always

networks:
  monitoring: {}

volumes:
  grafana_data: {}
  prometheus_data: { }

The individual config files as mounted in the docker-compose config:

# ./prometheus/prometheus.yml
global:
  scrape_interval: 10s
  evaluation_interval: 15s

scrape_configs:
  - job_name: internet-connectivity
    scrape_interval: 5s
    metrics_path: /probe
    params:
      module:
        - internet-connectivity
    static_configs:
      - targets:
        - https://www.gstatic.com/generate_204
        - https://connectivitycheck.gstatic.com/generate_204
        - https://captive.apple.com/hotspot-detect.html
        - https://www.apple.com/library/test/success.html
        - https://cp.cloudflare.com/
        - http://www.msftconnecttest.com/connecttest.txt
        - https://1.1.1.1/cdn-cgi/trace
    relabel_configs:
      - source_labels: [__address__]
        target_label: __param_target
      - source_labels: [__param_target]
        target_label: instance
      - target_label: __address__
        replacement: blackbox-exporter:9115

  - job_name: dns-upstream-health
    scrape_interval: 5s
    metrics_path: /probe
    params:
      module:
        - dns-upstream-health
    static_configs:
      - targets:
        - 8.8.8.8
        - 8.8.4.4
        - 1.1.1.1
        - 1.0.0.1
    relabel_configs:
      - source_labels: [__address__]
        target_label: __param_target
      - source_labels: [__param_target]
        target_label: instance
      - target_label: __address__
        replacement: blackbox-exporter:9115
# ./blackbox-exporter/conf.yaml
modules:
  internet-connectivity:
    prober: http
    timeout: 3s
    http:
      preferred_ip_protocol: ip4
      ip_protocol_fallback: false
  dns-upstream-health:
    prober: dns
    timeout: 2s
    dns:
      query_name: google.com
      query_type: A
      preferred_ip_protocol: ip4
      ip_protocol_fallback: false

In Grafana, use the following Prometheus query to construct and calculate the “number of 9s”:

sort_desc(log10(count_over_time(probe_success[$__range])) - log10(count_over_time(probe_success[$__range]) - sum_over_time(probe_success[$__range])))

Alternatively, refer to the following panel config blocks to replicate the exact same visualizations as mine. Note natel-discrete-panel plugin is used for one of the visualizations.

For the “number of 9s” view:

{
  "id": 553,
  "gridPos": {
    "h": 6,
    "w": 4,
    "x": 20,
    "y": 0
  },
  "type": "stat",
  "title": "Service Level",
  "transformations": [
    {
      "id": "renameByRegex",
      "options": {
        "regex": "(https?://[^/]*)/?.*",
        "renamePattern": "$1"
      }
    }
  ],
  "pluginVersion": "9.1.0",
  "description": "by counting of 9s, i.e. 99.9994% availability has a service level of 5. ",
  "fieldConfig": {
    "defaults": {
      "mappings": [],
      "thresholds": {
        "mode": "absolute",
        "steps": [
          {
            "color": "red",
            "value": null
          },
          {
            "color": "orange",
            "value": 2
          },
          {
            "color": "yellow",
            "value": 3
          },
          {
            "color": "green",
            "value": 4
          }
        ]
      },
      "color": {
        "mode": "thresholds"
      },
      "decimals": 1
    },
    "overrides": []
  },
  "options": {
    "reduceOptions": {
      "values": false,
      "calcs": [
        "lastNotNull"
      ],
      "fields": ""
    },
    "orientation": "auto",
    "textMode": "auto",
    "colorMode": "value",
    "graphMode": "none",
    "justifyMode": "auto"
  },
  "targets": [
    {
      "datasource": {
        "type": "prometheus",
        "uid": "000000002"
      },
      "exemplar": false,
      "expr": "sort_desc(log10(count_over_time(probe_success[$__range])) - log10(count_over_time(probe_success[$__range]) - sum_over_time(probe_success[$__range])))",
      "hide": false,
      "instant": true,
      "interval": "",
      "legendFormat": "{{ instance }}",
      "refId": "A"
    }
  ],
  "datasource": {
    "type": "prometheus",
    "uid": "000000002"
  }
}

For the timeline breakdown:

{
  "id": 551,
  "gridPos": {
    "h": 6,
    "w": 12,
    "x": 8,
    "y": 0
  },
  "type": "natel-discrete-panel",
  "title": "Connectivity",
  "transformations": [
    {
      "id": "renameByRegex",
      "options": {
        "regex": "(https?://[^/]*)/?.*",
        "renamePattern": "$1"
      }
    }
  ],
  "backgroundColor": "rgba(128,128,128,0.1)",
  "colorMaps": [
    {
      "$$hashKey": "object:288",
      "color": "#CCC",
      "text": "N/A"
    },
    {
      "$$hashKey": "object:291",
      "color": "#73BF69",
      "text": "Up"
    },
    {
      "$$hashKey": "object:294",
      "color": "#F2495C",
      "text": "Down"
    }
  ],
  "crosshairColor": "#8F070C",
  "description": "",
  "display": "timeline",
  "expandFromQueryS": 0,
  "extendLastValue": true,
  "highlightOnMouseover": false,
  "legendPercentDecimals": 2,
  "legendSortBy": "-ms",
  "lineColor": "rgba(0,0,0,0.1)",
  "metricNameColor": "#000000",
  "rangeMaps": [
    {
      "$$hashKey": "object:304",
      "from": "null",
      "text": "N/A",
      "to": "null"
    }
  ],
  "rowHeight": 15,
  "showDistinctCount": false,
  "showLegend": true,
  "showLegendCounts": false,
  "showLegendNames": true,
  "showLegendPercent": true,
  "showLegendTime": true,
  "showLegendValues": true,
  "showTimeAxis": true,
  "showTransitionCount": false,
  "targets": [
    {
      "datasource": {
        "type": "prometheus",
        "uid": "000000002"
      },
      "exemplar": true,
      "expr": "probe_success",
      "format": "time_series",
      "instant": false,
      "interval": "",
      "legendFormat": "{{ instance }}",
      "refId": "A"
    }
  ],
  "textSize": 12,
  "textSizeTime": 12,
  "timeOptions": [
    {
      "name": "Years",
      "value": "years"
    },
    {
      "name": "Months",
      "value": "months"
    },
    {
      "name": "Weeks",
      "value": "weeks"
    },
    {
      "name": "Days",
      "value": "days"
    },
    {
      "name": "Hours",
      "value": "hours"
    },
    {
      "name": "Minutes",
      "value": "minutes"
    },
    {
      "name": "Seconds",
      "value": "seconds"
    },
    {
      "name": "Milliseconds",
      "value": "milliseconds"
    }
  ],
  "timePrecision": {
    "name": "Minutes",
    "value": "minutes"
  },
  "timeTextColor": "#d8d9da",
  "units": "short",
  "use12HourClock": true,
  "useTimePrecision": false,
  "valueMaps": [
    {
      "$$hashKey": "object:302",
      "op": "=",
      "text": "N/A",
      "value": "null"
    },
    {
      "$$hashKey": "object:306",
      "op": "=",
      "text": "Up",
      "value": "1"
    },
    {
      "$$hashKey": "object:308",
      "op": "=",
      "text": "Down",
      "value": "0"
    }
  ],
  "valueTextColor": "#000000",
  "writeAllValues": false,
  "writeLastValue": false,
  "writeMetricNames": true,
  "datasource": {
    "type": "prometheus",
    "uid": "000000002"
  }
}

Leave a Reply

Your email address will not be published. Required fields are marked *