Repository: kopia/kopia
Pull request #2536 (2022-11-01 23:01:48)

build(deps): bump minimist from 1.2.6 to 1.2.7 in /app
Bumps [minimist](https://github.com/minimistjs/minimist) from 1.2.6 to 1.2.7.
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a href="https://github.com/minimistjs/minimist/blob/main/CHANGELOG.md">minimist's changelog</a>.</em></p>
<blockquote>
<h2><a href="https://github.com/minimistjs/minimist/compare/v1.2.6...v1.2.7">v1.2.7</a> - 2022-10-10</h2>
<h3>Commits</h3>
<ul>
<li>[meta] add <code>auto-changelog</code> <a href="https://github.com/minimistjs/minimist/commit/0ebf4ebcd5f7787a5524d31a849ef41316b83c3c"><code>0ebf4eb</code></a></li>
<li>[actions] add reusable workflows <a href="https://github.com/minimistjs/minimist/commit/e115b63fa9d3909f33b00a2db647ff79068388de"><code>e115b63</code></a></li>
<li>[eslint] add eslint; rules to enable later are warnings <a href="https://github.com/minimistjs/minimist/commit/f58745b9bb84348e1be72af7dbba5840c7c13013"><code>f58745b</code></a></li>
<li>[Dev Deps] switch from <code>covert</code> to <code>nyc</code> <a href="https://github.com/minimistjs/minimist/commit/ab033567b9c8b31117cb026dc7f1e592ce455c65"><code>ab03356</code></a></li>
<li>[readme] rename and add badges <a href="https://github.com/minimistjs/minimist/commit/236f4a07e4ebe5ee44f1496ec6974991ab293ffd"><code>236f4a0</code></a></li>
<li>[meta] create FUNDING.yml; add <code>funding</code> in package.json <a href="https://github.com/minimistjs/minimist/commit/783a49bfd47e8335d3098a8cac75662cf71eb32a"><code>783a49b</code></a></li>
<li>[meta] use <code>npmignore</code> to autogenerate an npmignore file <a href="https://github.com/minimistjs/minimist/commit/f81ece6aaec2fa14e69ff4f1e0407a8c4e2635a2"><code>f81ece6</code></a></li>
<li>Only apps should have lockfiles <a href="https://github.com/minimistjs/minimist/commit/56cad44c7f879b9bb5ec18fcc349308024a89bfc"><code>56cad44</code></a></li>
<li>[Dev Deps] update <code>covert</code>, <code>tape</code>; remove unnecessary <code>tap</code> <a href="https://github.com/minimistjs/minimist/commit/49c5f9fb7e6a92db9eb340cc679de92fb3aacded"><code>49c5f9f</code></a></li>
<li>[Tests] add <code>aud</code> in <code>posttest</code> <a href="https://github.com/minimistjs/minimist/commit/228ae938f3cd9db9dfd8bd7458b076a7b2aef280"><code>228ae93</code></a></li>
<li>[meta] add <code>safe-publish-latest</code> <a href="https://github.com/minimistjs/minimist/commit/01fc23f5104f85c75059972e01dd33796ab529ff"><code>01fc23f</code></a></li>
<li>[meta] update repo URLs <a href="https://github.com/minimistjs/minimist/commit/6b164c7d68e0b6bf32f894699effdfb7c63041dd"><code>6b164c7</code></a></li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/minimistjs/minimist/commit/c590d75b741a12b5423e2b299f38a7f7c7d25a18"><code>c590d75</code></a> v1.2.7</li>
<li><a href="https://github.com/minimistjs/minimist/commit/0ebf4ebcd5f7787a5524d31a849ef41316b83c3c"><code>0ebf4eb</code></a> [meta] add <code>auto-changelog</code></li>
<li><a href="https://github.com/minimistjs/minimist/commit/e115b63fa9d3909f33b00a2db647ff79068388de"><code>e115b63</code></a> [actions] add reusable workflows</li>
<li><a href="https://github.com/minimistjs/minimist/commit/01fc23f5104f85c75059972e01dd33796ab529ff"><code>01fc23f</code></a> [meta] add <code>safe-publish-latest</code></li>
<li><a href="https://github.com/minimistjs/minimist/commit/f58745b9bb84348e1be72af7dbba5840c7c13013"><code>f58745b</code></a> [eslint] add eslint; rules to enable later are warnings</li>
<li><a href="https://github.com/minimistjs/minimist/commit/228ae938f3cd9db9dfd8bd7458b076a7b2aef280"><code>228ae93</code></a> [Tests] add <code>aud</code> in <code>posttest</code></li>
<li><a href="https://github.com/minimistjs/minimist/commit/236f4a07e4ebe5ee44f1496ec6974991ab293ffd"><code>236f4a0</code></a> [readme] rename and add badges</li>
<li><a href="https://github.com/minimistjs/minimist/commit/ab033567b9c8b31117cb026dc7f1e592ce455c65"><code>ab03356</code></a> [Dev Deps] switch from <code>covert</code> to <code>nyc</code></li>
<li><a href="https://github.com/minimistjs/minimist/commit/49c5f9fb7e6a92db9eb340cc679de92fb3aacded"><code>49c5f9f</code></a> [Dev Deps] update <code>covert</code>, <code>tape</code>; remove unnecessary <code>tap</code></li>
<li><a href="https://github.com/minimistjs/minimist/commit/783a49bfd47e8335d3098a8cac75662cf71eb32a"><code>783a49b</code></a> [meta] create FUNDING.yml; add <code>funding</code> in package.json</li>
<li>Additional commits viewable in <a href="https://github.com/minimistjs/minimist/compare/v1.2.6...v1.2.7">compare view</a></li>
</ul>
</details>
<details>
<summary>Maintainer changes</summary>
<p>This version was pushed to npm by <a href="https://www.npmjs.com/~ljharb">ljharb</a>, a new releaser for minimist since your current version.</p>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=minimist&package-manager=npm_and_yarn&previous-version=1.2.6&new-version=1.2.7)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)


</details>

codecov[bot]: # [Codecov](https://codecov.io/gh/kopia/kopia/pull/2536?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia) Report
Base: **74.50**% // Head: **74.48**% // Decreases project coverage by **`-0.02%`** :warning:
> Coverage data is based on head [(`9ec309a`)](https://codecov.io/gh/kopia/kopia/pull/2536?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia) compared to base [(`b790779`)](https://codecov.io/gh/kopia/kopia/commit/b790779533d87ee9ec567ebf812d59e7f3142392?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia).
> Patch has no changes to coverable lines.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master    #2536      +/-   ##
==========================================
- Coverage   74.50%   74.48%   -0.03%     
==========================================
  Files         442      442              
  Lines       34891    34891              
==========================================
- Hits        25995    25987       -8     
- Misses       7014     7019       +5     
- Partials     1882     1885       +3     
```


| [Impacted Files](https://codecov.io/gh/kopia/kopia/pull/2536?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia) | Coverage Δ | |
|---|---|---|
| [internal/dirutil/mksubdirall.go](https://codecov.io/gh/kopia/kopia/pull/2536/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia#diff-aW50ZXJuYWwvZGlydXRpbC9ta3N1YmRpcmFsbC5nbw==) | `92.10% <0.00%> (-7.90%)` | :arrow_down: |
| [cli/command\_content\_verify.go](https://codecov.io/gh/kopia/kopia/pull/2536/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia#diff-Y2xpL2NvbW1hbmRfY29udGVudF92ZXJpZnkuZ28=) | `75.00% <0.00%> (-6.04%)` | :arrow_down: |
| [fs/ignorefs/ignorefs.go](https://codecov.io/gh/kopia/kopia/pull/2536/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia#diff-ZnMvaWdub3JlZnMvaWdub3JlZnMuZ28=) | `77.27% <0.00%> (-1.24%)` | :arrow_down: |
| [repo/content/index/index\_v1.go](https://codecov.io/gh/kopia/kopia/pull/2536/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia#diff-cmVwby9jb250ZW50L2luZGV4L2luZGV4X3YxLmdv) | `75.00% <0.00%> (-1.18%)` | :arrow_down: |
| [internal/epoch/epoch\_manager.go](https://codecov.io/gh/kopia/kopia/pull/2536/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia#diff-aW50ZXJuYWwvZXBvY2gvZXBvY2hfbWFuYWdlci5nbw==) | `84.81% <0.00%> (-0.49%)` | :arrow_down: |
| [repo/grpc\_repository\_client.go](https://codecov.io/gh/kopia/kopia/pull/2536/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia#diff-cmVwby9ncnBjX3JlcG9zaXRvcnlfY2xpZW50Lmdv) | `76.77% <0.00%> (+0.67%)` | :arrow_up: |
| [repo/open.go](https://codecov.io/gh/kopia/kopia/pull/2536/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia#diff-cmVwby9vcGVuLmdv) | `75.73% <0.00%> (+1.10%)` | :arrow_up: |
| [internal/cache/persistent\_lru\_cache.go](https://codecov.io/gh/kopia/kopia/pull/2536/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia#diff-aW50ZXJuYWwvY2FjaGUvcGVyc2lzdGVudF9scnVfY2FjaGUuZ28=) | `97.80% <0.00%> (+1.75%)` | :arrow_up: |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia)

</details>

[:umbrella: View full report at Codecov](https://codecov.io/gh/kopia/kopia/pull/2536?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia).   
:loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia).


Patch:
diff --git a/app/package-lock.json b/app/package-lock.json
index eb7bbb11..24ed5f58 100644
--- a/app/package-lock.json
+++ b/app/package-lock.json
@@ -14,7 +14,7 @@
         "electron-is-dev": "^2.0.0",
         "electron-log": "^4.4.8",
         "electron-updater": "^5.2.1",
-        "minimist": "^1.2.6",
+        "minimist": "^1.2.7",
         "uuid": "^8.3.2"
       },
       "devDependencies": {
@@ -24,7 +24,7 @@
         "dotenv": "^16.0.3",
         "electron": "^19.0.8",
         "electron-builder": "^23.3.3",
-        "electron-notarize": "^1.2.1",
+        "electron-notarize": "^1.2.2",
         "playwright": "^1.26.1",
         "playwright-core": "^1.26.0"
       }
@@ -1622,9 +1622,10 @@
       "integrity": "sha512-QQ4GvrXO+HkgqqEOYbi+DHL7hj5JM+nHi/j+qrN9zeeXVKy8ZABgbu4CnG+BBqDZ2+tbeq9tUC4DZfIWFU5AZA=="
     },
     "node_modules/electron-notarize": {
-      "version": "1.2.1",
-      "resolved": "https://registry.npmjs.org/electron-notarize/-/electron-notarize-1.2.1.tgz",
-      "integrity": "sha512-u/ECWhIrhkSQpZM4cJzVZ5TsmkaqrRo5LDC/KMbGF0sPkm53Ng59+M0zp8QVaql0obfJy9vlVT+4iOkAi2UDlA==",
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/electron-notarize/-/electron-notarize-1.2.2.tgz",
+      "integrity": "sha512-ZStVWYcWI7g87/PgjPJSIIhwQXOaw4/XeXU+pWqMMktSLHaGMLHdyPPN7Cmao7+Cr7fYufA16npdtMndYciHNw==",
+      "deprecated": "Please use @electron/notarize moving forward.  There is no API change, just a package name change",
       "dev": true,
       "dependencies": {
         "debug": "^4.1.1",
@@ -2657,9 +2658,12 @@
       }
     },
     "node_modules/minimist": {
-      "version": "1.2.6",
-      "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
-      "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q=="
+      "version": "1.2.7",
+      "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz",
+      "integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==",
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
     },
     "node_modules/minipass": {
       "version": "3.3.4",
@@ -4967,9 +4971,9 @@
       "integrity": "sha512-QQ4GvrXO+HkgqqEOYbi+DHL7hj5JM+nHi/j+qrN9zeeXVKy8ZABgbu4CnG+BBqDZ2+tbeq9tUC4DZfIWFU5AZA=="
     },
     "electron-notarize": {
-      "version": "1.2.1",
-      "resolved": "https://registry.npmjs.org/electron-notarize/-/electron-notarize-1.2.1.tgz",
-      "integrity": "sha512-u/ECWhIrhkSQpZM4cJzVZ5TsmkaqrRo5LDC/KMbGF0sPkm53Ng59+M0zp8QVaql0obfJy9vlVT+4iOkAi2UDlA==",
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/electron-notarize/-/electron-notarize-1.2.2.tgz",
+      "integrity": "sha512-ZStVWYcWI7g87/PgjPJSIIhwQXOaw4/XeXU+pWqMMktSLHaGMLHdyPPN7Cmao7+Cr7fYufA16npdtMndYciHNw==",
       "dev": true,
       "requires": {
         "debug": "^4.1.1",
@@ -5777,9 +5781,9 @@
       }
     },
     "minimist": {
-      "version": "1.2.6",
-      "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
-      "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q=="
+      "version": "1.2.7",
+      "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz",
+      "integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g=="
     },
     "minipass": {
       "version": "3.3.4",
diff --git a/app/package.json b/app/package.json
index 8d29fe4f..076d632a 100644
--- a/app/package.json
+++ b/app/package.json
@@ -8,7 +8,7 @@
     "electron-is-dev": "^2.0.0",
     "electron-log": "^4.4.8",
     "electron-updater": "^5.2.1",
-    "minimist": "^1.2.6",
+    "minimist": "^1.2.7",
     "uuid": "^8.3.2"
   },
   "author": {
@@ -116,7 +116,7 @@
     "dotenv": "^16.0.3",
     "electron": "^19.0.8",
     "electron-builder": "^23.3.3",
-    "electron-notarize": "^1.2.1",
+    "electron-notarize": "^1.2.2",
     "playwright": "^1.26.1",
     "playwright-core": "^1.26.0"
   },
diff --git a/go.mod b/go.mod
index 6c9abfa1..c4268c3a 100644
--- a/go.mod
+++ b/go.mod
@@ -3,13 +3,13 @@ module github.com/kopia/kopia
 go 1.19
 
 require (
-	cloud.google.com/go/storage v1.27.0
+	cloud.google.com/go/storage v1.28.0
 	github.com/Azure/azure-pipeline-go v0.2.3 // indirect
 	github.com/Azure/azure-storage-blob-go v0.15.0
 	github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962 // indirect
 	github.com/alecthomas/kingpin v1.3.8-0.20220615105907-eae6867f4166 // this is pulling master, which is newer than v2
 	github.com/alecthomas/units v0.0.0-20210927113745-59d0afb8317a
-	github.com/aws/aws-sdk-go v1.44.125
+	github.com/aws/aws-sdk-go v1.44.129
 	github.com/chmduquesne/rollinghash v4.0.0+incompatible
 	github.com/dustinkirkland/golang-petname v0.0.0-20191129215211-8e5a1ed0cff0
 	github.com/fatih/color v1.13.0
@@ -24,13 +24,13 @@ require (
 	github.com/google/uuid v1.3.0
 	github.com/gorilla/mux v1.8.0
 	github.com/hanwen/go-fuse/v2 v2.1.1-0.20220112183258-f57e95bda82d
-	github.com/klauspost/compress v1.15.11
+	github.com/klauspost/compress v1.15.12
 	github.com/klauspost/cpuid/v2 v2.1.1 // indirect
 	github.com/klauspost/pgzip v1.2.5
 	github.com/kylelemons/godebug v1.1.0
 	github.com/mattn/go-colorable v0.1.13
 	github.com/minio/md5-simd v1.1.2 // indirect
-	github.com/minio/minio-go/v7 v7.0.41
+	github.com/minio/minio-go/v7 v7.0.43
 	github.com/minio/sha256-simd v1.0.0 // indirect
 	github.com/natefinch/atomic v1.0.1
 	github.com/pierrec/lz4 v2.6.1+incompatible
@@ -38,26 +38,26 @@ require (
 	github.com/pkg/profile v1.6.0
 	github.com/pkg/sftp v1.13.5
 	github.com/pquerna/ffjson v0.0.0-20190930134022-aa0246cd15f7 // indirect
-	github.com/prometheus/client_golang v1.13.0
+	github.com/prometheus/client_golang v1.14.0
 	github.com/sanity-io/litter v1.5.5
 	github.com/sirupsen/logrus v1.9.0 // indirect
 	github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966
-	github.com/stretchr/testify v1.8.0
+	github.com/stretchr/testify v1.8.1
 	github.com/studio-b12/gowebdav v0.0.0-20211106090535-29e74efa701f
 	github.com/tg123/go-htpasswd v1.2.0
 	github.com/zalando/go-keyring v0.2.1
 	github.com/zeebo/blake3 v0.2.3
-	go.opencensus.io v0.23.0 // indirect
-	go.uber.org/zap v1.21.0
-	golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa
-	golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4
-	golang.org/x/net v0.0.0-20221012135044-0b7e1fb9d458
-	golang.org/x/oauth2 v0.0.0-20221006150949-b44042a4b9c1
+	go.opencensus.io v0.24.0 // indirect
+	go.uber.org/zap v1.23.0
+	golang.org/x/crypto v0.1.0
+	golang.org/x/mod v0.6.0
+	golang.org/x/net v0.1.0
+	golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783
 	golang.org/x/sync v0.1.0
-	golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec
-	golang.org/x/term v0.0.0-20210927222741-03fcf44c2211
+	golang.org/x/sys v0.1.0
+	golang.org/x/term v0.1.0
 	golang.org/x/text v0.4.0
-	google.golang.org/api v0.101.0
+	google.golang.org/api v0.103.0
 	google.golang.org/grpc v1.50.1
 	google.golang.org/protobuf v1.28.1
 	gopkg.in/ini.v1 v1.66.6 // indirect
@@ -66,7 +66,7 @@ require (
 )
 
 require (
-	cloud.google.com/go v0.104.0 // indirect
+	cloud.google.com/go v0.105.0 // indirect
 	github.com/Azure/azure-sdk-for-go/sdk/azcore v0.21.1 // indirect
 	github.com/Azure/azure-sdk-for-go/sdk/internal v0.8.3 // indirect
 	github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v0.3.0
@@ -80,7 +80,7 @@ require (
 	github.com/godbus/dbus/v5 v5.0.6 // indirect
 	github.com/golang/glog v1.0.0 // indirect
 	github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
-	github.com/googleapis/gax-go/v2 v2.6.0 // indirect
+	github.com/googleapis/gax-go/v2 v2.7.0 // indirect
 	github.com/jmespath/go-jmespath v0.4.0 // indirect
 	github.com/json-iterator/go v1.1.12 // indirect
 	github.com/kopia/htmluibuild v0.0.0-20220928042710-9fdd02afb1e7
@@ -91,7 +91,6 @@ require (
 	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
 	github.com/modern-go/reflect2 v1.0.2 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
-	github.com/prometheus/client_model v0.2.0 // indirect
 	github.com/prometheus/common v0.37.0
 	github.com/prometheus/procfs v0.8.0 // indirect
 	github.com/rs/xid v1.4.0 // indirect
@@ -99,7 +98,7 @@ require (
 	go.uber.org/multierr v1.8.0
 	golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
 	google.golang.org/appengine v1.6.7 // indirect
-	google.golang.org/genproto v0.0.0-20221018160656-63c7b68cfc55 // indirect
+	google.golang.org/genproto v0.0.0-20221027153422-115e99e71e1c // indirect
 )
 
 require (
@@ -107,16 +106,19 @@ require (
 	github.com/chromedp/chromedp v0.8.6
 	github.com/edsrzf/mmap-go v1.1.0
 	github.com/hashicorp/golang-lru v0.5.4
-	github.com/klauspost/reedsolomon v1.11.0
+	github.com/klauspost/reedsolomon v1.11.1
+	github.com/prometheus/client_model v0.3.0
 	go.opentelemetry.io/otel v1.11.1
-	go.opentelemetry.io/otel/exporters/jaeger v1.11.0
+	go.opentelemetry.io/otel/exporters/jaeger v1.11.1
 	go.opentelemetry.io/otel/sdk v1.11.1
 	go.opentelemetry.io/otel/trace v1.11.1
+	golang.org/x/exp v0.0.0-20221028150844-83b7d23a625f
 )
 
 require (
-	cloud.google.com/go/compute v1.10.0 // indirect
-	cloud.google.com/go/iam v0.3.0 // indirect
+	cloud.google.com/go/compute v1.12.1 // indirect
+	cloud.google.com/go/compute/metadata v0.2.1 // indirect
+	cloud.google.com/go/iam v0.6.0 // indirect
 	github.com/alessio/shellescape v1.4.1 // indirect
 	github.com/chromedp/sysutil v1.0.0 // indirect
 	github.com/go-logr/logr v1.2.3 // indirect
diff --git a/go.sum b/go.sum
index 0f3148b1..3ac13d52 100644
--- a/go.sum
+++ b/go.sum
@@ -13,36 +13,23 @@ cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKV
 cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
 cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
 cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
-cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
-cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
-cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg=
-cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8=
-cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0=
-cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY=
-cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM=
-cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY=
-cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ=
-cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI=
-cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4=
-cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc=
-cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA=
-cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A=
-cloud.google.com/go v0.104.0 h1:gSmWO7DY1vOm0MVU6DNXM11BWHHsTUmsC5cv1fuW5X8=
-cloud.google.com/go v0.104.0/go.mod h1:OO6xxXdJyvuJPcEPBLN9BJPD+jep5G1+2U5B5gkRYtA=
+cloud.google.com/go v0.105.0 h1:DNtEKRBAAzeS4KyIory52wWHuClNaXJ5x1F7xa4q+5Y=
+cloud.google.com/go v0.105.0/go.mod h1:PrLgOJNe5nfE9UMxKxgXj4mD3voiP+YQ6gdt6KMFOKM=
 cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
 cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
 cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
 cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
 cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
 cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
-cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow=
-cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM=
-cloud.google.com/go/compute v1.10.0 h1:aoLIYaA1fX3ywihqpBk2APQKOo20nXsp1GEZQbx5Jk4=
-cloud.google.com/go/compute v1.10.0/go.mod h1:ER5CLbMxl90o2jtNbGSbtfOpQKR0t15FOtRsugnLrlU=
+cloud.google.com/go/compute v1.12.1 h1:gKVJMEyqV5c/UnpzjjQbo3Rjvvqpr9B1DFSbJC4OXr0=
+cloud.google.com/go/compute v1.12.1/go.mod h1:e8yNOBcBONZU1vJKCvCoDw/4JQsA0dpM4x/6PIIOocU=
+cloud.google.com/go/compute/metadata v0.2.1 h1:efOwf5ymceDhK6PKMnnrTHP4pppY5L22mle96M1yP48=
+cloud.google.com/go/compute/metadata v0.2.1/go.mod h1:jgHgmJd2RKBGzXqF5LR2EZMGxBkeanZ9wwa75XHJgOM=
 cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
 cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
-cloud.google.com/go/iam v0.3.0 h1:exkAomrVUuzx9kWFI1wm3KI0uoDeUFPB4kKGzx6x+Gc=
-cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY=
+cloud.google.com/go/iam v0.6.0 h1:nsqQC88kT5Iwlm4MeNGTpfMWddp6NB/UOLFTH6m1QfQ=
+cloud.google.com/go/iam v0.6.0/go.mod h1:+1AH33ueBne5MzYccyMHtEKqLE4/kJOibtffMHDMFMc=
+cloud.google.com/go/longrunning v0.1.1 h1:y50CXG4j0+qvEukslYFBCrzaXX0qpFbBzc3PchSu/LE=
 cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
 cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
 cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
@@ -52,8 +39,8 @@ cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0Zeo
 cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
 cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
 cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
-cloud.google.com/go/storage v1.27.0 h1:YOO045NZI9RKfCj1c5A/ZtuuENUc8OAW+gHdGnDgyMQ=
-cloud.google.com/go/storage v1.27.0/go.mod h1:x9DOL8TK/ygDUMieqwfhdpQryTeEkhGKMi80i/iqR2s=
+cloud.google.com/go/storage v1.28.0 h1:DLrIZ6xkeZX6K70fU/boWx5INJumt6f+nwwWSHXzzGY=
+cloud.google.com/go/storage v1.28.0/go.mod h1:qlgZML35PXA3zoEnIkiPLY4/TOkUleufRlu6qmcf7sI=
 dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
 github.com/Azure/azure-pipeline-go v0.2.3 h1:7U9HBg1JFK3jHl5qmo4CTZKFTVgMwdFHMVtCdfBE21U=
 github.com/Azure/azure-pipeline-go v0.2.3/go.mod h1:x841ezTBIMG6O3lAcl8ATHnsOPVl2bqk7S3ta6S6u4k=
@@ -82,7 +69,6 @@ github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym
 github.com/GehirnInc/crypt v0.0.0-20190301055215-6c0105aabd46/go.mod h1:kC29dT1vFpj7py2OvG1khBdQpo3kInWP+6QipLbdngo=
 github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962 h1:KeNholpO2xKjgaaSyd+DyQRrsQjhbSeS7qe4nEw8aQw=
 github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962/go.mod h1:kC29dT1vFpj7py2OvG1khBdQpo3kInWP+6QipLbdngo=
-github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
 github.com/alecthomas/kingpin v1.3.8-0.20220615105907-eae6867f4166 h1:sdTv2mX7pjU3JyOTktjOYeT/bjlllCzdRqzKFjgLDvs=
 github.com/alecthomas/kingpin v1.3.8-0.20220615105907-eae6867f4166/go.mod h1:OaxaNlTGWmknTg9pwAYYTkaU/cXZYeLGizE8aey/CXU=
 github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
@@ -95,17 +81,14 @@ github.com/alecthomas/units v0.0.0-20210927113745-59d0afb8317a h1:E/8AP5dFtMhl5K
 github.com/alecthomas/units v0.0.0-20210927113745-59d0afb8317a/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE=
 github.com/alessio/shellescape v1.4.1 h1:V7yhSDDn8LP4lc4jS8pFkt0zCnzVJlG5JXy9BVKJUX0=
 github.com/alessio/shellescape v1.4.1/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30=
-github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
-github.com/aws/aws-sdk-go v1.44.125 h1:yIyCs6HX1BOj6SFTirvBwVM1tTfplKrJOyilIZPtKV8=
-github.com/aws/aws-sdk-go v1.44.125/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo=
+github.com/aws/aws-sdk-go v1.44.129 h1:yld8Rc8OCahLtenY1mnve4w1jVeBu/rSiscGzodaDOs=
+github.com/aws/aws-sdk-go v1.44.129/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo=
 github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
-github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
 github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
 github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
 github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
 github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
-github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
 github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
 github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
 github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
@@ -122,13 +105,6 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5P
 github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
 github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
 github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
-github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
-github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
-github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=
-github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
-github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
-github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
-github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
 github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
 github.com/danieljoos/wincred v1.1.0/go.mod h1:XYlo+eRTsVA9aHGp7NGjFkPla4m+DCL7hqDjlFjiygg=
 github.com/danieljoos/wincred v1.1.2 h1:QLdCxFs1/Yl4zduvBdcHB8goaYk9RARS2SgLLRuAyr0=
@@ -149,11 +125,6 @@ github.com/edsrzf/mmap-go v1.1.0/go.mod h1:19H/e8pUPLicwkyNgOykDXkJ9F0MHE+Z52B8E
 github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
 github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
 github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
-github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
-github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
-github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
-github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
-github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
 github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
 github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
 github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
@@ -162,7 +133,6 @@ github.com/foomo/htpasswd v0.0.0-20200116085101-e3a90e78da9c/go.mod h1:SHawtolbB
 github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
 github.com/frankban/quicktest v1.13.1 h1:xVm/f9seEhZFL9+n5kv5XLrGwy6elc4V9v/XFY2vmd8=
 github.com/frankban/quicktest v1.13.1/go.mod h1:NeW+ay9A/U67EYXNFA1nPE8e/tnQv/09mUdL/ijj8og=
-github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
 github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
@@ -209,8 +179,6 @@ github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt
 github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
 github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
 github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
-github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
-github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
 github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@@ -226,10 +194,8 @@ github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QD
 github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
 github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
 github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
-github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=
 github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
 github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
-github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
 github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
 github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
 github.com/google/fswalker v0.2.1-0.20200214223026-f0e929ba4126 h1:1Nop4Y5nFbyKDlexcWH3Mxo8FXNqH5D71NBDjYoMV9I=
@@ -241,21 +207,17 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
 github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
 github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
 github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
 github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no=
 github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
 github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
-github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
 github.com/google/martian/v3 v3.2.1 h1:d8MncMlErDFTwQGBK1xhv026j9kqhvw1Qv9IbWT1VLQ=
-github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk=
 github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
 github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
 github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
@@ -263,13 +225,6 @@ github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hf
 github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
 github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
 github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
-github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
-github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
-github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
-github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
-github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
-github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
-github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
 github.com/google/readahead v0.0.0-20161222183148-eaceba169032 h1:6Be3nkuJFyRfCgr6qTIzmRp8y9QwDIbqy/nYr9WDPos=
 github.com/google/readahead v0.0.0-20161222183148-eaceba169032/go.mod h1:qYysrqQXuV4tzsizt4oOQ6mrBZQ0xnQXP3ylXX8Jk5Y=
 github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
@@ -282,13 +237,10 @@ github.com/googleapis/enterprise-certificate-proxy v0.2.0 h1:y8Yozv7SZtlU//QXbez
 github.com/googleapis/enterprise-certificate-proxy v0.2.0/go.mod h1:8C0jb7/mgJe/9KK8Lm7X9ctZC2t60YyIpYEI16jx0Qg=
 github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
 github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
-github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0=
-github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM=
-github.com/googleapis/gax-go/v2 v2.6.0 h1:SXk3ABtQYDT/OH8jAyvEOQ58mgawq5C4o/4/89qN2ZU=
-github.com/googleapis/gax-go/v2 v2.6.0/go.mod h1:1mjbznJAPHFpesgE5ucqfYEscaz5kMdcIDwU/6+DDoY=
+github.com/googleapis/gax-go/v2 v2.7.0 h1:IcsPKeInNvYi7eqSaDjiZqDDKu5rsmunY0Y1YupQSSQ=
+github.com/googleapis/gax-go/v2 v2.7.0/go.mod h1:TEop28CZZQ2y+c0VxMUmu1lV+fQx57QpBWsYpwqHJx8=
 github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
 github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
-github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
 github.com/hanwen/go-fuse/v2 v2.1.1-0.20220112183258-f57e95bda82d h1:ibbzF2InxMOS+lLCphY9PHNKPURDUBNKaG6ErSq8gJQ=
 github.com/hanwen/go-fuse/v2 v2.1.1-0.20220112183258-f57e95bda82d/go.mod h1:B1nGE/6RBFyBRC1RRnf23UpwCdyJ31eukw34oAKukAc=
 github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
@@ -296,7 +248,6 @@ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ
 github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
 github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
 github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
-github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
 github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
 github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
 github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
@@ -314,8 +265,8 @@ github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/X
 github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
 github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
 github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
-github.com/klauspost/compress v1.15.11 h1:Lcadnb3RKGin4FYM/orgq0qde+nc15E5Cbqg4B9Sx9c=
-github.com/klauspost/compress v1.15.11/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM=
+github.com/klauspost/compress v1.15.12 h1:YClS/PImqYbn+UILDnqxQCZ3RehC9N318SU3kElDUEM=
+github.com/klauspost/compress v1.15.12/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM=
 github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
 github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
 github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
@@ -323,8 +274,8 @@ github.com/klauspost/cpuid/v2 v2.1.1 h1:t0wUqjowdm8ezddV5k0tLWVklVuvLJpoHeb4WBdy
 github.com/klauspost/cpuid/v2 v2.1.1/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
 github.com/klauspost/pgzip v1.2.5 h1:qnWYvvKqedOF2ulHpMG72XQol4ILEJ8k2wwRl/Km8oE=
 github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
-github.com/klauspost/reedsolomon v1.11.0 h1:fc24kMFf4I6dXJwSkVAsw8Za/dMcJrV5ImeDjG3ss1M=
-github.com/klauspost/reedsolomon v1.11.0/go.mod h1:FXLZzlJIdfqEnQLdUKWNRuMZg747hZ4oYp2Ml60Lb/k=
+github.com/klauspost/reedsolomon v1.11.1 h1:0gCWQXOB8pVe1Y5SGozDA5t2qoVxX3prsV+qHgI/Fik=
+github.com/klauspost/reedsolomon v1.11.1/go.mod h1:FXLZzlJIdfqEnQLdUKWNRuMZg747hZ4oYp2Ml60Lb/k=
 github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
 github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
 github.com/kopia/htmluibuild v0.0.0-20220928042710-9fdd02afb1e7 h1:WP5VfIQL7AaYkO4zTNSCsVOawTzudbc4tvLojvg0RKc=
@@ -359,8 +310,8 @@ github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0j
 github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
 github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
 github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
-github.com/minio/minio-go/v7 v7.0.41 h1:Qhc82nDRep+VSuDEPSawKUHkARnZI5st7acEqgqVX+k=
-github.com/minio/minio-go/v7 v7.0.41/go.mod h1:nCrRzjoSUQh8hgKKtu3Y708OLvRLtuASMg2/nvmbarw=
+github.com/minio/minio-go/v7 v7.0.43 h1:14Q4lwblqTdlAmba05oq5xL0VBLHi06zS4yLnIkz6hI=
+github.com/minio/minio-go/v7 v7.0.43/go.mod h1:nCrRzjoSUQh8hgKKtu3Y708OLvRLtuASMg2/nvmbarw=
 github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g=
 github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM=
 github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -396,13 +347,14 @@ github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5Fsn
 github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
 github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
 github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=
-github.com/prometheus/client_golang v1.13.0 h1:b71QUfeo5M8gq2+evJdTPfZhYMAU0uKPkyPJ7TPsloU=
-github.com/prometheus/client_golang v1.13.0/go.mod h1:vTeo+zgvILHsnnj/39Ou/1fPN5nJFOEMgftOUOmlvYQ=
+github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw=
+github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y=
 github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
 github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
 github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
-github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M=
 github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4=
+github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w=
 github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
 github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
 github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
@@ -416,7 +368,6 @@ github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1
 github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
 github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo=
 github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4=
-github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
 github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
 github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k=
 github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
@@ -431,21 +382,21 @@ github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0
 github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
 github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA=
 github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
-github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/objx v0.4.0 h1:M2gUjqZET1qApGOWNSnZ49BAIMX4F/1plDv3+l31EJ4=
 github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
 github.com/stretchr/testify v0.0.0-20161117074351-18a02ba4a312/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
 github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
-github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
 github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
+github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
 github.com/studio-b12/gowebdav v0.0.0-20211106090535-29e74efa701f h1:SLJx0nHhb2ZLlYNMAbrYsjwmVwXx4yRT48lNIxOp7ts=
 github.com/studio-b12/gowebdav v0.0.0-20211106090535-29e74efa701f/go.mod h1:gCcfDlA1Y7GqOaeEKw5l9dOGx1VLdc/HuQSlQAaZ30s=
 github.com/tg123/go-htpasswd v1.2.0 h1:UKp34m9H467/xklxUxU15wKRru7fwXoTojtxg25ITF0=
@@ -455,8 +406,6 @@ github.com/xhit/go-str2duration v1.2.0/go.mod h1:3cPSlfZlUHVlneIVfePFWcJZsuwf+P1
 github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
-github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
-github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
 github.com/zalando/go-keyring v0.2.1 h1:MBRN/Z8H4U5wEKXiD67YbDAr5cj/DOStmSga70/2qKc=
 github.com/zalando/go-keyring v0.2.1/go.mod h1:g63M2PPn0w5vjmEbwAX3ib5I+41zdm4esSETOn9Y6Dw=
 github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY=
@@ -470,28 +419,24 @@ go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
 go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
 go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
 go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
-go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
-go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M=
-go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
+go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
+go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
 go.opentelemetry.io/otel v1.11.1 h1:4WLLAmcfkmDk2ukNXJyq3/kiz/3UzCaYq6PskJsaou4=
 go.opentelemetry.io/otel v1.11.1/go.mod h1:1nNhXBbWSD0nsL38H6btgnFN2k4i0sNLHNNMZMSbUGE=
-go.opentelemetry.io/otel/exporters/jaeger v1.11.0 h1:Sv2valcFfMlfu6g8USSS+ZUN5vwbuGj1aY/CFtMG33w=
-go.opentelemetry.io/otel/exporters/jaeger v1.11.0/go.mod h1:nRgyJbgJ0hmaUdHwyDpTTfBYz61cTTeeGhVzfQc+FsI=
+go.opentelemetry.io/otel/exporters/jaeger v1.11.1 h1:F9Io8lqWdGyIbY3/SOGki34LX/l+7OL0gXNxjqwcbuQ=
+go.opentelemetry.io/otel/exporters/jaeger v1.11.1/go.mod h1:lRa2w3bQ4R4QN6zYsDgy7tEezgoKEu7Ow2g35Y75+KI=
 go.opentelemetry.io/otel/sdk v1.11.1 h1:F7KmQgoHljhUuJyA+9BiU+EkJfyX5nVVF4wyzWZpKxs=
 go.opentelemetry.io/otel/sdk v1.11.1/go.mod h1:/l3FE4SupHJ12TduVjUkZtlfFqDCQJlOlithYrdktys=
 go.opentelemetry.io/otel/trace v1.11.1 h1:ofxdnzsNrGBYXbP7t7zpUK281+go5rF7dvdIZXF8gdQ=
 go.opentelemetry.io/otel/trace v1.11.1/go.mod h1:f/Q9G7vzk5u91PhbmKbg1Qn0rzH1LJ4vbPHFGkTPtOk=
-go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
 go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
 go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
 go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
 go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI=
-go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
-go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
 go.uber.org/multierr v1.8.0 h1:dg6GjLku4EH+249NNmoIciG9N/jURbDG+pFlTkhzIC8=
 go.uber.org/multierr v1.8.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak=
-go.uber.org/zap v1.21.0 h1:WefMeulhovoZ2sYXz7st6K0sLj7bBhpiFaud4r4zST8=
-go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw=
+go.uber.org/zap v1.23.0 h1:OjGQ5KQDEUawVHxNwQgPpiypGHOxo2mNZsOqTak4fFY=
+go.uber.org/zap v1.23.0/go.mod h1:D+nX8jyLsMHMYrln8A0rJjFt/T/9/bGgIhAqxv5URuY=
 golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
@@ -503,8 +448,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
 golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
-golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa h1:zuSxTR4o9y82ebqCUJYNGJbGPo6sKVl54f/TVDObg1c=
-golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU=
+golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -515,6 +460,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
 golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
 golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
+golang.org/x/exp v0.0.0-20221028150844-83b7d23a625f h1:Al51T6tzvuh3oiwX11vex3QgJ2XTedFPGmbEVh8cdoc=
+golang.org/x/exp v0.0.0-20221028150844-83b7d23a625f/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
 golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
 golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
 golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@@ -527,8 +474,6 @@ golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHl
 golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
 golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
 golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
-golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
-golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
 golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
 golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
 golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
@@ -537,11 +482,8 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB
 golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
 golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s=
-golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/mod v0.6.0 h1:b9gGHsz9/HhJ3HF5DHQytPpuwocVTChQJK3AvoLRD5I=
+golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI=
 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -572,42 +514,24 @@ golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81R
 golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
 golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
 golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
-golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
-golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
-golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
-golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
-golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
-golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
-golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
-golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
 golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
-golang.org/x/net v0.0.0-20221012135044-0b7e1fb9d458 h1:MgJ6t2zo8v0tbmLCueaCbF1RM+TtB0rs3Lv8DGtOIpY=
-golang.org/x/net v0.0.0-20221012135044-0b7e1fb9d458/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
+golang.org/x/net v0.1.0 h1:hZ/3BUoy5aId7sCpA/Tc5lt8DkFgdVS2onTpJsZ/fl0=
+golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
-golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
 golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
 golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
-golang.org/x/oauth2 v0.0.0-20221006150949-b44042a4b9c1 h1:3VPzK7eqH25j7GYw5w6g/GzNRc0/fYtrxz27z1gD4W0=
-golang.org/x/oauth2 v0.0.0-20221006150949-b44042a4b9c1/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg=
+golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783 h1:nt+Q6cXKz4MosCSpnbMtqiQ8Oz0pxTef2B4Vca2lvfk=
+golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -616,9 +540,7 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
 golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
 golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -654,52 +576,31 @@ golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20201207223542-d4d67f95c62d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210819135213-f52c844e1c1c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec h1:BkDtF2Ih9xZ7le9ndzTA7KJow28VbQW3odyk/8drmuI=
-golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U=
+golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
-golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.1.0 h1:g6Z6vPFA9dYBAF7DWcH6sCcOntplXsDKcliusYijMlw=
+golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
 golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg=
@@ -747,17 +648,6 @@ golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roY
 golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
 golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
 golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
-golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
-golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
-golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
-golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
-golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
-golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
-golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -780,25 +670,8 @@ google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0M
 google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
 google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
 google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
-google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
-google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
-google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
-google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU=
-google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94=
-google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo=
-google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4=
-google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw=
-google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU=
-google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k=
-google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE=
-google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE=
-google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI=
-google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I=
-google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo=
-google.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g=
-google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA=
-google.golang.org/api v0.101.0 h1:lJPPeEBIRxGpGLwnBTam1NPEM8Z2BmmXEd3z812pjwM=
-google.golang.org/api v0.101.0/go.mod h1:CjxAAWWt3A3VrUE2IGDY2bgK5qhoG/OkyWVlYcP05MY=
+google.golang.org/api v0.103.0 h1:9yuVqlu2JCvcLg9p8S3fcFLZij8EPSyvODIY1rkMizQ=
+google.golang.org/api v0.103.0/go.mod h1:hGtW6nK1AC+d9si/UBhw8Xli+QMOf6xyNAyJw4qU9w0=
 google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
 google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
 google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
@@ -830,49 +703,14 @@ google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfG
 google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
 google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
 google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
 google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
 google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
 google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
 google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
 google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
 google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=
-google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A=
-google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
-google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
-google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
-google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24=
-google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k=
-google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k=
-google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=
-google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=
-google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w=
-google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
-google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
-google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
-google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
-google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
-google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
-google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
-google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
-google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
-google.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
-google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
-google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
-google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
-google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
-google.golang.org/genproto v0.0.0-20221018160656-63c7b68cfc55 h1:U1u4KB2kx6KR/aJDjQ97hZ15wQs8ZPvDcGcRynBhkvg=
-google.golang.org/genproto v0.0.0-20221018160656-63c7b68cfc55/go.mod h1:45EK0dUbEZ2NHjCeAd2LXmyjAgGUGrpGROgjhC3ADck=
+google.golang.org/genproto v0.0.0-20221027153422-115e99e71e1c h1:QgY/XxIAIeccR+Ca/rDdKubLIU9rcJ3xfy1DC/Wd2Oo=
+google.golang.org/genproto v0.0.0-20221027153422-115e99e71e1c/go.mod h1:CGI5F/G+E5bKwmfYo09AXuVN4dD894kIKUFmVbP2/Fo=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
 google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
@@ -885,24 +723,9 @@ google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKa
 google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
 google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
 google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
-google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
-google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
 google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
-google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
-google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
-google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
-google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
-google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
-google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
-google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
-google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
-google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
-google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
-google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
-google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
 google.golang.org/grpc v1.50.1 h1:DS/BukOZWp8s6p4Dt/tOaJaTQyPyOoCcrjroHuCeLzY=
 google.golang.org/grpc v1.50.1/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI=
-google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
 google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
 google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
 google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
@@ -915,7 +738,6 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj
 google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
 google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
-google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
 google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
 google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
 gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
@@ -931,7 +753,6 @@ gopkg.in/kothar/go-backblaze.v0 v0.0.0-20210124194846-35409b867216 h1:2TSTkQ8PMv
 gopkg.in/kothar/go-backblaze.v0 v0.0.0-20210124194846-35409b867216/go.mod h1:zJ2QpyDCYo1KvLXlmdnFlQAyF/Qfth0fB8239Qg7BIE=
 gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
diff --git a/internal/cache/cache_metrics.go b/internal/cache/cache_metrics.go
index fc0a68ae..faf6bbb1 100644
--- a/internal/cache/cache_metrics.go
+++ b/internal/cache/cache_metrics.go
@@ -1,66 +1,71 @@
 package cache
 
-import (
-	"github.com/prometheus/client_golang/prometheus"
-	"github.com/prometheus/client_golang/prometheus/promauto"
-)
+import "github.com/kopia/kopia/internal/metrics"
 
-//nolint:gochecknoglobals,promlinter
-var (
-	metricHitCount = promauto.NewCounter(prometheus.CounterOpts{
-		Name: "kopia_content_cache_hit_count",
-		Help: "Number of time content was retrieved from the cache",
-	})
+type metricsStruct struct {
+	metricHitCount                *metrics.Counter
+	metricHitBytes                *metrics.Counter
+	metricMissCount               *metrics.Counter
+	metricMalformedCacheDataCount *metrics.Counter
+	metricMissBytes               *metrics.Counter
+	metricMissErrors              *metrics.Counter
+	metricStoreErrors             *metrics.Counter
+}
+
+func initMetricsStruct(mr *metrics.Registry, cacheID string) metricsStruct {
+	labels := map[string]string{
+		"cache": cacheID,
+	}
 
-	metricHitBytes = promauto.NewCounter(prometheus.CounterOpts{
-		Name: "kopia_content_cache_hit_bytes",
-		Help: "Number of bytes retrieved from the cache",
-	})
+	return metricsStruct{
+		metricHitCount: mr.CounterInt64(
+			"cache_hit",
+			"Number of time content was retrieved from the cache", labels),
 
-	metricMissCount = promauto.NewCounter(prometheus.CounterOpts{
-		Name: "kopia_content_cache_miss_count",
-		Help: "Number of time content was not found in the cache and fetched from the storage",
-	})
+		metricHitBytes: mr.CounterInt64(
+			"cache_hit_bytes",
+			"Number of bytes retrieved from the cache", labels),
 
-	metricMalformedCacheDataCount = promauto.NewCounter(prometheus.CounterOpts{
-		Name: "kopia_content_cache_malformed",
-		Help: "Number of times malformed content was read from the cache",
-	})
+		metricMissCount: mr.CounterInt64(
+			"cache_miss",
+			"Number of time content was not found in the cache and fetched from the storage", labels),
 
-	metricMissBytes = promauto.NewCounter(prometheus.CounterOpts{
-		Name: "kopia_content_cache_missed_bytes",
-		Help: "Number of bytes retrieved from the underlying storage",
-	})
+		metricMalformedCacheDataCount: mr.CounterInt64(
+			"cache_malformed",
+			"Number of times malformed content was read from the cache", labels),
 
-	metricMissErrors = promauto.NewCounter(prometheus.CounterOpts{
-		Name: "kopia_content_cache_miss_error_count",
-		Help: "Number of time content could not be found in the underlying storage",
-	})
+		metricMissBytes: mr.CounterInt64(
+			"cache_miss_bytes",
+			"Number of bytes retrieved from the underlying storage", labels),
 
-	metricStoreErrors = promauto.NewCounter(prometheus.CounterOpts{
-		Name: "kopia_content_cache_store_error_count",
-		Help: "Number of time content could not be saved in the cache",
-	})
-)
+		metricMissErrors: mr.CounterInt64(
+			"cache_miss_errors",
+			"Number of time content could not be found in the underlying storage", labels),
+
+		metricStoreErrors: mr.CounterInt64(
+			"cache_store_errors",
+			"Number of time content could not be saved in the cache", labels),
+	}
+}
 
-func reportMissError() {
-	metricMissErrors.Inc()
+func (s *metricsStruct) reportMissError() {
+	s.metricMissErrors.Add(1)
 }
 
-func reportMissBytes(length int64) {
-	metricMissCount.Inc()
-	metricMissBytes.Add(float64(length))
+func (s *metricsStruct) reportMissBytes(length int64) {
+	s.metricMissCount.Add(1)
+	s.metricMissBytes.Add(length)
 }
 
-func reportHitBytes(length int64) {
-	metricHitCount.Inc()
-	metricHitBytes.Add(float64(length))
+func (s *metricsStruct) reportHitBytes(length int64) {
+	s.metricHitCount.Add(1)
+	s.metricHitBytes.Add(length)
 }
 
-func reportMalformedData() {
-	metricMalformedCacheDataCount.Inc()
+func (s *metricsStruct) reportMalformedData() {
+	s.metricMalformedCacheDataCount.Add(1)
 }
 
-func reportStoreError() {
-	metricStoreErrors.Inc()
+func (s *metricsStruct) reportStoreError() {
+	s.metricStoreErrors.Add(1)
 }
diff --git a/internal/cache/content_cache.go b/internal/cache/content_cache.go
index 9b077de9..74b5a5f0 100644
--- a/internal/cache/content_cache.go
+++ b/internal/cache/content_cache.go
@@ -7,6 +7,7 @@
 
 	"github.com/kopia/kopia/internal/gather"
 	"github.com/kopia/kopia/internal/impossible"
+	"github.com/kopia/kopia/internal/metrics"
 	"github.com/kopia/kopia/repo/blob"
 )
 
@@ -95,12 +96,12 @@ func (c *contentCacheImpl) getContentFromFullBlob(ctx context.Context, blobID bl
 func (c *contentCacheImpl) fetchBlobInternal(ctx context.Context, blobID blob.ID, blobData *gather.WriteBuffer) error {
 	// read the entire blob
 	if err := c.st.GetBlob(ctx, blobID, 0, -1, blobData); err != nil {
-		reportMissError()
+		c.pc.reportMissError()
 
 		return errors.Wrapf(err, "failed to get blob with ID %s", blobID)
 	}
 
-	reportMissBytes(int64(blobData.Length()))
+	c.pc.reportMissBytes(int64(blobData.Length()))
 
 	// store the whole blob in the cache.
 	c.pc.Put(ctx, BlobIDCacheKey(blobID), blobData.Bytes())
@@ -131,12 +132,12 @@ func (c *contentCacheImpl) getContentFromFullOrPartialBlob(ctx context.Context,
 	}
 
 	if err := c.st.GetBlob(ctx, blobID, offset, length, output); err != nil {
-		reportMissError()
+		c.pc.reportMissError()
 
 		return errors.Wrapf(err, "failed to get blob with ID %s", blobID)
 	}
 
-	reportMissBytes(int64(output.Length()))
+	c.pc.reportMissBytes(int64(output.Length()))
 
 	c.pc.Put(ctx, ContentIDCacheKey(contentID), output.Bytes())
 
@@ -173,7 +174,7 @@ func (c *contentCacheImpl) CacheStorage() Storage {
 }
 
 // NewContentCache creates new content cache for data contents.
-func NewContentCache(ctx context.Context, st blob.Storage, opt Options) (ContentCache, error) {
+func NewContentCache(ctx context.Context, st blob.Storage, opt Options, mr *metrics.Registry) (ContentCache, error) {
 	cacheStorage := opt.Storage
 	if cacheStorage == nil {
 		if opt.BaseCacheDirectory == "" {
@@ -188,7 +189,7 @@ func NewContentCache(ctx context.Context, st blob.Storage, opt Options) (Content
 		}
 	}
 
-	pc, err := NewPersistentCache(ctx, opt.CacheSubDir, cacheStorage, ChecksumProtection(opt.HMACSecret), opt.Sweep)
+	pc, err := NewPersistentCache(ctx, opt.CacheSubDir, cacheStorage, ChecksumProtection(opt.HMACSecret), opt.Sweep, mr)
 	if err != nil {
 		return nil, errors.Wrap(err, "unable to create base cache")
 	}
diff --git a/internal/cache/persistent_lru_cache.go b/internal/cache/persistent_lru_cache.go
index 474c31e1..240dbd91 100644
--- a/internal/cache/persistent_lru_cache.go
+++ b/internal/cache/persistent_lru_cache.go
@@ -14,6 +14,7 @@
 	"github.com/kopia/kopia/internal/clock"
 	"github.com/kopia/kopia/internal/ctxutil"
 	"github.com/kopia/kopia/internal/gather"
+	"github.com/kopia/kopia/internal/metrics"
 	"github.com/kopia/kopia/internal/releasable"
 	"github.com/kopia/kopia/internal/timetrack"
 	"github.com/kopia/kopia/repo/blob"
@@ -51,6 +52,8 @@ type PersistentCache struct {
 	periodicSweepClosed  chan struct{}
 
 	mutexCache *lru.Cache
+
+	metricsStruct
 }
 
 // CacheStorage returns cache storage.
@@ -104,12 +107,12 @@ func (c *PersistentCache) GetOrLoad(ctx context.Context, key string, fetch func(
 	}
 
 	if err := fetch(output); err != nil {
-		reportMissError()
+		c.reportMissError()
 
 		return err
 	}
 
-	reportMissBytes(int64(output.Length()))
+	c.reportMissBytes(int64(output.Length()))
 
 	c.Put(ctx, key, output.Bytes())
 
@@ -140,7 +143,7 @@ func (c *PersistentCache) GetPartial(ctx context.Context, key string, offset, le
 
 		if err := prot.Verify(key, tmp.Bytes(), output); err == nil {
 			// cache hit
-			reportHitBytes(int64(output.Length()))
+			c.reportHitBytes(int64(output.Length()))
 
 			// cache hit
 			c.cacheStorage.TouchBlob(ctx, blob.ID(key), c.sweep.TouchThreshold) //nolint:errcheck
@@ -149,7 +152,7 @@ func (c *PersistentCache) GetPartial(ctx context.Context, key string, offset, le
 		}
 
 		// delete invalid blob
-		reportMalformedData()
+		c.reportMalformedData()
 
 		if err := c.cacheStorage.DeleteBlob(ctx, blob.ID(key)); err != nil && !errors.Is(err, blob.ErrBlobNotFound) {
 			log(ctx).Errorf("unable to delete %v entry %v: %v", c.description, key, err)
@@ -162,7 +165,7 @@ func (c *PersistentCache) GetPartial(ctx context.Context, key string, offset, le
 		l = 0
 	}
 
-	reportMissBytes(l)
+	c.reportMissBytes(l)
 
 	return false
 }
@@ -181,7 +184,7 @@ func (c *PersistentCache) Put(ctx context.Context, key string, data gather.Bytes
 	c.storageProtection.Protect(key, data, &protected)
 
 	if err := c.cacheStorage.PutBlob(ctx, blob.ID(key), protected.Bytes(), blob.PutOptions{}); err != nil {
-		reportStoreError()
+		c.reportStoreError()
 
 		log(ctx).Errorf("unable to add %v to %v: %v", key, c.description, err)
 	}
@@ -331,7 +334,7 @@ func (s SweepSettings) applyDefaults() SweepSettings {
 }
 
 // NewPersistentCache creates the persistent cache in the provided storage.
-func NewPersistentCache(ctx context.Context, description string, cacheStorage Storage, storageProtection StorageProtection, sweep SweepSettings) (*PersistentCache, error) {
+func NewPersistentCache(ctx context.Context, description string, cacheStorage Storage, storageProtection StorageProtection, sweep SweepSettings, mr *metrics.Registry) (*PersistentCache, error) {
 	if cacheStorage == nil {
 		return nil, nil
 	}
@@ -348,6 +351,7 @@ func NewPersistentCache(ctx context.Context, description string, cacheStorage St
 		periodicSweepClosed: make(chan struct{}),
 		description:         description,
 		storageProtection:   storageProtection,
+		metricsStruct:       initMetricsStruct(mr, description),
 	}
 
 	c.mutexCache, _ = lru.New(mutexCacheSize)
diff --git a/internal/metrics/metrics_counter.go b/internal/metrics/metrics_counter.go
new file mode 100644
index 00000000..b6a19a64
--- /dev/null
+++ b/internal/metrics/metrics_counter.go
@@ -0,0 +1,65 @@
+package metrics
+
+import (
+	"sync/atomic"
+
+	"github.com/prometheus/client_golang/prometheus"
+)
+
+// Counter represents a monotonically increasing int64 counter value.
+type Counter struct {
+	state atomic.Int64
+
+	prom prometheus.Counter
+}
+
+// Add adds a value to a counter.
+func (c *Counter) Add(v int64) {
+	if c == nil {
+		return
+	}
+
+	c.prom.Add(float64(v))
+	c.state.Add(v)
+}
+
+// newState initializes counter state and returns previous state or nil.
+func (c *Counter) newState() int64 {
+	return c.state.Swap(0)
+}
+
+// Snapshot captures the momentary state of a counter.
+func (c *Counter) Snapshot() int64 {
+	if c == nil {
+		return 0
+	}
+
+	return c.state.Load()
+}
+
+// CounterInt64 gets a persistent int64 counter with the provided name.
+func (r *Registry) CounterInt64(name, help string, labels map[string]string) *Counter {
+	if r == nil {
+		return nil
+	}
+
+	r.mu.Lock()
+	defer r.mu.Unlock()
+
+	fullName := name + labelsSuffix(labels)
+
+	c := r.allCounters[fullName]
+	if c == nil {
+		c = &Counter{
+			prom: getPrometheusCounter(prometheus.CounterOpts{
+				Name: prometheusPrefix + name + prometheusCounterSuffix,
+				Help: help,
+			}, labels),
+		}
+
+		c.newState()
+		r.allCounters[fullName] = c
+	}
+
+	return c
+}
diff --git a/internal/metrics/metrics_distribution.go b/internal/metrics/metrics_distribution.go
new file mode 100644
index 00000000..dff44b39
--- /dev/null
+++ b/internal/metrics/metrics_distribution.go
@@ -0,0 +1,186 @@
+package metrics
+
+import (
+	"sync"
+	"sync/atomic"
+	"time"
+
+	"github.com/prometheus/client_golang/prometheus"
+	"golang.org/x/exp/constraints"
+)
+
+// DistributionState captures momentary state of a Distribution.
+type DistributionState[T constraints.Float | constraints.Integer] struct {
+	Min              T
+	Max              T
+	Sum              T
+	Count            int64
+	BucketCounters   []int64
+	BucketThresholds []T
+}
+
+// Mean returns arithmetic mean value captured in the distribution.
+func (s *DistributionState[T]) Mean() T {
+	if s.Count == 0 {
+		return 0
+	}
+
+	return s.Sum / T(s.Count)
+}
+
+// Distribution measures distribution/summary of values.
+type Distribution[T constraints.Integer | constraints.Float] struct {
+	mu               sync.Mutex
+	state            atomic.Pointer[DistributionState[T]]
+	bucketThresholds []T
+
+	prom            prometheus.Observer
+	prometheusScale float64
+}
+
+// Observe adds the provided observation value to the summary.
+func (d *Distribution[T]) Observe(value T) {
+	if d == nil {
+		return
+	}
+
+	st := d.state.Load()
+	b := bucketForThresholds(d.bucketThresholds, value)
+
+	d.prom.Observe(float64(value) / d.prometheusScale)
+
+	d.mu.Lock()
+	defer d.mu.Unlock()
+
+	st.Sum += value
+	st.Count++
+
+	if st.Count == 1 {
+		st.Max = value
+		st.Min = value
+	} else {
+		if value > st.Max {
+			st.Max = value
+		}
+
+		if value < st.Min {
+			st.Min = value
+		}
+	}
+
+	st.BucketCounters[b]++
+}
+
+// Snapshot returns a snapshot of the distribution state.
+func (d *Distribution[T]) Snapshot() DistributionState[T] {
+	if d == nil {
+		return DistributionState[T]{}
+	}
+
+	d.mu.Lock()
+	defer d.mu.Unlock()
+
+	s := d.state.Load()
+
+	return DistributionState[T]{
+		Sum:            s.Sum,
+		Count:          s.Count,
+		Min:            s.Min,
+		Max:            s.Max,
+		BucketCounters: append([]int64(nil), s.BucketCounters...),
+	}
+}
+
+func (d *Distribution[T]) newState() *DistributionState[T] {
+	d.mu.Lock()
+	defer d.mu.Unlock()
+
+	return d.state.Swap(&DistributionState[T]{
+		BucketCounters:   make([]int64, len(d.bucketThresholds)+1),
+		BucketThresholds: d.bucketThresholds,
+	})
+}
+
+// DurationDistribution gets a persistent duration distribution with the provided name.
+func (r *Registry) DurationDistribution(name, help string, thresholds *Thresholds[time.Duration], labels map[string]string) *Distribution[time.Duration] {
+	if r == nil {
+		return nil
+	}
+
+	r.mu.Lock()
+	defer r.mu.Unlock()
+
+	fullName := name + labelsSuffix(labels)
+	h := r.allDurationDistributions[fullName]
+
+	if h == nil {
+		var promBuckets []float64
+
+		// convert from nanoseconds to seconds
+		promScale := thresholds.promScale
+		for _, v := range thresholds.values {
+			promBuckets = append(promBuckets, float64(v)/promScale)
+		}
+
+		h = &Distribution[time.Duration]{
+			bucketThresholds: thresholds.values,
+			prometheusScale:  promScale,
+			prom: getPrometheusHistogram(
+				prometheus.HistogramOpts{
+					Name:    prometheusPrefix + name + thresholds.prometheusSuffix,
+					Help:    help,
+					Buckets: promBuckets,
+				},
+				labels,
+			),
+		}
+
+		h.newState()
+
+		r.allDurationDistributions[fullName] = h
+	}
+
+	return h
+}
+
+// SizeDistribution gets a persistent size distribution with the provided name.
+func (r *Registry) SizeDistribution(name, help string, thresholds *Thresholds[int64], labels map[string]string) *Distribution[int64] {
+	if r == nil {
+		return nil
+	}
+
+	r.mu.Lock()
+	defer r.mu.Unlock()
+
+	fullName := name + labelsSuffix(labels)
+	h := r.allSizeDistributions[fullName]
+
+	if h == nil {
+		var promBuckets []float64
+
+		// convert from nanoseconds to seconds
+		promScale := thresholds.promScale
+		for _, v := range thresholds.values {
+			promBuckets = append(promBuckets, float64(v)/promScale)
+		}
+
+		h = &Distribution[int64]{
+			bucketThresholds: thresholds.values,
+			prometheusScale:  promScale,
+			prom: getPrometheusHistogram(
+				prometheus.HistogramOpts{
+					Name:    prometheusPrefix + name + thresholds.prometheusSuffix,
+					Help:    help,
+					Buckets: promBuckets,
+				},
+				labels,
+			),
+		}
+
+		h.newState()
+
+		r.allSizeDistributions[fullName] = h
+	}
+
+	return h
+}
diff --git a/internal/metrics/metrics_gauge.go b/internal/metrics/metrics_gauge.go
new file mode 100644
index 00000000..dcccee76
--- /dev/null
+++ b/internal/metrics/metrics_gauge.go
@@ -0,0 +1,68 @@
+package metrics
+
+import (
+	"sync/atomic"
+
+	"github.com/prometheus/client_golang/prometheus"
+)
+
+// Gauge is a metric representing momentary state.
+type Gauge struct {
+	state atomic.Int64
+
+	prom prometheus.Gauge
+}
+
+// Set sets the value of a gauge.
+func (c *Gauge) Set(v int64) {
+	if c == nil {
+		return
+	}
+
+	if c.prom != nil {
+		c.prom.Set(float64(v))
+	}
+
+	c.state.Store(v)
+}
+
+// newState initializes counter state and returns previous state or 0.
+func (c *Gauge) newState() int64 {
+	return c.state.Swap(0)
+}
+
+// Snapshot captures a momentary state of the gauge.
+func (c *Gauge) Snapshot() int64 {
+	if c == nil {
+		return 0
+	}
+
+	return c.state.Load()
+}
+
+// Gauge gets a persistent gauge with the provided name.
+func (r *Registry) Gauge(name, help string, labels map[string]string) *Gauge {
+	if r == nil {
+		return nil
+	}
+
+	r.mu.Lock()
+	defer r.mu.Unlock()
+
+	fullName := name + labelsSuffix(labels)
+
+	c := r.allGauges[fullName]
+	if c == nil {
+		c = &Gauge{
+			prom: getPrometheusGauge(prometheus.GaugeOpts{
+				Name: prometheusPrefix + name,
+				Help: help,
+			}, labels),
+		}
+		c.newState()
+
+		r.allGauges[fullName] = c
+	}
+
+	return c
+}
diff --git a/internal/metrics/metrics_registry.go b/internal/metrics/metrics_registry.go
new file mode 100644
index 00000000..1a61c39b
--- /dev/null
+++ b/internal/metrics/metrics_registry.go
@@ -0,0 +1,119 @@
+// Package metrics provides unified way of emitting metrics inside Kopia.
+package metrics
+
+import (
+	"context"
+	"strings"
+	"sync"
+	"time"
+
+	"github.com/kopia/kopia/repo/logging"
+)
+
+var log = logging.Module("metrics")
+
+// Registry groups together all metrics stored in the repository and provides ways of accessing them.
+type Registry struct {
+	mu                       sync.Mutex
+	allCounters              map[string]*Counter
+	allGauges                map[string]*Gauge
+	allThroughput            map[string]*Throughput
+	allDurationDistributions map[string]*Distribution[time.Duration]
+	allSizeDistributions     map[string]*Distribution[int64]
+}
+
+// Snapshot captures the state of all metrics.
+type Snapshot struct {
+	Counters              map[string]int64
+	Gauges                map[string]int64
+	DurationDistributions map[string]DistributionState[time.Duration]
+	SizeDistributions     map[string]DistributionState[int64]
+}
+
+// Snapshot captures the snapshot of all metrics.
+func (r *Registry) Snapshot() Snapshot {
+	s := Snapshot{
+		Counters:              map[string]int64{},
+		Gauges:                map[string]int64{},
+		DurationDistributions: map[string]DistributionState[time.Duration]{},
+		SizeDistributions:     map[string]DistributionState[int64]{},
+	}
+
+	for k, c := range r.allCounters {
+		s.Counters[k] = c.Snapshot()
+	}
+
+	for k, c := range r.allGauges {
+		s.Gauges[k] = c.Snapshot()
+	}
+
+	for k, c := range r.allDurationDistributions {
+		s.DurationDistributions[k] = c.Snapshot()
+	}
+
+	for k, c := range r.allSizeDistributions {
+		s.SizeDistributions[k] = c.Snapshot()
+	}
+
+	return s
+}
+
+// Close closes the metrics registry.
+func (r *Registry) Close(ctx context.Context) error {
+	return nil
+}
+
+// Log logs all metrics in the registry.
+func (r *Registry) Log(ctx context.Context) error {
+	if r == nil {
+		return nil
+	}
+
+	s := r.Snapshot()
+
+	for n, val := range s.Counters {
+		log(ctx).Debugw("COUNTER", "name", n, "value", val)
+	}
+
+	for n, val := range s.Gauges {
+		log(ctx).Debugw("GAUGE", "name", n, "value", val)
+	}
+
+	for n, st := range s.DurationDistributions {
+		log(ctx).Debugw("DURATION-DISTRIBUTION", "name", n, "counters", st.BucketCounters, "cnt", st.Count, "sum", st.Sum, "min", st.Min, "avg", st.Mean(), "max", st.Max)
+	}
+
+	for n, st := range s.SizeDistributions {
+		if st.Count > 0 {
+			log(ctx).Debugw("SIZE-DISTRIBUTION", "name", n, "counters", st.BucketCounters, "cnt", st.Count, "sum", st.Sum, "min", st.Min, "avg", st.Mean(), "max", st.Max)
+		}
+	}
+
+	return nil
+}
+
+// NewRegistry returns new metrics registry.
+func NewRegistry() *Registry {
+	e := &Registry{
+		allCounters:              map[string]*Counter{},
+		allGauges:                map[string]*Gauge{},
+		allDurationDistributions: map[string]*Distribution[time.Duration]{},
+		allSizeDistributions:     map[string]*Distribution[int64]{},
+		allThroughput:            map[string]*Throughput{},
+	}
+
+	return e
+}
+
+func labelsSuffix(l map[string]string) string {
+	if len(l) == 0 {
+		return ""
+	}
+
+	var params []string
+	for k, v := range l {
+		params = append(params, k+":"+v)
+	}
+
+	return "[" + strings.Join(params, ";") + "]"
+}
diff --git a/internal/metrics/metrics_thresholds.go b/internal/metrics/metrics_thresholds.go
new file mode 100644
index 00000000..f56d3a5b
--- /dev/null
+++ b/internal/metrics/metrics_thresholds.go
@@ -0,0 +1,129 @@
+package metrics
+
+import (
+	"time"
+
+	"golang.org/x/exp/constraints"
+)
+
+// Thresholds encapsulates a set of bucket thresholds used in Summary[T].
+type Thresholds[T constraints.Float | constraints.Integer] struct {
+	values           []T
+	promScale        float64
+	prometheusSuffix string
+}
+
+// ISOBytesThresholds is a set of thresholds for sizes.
+//
+//nolint:gochecknoglobals
+var ISOBytesThresholds = &Thresholds[int64]{
+	[]int64{
+		100,
+		250,
+		500,
+		1_000,
+		2_500,
+		5_000,
+		10_000,
+		25_000,
+		50_000,
+		100_000,
+		250_000,
+		500_000,
+		1_000_000,
+		2_500_000,
+		5_000_000,
+		10_000_000,
+		25_000_000,
+		50_000_000,
+		100_000_000,
+		250_000_000,
+		500_000_000,
+	},
+	1,
+	"",
+}
+
+// IOLatencyThresholds is a set of thresholds that can represent IO latencies from 500us to 50s.
+//
+//nolint:gochecknoglobals
+var IOLatencyThresholds = &Thresholds[time.Duration]{
+	[]time.Duration{
+		500 * time.Microsecond,
+
+		1000 * time.Microsecond,
+		2500 * time.Microsecond,
+		5000 * time.Microsecond,
+
+		10 * time.Millisecond,
+		25 * time.Millisecond,
+		50 * time.Millisecond,
+
+		100 * time.Millisecond,
+		250 * time.Millisecond,
+		500 * time.Millisecond,
+
+		1000 * time.Millisecond,
+		2500 * time.Millisecond,
+		5000 * time.Millisecond,
+
+		10 * time.Second,
+		25 * time.Second,
+		50 * time.Second,
+	},
+	1e6, // store as milliseconds
+	"_ms",
+}
+
+// CPULatencyThresholds is a set of thresholds that can represent CPU latencies from 10us to 5s.
+//
+//nolint:gochecknoglobals
+var CPULatencyThresholds = &Thresholds[time.Duration]{
+	[]time.Duration{
+		10 * time.Microsecond,
+		25 * time.Microsecond,
+		50 * time.Microsecond,
+
+		100 * time.Microsecond,
+		250 * time.Microsecond,
+		500 * time.Microsecond,
+
+		1000 * time.Microsecond,
+		2500 * time.Microsecond,
+		5000 * time.Microsecond,
+
+		10 * time.Millisecond,
+		25 * time.Millisecond,
+		50 * time.Millisecond,
+
+		100 * time.Millisecond,
+		250 * time.Millisecond,
+		500 * time.Millisecond,
+
+		1000 * time.Millisecond,
+		2500 * time.Millisecond,
+		5000 * time.Millisecond,
+	},
+	1, // export to Prometheus as nanoseconds
+	"_ns",
+}
+
+func bucketForThresholds[T constraints.Integer | constraints.Float](thresholds []T, d T) int {
+	l, r := 0, len(thresholds)-1
+
+	for l <= r {
+		m := (l + r) >> 1
+		mid := thresholds[m]
+
+		switch {
+		case mid < d:
+			l = m + 1
+		case mid > d:
+			r = m - 1
+		default:
+			return m
+		}
+	}
+
+	return l
+}
diff --git a/internal/metrics/metrics_throughput.go b/internal/metrics/metrics_throughput.go
new file mode 100644
index 00000000..db2a517a
--- /dev/null
+++ b/internal/metrics/metrics_throughput.go
@@ -0,0 +1,42 @@
+package metrics
+
+import (
+	"time"
+)
+
+// Throughput measures throughput by keeping track of total value and total duration.
+type Throughput struct {
+	totalBytes    *Counter
+	totalDuration *Counter
+}
+
+// Observe increases duration and total value counters based on the amount of time to process particular amount of data.
+func (c *Throughput) Observe(size int64, dt time.Duration) {
+	if c == nil {
+		return
+	}
+
+	c.totalBytes.Add(size)
+	c.totalDuration.Add(dt.Nanoseconds())
+}
+
+// Throughput gets a persistent counter with the provided name.
+func (r *Registry) Throughput(name, help string, labels map[string]string) *Throughput {
+	if r == nil {
+		return nil
+	}
+
+	fullName := name + labelsSuffix(labels)
+
+	c := r.allThroughput[fullName]
+	if c == nil {
+		c = &Throughput{
+			totalBytes:    r.CounterInt64(name+"_bytes", help, labels),
+			totalDuration: r.CounterInt64(name+"_duration_nanos", help, labels),
+		}
+
+		r.allThroughput[fullName] = c
+	}
+
+	return c
+}
diff --git a/internal/metrics/prom_cache.go b/internal/metrics/prom_cache.go
new file mode 100644
index 00000000..69a1121a
--- /dev/null
+++ b/internal/metrics/prom_cache.go
@@ -0,0 +1,64 @@
+package metrics
+
+import (
+	"sync"
+
+	"github.com/prometheus/client_golang/prometheus"
+	"github.com/prometheus/client_golang/prometheus/promauto"
+	"golang.org/x/exp/maps"
+)
+
+const (
+	prometheusCounterSuffix = "_total"
+	prometheusPrefix        = "kopia_"
+)
+
+//nolint:gochecknoglobals
+var (
+	promCacheMutex sync.Mutex
+	promCounters   = map[string]*prometheus.CounterVec{}
+	promGauges     = map[string]*prometheus.GaugeVec{}
+	promHistograms = map[string]*prometheus.HistogramVec{}
+)
+
+func getPrometheusCounter(opts prometheus.CounterOpts, labels map[string]string) prometheus.Counter {
+	promCacheMutex.Lock()
+	defer promCacheMutex.Unlock()
+
+	prom := promCounters[opts.Name]
+	if prom == nil {
+		prom = promauto.NewCounterVec(opts, maps.Keys(labels))
+
+		promCounters[opts.Name] = prom
+	}
+
+	return prom.WithLabelValues(maps.Values(labels)...)
+}
+
+func getPrometheusGauge(opts prometheus.GaugeOpts, labels map[string]string) prometheus.Gauge {
+	promCacheMutex.Lock()
+	defer promCacheMutex.Unlock()
+
+	prom := promGauges[opts.Name]
+	if prom == nil {
+		prom = promauto.NewGaugeVec(opts, maps.Keys(labels))
+
+		promGauges[opts.Name] = prom
+	}
+
+	return prom.WithLabelValues(maps.Values(labels)...)
+}
+
+func getPrometheusHistogram(opts prometheus.HistogramOpts, labels map[string]string) prometheus.Observer {
+	promCacheMutex.Lock()
+	defer promCacheMutex.Unlock()
+
+	prom := promHistograms[opts.Name]
+	if prom == nil {
+		prom = promauto.NewHistogramVec(opts, maps.Keys(labels))
+
+		promHistograms[opts.Name] = prom
+	}
+
+	return prom.WithLabelValues(maps.Values(labels)...)
+}
diff --git a/repo/api_server_repository.go b/repo/api_server_repository.go
index cc54ccdc..e86cb93b 100644
--- a/repo/api_server_repository.go
+++ b/repo/api_server_repository.go
@@ -13,6 +13,7 @@
 	"github.com/kopia/kopia/internal/cache"
 	"github.com/kopia/kopia/internal/clock"
 	"github.com/kopia/kopia/internal/gather"
+	"github.com/kopia/kopia/internal/metrics"
 	"github.com/kopia/kopia/internal/remoterepoapi"
 	"github.com/kopia/kopia/repo/compression"
 	"github.com/kopia/kopia/repo/content"
@@ -39,6 +40,7 @@ type apiServerRepository struct {
 	cliOpts                          ClientOptions
 	omgr                             *object.Manager
 	wso                              WriteSessionOptions
+	metricsRegistry                  *metrics.Registry
 
 	isSharedReadOnlySession bool
 	contentCache            *cache.PersistentCache
@@ -155,7 +157,7 @@ func (r *apiServerRepository) NewWriter(ctx context.Context, opt WriteSessionOpt
 	w := &r2
 
 	// create object manager using a remote repo as contentManager implementation.
-	omgr, err := object.NewObjectManager(ctx, w, r.objectFormat)
+	omgr, err := object.NewObjectManager(ctx, w, r.objectFormat, r.metricsRegistry)
 	if err != nil {
 		return nil, nil, errors.Wrap(err, "error initializing object manager")
 	}
@@ -273,10 +275,15 @@ func (r *apiServerRepository) PrefetchContents(ctx context.Context, contentIDs [
 	return resp.ContentIDs
 }
 
+// Metrics provides access to the metrics registry.
+func (r *apiServerRepository) Metrics() *metrics.Registry {
+	return r.metricsRegistry
+}
+
 var _ Repository = (*apiServerRepository)(nil)
 
 // openRestAPIRepository connects remote repository over Kopia API.
-func openRestAPIRepository(ctx context.Context, si *APIServerInfo, cliOpts ClientOptions, contentCache *cache.PersistentCache, password string) (Repository, error) {
+func openRestAPIRepository(ctx context.Context, si *APIServerInfo, cliOpts ClientOptions, contentCache *cache.PersistentCache, password string, mr *metrics.Registry) (Repository, error) {
 	cli, err := apiclient.NewKopiaAPIClient(apiclient.Options{
 		BaseURL:                             si.BaseURL,
 		TrustedServerCertificateFingerprint: si.TrustedServerCertificateFingerprint,
@@ -296,6 +303,7 @@ func openRestAPIRepository(ctx context.Context, si *APIServerInfo, cliOpts Clien
 			OnUpload: func(i int64) {},
 		},
 		isSharedReadOnlySession: true,
+		metricsRegistry:         mr,
 	}
 
 	var p remoterepoapi.Parameters
@@ -314,7 +322,7 @@ func openRestAPIRepository(ctx context.Context, si *APIServerInfo, cliOpts Clien
 	rr.serverSupportsContentCompression = p.SupportsContentCompression
 
 	// create object manager using rr as contentManager implementation.
-	omgr, err := object.NewObjectManager(ctx, rr, rr.objectFormat)
+	omgr, err := object.NewObjectManager(ctx, rr, rr.objectFormat, mr)
 	if err != nil {
 		return nil, errors.Wrap(err, "error initializing object manager")
 	}
diff --git a/repo/blob/storagemetrics/storage_metrics.go b/repo/blob/storagemetrics/storage_metrics.go
new file mode 100644
index 00000000..6fe1de01
--- /dev/null
+++ b/repo/blob/storagemetrics/storage_metrics.go
@@ -0,0 +1,228 @@
+// Package storagemetrics implements wrapper around Storage that adds metrics around all activity.
+package storagemetrics
+
+import (
+	"context"
+	"time"
+
+	"github.com/kopia/kopia/internal/metrics"
+	"github.com/kopia/kopia/internal/timetrack"
+	"github.com/kopia/kopia/repo/blob"
+)
+
+type blobMetrics struct {
+	base blob.Storage
+
+	downloadedBytesPartial *metrics.Counter
+	downloadedBytesFull    *metrics.Counter
+	uploadedBytes          *metrics.Counter
+	listBlobItems          *metrics.Counter
+
+	getBlobPartialDuration *metrics.Distribution[time.Duration]
+	getBlobFullDuration    *metrics.Distribution[time.Duration]
+	putBlobDuration        *metrics.Distribution[time.Duration]
+	getCapacityDuration    *metrics.Distribution[time.Duration]
+	getMetadataDuration    *metrics.Distribution[time.Duration]
+	deleteBlobDuration     *metrics.Distribution[time.Duration]
+	listBlobsDuration      *metrics.Distribution[time.Duration]
+	closeDuration          *metrics.Distribution[time.Duration]
+	flushCachesDuration    *metrics.Distribution[time.Duration]
+
+	getBlobErrors     *metrics.Counter
+	getCapacityErrors *metrics.Counter
+	getMetadataErrors *metrics.Counter
+	putBlobErrors     *metrics.Counter
+	deleteBlobErrors  *metrics.Counter
+	listBlobsErrors   *metrics.Counter
+	closeErrors       *metrics.Counter
+	flushCachesErrors *metrics.Counter
+}
+
+func (s *blobMetrics) GetBlob(ctx context.Context, id blob.ID, offset, length int64, output blob.OutputBuffer) error {
+	timer := timetrack.StartTimer()
+	err := s.base.GetBlob(ctx, id, offset, length, output)
+	dt := timer.Elapsed()
+
+	if length < 0 {
+		s.downloadedBytesFull.Add(int64(output.Length()))
+		s.getBlobFullDuration.Observe(dt)
+	} else {
+		s.downloadedBytesPartial.Add(int64(output.Length()))
+		s.getBlobPartialDuration.Observe(dt)
+	}
+
+	if err != nil {
+		s.getBlobErrors.Add(1)
+	}
+
+	//nolint:wrapcheck
+	return err
+}
+
+func (s *blobMetrics) GetCapacity(ctx context.Context) (blob.Capacity, error) {
+	timer := timetrack.StartTimer()
+	c, err := s.base.GetCapacity(ctx)
+	dt := timer.Elapsed()
+
+	s.getCapacityDuration.Observe(dt)
+
+	if err != nil {
+		s.getCapacityErrors.Add(1)
+	}
+
+	//nolint:wrapcheck
+	return c, err
+}
+
+func (s *blobMetrics) GetMetadata(ctx context.Context, id blob.ID) (blob.Metadata, error) {
+	timer := timetrack.StartTimer()
+	result, err := s.base.GetMetadata(ctx, id)
+	dt := timer.Elapsed()
+
+	s.getMetadataDuration.Observe(dt)
+
+	if err != nil {
+		s.getMetadataErrors.Add(1)
+	}
+
+	//nolint:wrapcheck
+	return result, err
+}
+
+func (s *blobMetrics) PutBlob(ctx context.Context, id blob.ID, data blob.Bytes, opts blob.PutOptions) error {
+	timer := timetrack.StartTimer()
+	err := s.base.PutBlob(ctx, id, data, opts)
+	dt := timer.Elapsed()
+
+	s.putBlobDuration.Observe(dt)
+
+	if err != nil {
+		s.putBlobErrors.Add(1)
+	} else {
+		s.uploadedBytes.Add(int64(data.Length()))
+	}
+
+	//nolint:wrapcheck
+	return err
+}
+
+func (s *blobMetrics) DeleteBlob(ctx context.Context, id blob.ID) error {
+	timer := timetrack.StartTimer()
+	err := s.base.DeleteBlob(ctx, id)
+	dt := timer.Elapsed()
+
+	s.deleteBlobDuration.Observe(dt)
+
+	if err != nil {
+		s.deleteBlobErrors.Add(1)
+	}
+
+	//nolint:wrapcheck
+	return err
+}
+
+func (s *blobMetrics) ListBlobs(ctx context.Context, prefix blob.ID, callback func(blob.Metadata) error) error {
+	timer := timetrack.StartTimer()
+	cnt := int64(0)
+	err := s.base.ListBlobs(ctx, prefix, func(bi blob.Metadata) error {
+		cnt++
+		return callback(bi)
+	})
+	dt := timer.Elapsed()
+
+	s.listBlobItems.Add(cnt)
+	s.listBlobsDuration.Observe(dt)
+
+	if err != nil {
+		s.listBlobsErrors.Add(1)
+	}
+
+	//nolint:wrapcheck
+	return err
+}
+
+func (s *blobMetrics) Close(ctx context.Context) error {
+	timer := timetrack.StartTimer()
+	err := s.base.Close(ctx)
+	dt := timer.Elapsed()
+
+	s.closeDuration.Observe(dt)
+
+	if err != nil {
+		s.closeErrors.Add(1)
+	}
+
+	//nolint:wrapcheck
+	return err
+}
+
+func (s *blobMetrics) ConnectionInfo() blob.ConnectionInfo {
+	return s.base.ConnectionInfo()
+}
+
+func (s *blobMetrics) DisplayName() string {
+	return s.base.DisplayName()
+}
+
+func (s *blobMetrics) FlushCaches(ctx context.Context) error {
+	timer := timetrack.StartTimer()
+	err := s.base.FlushCaches(ctx)
+	dt := timer.Elapsed()
+
+	s.flushCachesDuration.Observe(dt)
+
+	if err != nil {
+		s.flushCachesErrors.Add(1)
+	}
+
+	//nolint:wrapcheck
+	return err
+}
+
+// NewWrapper returns a Storage wrapper that logs all storage commands.
+func NewWrapper(wrapped blob.Storage, mr *metrics.Registry) blob.Storage {
+	durationSummaryForMethod := func(m string) *metrics.Distribution[time.Duration] {
+		return mr.DurationDistribution(
+			"blob_storage_latency",
+			"Latency of blob storage operation by method",
+			metrics.IOLatencyThresholds,
+			map[string]string{"method": m},
+		)
+	}
+
+	errorCounterForMethod := func(m string) *metrics.Counter {
+		return mr.CounterInt64(
+			"blob_errors",
+			"Number of storage operation errors by method",
+			map[string]string{"method": m},
+		)
+	}
+
+	return &blobMetrics{
+		base: wrapped,
+
+		downloadedBytesPartial: mr.CounterInt64("blob_download_partial_blob_bytes", "Number of bytes downloaded as partial blobs", nil),
+		downloadedBytesFull:    mr.CounterInt64("blob_download_full_blob_bytes", "Number of bytes downloaded as full blobs", nil),
+		uploadedBytes:          mr.CounterInt64("blob_upload_bytes", "Number of bytes uploaded", nil),
+		listBlobItems:          mr.CounterInt64("blob_list_items", "Number of list items returned", nil),
+
+		getBlobPartialDuration: durationSummaryForMethod("GetBlob-partial"),
+		getBlobFullDuration:    durationSummaryForMethod("GetBlob-full"),
+		getCapacityDuration:    durationSummaryForMethod("GetCapacity"),
+		getMetadataDuration:    durationSummaryForMethod("GetMetadata"),
+		putBlobDuration:        durationSummaryForMethod("PutBlob"),
+		deleteBlobDuration:     durationSummaryForMethod("DeleteBlob"),
+		listBlobsDuration:      durationSummaryForMethod("ListBlobs"),
+		closeDuration:          durationSummaryForMethod("Close"),
+		flushCachesDuration:    durationSummaryForMethod("FlushCaches"),
+
+		getBlobErrors:     errorCounterForMethod("GetBlob"),
+		getCapacityErrors: errorCounterForMethod("GetCapacity"),
+		getMetadataErrors: errorCounterForMethod("GetMetadata"),
+		putBlobErrors:     errorCounterForMethod("PutBlob"),
+		deleteBlobErrors:  errorCounterForMethod("DeleteBlob"),
+		listBlobsErrors:   errorCounterForMethod("ListBlobs"),
+		closeErrors:       errorCounterForMethod("Close"),
+		flushCachesErrors: errorCounterForMethod("FlushCaches"),
+	}
+}
diff --git a/repo/content/caching_options.go b/repo/content/caching_options.go
index 36657077..f456d975 100644
--- a/repo/content/caching_options.go
+++ b/repo/content/caching_options.go
@@ -1,6 +1,9 @@
 package content
 
-import "time"
+import (
+	"path/filepath"
+	"time"
+)
 
 // DurationSeconds represents the duration in seconds.
 type DurationSeconds float64
@@ -36,3 +39,16 @@ func (c *CachingOptions) CloneOrDefault() *CachingOptions {
 
 	return &c2
 }
+
+// CacheSubdirOrEmpty returns path to a cache subdirectory or empty string if cache is disabled.
+func (c *CachingOptions) CacheSubdirOrEmpty(subdir string) string {
+	if c == nil {
+		return ""
+	}
+
+	if c.CacheDirectory == "" {
+		return ""
+	}
+
+	return filepath.Join(c.CacheDirectory, subdir)
+}
diff --git a/repo/content/committed_read_manager.go b/repo/content/committed_read_manager.go
index 4ce633cf..a04a45e8 100644
--- a/repo/content/committed_read_manager.go
+++ b/repo/content/committed_read_manager.go
@@ -16,7 +16,9 @@
 	"github.com/kopia/kopia/internal/epoch"
 	"github.com/kopia/kopia/internal/gather"
 	"github.com/kopia/kopia/internal/listcache"
+	"github.com/kopia/kopia/internal/metrics"
 	"github.com/kopia/kopia/internal/ownwrites"
+	"github.com/kopia/kopia/internal/timetrack"
 	"github.com/kopia/kopia/repo/blob"
 	"github.com/kopia/kopia/repo/blob/filesystem"
 	"github.com/kopia/kopia/repo/blob/sharded"
@@ -113,6 +115,8 @@ type SharedManager struct {
 	contextLogger      logging.Logger
 	internalLogManager *internalLogManager
 	internalLogger     *zap.SugaredLogger // backing logger for 'sharedBaseLogger'
+
+	metricsStruct
 }
 
 func (sm *SharedManager) readPackFileLocalIndex(ctx context.Context, packFile blob.ID, packFileLength int64, output *gather.WriteBuffer) error {
@@ -284,19 +288,26 @@ func (sm *SharedManager) decryptContentAndVerify(payload gather.Bytes, bi Info,
 		return errors.Errorf("unsupported compressor %x", h)
 	}
 
+	t0 := timetrack.StartTimer()
+
 	if err := c.Decompress(output, tmp.Bytes().Reader(), true); err != nil {
 		return errors.Wrap(err, "error decompressing")
 	}
 
+	sm.decompressedBytes.Observe(int64(tmp.Length()), t0.Elapsed())
+
 	return nil
 }
 
 func (sm *SharedManager) decryptAndVerify(encrypted gather.Bytes, iv []byte, output *gather.WriteBuffer) error {
+	t0 := timetrack.StartTimer()
+
 	if err := sm.format.Encryptor().Decrypt(encrypted, iv, output); err != nil {
 		sm.Stats.foundInvalidContent()
 		return errors.Wrap(err, "decrypt")
 	}
 
+	sm.decryptedBytes.Observe(int64(encrypted.Length()), t0.Elapsed())
 	sm.Stats.foundValidContent()
 	sm.Stats.decrypted(output.Length())
 
@@ -384,7 +395,7 @@ func (sm *SharedManager) namedLogger(n string) logging.Logger {
 	return sm.contextLogger
 }
 
-func (sm *SharedManager) setupReadManagerCaches(ctx context.Context, caching *CachingOptions) error {
+func (sm *SharedManager) setupReadManagerCaches(ctx context.Context, caching *CachingOptions, mr *metrics.Registry) error {
 	dataCache, err := cache.NewContentCache(ctx, sm.st, cache.Options{
 		BaseCacheDirectory: caching.CacheDirectory,
 		CacheSubDir:        "contents",
@@ -393,7 +404,7 @@ func (sm *SharedManager) setupReadManagerCaches(ctx context.Context, caching *Ca
 			MaxSizeBytes: caching.MaxCacheSizeBytes,
 			MinSweepAge:  caching.MinContentSweepAge.DurationOrDefault(DefaultDataCacheSweepAge),
 		},
-	})
+	}, mr)
 	if err != nil {
 		return errors.Wrap(err, "unable to initialize content cache")
 	}
@@ -412,7 +423,7 @@ func (sm *SharedManager) setupReadManagerCaches(ctx context.Context, caching *Ca
 			MaxSizeBytes: metadataCacheSize,
 			MinSweepAge:  caching.MinMetadataSweepAge.DurationOrDefault(DefaultMetadataCacheSweepAge),
 		},
-	})
+	}, mr)
 	if err != nil {
 		return errors.Wrap(err, "unable to initialize metadata cache")
 	}
@@ -422,10 +433,10 @@ func (sm *SharedManager) setupReadManagerCaches(ctx context.Context, caching *Ca
 		return errors.Wrap(err, "unable to initialize index blob cache storage")
 	}
 
-	indexBlobCache, err := cache.NewPersistentCache(ctx, "index blob cache", indexBlobStorage, cache.ChecksumProtection(caching.HMACSecret), cache.SweepSettings{
+	indexBlobCache, err := cache.NewPersistentCache(ctx, "index-blobs", indexBlobStorage, cache.ChecksumProtection(caching.HMACSecret), cache.SweepSettings{
 		MaxSizeBytes: metadataCacheSize,
 		MinSweepAge:  caching.MinMetadataSweepAge.DurationOrDefault(DefaultMetadataCacheSweepAge),
-	})
+	}, mr)
 	if err != nil {
 		return errors.Wrap(err, "unable to create index blob cache")
 	}
@@ -552,7 +563,15 @@ func (sm *SharedManager) release(ctx context.Context) error {
 
 	sm.indexBlobManagerV1.epochMgr.Flush()
 
-	return errors.Wrap(sm.st.Close(ctx), "error closing storage")
+	if err := sm.st.Close(ctx); err != nil {
+		return errors.Wrap(err, "error closing storage")
+	}
+
+	if err := sm.metricsEmitter.Close(ctx); err != nil {
+		return errors.Wrap(err, "error closing metrics")
+	}
+
+	return nil
 }
 
 // AlsoLogToContentLog wraps the provided content so that all logs are also sent to
@@ -571,7 +590,7 @@ func (sm *SharedManager) shouldRefreshIndexes() bool {
 }
 
 // NewSharedManager returns SharedManager that is used by SessionWriteManagers on top of a repository.
-func NewSharedManager(ctx context.Context, st blob.Storage, prov format.Provider, caching *CachingOptions, opts *ManagerOptions) (*SharedManager, error) {
+func NewSharedManager(ctx context.Context, st blob.Storage, prov format.Provider, caching *CachingOptions, opts *ManagerOptions, mr *metrics.Registry) (*SharedManager, error) {
 	opts = opts.CloneOrDefault()
 	if opts.TimeNow == nil {
 		opts.TimeNow = clock.Now
@@ -601,6 +620,8 @@ func NewSharedManager(ctx context.Context, st blob.Storage, prov format.Provider
 		internalLogManager:      ilm,
 		internalLogger:          internalLog,
 		contextLogger:           logging.Module(FormatLogModule)(ctx),
+
+		metricsStruct: initMetricsStruct(mr),
 	}
 
 	// remember logger defined for the context.
@@ -608,7 +629,7 @@ func NewSharedManager(ctx context.Context, st blob.Storage, prov format.Provider
 
 	caching = caching.CloneOrDefault()
 
-	if err := sm.setupReadManagerCaches(ctx, caching); err != nil {
+	if err := sm.setupReadManagerCaches(ctx, caching, mr); err != nil {
 		return nil, errors.Wrap(err, "error setting up read manager caches")
 	}
 
diff --git a/repo/content/content_manager.go b/repo/content/content_manager.go
index 2459882f..9c0ac09f 100644
--- a/repo/content/content_manager.go
+++ b/repo/content/content_manager.go
@@ -16,6 +16,7 @@
 	"github.com/kopia/kopia/internal/cache"
 	"github.com/kopia/kopia/internal/clock"
 	"github.com/kopia/kopia/internal/gather"
+	"github.com/kopia/kopia/internal/timetrack"
 	"github.com/kopia/kopia/repo/blob"
 	"github.com/kopia/kopia/repo/compression"
 	"github.com/kopia/kopia/repo/content/index"
@@ -787,6 +788,11 @@ func (bm *WriteManager) SupportsContentCompression() (bool, error) {
 // WriteContent saves a given content of data to a pack group with a provided name and returns a contentID
 // that's based on the contents of data written.
 func (bm *WriteManager) WriteContent(ctx context.Context, data gather.Bytes, prefix index.IDPrefix, comp compression.HeaderID) (ID, error) {
+	t0 := timetrack.StartTimer()
+	defer func() {
+		bm.writeContentBytes.Observe(int64(data.Length()), t0.Elapsed())
+	}()
+
 	mp, mperr := bm.format.GetMutableParameters()
 	if mperr != nil {
 		return EmptyID, errors.Wrap(mperr, "mutable parameters")
@@ -796,8 +802,6 @@ func (bm *WriteManager) WriteContent(ctx context.Context, data gather.Bytes, pre
 		return EmptyID, err
 	}
 
-	reportContentWriteBytes(int64(data.Length()))
-
 	if err := prefix.ValidateSingle(); err != nil {
 		return EmptyID, errors.Wrap(err, "invalid prefix")
 	}
@@ -824,6 +828,9 @@ func (bm *WriteManager) WriteContent(ctx context.Context, data gather.Bytes, pre
 	// content already tracked
 	if err == nil {
 		if !bi.GetDeleted() {
+			bm.deduplicatedContents.Add(1)
+			bm.deduplicatedBytes.Add(int64(data.Length()))
+
 			return contentID, nil
 		}
 
@@ -840,14 +847,16 @@ func (bm *WriteManager) WriteContent(ctx context.Context, data gather.Bytes, pre
 
 // GetContent gets the contents of a given content. If the content is not found returns ErrContentNotFound.
 func (bm *WriteManager) GetContent(ctx context.Context, contentID ID) (v []byte, err error) {
+	t0 := timetrack.StartTimer()
+
 	defer func() {
 		switch {
 		case err == nil:
-			reportContentGetBytes(int64(len(v)))
+			bm.getContentBytes.Observe(int64(len(v)), t0.Elapsed())
 		case errors.Is(err, ErrContentNotFound):
-			reportContentGetNotFound()
+			bm.getContentNotFoundCount.Add(1)
 		default:
-			reportContentGetError()
+			bm.getContentErrorCount.Add(1)
 		}
 	}()
 
@@ -970,7 +979,7 @@ func NewManagerForTesting(ctx context.Context, st blob.Storage, f format.Provide
 		options.TimeNow = clock.Now
 	}
 
-	sharedManager, err := NewSharedManager(ctx, st, f, caching, options)
+	sharedManager, err := NewSharedManager(ctx, st, f, caching, options, nil)
 	if err != nil {
 		return nil, errors.Wrap(err, "error initializing read manager")
 	}
@@ -1001,7 +1010,10 @@ func NewWriteManager(ctx context.Context, sm *SharedManager, options SessionOpti
 		packIndexBuilder:      make(index.Builder),
 		sessionUser:           options.SessionUser,
 		sessionHost:           options.SessionHost,
-		onUpload:              options.OnUpload,
+		onUpload: func(numBytes int64) {
+			options.OnUpload(numBytes)
+			sm.uploadedBytes.Add(numBytes)
+		},
 
 		log: sm.namedLogger(writeManagerID),
 	}
diff --git a/repo/content/content_manager_lock_free.go b/repo/content/content_manager_lock_free.go
index ba8cfa71..8e769f07 100644
--- a/repo/content/content_manager_lock_free.go
+++ b/repo/content/content_manager_lock_free.go
@@ -13,6 +13,7 @@
 	"go.opentelemetry.io/otel/trace"
 
 	"github.com/kopia/kopia/internal/gather"
+	"github.com/kopia/kopia/internal/timetrack"
 	"github.com/kopia/kopia/repo/blob"
 	"github.com/kopia/kopia/repo/compression"
 	"github.com/kopia/kopia/repo/content/index"
@@ -50,22 +51,36 @@ func (sm *SharedManager) maybeCompressAndEncryptDataForPacking(data gather.Bytes
 			return NoCompression, errors.Errorf("unsupported compressor %x", comp)
 		}
 
+		t0 := timetrack.StartTimer()
+
 		if err := c.Compress(&tmp, data.Reader()); err != nil {
 			return NoCompression, errors.Wrap(err, "compression error")
 		}
 
+		sm.compressionAttemptedBytes.Observe(int64(data.Length()), t0.Elapsed())
+
 		if cd := tmp.Length(); cd >= data.Length() {
 			// data was not compressible enough.
 			comp = NoCompression
+
+			sm.nonCompressibleBytes.Add(int64(data.Length()))
 		} else {
+			sm.compressionSavings.Add(int64(data.Length()) - int64(cd))
+			sm.compressibleBytes.Add(int64(data.Length()))
 			data = tmp.Bytes()
 		}
 	}
 
+	sm.afterCompressionBytes.Add(int64(data.Length()))
+
+	t1 := timetrack.StartTimer()
+
 	if err := sm.format.Encryptor().Encrypt(data, iv, output); err != nil {
 		return NoCompression, errors.Wrap(err, "unable to encrypt")
 	}
 
+	sm.encryptedBytes.Observe(int64(output.Length()), t1.Elapsed())
+
 	sm.Stats.encrypted(data.Length())
 
 	return comp, nil
@@ -181,8 +196,11 @@ func (sm *SharedManager) writePackFileNotLocked(ctx context.Context, packFile bl
 
 func (sm *SharedManager) hashData(output []byte, data gather.Bytes) []byte {
 	// Hash the content and compute encryption key.
+	t0 := timetrack.StartTimer()
 	contentID := sm.format.HashFunc()(output, data)
 	sm.Stats.hashedContent(data.Length())
 
+	sm.hashedBytes.Observe(int64(data.Length()), t0.Elapsed())
+
 	return contentID
 }
diff --git a/repo/content/content_manager_metrics.go b/repo/content/content_manager_metrics.go
index c979532b..20d625e8 100644
--- a/repo/content/content_manager_metrics.go
+++ b/repo/content/content_manager_metrics.go
@@ -1,54 +1,62 @@
 package content
 
 import (
-	"github.com/prometheus/client_golang/prometheus"
-	"github.com/prometheus/client_golang/prometheus/promauto"
+	"github.com/kopia/kopia/internal/metrics"
 )
 
-// content cache metrics.
-//
-//nolint:gochecknoglobals,promlinter
-var (
-	metricContentGetCount = promauto.NewCounter(prometheus.CounterOpts{
-		Name: "kopia_content_get_count",
-		Help: "Number of time GetContent() was called",
-	})
-	metricContentGetNotFoundCount = promauto.NewCounter(prometheus.CounterOpts{
-		Name: "kopia_content_get_not_found_count",
-		Help: "Number of time GetContent() was called and the result was not found",
-	})
-	metricContentGetErrorCount = promauto.NewCounter(prometheus.CounterOpts{
-		Name: "kopia_content_get_error_count",
-		Help: "Number of time GetContent() was called and the result was an error",
-	})
-	metricContentGetBytes = promauto.NewCounter(prometheus.CounterOpts{
-		Name: "kopia_content_get_bytes",
-		Help: "Number of bytes retrieved using GetContent",
-	})
-	metricContentWriteContentCount = promauto.NewCounter(prometheus.CounterOpts{
-		Name: "kopia_content_write_count",
-		Help: "Number of time WriteContent() was called",
-	})
-	metricContentWriteContentBytes = promauto.NewCounter(prometheus.CounterOpts{
-		Name: "kopia_content_write_bytes",
-		Help: "Number of bytes passed to WriteContent()",
-	})
-)
+type metricsStruct struct {
+	metricsEmitter *metrics.Registry
 
-func reportContentWriteBytes(length int64) {
-	metricContentWriteContentCount.Inc()
-	metricContentWriteContentBytes.Add(float64(length))
-}
+	uploadedBytes *metrics.Counter
 
-func reportContentGetBytes(length int64) {
-	metricContentGetCount.Inc()
-	metricContentGetBytes.Add(float64(length))
-}
+	getContentBytes         *metrics.Throughput
+	getContentErrorCount    *metrics.Counter
+	getContentNotFoundCount *metrics.Counter
+
+	// number of bytes written
+	writeContentBytes *metrics.Throughput
+
+	// number of bytes after compression was applied (if any).
+	afterCompressionBytes *metrics.Counter
+
+	nonCompressibleBytes *metrics.Counter
+
+	compressibleBytes  *metrics.Counter
+	compressionSavings *metrics.Counter
 
-func reportContentGetError() {
-	metricContentGetErrorCount.Inc()
+	hashedBytes               *metrics.Throughput
+	encryptedBytes            *metrics.Throughput
+	decryptedBytes            *metrics.Throughput
+	compressionAttemptedBytes *metrics.Throughput
+	decompressedBytes         *metrics.Throughput
+
+	deduplicatedBytes    *metrics.Counter
+	deduplicatedContents *metrics.Counter
 }
 
-func reportContentGetNotFound() {
-	metricContentGetNotFoundCount.Inc()
+func initMetricsStruct(mr *metrics.Registry) metricsStruct {
+	return metricsStruct{
+		metricsEmitter: mr,
+
+		uploadedBytes:           mr.CounterInt64("content_uploaded_bytes", "Number of bytes uploaded from content manager.", nil),
+		getContentErrorCount:    mr.CounterInt64("content_get_error_count", "Number of time GetContent() was called and the result was an error", nil),
+		getContentNotFoundCount: mr.CounterInt64("content_get_not_found_count", "Number of time GetContent() was called and the result was not found", nil),
+		deduplicatedContents:    mr.CounterInt64("content_deduplicated", "Number of contents deduplicated.", nil),
+		deduplicatedBytes:       mr.CounterInt64("content_deduplicated_bytes", "Number of bytes deduplicated.", nil),
+
+		writeContentBytes: mr.Throughput("content_write", "WriteContent throughput (before deduplication)", nil),
+		hashedBytes:       mr.Throughput("content_hashed", "Hashing throughput.", nil),
+
+		afterCompressionBytes: mr.CounterInt64("content_after_compression_bytes", "Number of bytes after deduplication and compression", nil),
+		nonCompressibleBytes:  mr.CounterInt64("content_non_compressible_bytes", "Number of bytes that were found to be non-compressible stage.", nil),
+		compressibleBytes:     mr.CounterInt64("content_compressible_bytes", "Number of bytes that were found to be compressible.", nil),
+		compressionSavings:    mr.CounterInt64("content_compression_savings_bytes", "Number of bytes that were saved due to compression.", nil),
+
+		encryptedBytes:            mr.Throughput("content_encrypted", "Encryption throughput.", nil),
+		compressionAttemptedBytes: mr.Throughput("content_compression_attempted", "Compression throughput.", nil),
+
+		getContentBytes:   mr.Throughput("content_read", "Number of bytes read", nil),
+		decryptedBytes:    mr.Throughput("content_decrypted", "Decryption throughput.", nil),
+		decompressedBytes: mr.Throughput("content_decompressed", "Decompression throughput.", nil),
+	}
 }
diff --git a/repo/grpc_repository_client.go b/repo/grpc_repository_client.go
index f5f8a7a6..cb87d8ae 100644
--- a/repo/grpc_repository_client.go
+++ b/repo/grpc_repository_client.go
@@ -22,6 +22,7 @@
 	"github.com/kopia/kopia/internal/ctxutil"
 	"github.com/kopia/kopia/internal/gather"
 	apipb "github.com/kopia/kopia/internal/grpcapi"
+	"github.com/kopia/kopia/internal/metrics"
 	"github.com/kopia/kopia/internal/retry"
 	"github.com/kopia/kopia/internal/tlsutil"
 	"github.com/kopia/kopia/repo/blob"
@@ -85,6 +86,8 @@ type grpcRepositoryClient struct {
 	contentCache *cache.PersistentCache
 
 	recent recentlyRead
+
+	metricsRegistry *metrics.Registry
 }
 
 type grpcInnerSession struct {
@@ -458,7 +461,7 @@ func (r *grpcInnerSession) Flush(ctx context.Context) error {
 }
 
 func (r *grpcRepositoryClient) NewWriter(ctx context.Context, opt WriteSessionOptions) (context.Context, RepositoryWriter, error) {
-	w, err := newGRPCAPIRepositoryForConnection(ctx, r.conn, r.connRefCount, r.cliOpts, opt, r.contentCache, false)
+	w, err := newGRPCAPIRepositoryForConnection(ctx, r.conn, r.connRefCount, r.cliOpts, opt, r.contentCache, false, r.metricsRegistry)
 	if err != nil {
 		return nil, nil, err
 	}
@@ -770,7 +773,7 @@ func (c grpcCreds) RequireTransportSecurity() bool {
 
 // OpenGRPCAPIRepository opens the Repository based on remote GRPC server.
 // The APIServerInfo must have the address of the repository as 'https://host:port'
-func OpenGRPCAPIRepository(ctx context.Context, si *APIServerInfo, cliOpts ClientOptions, contentCache *cache.PersistentCache, password string) (Repository, error) {
+func OpenGRPCAPIRepository(ctx context.Context, si *APIServerInfo, cliOpts ClientOptions, contentCache *cache.PersistentCache, password string, mr *metrics.Registry) (Repository, error) {
 	var transportCreds credentials.TransportCredentials
 
 	if si.TrustedServerCertificateFingerprint != "" {
@@ -801,7 +804,7 @@ func OpenGRPCAPIRepository(ctx context.Context, si *APIServerInfo, cliOpts Clien
 		return nil, errors.Wrap(err, "dial error")
 	}
 
-	rep, err := newGRPCAPIRepositoryForConnection(ctx, conn, new(int32), cliOpts, WriteSessionOptions{}, contentCache, true)
+	rep, err := newGRPCAPIRepositoryForConnection(ctx, conn, new(int32), cliOpts, WriteSessionOptions{}, contentCache, true, mr)
 	if err != nil {
 		return nil, err
 	}
@@ -868,8 +871,22 @@ func (r *grpcRepositoryClient) killInnerSession() {
 	}
 }
 
+// Metrics provides access to the metrics registry.
+func (r *grpcRepositoryClient) Metrics() *metrics.Registry {
+	return r.metricsRegistry
+}
+
 // newGRPCAPIRepositoryForConnection opens GRPC-based repository connection.
-func newGRPCAPIRepositoryForConnection(ctx context.Context, conn *grpc.ClientConn, connRefCount *int32, cliOpts ClientOptions, opt WriteSessionOptions, contentCache *cache.PersistentCache, transparentRetries bool) (*grpcRepositoryClient, error) {
+func newGRPCAPIRepositoryForConnection(
+	ctx context.Context,
+	conn *grpc.ClientConn,
+	connRefCount *int32,
+	cliOpts ClientOptions,
+	opt WriteSessionOptions,
+	contentCache *cache.PersistentCache,
+	transparentRetries bool,
+	mr *metrics.Registry,
+) (*grpcRepositoryClient, error) {
 	if opt.OnUpload == nil {
 		opt.OnUpload = func(i int64) {}
 	}
@@ -883,6 +900,7 @@ func newGRPCAPIRepositoryForConnection(ctx context.Context, conn *grpc.ClientCon
 		isReadOnly:         cliOpts.ReadOnly,
 		contentCache:       contentCache,
 		asyncWritesWG:      new(errgroup.Group),
+		metricsRegistry:    mr,
 	}
 
 	return inSessionWithoutRetry(ctx, rr, func(ctx context.Context, sess *grpcInnerSession) (*grpcRepositoryClient, error) {
@@ -900,7 +918,7 @@ func newGRPCAPIRepositoryForConnection(ctx context.Context, conn *grpc.ClientCon
 
 		rr.serverSupportsContentCompression = p.SupportsContentCompression
 
-		rr.omgr, err = object.NewObjectManager(ctx, rr, rr.objectFormat)
+		rr.omgr, err = object.NewObjectManager(ctx, rr, rr.objectFormat, rr.metricsRegistry)
 		if err != nil {
 			return nil, errors.Wrap(err, "unable to initialize object manager")
 		}
diff --git a/repo/manifest/manifest_manager.go b/repo/manifest/manifest_manager.go
index dc945295..ba01dc82 100644
--- a/repo/manifest/manifest_manager.go
+++ b/repo/manifest/manifest_manager.go
@@ -14,6 +14,7 @@
 
 	"github.com/kopia/kopia/internal/clock"
 	"github.com/kopia/kopia/internal/gather"
+	"github.com/kopia/kopia/internal/metrics"
 	"github.com/kopia/kopia/repo/compression"
 	"github.com/kopia/kopia/repo/content"
 	"github.com/kopia/kopia/repo/logging"
@@ -199,7 +200,7 @@ func cloneEntryMetadata(e *manifestEntry) *EntryMetadata {
 // matchesLabels returns true when all entries in 'b' are found in the 'a'.
 func matchesLabels(a, b map[string]string) bool {
 	for k, v := range b {
-		if a[k] != v {
+		if av, ok := a[k]; !ok || av != v {
 			return false
 		}
 	}
@@ -266,7 +267,7 @@ type ManagerOptions struct {
 }
 
 // NewManager returns new manifest manager for the provided content manager.
-func NewManager(ctx context.Context, b contentManager, options ManagerOptions) (*Manager, error) {
+func NewManager(ctx context.Context, b contentManager, options ManagerOptions, mr *metrics.Registry) (*Manager, error) {
 	timeNow := options.TimeNow
 	if timeNow == nil {
 		timeNow = clock.Now
diff --git a/repo/object/object_manager.go b/repo/object/object_manager.go
index 10078ada..ce3341cc 100644
--- a/repo/object/object_manager.go
+++ b/repo/object/object_manager.go
@@ -9,6 +9,7 @@
 	"github.com/pkg/errors"
 
 	"github.com/kopia/kopia/internal/gather"
+	"github.com/kopia/kopia/internal/metrics"
 	"github.com/kopia/kopia/repo/compression"
 	"github.com/kopia/kopia/repo/content"
 	"github.com/kopia/kopia/repo/format"
@@ -197,7 +198,7 @@ func PrefetchBackingContents(ctx context.Context, contentMgr contentManager, obj
 }
 
 // NewObjectManager creates an ObjectManager with the specified content manager and format.
-func NewObjectManager(ctx context.Context, bm contentManager, f format.ObjectFormat) (*Manager, error) {
+func NewObjectManager(ctx context.Context, bm contentManager, f format.ObjectFormat, mr *metrics.Registry) (*Manager, error) {
 	om := &Manager{
 		contentMgr: bm,
 		Format:     f,
diff --git a/repo/open.go b/repo/open.go
index f299e1ef..a57751ee 100644
--- a/repo/open.go
+++ b/repo/open.go
@@ -15,11 +15,13 @@
 	"github.com/kopia/kopia/internal/cache"
 	"github.com/kopia/kopia/internal/epoch"
 	"github.com/kopia/kopia/internal/feature"
+	"github.com/kopia/kopia/internal/metrics"
 	"github.com/kopia/kopia/internal/retry"
 	"github.com/kopia/kopia/repo/blob"
 	"github.com/kopia/kopia/repo/blob/beforeop"
 	loggingwrapper "github.com/kopia/kopia/repo/blob/logging"
 	"github.com/kopia/kopia/repo/blob/readonly"
+	"github.com/kopia/kopia/repo/blob/storagemetrics"
 	"github.com/kopia/kopia/repo/blob/throttling"
 	"github.com/kopia/kopia/repo/content"
 	"github.com/kopia/kopia/repo/format"
@@ -123,7 +125,7 @@ func Open(ctx context.Context, configFile, password string, options *Options) (r
 	return openDirect(ctx, configFile, lc, password, options)
 }
 
-func getContentCacheOrNil(ctx context.Context, opt *content.CachingOptions, password string) (*cache.PersistentCache, error) {
+func getContentCacheOrNil(ctx context.Context, opt *content.CachingOptions, password string, mr *metrics.Registry) (*cache.PersistentCache, error) {
 	opt = opt.CloneOrDefault()
 
 	cs, err := cache.NewStorageOrNil(ctx, opt.CacheDirectory, opt.MaxCacheSizeBytes, "server-contents")
@@ -149,7 +151,7 @@ func getContentCacheOrNil(ctx context.Context, opt *content.CachingOptions, pass
 	pc, err := cache.NewPersistentCache(ctx, "cache-storage", cs, prot, cache.SweepSettings{
 		MaxSizeBytes: opt.MaxCacheSizeBytes,
 		MinSweepAge:  opt.MinContentSweepAge.DurationOrDefault(content.DefaultDataCacheSweepAge),
-	})
+	}, mr)
 	if err != nil {
 		return nil, errors.Wrap(err, "unable to open persistent cache")
 	}
@@ -159,16 +161,18 @@ func getContentCacheOrNil(ctx context.Context, opt *content.CachingOptions, pass
 
 // OpenAPIServer connects remote repository over Kopia API.
 func OpenAPIServer(ctx context.Context, si *APIServerInfo, cliOpts ClientOptions, cachingOptions *content.CachingOptions, password string) (Repository, error) {
-	contentCache, err := getContentCacheOrNil(ctx, cachingOptions, password)
+	me := metrics.NewRegistry()
+
+	contentCache, err := getContentCacheOrNil(ctx, cachingOptions, password, me)
 	if err != nil {
 		return nil, errors.Wrap(err, "error opening content cache")
 	}
 
 	if si.DisableGRPC {
-		return openRestAPIRepository(ctx, si, cliOpts, contentCache, password)
+		return openRestAPIRepository(ctx, si, cliOpts, contentCache, password, me)
 	}
 
-	return OpenGRPCAPIRepository(ctx, si, cliOpts, contentCache, password)
+	return OpenGRPCAPIRepository(ctx, si, cliOpts, contentCache, password, me)
 }
 
 // openDirect opens the repository that directly manipulates blob storage..
@@ -211,6 +215,9 @@ func openWithConfig(ctx context.Context, st blob.Storage, cliOpts ClientOptions,
 		DisableInternalLog: options.DisableInternalLog,
 	}
 
+	me := metrics.NewRegistry()
+	st = storagemetrics.NewWrapper(st, me)
+
 	fmgr, ferr := format.NewManager(ctx, st, cacheOpts.CacheDirectory, cliOpts.FormatBlobCacheDuration, password, cmOpts.TimeNow)
 	if ferr != nil {
 		return nil, errors.Wrap(ferr, "unable to create format manager")
@@ -279,7 +286,7 @@ func openWithConfig(ctx context.Context, st blob.Storage, cliOpts ClientOptions,
 	// background/interleaving upgrade lock storage monitor
 	st = upgradeLockMonitor(fmgr, options.UpgradeOwnerID, st, cmOpts.TimeNow, options.OnFatalError, options.TestOnlyIgnoreMissingRequiredFeatures)
 
-	scm, ferr := content.NewSharedManager(ctx, st, fmgr, cacheOpts, cmOpts)
+	scm, ferr := content.NewSharedManager(ctx, st, fmgr, cacheOpts, cmOpts, me)
 	if ferr != nil {
 		return nil, errors.Wrap(ferr, "unable to create shared content manager")
 	}
@@ -289,12 +296,12 @@ func openWithConfig(ctx context.Context, st blob.Storage, cliOpts ClientOptions,
 		SessionHost: cliOpts.Hostname,
 	}, "")
 
-	om, ferr := object.NewObjectManager(ctx, cm, fmgr.ObjectFormat())
+	om, ferr := object.NewObjectManager(ctx, cm, fmgr.ObjectFormat(), me)
 	if ferr != nil {
 		return nil, errors.Wrap(ferr, "unable to open object manager")
 	}
 
-	manifests, ferr := manifest.NewManager(ctx, cm, manifest.ManagerOptions{TimeNow: cmOpts.TimeNow})
+	manifests, ferr := manifest.NewManager(ctx, cm, manifest.ManagerOptions{TimeNow: cmOpts.TimeNow}, me)
 	if ferr != nil {
 		return nil, errors.Wrap(ferr, "unable to open manifests")
 	}
@@ -306,13 +313,14 @@ func openWithConfig(ctx context.Context, st blob.Storage, cliOpts ClientOptions,
 		mmgr:  manifests,
 		sm:    scm,
 		directRepositoryParameters: directRepositoryParameters{
-			cachingOptions: *cacheOpts,
-			fmgr:           fmgr,
-			timeNow:        cmOpts.TimeNow,
-			cliOpts:        cliOpts,
-			configFile:     configFile,
-			nextWriterID:   new(int32),
-			throttler:      throttler,
+			cachingOptions:  *cacheOpts,
+			fmgr:            fmgr,
+			timeNow:         cmOpts.TimeNow,
+			cliOpts:         cliOpts,
+			configFile:      configFile,
+			nextWriterID:    new(int32),
+			throttler:       throttler,
+			metricsRegistry: me,
 		},
 		closed: make(chan struct{}),
 	}
diff --git a/repo/repository.go b/repo/repository.go
index 42e371fd..f2d932b3 100644
--- a/repo/repository.go
+++ b/repo/repository.go
@@ -10,6 +10,7 @@
 	"go.opentelemetry.io/otel"
 
 	"github.com/kopia/kopia/internal/clock"
+	"github.com/kopia/kopia/internal/metrics"
 	"github.com/kopia/kopia/repo/blob"
 	"github.com/kopia/kopia/repo/blob/throttling"
 	"github.com/kopia/kopia/repo/content"
@@ -37,6 +38,7 @@ type Repository interface {
 	UpdateDescription(d string)
 	Refresh(ctx context.Context) error
 	Close(ctx context.Context) error
+	Metrics() *metrics.Registry
 }
 
 // RepositoryWriter provides methods to write to a repository.
@@ -87,13 +89,14 @@ type DirectRepositoryWriter interface {
 }
 
 type directRepositoryParameters struct {
-	configFile     string
-	cachingOptions content.CachingOptions
-	cliOpts        ClientOptions
-	timeNow        func() time.Time
-	fmgr           *format.Manager
-	nextWriterID   *int32
-	throttler      throttling.SettableThrottler
+	configFile      string
+	cachingOptions  content.CachingOptions
+	cliOpts         ClientOptions
+	timeNow         func() time.Time
+	fmgr            *format.Manager
+	nextWriterID    *int32
+	throttler       throttling.SettableThrottler
+	metricsRegistry *metrics.Registry
 }
 
 // directRepository is an implementation of repository that directly manipulates underlying storage.
@@ -248,12 +251,12 @@ func (r *directRepository) NewDirectWriter(ctx context.Context, opt WriteSession
 
 	mmgr, err := manifest.NewManager(ctx, cmgr, manifest.ManagerOptions{
 		TimeNow: r.timeNow,
-	})
+	}, r.metricsRegistry)
 	if err != nil {
 		return nil, nil, errors.Wrap(err, "error creating manifest manager")
 	}
 
-	omgr, err := object.NewObjectManager(ctx, cmgr, r.omgr.Format)
+	omgr, err := object.NewObjectManager(ctx, cmgr, r.omgr.Format, r.metricsRegistry)
 	if err != nil {
 		return nil, nil, errors.Wrap(err, "error creating object manager")
 	}
@@ -300,6 +303,11 @@ func (r *directRepository) Flush(ctx context.Context) error {
 	return errors.Wrap(r.cmgr.Flush(ctx), "error flushing contents")
 }
 
+// Metrics provides access to metrics registry.
+func (r *directRepository) Metrics() *metrics.Registry {
+	return r.metricsRegistry
+}
+
 // ObjectFormat returns the object format.
 func (r *directRepository) ObjectFormat() format.ObjectFormat {
 	return r.omgr.Format


Test patch:
diff --git a/cli/observability_flags_test.go b/cli/observability_flags_test.go
index 2c26d59e..9eb58c01 100644
--- a/cli/observability_flags_test.go
+++ b/cli/observability_flags_test.go
@@ -69,7 +69,7 @@ func TestMetricsPushFlags(t *testing.T) {
 
 	for _, b := range bodies {
 		// make sure bodies include some kopia metrics, don't need more
-		require.Contains(t, b, "kopia_content_cache_hit_bytes")
+		require.Contains(t, b, "kopia_cache_hit_bytes_total")
 	}
 
 	env.RunAndExpectFailure(t, "repo", "status",
diff --git a/internal/cache/content_cache_concurrency_test.go b/internal/cache/content_cache_concurrency_test.go
index a2b74912..9b42cf09 100644
--- a/internal/cache/content_cache_concurrency_test.go
+++ b/internal/cache/content_cache_concurrency_test.go
@@ -27,7 +27,7 @@ func newContentDataCache(ctx context.Context, st blob.Storage, cacheStorage cach
 		Sweep: cache.SweepSettings{
 			MaxSizeBytes: 100,
 		},
-	})
+	}, nil)
 }
 
 func newContentMetadataCache(ctx context.Context, st blob.Storage, cacheStorage cache.Storage) (cache.ContentCache, error) {
@@ -38,7 +38,7 @@ func newContentMetadataCache(ctx context.Context, st blob.Storage, cacheStorage
 		Sweep: cache.SweepSettings{
 			MaxSizeBytes: 100,
 		},
-	})
+	}, nil)
 }
 
 func TestPrefetchBlocksGetContent_DataCache(t *testing.T) {
diff --git a/internal/cache/content_cache_data_test.go b/internal/cache/content_cache_data_test.go
index 64c69385..7e29660d 100644
--- a/internal/cache/content_cache_data_test.go
+++ b/internal/cache/content_cache_data_test.go
@@ -27,7 +27,7 @@ func TestContentCacheForData(t *testing.T) {
 		Sweep: cache.SweepSettings{
 			MaxSizeBytes: 100,
 		},
-	})
+	}, nil)
 	require.NoError(t, err)
 
 	var tmp gather.WriteBuffer
@@ -78,7 +78,7 @@ func TestContentCacheForData_Passthrough(t *testing.T) {
 
 	ctx := testlogging.Context(t)
 
-	dataCache, err := cache.NewContentCache(ctx, underlying, cache.Options{})
+	dataCache, err := cache.NewContentCache(ctx, underlying, cache.Options{}, nil)
 
 	require.NoError(t, err)
 	require.NoError(t, underlying.PutBlob(ctx, "blob1", gather.FromSlice([]byte{1, 2, 3, 4, 5, 6}), blob.PutOptions{}))
diff --git a/internal/cache/content_cache_metadata_test.go b/internal/cache/content_cache_metadata_test.go
index 069c2cd3..ffc14240 100644
--- a/internal/cache/content_cache_metadata_test.go
+++ b/internal/cache/content_cache_metadata_test.go
@@ -29,7 +29,7 @@ func TestContentCacheForMetadata(t *testing.T) {
 		Sweep: cache.SweepSettings{
 			MaxSizeBytes: 100,
 		},
-	})
+	}, nil)
 	require.NoError(t, err)
 
 	cacheStorage := metadataCache.CacheStorage()
@@ -80,7 +80,7 @@ func TestContentCacheForMetadata_Passthrough(t *testing.T) {
 		Sweep: cache.SweepSettings{
 			MaxSizeBytes: 100,
 		},
-	})
+	}, nil)
 
 	require.NoError(t, err)
 	require.NoError(t, underlying.PutBlob(ctx, "blob1", gather.FromSlice([]byte{1, 2, 3, 4, 5, 6}), blob.PutOptions{}))
diff --git a/internal/cache/content_cache_test.go b/internal/cache/content_cache_test.go
index 28aa9190..2a546b56 100644
--- a/internal/cache/content_cache_test.go
+++ b/internal/cache/content_cache_test.go
@@ -62,7 +62,7 @@ func TestCacheExpiration(t *testing.T) {
 			SweepFrequency: 500 * time.Millisecond,
 			TouchThreshold: -1,
 		},
-	})
+	}, nil)
 
 	require.NoError(t, err)
 
@@ -121,7 +121,7 @@ func TestDiskContentCache(t *testing.T) {
 		Sweep: cache.SweepSettings{
 			MaxSizeBytes: maxBytes,
 		},
-	})
+	}, nil)
 	require.NoError(t, err)
 
 	defer cc.Close(ctx)
@@ -208,7 +208,7 @@ func TestCacheFailureToOpen(t *testing.T) {
 	_, err := cache.NewContentCache(testlogging.Context(t), underlyingStorage, cache.Options{
 		Storage: withoutTouchBlob{faultyCache},
 		Sweep:   cache.SweepSettings{MaxSizeBytes: 10000},
-	})
+	}, nil)
 	require.Error(t, err)
 	require.ErrorContains(t, err, someError.Error())
 
@@ -218,7 +218,7 @@ func TestCacheFailureToOpen(t *testing.T) {
 	cc, err := cache.NewContentCache(ctx, underlyingStorage, cache.Options{
 		Storage: withoutTouchBlob{faultyCache},
 		Sweep:   cache.SweepSettings{MaxSizeBytes: 10000},
-	})
+	}, nil)
 	require.NoError(t, err)
 
 	cc.Close(ctx)
@@ -235,7 +235,7 @@ func TestCacheFailureToWrite(t *testing.T) {
 	cc, err := cache.NewContentCache(testlogging.Context(t), underlyingStorage, cache.Options{
 		Storage: withoutTouchBlob{faultyCache},
 		Sweep:   cache.SweepSettings{MaxSizeBytes: 10000},
-	})
+	}, nil)
 	require.NoError(t, err)
 
 	ctx := testlogging.Context(t)
@@ -270,7 +270,7 @@ func TestCacheFailureToRead(t *testing.T) {
 	cc, err := cache.NewContentCache(testlogging.Context(t), underlyingStorage, cache.Options{
 		Storage: withoutTouchBlob{faultyCache},
 		Sweep:   cache.SweepSettings{MaxSizeBytes: 10000},
-	})
+	}, nil)
 	require.NoError(t, err)
 
 	ctx := testlogging.Context(t)
diff --git a/internal/cache/persistent_lru_cache_test.go b/internal/cache/persistent_lru_cache_test.go
index 8dad3a3f..24dec137 100644
--- a/internal/cache/persistent_lru_cache_test.go
+++ b/internal/cache/persistent_lru_cache_test.go
@@ -29,7 +29,7 @@ func TestPersistentLRUCache(t *testing.T) {
 		MaxSizeBytes:   maxSizeBytes,
 		TouchThreshold: cache.DefaultTouchThreshold,
 		SweepFrequency: cache.DefaultSweepFrequency,
-	})
+	}, nil)
 	require.NoError(t, err)
 
 	var tmp gather.WriteBuffer
@@ -71,7 +71,7 @@ func TestPersistentLRUCache(t *testing.T) {
 		MaxSizeBytes:   maxSizeBytes,
 		TouchThreshold: cache.DefaultTouchThreshold,
 		SweepFrequency: cache.DefaultSweepFrequency,
-	})
+	}, nil)
 	require.NoError(t, err)
 
 	verifyCached(ctx, t, pc, "key1", nil)
@@ -85,7 +85,7 @@ func TestPersistentLRUCache(t *testing.T) {
 		MaxSizeBytes:   maxSizeBytes,
 		TouchThreshold: cache.DefaultTouchThreshold,
 		SweepFrequency: cache.DefaultSweepFrequency,
-	})
+	}, nil)
 	require.NoError(t, err)
 
 	someError := errors.Errorf("some error")
@@ -135,7 +135,7 @@ func TestPersistentLRUCache_Invalid(t *testing.T) {
 
 	fs.AddFault(blobtesting.MethodGetMetadata).ErrorInstead(someError)
 
-	pc, err := cache.NewPersistentCache(ctx, "test", fc, nil, cache.SweepSettings{})
+	pc, err := cache.NewPersistentCache(ctx, "test", fc, nil, cache.SweepSettings{}, nil)
 	require.ErrorIs(t, err, someError)
 	require.Nil(t, pc)
 }
@@ -153,7 +153,7 @@ func TestPersistentLRUCache_GetDeletesInvalidBlob(t *testing.T) {
 	fs := blobtesting.NewFaultyStorage(st)
 	fc := faultyCache{fs}
 
-	pc, err := cache.NewPersistentCache(ctx, "test", fc, cache.ChecksumProtection([]byte{1, 2, 3}), cache.SweepSettings{})
+	pc, err := cache.NewPersistentCache(ctx, "test", fc, cache.ChecksumProtection([]byte{1, 2, 3}), cache.SweepSettings{}, nil)
 	require.NoError(t, err)
 
 	pc.Put(ctx, "key", gather.FromSlice([]byte{1, 2, 3}))
@@ -183,7 +183,7 @@ func TestPersistentLRUCache_PutIgnoresStorageFailure(t *testing.T) {
 	fs := blobtesting.NewFaultyStorage(st)
 	fc := faultyCache{fs}
 
-	pc, err := cache.NewPersistentCache(ctx, "test", fc, cache.ChecksumProtection([]byte{1, 2, 3}), cache.SweepSettings{})
+	pc, err := cache.NewPersistentCache(ctx, "test", fc, cache.ChecksumProtection([]byte{1, 2, 3}), cache.SweepSettings{}, nil)
 	require.NoError(t, err)
 
 	fs.AddFault(blobtesting.MethodPutBlob).ErrorInstead(someError)
@@ -213,7 +213,7 @@ func TestPersistentLRUCache_SweepMinSweepAge(t *testing.T) {
 		SweepFrequency: 100 * time.Millisecond,
 		MaxSizeBytes:   1000,
 		MinSweepAge:    10 * time.Second,
-	})
+	}, nil)
 	require.NoError(t, err)
 	pc.Put(ctx, "key", gather.FromSlice([]byte{1, 2, 3}))
 	pc.Put(ctx, "key2", gather.FromSlice(bytes.Repeat([]byte{1, 2, 3}, 1e6)))
@@ -241,7 +241,7 @@ func TestPersistentLRUCache_SweepIgnoresErrors(t *testing.T) {
 	pc, err := cache.NewPersistentCache(ctx, "test", fc, cache.ChecksumProtection([]byte{1, 2, 3}), cache.SweepSettings{
 		SweepFrequency: 100 * time.Millisecond,
 		MaxSizeBytes:   1000,
-	})
+	}, nil)
 	require.NoError(t, err)
 
 	// ignore delete errors forever
@@ -275,7 +275,7 @@ func TestPersistentLRUCache_Sweep1(t *testing.T) {
 		SweepFrequency: 10 * time.Millisecond,
 		MaxSizeBytes:   1,
 		MinSweepAge:    0 * time.Second,
-	})
+	}, nil)
 	require.NoError(t, err)
 	pc.Put(ctx, "key", gather.FromSlice([]byte{1, 2, 3}))
 	pc.Put(ctx, "key", gather.FromSlice(bytes.Repeat([]byte{1, 2, 3}, 1e6)))
@@ -329,7 +329,7 @@ func TestPersistentLRUCache_Defaults(t *testing.T) {
 
 	pc, err := cache.NewPersistentCache(ctx, "testing", cs, nil, cache.SweepSettings{
 		MaxSizeBytes: maxSizeBytes,
-	})
+	}, nil)
 	require.NoError(t, err)
 
 	defer pc.Close(ctx)
diff --git a/internal/metrics/metric_test.go b/internal/metrics/metric_test.go
new file mode 100644
index 00000000..5c667044
--- /dev/null
+++ b/internal/metrics/metric_test.go
@@ -0,0 +1,54 @@
+package metrics_test
+
+import (
+	"testing"
+
+	"github.com/prometheus/client_golang/prometheus"
+	io_prometheus_client "github.com/prometheus/client_model/go"
+	"github.com/stretchr/testify/require"
+)
+
+func mustFindMetric(t *testing.T, wantName string, wantType io_prometheus_client.MetricType, wantLabels map[string]string) *io_prometheus_client.Metric {
+	t.Helper()
+
+	mf, err := prometheus.DefaultGatherer.Gather()
+	require.NoError(t, err)
+
+	for _, f := range mf {
+		if f.GetName() != wantName {
+			continue
+		}
+
+		if f.GetType() != wantType {
+			continue
+		}
+
+		for _, l := range f.Metric {
+			if len(l.Label) != len(wantLabels) {
+				continue
+			}
+
+			found := true
+
+			for _, lab := range l.Label {
+				if wantLabels[lab.GetName()] != lab.GetValue() {
+					found = false
+				}
+			}
+
+			if found {
+				return l
+			}
+		}
+	}
+
+	for _, f := range mf {
+		for _, l := range f.Metric {
+			t.Logf("  %v %v %v", f.GetName(), f.GetType(), l.Label)
+		}
+	}
+
+	require.Failf(t, "metric %v not found", wantName)
+
+	return nil
+}
diff --git a/internal/metrics/metrics_counter_test.go b/internal/metrics/metrics_counter_test.go
new file mode 100644
index 00000000..6ac56d5c
--- /dev/null
+++ b/internal/metrics/metrics_counter_test.go
@@ -0,0 +1,66 @@
+package metrics_test
+
+import (
+	"testing"
+
+	prommodel "github.com/prometheus/client_model/go"
+	"github.com/stretchr/testify/require"
+
+	"github.com/kopia/kopia/internal/metrics"
+)
+
+func TestCounter_Nil(t *testing.T) {
+	var e *metrics.Registry
+	cnt := e.CounterInt64("aaa", "bbb", nil)
+	require.Nil(t, cnt)
+	cnt.Add(33)
+	require.Equal(t, int64(0), cnt.Snapshot())
+}
+
+func TestCounter_NoLabels(t *testing.T) {
+	e := metrics.NewRegistry()
+	cnt := e.CounterInt64("some_gauge", "some-help", nil)
+
+	require.Equal(t, 0.0,
+		mustFindMetric(t, "kopia_some_gauge_total", prommodel.MetricType_COUNTER, nil).
+			GetCounter().GetValue())
+	cnt.Add(33)
+	require.Equal(t, 33.0,
+		mustFindMetric(t, "kopia_some_gauge_total", prommodel.MetricType_COUNTER, nil).
+			GetCounter().GetValue())
+	cnt.Add(100)
+	require.Equal(t, 133.0,
+		mustFindMetric(t, "kopia_some_gauge_total", prommodel.MetricType_COUNTER, nil).
+			GetCounter().GetValue())
+
+	require.Equal(t, int64(133), cnt.Snapshot())
+}
+
+func TestCounter_WithLabels(t *testing.T) {
+	e := metrics.NewRegistry()
+	cnt1 := e.CounterInt64("some_gauge2", "some-help", map[string]string{"key1": "label1"})
+	cnt2 := e.CounterInt64("some_gauge2", "some-help", map[string]string{"key1": "label2"})
+
+	require.Equal(t, 0.0,
+		mustFindMetric(t, "kopia_some_gauge2_total", prommodel.MetricType_COUNTER, map[string]string{"key1": "label1"}).
+			GetCounter().GetValue())
+	require.Equal(t, 0.0,
+		mustFindMetric(t, "kopia_some_gauge2_total", prommodel.MetricType_COUNTER, map[string]string{"key1": "label2"}).
+			GetCounter().GetValue())
+	cnt1.Add(33)
+	cnt2.Add(44)
+	require.Equal(t, 44.0,
+		mustFindMetric(t, "kopia_some_gauge2_total", prommodel.MetricType_COUNTER, map[string]string{"key1": "label2"}).
+			GetCounter().GetValue())
+	require.Equal(t, 33.0,
+		mustFindMetric(t, "kopia_some_gauge2_total", prommodel.MetricType_COUNTER, map[string]string{"key1": "label1"}).
+			GetCounter().GetValue())
+	cnt1.Add(100)
+	cnt2.Add(100)
+	require.Equal(t, 133.0,
+		mustFindMetric(t, "kopia_some_gauge2_total", prommodel.MetricType_COUNTER, map[string]string{"key1": "label1"}).
+			GetCounter().GetValue())
+	require.Equal(t, 144.0,
+		mustFindMetric(t, "kopia_some_gauge2_total", prommodel.MetricType_COUNTER, map[string]string{"key1": "label2"}).
+			GetCounter().GetValue())
+}
diff --git a/internal/metrics/metrics_distribution_test.go b/internal/metrics/metrics_distribution_test.go
new file mode 100644
index 00000000..d38393a3
--- /dev/null
+++ b/internal/metrics/metrics_distribution_test.go
@@ -0,0 +1,23 @@
+package metrics
+
+import (
+	"math"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestBucketForThresholds(t *testing.T) {
+	buckets := IOLatencyThresholds.values
+	n := len(buckets)
+
+	assert.Equal(t, 0, bucketForThresholds(buckets, buckets[0]-1))
+
+	for i := 0; i < n; i++ {
+		assert.Equal(t, i, bucketForThresholds(buckets, buckets[i]-1))
+		assert.Equal(t, i, bucketForThresholds(buckets, buckets[i]))
+		assert.Equal(t, i+1, bucketForThresholds(buckets, buckets[i]+1), "looking for %v", buckets[i]+1)
+	}
+
+	assert.Equal(t, n, bucketForThresholds(buckets, math.MaxInt64))
+}
diff --git a/internal/metrics/metrics_duration_distribution_test.go b/internal/metrics/metrics_duration_distribution_test.go
new file mode 100644
index 00000000..0bba97e5
--- /dev/null
+++ b/internal/metrics/metrics_duration_distribution_test.go
@@ -0,0 +1,142 @@
+package metrics_test
+
+import (
+	"testing"
+	"time"
+
+	prommodel "github.com/prometheus/client_model/go"
+	"github.com/stretchr/testify/require"
+
+	"github.com/kopia/kopia/internal/metrics"
+)
+
+func TestDurationDistribution_Nil(t *testing.T) {
+	var e *metrics.Registry
+	dist := e.DurationDistribution("aaa", "bbb", metrics.IOLatencyThresholds, nil)
+	require.Nil(t, dist)
+	dist.Observe(time.Second)
+	require.Equal(t, metrics.DistributionState[time.Duration]{}, dist.Snapshot())
+}
+
+func TestSizeDistribution_Nil(t *testing.T) {
+	var e *metrics.Registry
+	cnt := e.SizeDistribution("aaa", "bbb", metrics.ISOBytesThresholds, nil)
+	require.Nil(t, cnt)
+	cnt.Observe(333)
+}
+
+func TestDurationDistribution_NoLabels(t *testing.T) {
+	e := metrics.NewRegistry()
+	cnt := e.DurationDistribution("some_dur_dist", "some-help", metrics.IOLatencyThresholds, nil)
+	cnt.Observe(time.Second)
+
+	h1 := mustFindMetric(t, "kopia_some_dur_dist_ms", prommodel.MetricType_HISTOGRAM, nil)
+	require.Equal(t, uint64(1), h1.GetHistogram().GetSampleCount())
+	require.Equal(t, 1000.0, h1.GetHistogram().GetSampleSum())
+
+	cnt.Observe(30 * time.Second)
+
+	h1 = mustFindMetric(t, "kopia_some_dur_dist_ms", prommodel.MetricType_HISTOGRAM, nil)
+	require.Equal(t, uint64(2), h1.GetHistogram().GetSampleCount())
+	require.Equal(t, 31000.0, h1.GetHistogram().GetSampleSum())
+}
+
+func TestDurationDistribution_WithLabels(t *testing.T) {
+	e := metrics.NewRegistry()
+	cnt1 := e.DurationDistribution("some_dur_dist2", "some-help", metrics.IOLatencyThresholds, map[string]string{"key1": "label1"})
+	cnt2 := e.DurationDistribution("some_dur_dist2", "some-help", metrics.IOLatencyThresholds, map[string]string{"key1": "label2"})
+
+	snap0 := cnt1.Snapshot()
+
+	require.Equal(t, int64(0), snap0.Count)
+	require.Equal(t, 0*time.Second, snap0.Min)
+	require.Equal(t, 0*time.Second, snap0.Max)
+	require.Equal(t, 0*time.Second, snap0.Sum)
+	require.Equal(t, 0*time.Second, snap0.Mean())
+
+	cnt1.Observe(time.Second)
+	cnt2.Observe(time.Hour)
+
+	h1 := mustFindMetric(t, "kopia_some_dur_dist2_ms", prommodel.MetricType_HISTOGRAM, map[string]string{"key1": "label1"})
+	h2 := mustFindMetric(t, "kopia_some_dur_dist2_ms", prommodel.MetricType_HISTOGRAM, map[string]string{"key1": "label2"})
+	require.Equal(t, uint64(1), h1.GetHistogram().GetSampleCount())
+	require.Equal(t, 1000.0, h1.GetHistogram().GetSampleSum())
+	require.Equal(t, uint64(1), h2.GetHistogram().GetSampleCount())
+	require.Equal(t, 3600000.0, h2.GetHistogram().GetSampleSum())
+
+	cnt1.Observe(30 * time.Second)
+	cnt2.Observe(50 * time.Second)
+
+	h1 = mustFindMetric(t, "kopia_some_dur_dist2_ms", prommodel.MetricType_HISTOGRAM, map[string]string{"key1": "label1"})
+	h2 = mustFindMetric(t, "kopia_some_dur_dist2_ms", prommodel.MetricType_HISTOGRAM, map[string]string{"key1": "label2"})
+	require.Equal(t, uint64(2), h1.GetHistogram().GetSampleCount())
+	require.Equal(t, 31000.0, h1.GetHistogram().GetSampleSum())
+	require.Equal(t, uint64(2), h2.GetHistogram().GetSampleCount())
+	require.Equal(t, 3650000.0, h2.GetHistogram().GetSampleSum())
+
+	snap1 := cnt1.Snapshot()
+
+	require.Equal(t, int64(2), snap1.Count)
+	require.Equal(t, time.Second, snap1.Min)
+	require.Equal(t, 30*time.Second, snap1.Max)
+	require.Equal(t, 31*time.Second, snap1.Sum)
+	require.Equal(t, 15500*time.Millisecond, snap1.Mean())
+
+	snap2 := cnt2.Snapshot()
+
+	require.Equal(t, int64(2), snap2.Count)
+	require.Equal(t, 50*time.Second, snap2.Min)
+	require.Equal(t, time.Hour, snap2.Max)
+	require.Equal(t, time.Hour+50*time.Second, snap2.Sum)
+	require.Equal(t, 30*time.Minute+25*time.Second, snap2.Mean())
+}
+
+func TestSizeDistribution_WithLabels(t *testing.T) {
+	e := metrics.NewRegistry()
+	cnt1 := e.SizeDistribution("some_size_dist", "some-help", metrics.ISOBytesThresholds, map[string]string{"key1": "label1"})
+	cnt2 := e.SizeDistribution("some_size_dist", "some-help", metrics.ISOBytesThresholds, map[string]string{"key1": "label2"})
+
+	snap0 := cnt1.Snapshot()
+
+	require.Equal(t, int64(0), snap0.Count)
+	require.Equal(t, int64(0), snap0.Min)
+	require.Equal(t, int64(0), snap0.Max)
+	require.Equal(t, int64(0), snap0.Sum)
+	require.Equal(t, int64(0), snap0.Mean())
+
+	cnt1.Observe(1000)
+	cnt2.Observe(1e6)
+
+	h1 := mustFindMetric(t, "kopia_some_size_dist", prommodel.MetricType_HISTOGRAM, map[string]string{"key1": "label1"})
+	h2 := mustFindMetric(t, "kopia_some_size_dist", prommodel.MetricType_HISTOGRAM, map[string]string{"key1": "label2"})
+	require.Equal(t, uint64(1), h1.GetHistogram().GetSampleCount())
+	require.Equal(t, 1000.0, h1.GetHistogram().GetSampleSum())
+	require.Equal(t, uint64(1), h2.GetHistogram().GetSampleCount())
+	require.Equal(t, 1.0e6, h2.GetHistogram().GetSampleSum())
+
+	cnt1.Observe(300)
+	cnt2.Observe(50e6)
+
+	h1 = mustFindMetric(t, "kopia_some_size_dist", prommodel.MetricType_HISTOGRAM, map[string]string{"key1": "label1"})
+	h2 = mustFindMetric(t, "kopia_some_size_dist", prommodel.MetricType_HISTOGRAM, map[string]string{"key1": "label2"})
+	require.Equal(t, uint64(2), h1.GetHistogram().GetSampleCount())
+	require.Equal(t, 1300.0, h1.GetHistogram().GetSampleSum())
+	require.Equal(t, uint64(2), h2.GetHistogram().GetSampleCount())
+	require.Equal(t, 51.0e6, h2.GetHistogram().GetSampleSum())
+
+	snap1 := cnt1.Snapshot()
+
+	require.Equal(t, int64(2), snap1.Count)
+	require.Equal(t, int64(300), snap1.Min)
+	require.Equal(t, int64(1000), snap1.Max)
+	require.Equal(t, int64(1300), snap1.Sum)
+	require.Equal(t, int64(650), snap1.Mean())
+
+	snap2 := cnt2.Snapshot()
+
+	require.Equal(t, int64(2), snap2.Count)
+	require.Equal(t, int64(1000000), snap2.Min)
+	require.Equal(t, int64(50000000), snap2.Max)
+	require.Equal(t, int64(51000000), snap2.Sum)
+	require.Equal(t, int64(25500000), snap2.Mean())
+}
diff --git a/internal/metrics/metrics_gauge_test.go b/internal/metrics/metrics_gauge_test.go
new file mode 100644
index 00000000..4be217be
--- /dev/null
+++ b/internal/metrics/metrics_gauge_test.go
@@ -0,0 +1,66 @@
+package metrics_test
+
+import (
+	"testing"
+
+	prommodel "github.com/prometheus/client_model/go"
+	"github.com/stretchr/testify/require"
+
+	"github.com/kopia/kopia/internal/metrics"
+)
+
+func TestGauge_Nil(t *testing.T) {
+	var mr *metrics.Registry
+	g := mr.Gauge("aaa", "bbb", nil)
+	require.Nil(t, g)
+	g.Set(33)
+	require.Equal(t, int64(0), g.Snapshot())
+}
+
+func TestGauge_NoLabels(t *testing.T) {
+	mr := metrics.NewRegistry()
+	g := mr.Gauge("some_gauge", "some-help", nil)
+
+	require.Equal(t, 0.0,
+		mustFindMetric(t, "kopia_some_gauge", prommodel.MetricType_GAUGE, nil).
+			GetGauge().GetValue())
+	g.Set(33)
+	require.Equal(t, 33.0,
+		mustFindMetric(t, "kopia_some_gauge", prommodel.MetricType_GAUGE, nil).
+			GetGauge().GetValue())
+	g.Set(133)
+	require.Equal(t, 133.0,
+		mustFindMetric(t, "kopia_some_gauge", prommodel.MetricType_GAUGE, nil).
+			GetGauge().GetValue())
+
+	require.Equal(t, int64(133), g.Snapshot())
+}
+
+func TestGauge_WithLabels(t *testing.T) {
+	mr := metrics.NewRegistry()
+	g1 := mr.Gauge("some_gauge2", "some-help", map[string]string{"key1": "label1"})
+	g2 := mr.Gauge("some_gauge2", "some-help", map[string]string{"key1": "label2"})
+
+	require.Equal(t, 0.0,
+		mustFindMetric(t, "kopia_some_gauge2", prommodel.MetricType_GAUGE, map[string]string{"key1": "label1"}).
+			GetGauge().GetValue())
+	require.Equal(t, 0.0,
+		mustFindMetric(t, "kopia_some_gauge2", prommodel.MetricType_GAUGE, map[string]string{"key1": "label2"}).
+			GetGauge().GetValue())
+	g1.Set(33)
+	g2.Set(44)
+	require.Equal(t, 44.0,
+		mustFindMetric(t, "kopia_some_gauge2", prommodel.MetricType_GAUGE, map[string]string{"key1": "label2"}).
+			GetGauge().GetValue())
+	require.Equal(t, 33.0,
+		mustFindMetric(t, "kopia_some_gauge2", prommodel.MetricType_GAUGE, map[string]string{"key1": "label1"}).
+			GetGauge().GetValue())
+	g1.Set(133)
+	g2.Set(144)
+	require.Equal(t, 133.0,
+		mustFindMetric(t, "kopia_some_gauge2", prommodel.MetricType_GAUGE, map[string]string{"key1": "label1"}).
+			GetGauge().GetValue())
+	require.Equal(t, 144.0,
+		mustFindMetric(t, "kopia_some_gauge2", prommodel.MetricType_GAUGE, map[string]string{"key1": "label2"}).
+			GetGauge().GetValue())
+}
diff --git a/internal/metrics/metrics_registry_test.go b/internal/metrics/metrics_registry_test.go
new file mode 100644
index 00000000..1197ffaf
--- /dev/null
+++ b/internal/metrics/metrics_registry_test.go
@@ -0,0 +1,49 @@
+package metrics_test
+
+import (
+	"bytes"
+	"context"
+	"sort"
+	"strings"
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/require"
+
+	"github.com/kopia/kopia/internal/metrics"
+	"github.com/kopia/kopia/repo/logging"
+)
+
+func TestMetricEmitter_Nil(t *testing.T) {
+	var m *metrics.Registry
+
+	m.Log(context.Background())
+	m.Close(context.Background())
+}
+
+func TestMetricEmitter_NotNil(t *testing.T) {
+	var buf bytes.Buffer
+
+	ctx := logging.WithLogger(context.Background(), logging.ToWriter(&buf))
+
+	r := metrics.NewRegistry()
+	r.CounterInt64("c1", "h1", nil).Add(33)
+	r.Gauge("g1", "h1", nil).Set(44)
+	r.Throughput("t1", "h1", nil).Observe(44, time.Second)
+	r.DurationDistribution("d1", "h1", metrics.IOLatencyThresholds, nil).Observe(33 * time.Second)
+	r.SizeDistribution("s1", "h1", metrics.ISOBytesThresholds, nil).Observe(333)
+	require.NoError(t, r.Log(ctx))
+	require.NoError(t, r.Close(ctx))
+
+	lines := strings.Split(strings.TrimSpace(buf.String()), "\n")
+	sort.Strings(lines)
+
+	require.Equal(t, []string{
+		"COUNTER\t{\"name\":\"c1\",\"value\":33}",
+		"COUNTER\t{\"name\":\"t1_bytes\",\"value\":44}",
+		"COUNTER\t{\"name\":\"t1_duration_nanos\",\"value\":1000000000}",
+		"DURATION-DISTRIBUTION\t{\"name\":\"d1\",\"counters\":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0],\"cnt\":1,\"sum\":\"33s\",\"min\":\"33s\",\"avg\":\"33s\",\"max\":\"33s\"}",
+		"GAUGE\t{\"name\":\"g1\",\"value\":44}",
+		"SIZE-DISTRIBUTION\t{\"name\":\"s1\",\"counters\":[0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],\"cnt\":1,\"sum\":333,\"min\":333,\"avg\":333,\"max\":333}",
+	}, lines)
+}
diff --git a/internal/metrics/metrics_throughput_test.go b/internal/metrics/metrics_throughput_test.go
new file mode 100644
index 00000000..80618e15
--- /dev/null
+++ b/internal/metrics/metrics_throughput_test.go
@@ -0,0 +1,41 @@
+package metrics_test
+
+import (
+	"testing"
+	"time"
+
+	prommodel "github.com/prometheus/client_model/go"
+	"github.com/stretchr/testify/require"
+
+	"github.com/kopia/kopia/internal/metrics"
+)
+
+func TestThroughput_Nil(t *testing.T) {
+	var r *metrics.Registry
+
+	th := r.Throughput("some_throughput", "some-help", nil)
+	th.Observe(1, time.Second)
+}
+
+func TestThroughput_NotNil(t *testing.T) {
+	r := metrics.NewRegistry()
+
+	th := r.Throughput("some_throughput2", "some-help", nil)
+
+	require.Equal(t, 0.0,
+		mustFindMetric(t, "kopia_some_throughput2_bytes_total", prommodel.MetricType_COUNTER, nil).
+			GetCounter().GetValue())
+	require.Equal(t, 0.0,
+		mustFindMetric(t, "kopia_some_throughput2_duration_nanos_total", prommodel.MetricType_COUNTER, nil).
+			GetCounter().GetValue())
+
+	th.Observe(500, 500*time.Millisecond)
+	th.Observe(1, time.Second)
+
+	require.Equal(t, 501.0,
+		mustFindMetric(t, "kopia_some_throughput2_bytes_total", prommodel.MetricType_COUNTER, nil).
+			GetCounter().GetValue())
+	require.Equal(t, 1.5e9,
+		mustFindMetric(t, "kopia_some_throughput2_duration_nanos_total", prommodel.MetricType_COUNTER, nil).
+			GetCounter().GetValue())
+}
diff --git a/internal/server/server_test.go b/internal/server/server_test.go
index 75a865d4..753ad5ca 100644
--- a/internal/server/server_test.go
+++ b/internal/server/server_test.go
@@ -140,7 +140,7 @@ func TestGPRServer_AuthenticationError(t *testing.T) {
 	if _, err := repo.OpenGRPCAPIRepository(ctx, apiServerInfo, repo.ClientOptions{
 		Username: "bad-username",
 		Hostname: "bad-hostname",
-	}, nil, "bad-password"); err == nil {
+	}, nil, "bad-password", nil); err == nil {
 		t.Fatal("unexpected success when connecting with invalid username")
 	}
 }
diff --git a/repo/blob/storagemetrics/storage_metrics_test.go b/repo/blob/storagemetrics/storage_metrics_test.go
new file mode 100644
index 00000000..14bf0c52
--- /dev/null
+++ b/repo/blob/storagemetrics/storage_metrics_test.go
@@ -0,0 +1,288 @@
+package storagemetrics_test
+
+import (
+	"testing"
+
+	"github.com/pkg/errors"
+	"github.com/stretchr/testify/require"
+
+	"github.com/kopia/kopia/internal/blobtesting"
+	"github.com/kopia/kopia/internal/gather"
+	"github.com/kopia/kopia/internal/metrics"
+	"github.com/kopia/kopia/internal/testlogging"
+	"github.com/kopia/kopia/repo/blob"
+	"github.com/kopia/kopia/repo/blob/storagemetrics"
+)
+
+func TestStorageMetrics_PutBlob(t *testing.T) {
+	ctx := testlogging.Context(t)
+	someError := errors.Errorf("foo")
+	st := blobtesting.NewMapStorage(blobtesting.DataMap{}, nil, nil)
+
+	fs := blobtesting.NewFaultyStorage(st)
+	fs.AddFault(blobtesting.MethodPutBlob).ErrorInstead(someError)
+
+	mr := metrics.NewRegistry()
+	ms := storagemetrics.NewWrapper(fs, mr)
+	snap := mr.Snapshot()
+
+	requireCounterValue(t, snap, "blob_upload_bytes", 0)
+	requireCounterValue(t, snap, "blob_errors[method:PutBlob]", 0)
+
+	require.ErrorIs(t, ms.PutBlob(ctx, "someBlob1", gather.FromSlice([]byte{1, 2, 3, 4}), blob.PutOptions{}), someError)
+	require.NoError(t, ms.PutBlob(ctx, "someBlob", gather.FromSlice([]byte{1, 2, 3}), blob.PutOptions{}))
+
+	snap = mr.Snapshot()
+	requireCounterValue(t, snap, "blob_errors[method:PutBlob]", 1)
+	requireCounterValue(t, snap, "blob_upload_bytes", 3)
+
+	d := snap.DurationDistributions["blob_storage_latency[method:PutBlob]"]
+	require.EqualValues(t, 2, d.Count)
+}
+
+func TestStorageMetrics_GetBlob(t *testing.T) {
+	ctx := testlogging.Context(t)
+	someError := errors.Errorf("foo")
+	st := blobtesting.NewMapStorage(blobtesting.DataMap{}, nil, nil)
+
+	require.NoError(t, st.PutBlob(ctx, "someBlob", gather.FromSlice([]byte{1, 2, 3, 4, 5}), blob.PutOptions{}))
+
+	fs := blobtesting.NewFaultyStorage(st)
+	fs.AddFault(blobtesting.MethodGetBlob).ErrorInstead(someError)
+
+	mr := metrics.NewRegistry()
+	ms := storagemetrics.NewWrapper(fs, mr)
+	snap := mr.Snapshot()
+
+	requireCounterValue(t, snap, "blob_download_full_blob_bytes", 0)
+	requireCounterValue(t, snap, "blob_download_partial_blob_bytes", 0)
+	requireCounterValue(t, snap, "blob_errors[method:GetBlob]", 0)
+
+	var tmp gather.WriteBuffer
+	defer tmp.Close()
+
+	require.ErrorIs(t, ms.GetBlob(ctx, "someBlob", 0, -1, &tmp), someError)
+	require.NoError(t, ms.GetBlob(ctx, "someBlob", 0, -1, &tmp))
+
+	snap = mr.Snapshot()
+	requireCounterValue(t, snap, "blob_errors[method:GetBlob]", 1)
+	requireCounterValue(t, snap, "blob_download_full_blob_bytes", 5)
+
+	d := snap.DurationDistributions["blob_storage_latency[method:GetBlob-full]"]
+	require.EqualValues(t, 2, d.Count)
+
+	require.NoError(t, ms.GetBlob(ctx, "someBlob", 2, 2, &tmp))
+
+	snap = mr.Snapshot()
+	requireCounterValue(t, snap, "blob_errors[method:GetBlob]", 1)
+	requireCounterValue(t, snap, "blob_download_full_blob_bytes", 5)
+	require.Equal(t, 2, tmp.Length())
+
+	d = snap.DurationDistributions["blob_storage_latency[method:GetBlob-partial]"]
+	require.EqualValues(t, 1, d.Count)
+}
+
+func TestStorageMetrics_GetMetadata(t *testing.T) {
+	ctx := testlogging.Context(t)
+	someError := errors.Errorf("foo")
+	st := blobtesting.NewMapStorage(blobtesting.DataMap{}, nil, nil)
+
+	require.NoError(t, st.PutBlob(ctx, "someBlob", gather.FromSlice([]byte{1, 2, 3, 4, 5}), blob.PutOptions{}))
+
+	fs := blobtesting.NewFaultyStorage(st)
+	fs.AddFault(blobtesting.MethodGetMetadata).ErrorInstead(someError)
+
+	mr := metrics.NewRegistry()
+	ms := storagemetrics.NewWrapper(fs, mr)
+	snap := mr.Snapshot()
+
+	requireCounterValue(t, snap, "blob_errors[method:GetMetadata]", 0)
+
+	var tmp gather.WriteBuffer
+	defer tmp.Close()
+
+	_, err := ms.GetMetadata(ctx, "someBlob")
+	require.ErrorIs(t, err, someError)
+
+	_, err = ms.GetMetadata(ctx, "someBlob")
+	require.NoError(t, err)
+
+	snap = mr.Snapshot()
+	requireCounterValue(t, snap, "blob_errors[method:GetMetadata]", 1)
+
+	d := snap.DurationDistributions["blob_storage_latency[method:GetMetadata]"]
+	require.EqualValues(t, 2, d.Count)
+}
+
+func TestStorageMetrics_GetCapacity(t *testing.T) {
+	ctx := testlogging.Context(t)
+	someError := errors.Errorf("foo")
+	st := blobtesting.NewMapStorage(blobtesting.DataMap{}, nil, nil)
+
+	fs := blobtesting.NewFaultyStorage(st)
+	fs.AddFault(blobtesting.MethodGetCapacity).ErrorInstead(someError)
+
+	mr := metrics.NewRegistry()
+	ms := storagemetrics.NewWrapper(fs, mr)
+	snap := mr.Snapshot()
+
+	requireCounterValue(t, snap, "blob_errors[method:GetCapacity]", 0)
+
+	var tmp gather.WriteBuffer
+	defer tmp.Close()
+
+	_, err := ms.GetCapacity(ctx)
+	require.ErrorIs(t, err, someError)
+
+	_, err = ms.GetCapacity(ctx)
+	require.ErrorIs(t, err, blob.ErrNotAVolume)
+
+	snap = mr.Snapshot()
+	requireCounterValue(t, snap, "blob_errors[method:GetCapacity]", 2)
+
+	d := snap.DurationDistributions["blob_storage_latency[method:GetCapacity]"]
+	require.EqualValues(t, 2, d.Count)
+}
+
+func TestStorageMetrics_DeleteBlob(t *testing.T) {
+	ctx := testlogging.Context(t)
+	someError := errors.Errorf("foo")
+	st := blobtesting.NewMapStorage(blobtesting.DataMap{}, nil, nil)
+
+	require.NoError(t, st.PutBlob(ctx, "someBlob", gather.FromSlice([]byte{1, 2, 3, 4, 5}), blob.PutOptions{}))
+
+	fs := blobtesting.NewFaultyStorage(st)
+	fs.AddFault(blobtesting.MethodDeleteBlob).ErrorInstead(someError)
+
+	mr := metrics.NewRegistry()
+	ms := storagemetrics.NewWrapper(fs, mr)
+	snap := mr.Snapshot()
+
+	requireCounterValue(t, snap, "blob_errors[method:DeleteBlob]", 0)
+
+	var tmp gather.WriteBuffer
+	defer tmp.Close()
+
+	err := ms.DeleteBlob(ctx, "someBlob")
+	require.ErrorIs(t, err, someError)
+
+	err = ms.DeleteBlob(ctx, "someBlob")
+	require.NoError(t, err)
+
+	snap = mr.Snapshot()
+	requireCounterValue(t, snap, "blob_errors[method:DeleteBlob]", 1)
+
+	d := snap.DurationDistributions["blob_storage_latency[method:DeleteBlob]"]
+	require.EqualValues(t, 2, d.Count)
+}
+
+func TestStorageMetrics_Close(t *testing.T) {
+	ctx := testlogging.Context(t)
+	someError := errors.Errorf("foo")
+	st := blobtesting.NewMapStorage(blobtesting.DataMap{}, nil, nil)
+
+	fs := blobtesting.NewFaultyStorage(st)
+	fs.AddFault(blobtesting.MethodClose).ErrorInstead(someError)
+
+	mr := metrics.NewRegistry()
+	ms := storagemetrics.NewWrapper(fs, mr)
+	snap := mr.Snapshot()
+
+	requireCounterValue(t, snap, "blob_errors[method:Close]", 0)
+
+	var tmp gather.WriteBuffer
+	defer tmp.Close()
+
+	err := ms.Close(ctx)
+	require.ErrorIs(t, err, someError)
+
+	err = ms.Close(ctx)
+	require.NoError(t, err)
+
+	snap = mr.Snapshot()
+	requireCounterValue(t, snap, "blob_errors[method:Close]", 1)
+
+	d := snap.DurationDistributions["blob_storage_latency[method:Close]"]
+	require.EqualValues(t, 2, d.Count)
+}
+
+func TestStorageMetrics_FlushCaches(t *testing.T) {
+	ctx := testlogging.Context(t)
+	someError := errors.Errorf("foo")
+	st := blobtesting.NewMapStorage(blobtesting.DataMap{}, nil, nil)
+
+	fs := blobtesting.NewFaultyStorage(st)
+	fs.AddFault(blobtesting.MethodFlushCaches).ErrorInstead(someError)
+
+	mr := metrics.NewRegistry()
+	ms := storagemetrics.NewWrapper(fs, mr)
+	snap := mr.Snapshot()
+
+	requireCounterValue(t, snap, "blob_errors[method:FlushCaches]", 0)
+
+	var tmp gather.WriteBuffer
+	defer tmp.Close()
+
+	err := ms.FlushCaches(ctx)
+	require.ErrorIs(t, err, someError)
+
+	err = ms.FlushCaches(ctx)
+	require.NoError(t, err)
+
+	snap = mr.Snapshot()
+	requireCounterValue(t, snap, "blob_errors[method:FlushCaches]", 1)
+
+	d := snap.DurationDistributions["blob_storage_latency[method:FlushCaches]"]
+	require.EqualValues(t, 2, d.Count)
+}
+
+func TestStorageMetrics_ListBlobs(t *testing.T) {
+	ctx := testlogging.Context(t)
+	someError := errors.Errorf("foo")
+	st := blobtesting.NewMapStorage(blobtesting.DataMap{}, nil, nil)
+	require.NoError(t, st.PutBlob(ctx, "someBlob1", gather.FromSlice([]byte{1, 2, 3, 4, 5}), blob.PutOptions{}))
+	require.NoError(t, st.PutBlob(ctx, "someBlob2", gather.FromSlice([]byte{1, 2, 3, 4, 5}), blob.PutOptions{}))
+	require.NoError(t, st.PutBlob(ctx, "someBlob3", gather.FromSlice([]byte{1, 2, 3, 4, 5}), blob.PutOptions{}))
+
+	fs := blobtesting.NewFaultyStorage(st)
+	fs.AddFault(blobtesting.MethodListBlobs).ErrorInstead(someError)
+
+	mr := metrics.NewRegistry()
+	ms := storagemetrics.NewWrapper(fs, mr)
+	snap := mr.Snapshot()
+
+	requireCounterValue(t, snap, "blob_errors[method:ListBlobs]", 0)
+	requireCounterValue(t, snap, "blob_list_items", 0)
+
+	var tmp gather.WriteBuffer
+	defer tmp.Close()
+
+	err := ms.ListBlobs(ctx, "", func(bm blob.Metadata) error { return nil })
+	require.ErrorIs(t, err, someError)
+
+	err = ms.ListBlobs(ctx, "", func(bm blob.Metadata) error { return nil })
+	require.NoError(t, err)
+
+	snap = mr.Snapshot()
+	requireCounterValue(t, snap, "blob_errors[method:ListBlobs]", 1)
+
+	d := snap.DurationDistributions["blob_storage_latency[method:ListBlobs]"]
+	require.EqualValues(t, 2, d.Count)
+	requireCounterValue(t, snap, "blob_list_items", 3)
+}
+
+func TestStorageMetrics_Misc(t *testing.T) {
+	st := blobtesting.NewMapStorage(blobtesting.DataMap{}, nil, nil)
+	mr := metrics.NewRegistry()
+	ms := storagemetrics.NewWrapper(st, mr)
+	require.Equal(t, st.ConnectionInfo(), ms.ConnectionInfo())
+	require.Equal(t, st.DisplayName(), ms.DisplayName())
+}
+
+func requireCounterValue(t *testing.T, snap metrics.Snapshot, key string, want int64) {
+	t.Helper()
+
+	v, ok := snap.Counters[key]
+	require.True(t, ok)
+	require.EqualValues(t, want, v)
+}
diff --git a/repo/manifest/manifest_manager_test.go b/repo/manifest/manifest_manager_test.go
index 73afc4dd..b31ffa8e 100644
--- a/repo/manifest/manifest_manager_test.go
+++ b/repo/manifest/manifest_manager_test.go
@@ -32,14 +32,17 @@ func TestManifest(t *testing.T) {
 	item1 := map[string]int{"foo": 1, "bar": 2}
 	item2 := map[string]int{"foo": 2, "bar": 3}
 	item3 := map[string]int{"foo": 3, "bar": 4}
+	item4 := map[string]int{"foo": 4, "bar": 5}
 
 	labels1 := map[string]string{"type": "item", "color": "red"}
 	labels2 := map[string]string{"type": "item", "color": "blue", "shape": "square"}
 	labels3 := map[string]string{"type": "item", "shape": "square", "color": "red"}
+	labels4 := map[string]string{"type": "item", "rounded": ""}
 
 	id1 := addAndVerify(ctx, t, mgr, labels1, item1)
 	id2 := addAndVerify(ctx, t, mgr, labels2, item2)
 	id3 := addAndVerify(ctx, t, mgr, labels3, item3)
+	id4 := addAndVerify(ctx, t, mgr, labels4, item4)
 
 	cases := []struct {
 		criteria map[string]string
@@ -51,6 +54,7 @@ func TestManifest(t *testing.T) {
 		{map[string]string{"color": "red", "shape": "square"}, []ID{id3}},
 		{map[string]string{"color": "blue", "shape": "square"}, []ID{id2}},
 		{map[string]string{"color": "red", "shape": "circle"}, nil},
+		{map[string]string{"rounded": ""}, []ID{id4}},
 	}
 
 	// verify before flush
@@ -61,6 +65,7 @@ func TestManifest(t *testing.T) {
 	verifyItem(ctx, t, mgr, id1, labels1, item1)
 	verifyItem(ctx, t, mgr, id2, labels2, item2)
 	verifyItem(ctx, t, mgr, id3, labels3, item3)
+	verifyItem(ctx, t, mgr, id4, labels4, item4)
 
 	if err := mgr.Flush(ctx); err != nil {
 		t.Errorf("flush error: %v", err)
@@ -78,6 +83,7 @@ func TestManifest(t *testing.T) {
 	verifyItem(ctx, t, mgr, id1, labels1, item1)
 	verifyItem(ctx, t, mgr, id2, labels2, item2)
 	verifyItem(ctx, t, mgr, id3, labels3, item3)
+	verifyItem(ctx, t, mgr, id4, labels4, item4)
 
 	// flush underlying content manager and verify in new manifest manager.
 	mgr.b.Flush(ctx)
@@ -90,6 +96,7 @@ func TestManifest(t *testing.T) {
 	verifyItem(ctx, t, mgr2, id1, labels1, item1)
 	verifyItem(ctx, t, mgr2, id2, labels2, item2)
 	verifyItem(ctx, t, mgr2, id3, labels3, item3)
+	verifyItem(ctx, t, mgr2, id4, labels4, item4)
 
 	if err := mgr2.Flush(ctx); err != nil {
 		t.Errorf("flush error: %v", err)
@@ -136,6 +143,7 @@ func(ci content.Info) error {
 	verifyItem(ctx, t, mgr3, id1, labels1, item1)
 	verifyItem(ctx, t, mgr3, id2, labels2, item2)
 	verifyItemNotFound(ctx, t, mgr3, id3)
+	verifyItem(ctx, t, mgr3, id4, labels4, item4)
 }
 
 func TestManifestInitCorruptedBlock(t *testing.T) {
@@ -161,7 +169,7 @@ func TestManifestInitCorruptedBlock(t *testing.T) {
 
 	t.Cleanup(func() { bm0.Close(ctx) })
 
-	mgr, err := NewManager(ctx, bm, ManagerOptions{})
+	mgr, err := NewManager(ctx, bm, ManagerOptions{}, nil)
 	if err != nil {
 		t.Fatalf("err: %v", err)
 	}
@@ -187,7 +195,7 @@ func TestManifestInitCorruptedBlock(t *testing.T) {
 
 	t.Cleanup(func() { bm.Close(ctx) })
 
-	mgr, err = NewManager(ctx, bm, ManagerOptions{})
+	mgr, err = NewManager(ctx, bm, ManagerOptions{}, nil)
 	if err != nil {
 		t.Fatalf("err: %v", err)
 	}
@@ -319,7 +327,7 @@ func newManagerForTesting(ctx context.Context, t *testing.T, data blobtesting.Da
 
 	t.Cleanup(func() { bm.Close(ctx) })
 
-	mm, err := NewManager(ctx, bm, ManagerOptions{})
+	mm, err := NewManager(ctx, bm, ManagerOptions{}, nil)
 	require.NoError(t, err)
 
 	return mm
diff --git a/repo/object/object_manager_test.go b/repo/object/object_manager_test.go
index d1945809..b5d0fea8 100644
--- a/repo/object/object_manager_test.go
+++ b/repo/object/object_manager_test.go
@@ -112,7 +112,7 @@ func setupTest(t *testing.T, compressionHeaderID map[content.ID]compression.Head
 
 	r, err := NewObjectManager(testlogging.Context(t), fcm, format.ObjectFormat{
 		Splitter: "FIXED-1M",
-	})
+	}, nil)
 	if err != nil {
 		t.Fatalf("can't create object manager: %v", err)
 	}
@@ -283,7 +283,7 @@ func TestObjectWriterRaceBetweenCheckpointAndResult(t *testing.T) {
 
 	om, err := NewObjectManager(testlogging.Context(t), fcm, format.ObjectFormat{
 		Splitter: "FIXED-1M",
-	})
+	}, nil)
 	if err != nil {
 		t.Fatalf("can't create object manager: %v", err)
 	}
diff --git a/repo/repository_test.go b/repo/repository_test.go
index f4f703ce..22d04284 100644
--- a/repo/repository_test.go
+++ b/repo/repository_test.go
@@ -647,6 +647,72 @@ func (s *formatSpecificTestSuite) TestChangePassword(t *testing.T) {
 	}
 }
 
+func TestMetrics_CompressibleData(t *testing.T) {
+	ctx, env := repotesting.NewEnvironment(t, repotesting.FormatNotImportant)
+	_ = ctx
+	ms := env.Repository.Metrics().Snapshot()
+
+	require.EqualValues(t, 0, ensureMapEntry(t, ms.Counters, "content_write_bytes"))
+
+	var (
+		inputData = bytes.Repeat([]byte{1, 2, 3, 4}, 100)
+		count     = 0
+		oid       object.ID
+	)
+
+	for ensureMapEntry(t, env.Repository.Metrics().Snapshot().Counters, "content_write_duration_nanos") < 5e6 {
+		w := env.RepositoryWriter.NewObjectWriter(ctx, object.WriterOptions{
+			Compressor: "gzip",
+		})
+		w.Write(inputData)
+
+		var err error
+
+		oid, err = w.Result()
+		require.NoError(t, err)
+
+		count++
+	}
+
+	ms = env.Repository.Metrics().Snapshot()
+	require.EqualValues(t, count*len(inputData), ensureMapEntry(t, ms.Counters, "content_write_bytes"))
+	require.EqualValues(t, count*len(inputData), ensureMapEntry(t, ms.Counters, "content_hashed_bytes"))
+	require.EqualValues(t, len(inputData), ensureMapEntry(t, ms.Counters, "content_compression_attempted_bytes"))
+
+	// this is what 100x{1,2,3,4} compresses down to using gzip, it's also
+	// the number of bytes that go into encryption.
+	const compressedByteCount = 36
+
+	const encryptionOverhead = 28
+
+	require.EqualValues(t, compressedByteCount, ensureMapEntry(t, ms.Counters, "content_after_compression_bytes"))
+	require.EqualValues(t, len(inputData), ensureMapEntry(t, ms.Counters, "content_compressible_bytes"))
+	require.EqualValues(t, 0, ensureMapEntry(t, ms.Counters, "content_non_compressible_bytes"))
+	require.EqualValues(t, len(inputData)-compressedByteCount, ensureMapEntry(t, ms.Counters, "content_compression_savings_bytes"))
+	require.EqualValues(t, compressedByteCount+encryptionOverhead, ensureMapEntry(t, ms.Counters, "content_encrypted_bytes"))
+
+	r, err := env.RepositoryWriter.OpenObject(ctx, oid)
+	require.NoError(t, err)
+
+	data, err := io.ReadAll(r)
+	require.NoError(t, err)
+	require.Equal(t, inputData, data)
+
+	ms = env.Repository.Metrics().Snapshot()
+	require.EqualValues(t, len(inputData), ensureMapEntry(t, ms.Counters, "content_read_bytes"))
+	require.EqualValues(t, compressedByteCount, ensureMapEntry(t, ms.Counters, "content_decompressed_bytes"))
+	require.EqualValues(t, compressedByteCount+encryptionOverhead, ensureMapEntry(t, ms.Counters, "content_decrypted_bytes"))
+}
+
+func ensureMapEntry[T any](t *testing.T, m map[string]T, key string) T {
+	t.Helper()
+
+	actual, got := m[key]
+	require.True(t, got, key)
+
+	return actual
+}
+
 func TestDeriveKey(t *testing.T) {
 	testPurpose := []byte{0, 0, 0, 0}
 	testKeyLength := 8