Repository: kopia/kopia
Pull request #2592 (2022-11-21 23:14:53)

build(deps): bump github.com/minio/minio-go/v7 from 7.0.43 to 7.0.44
Bumps [github.com/minio/minio-go/v7](https://github.com/minio/minio-go) from 7.0.43 to 7.0.44.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/minio/minio-go/releases">github.com/minio/minio-go/v7's releases</a>.</em></p>
<blockquote>
<h2>Bugfix Release</h2>
<h2>What's Changed</h2>
<ul>
<li>fix: allow any content using starts-with with an empty value by <a href="https://github.com/fortuneforme"><code>@​fortuneforme</code></a> in <a href="https://github-redirect.dependabot.com/minio/minio-go/pull/1718">minio/minio-go#1718</a></li>
<li>Added NewArnFromString to generalize the functionality of ARN parsing by <a href="https://github.com/vgarvardt"><code>@​vgarvardt</code></a> in <a href="https://github-redirect.dependabot.com/minio/minio-go/pull/1721">minio/minio-go#1721</a></li>
<li>Update s3-endpoints.go: add missing regions by <a href="https://github.com/scottgerring"><code>@​scottgerring</code></a> in <a href="https://github-redirect.dependabot.com/minio/minio-go/pull/1726">minio/minio-go#1726</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/fortuneforme"><code>@​fortuneforme</code></a> made their first contribution in <a href="https://github-redirect.dependabot.com/minio/minio-go/pull/1718">minio/minio-go#1718</a></li>
<li><a href="https://github.com/vgarvardt"><code>@​vgarvardt</code></a> made their first contribution in <a href="https://github-redirect.dependabot.com/minio/minio-go/pull/1721">minio/minio-go#1721</a></li>
<li><a href="https://github.com/scottgerring"><code>@​scottgerring</code></a> made their first contribution in <a href="https://github-redirect.dependabot.com/minio/minio-go/pull/1726">minio/minio-go#1726</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a href="https://github.com/minio/minio-go/compare/v7.0.43...v7.0.44">https://github.com/minio/minio-go/compare/v7.0.43...v7.0.44</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/minio/minio-go/commit/bf998048003996a03a3099dfd1cec5de2829fbeb"><code>bf99804</code></a> add missing S3 regions (<a href="https://github-redirect.dependabot.com/minio/minio-go/issues/1726">#1726</a>)</li>
<li><a href="https://github.com/minio/minio-go/commit/39267760ed9bb78c88d17be63fcafcefb767a366"><code>3926776</code></a> doc: advise defer Close() on the objectReader</li>
<li><a href="https://github.com/minio/minio-go/commit/ff48c35dc8ca65b22d6d5081c1070015077cce89"><code>ff48c35</code></a> Added NewArnFromString to generalize the functionality of ARN parsing (<a href="https://github-redirect.dependabot.com/minio/minio-go/issues/1721">#1721</a>)</li>
<li><a href="https://github.com/minio/minio-go/commit/d6fbd4980ec84fb532ad7b46ee097e007392780b"><code>d6fbd49</code></a> fix: allow any content using starts-with with an empty value (<a href="https://github-redirect.dependabot.com/minio/minio-go/issues/1718">#1718</a>)</li>
<li><a href="https://github.com/minio/minio-go/commit/36e5116001d9e05dc27e8ecc790d261bd1632906"><code>36e5116</code></a> Update version to next release</li>
<li>See full diff in <a href="https://github.com/minio/minio-go/compare/v7.0.43...v7.0.44">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=github.com/minio/minio-go/v7&package-manager=go_modules&previous-version=7.0.43&new-version=7.0.44)](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 will merge this PR once CI passes on it, as requested by @kopia-builder.

[//]: # (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/2592?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia) Report
Base: **74.97**% // Head: **74.88**% // Decreases project coverage by **`-0.08%`** :warning:
> Coverage data is based on head [(`2bb9183`)](https://codecov.io/gh/kopia/kopia/pull/2592?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia) compared to base [(`b5dca25`)](https://codecov.io/gh/kopia/kopia/commit/b5dca25d52e52ac323f4592b4d7582592c0c96cb?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    #2592      +/-   ##
==========================================
- Coverage   74.97%   74.88%   -0.09%     
==========================================
  Files         451      451              
  Lines       35490    35490              
==========================================
- Hits        26608    26577      -31     
- Misses       7006     7027      +21     
- Partials     1876     1886      +10     
```


| [Impacted Files](https://codecov.io/gh/kopia/kopia/pull/2592?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/2592/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: |
| [internal/server/source\_manager.go](https://codecov.io/gh/kopia/kopia/pull/2592/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia#diff-aW50ZXJuYWwvc2VydmVyL3NvdXJjZV9tYW5hZ2VyLmdv) | `80.66% <0.00%> (-3.31%)` | :arrow_down: |
| [internal/providervalidation/providervalidation.go](https://codecov.io/gh/kopia/kopia/pull/2592/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia#diff-aW50ZXJuYWwvcHJvdmlkZXJ2YWxpZGF0aW9uL3Byb3ZpZGVydmFsaWRhdGlvbi5nbw==) | `63.09% <0.00%> (-2.59%)` | :arrow_down: |
| [snapshot/policy/policy\_manager.go](https://codecov.io/gh/kopia/kopia/pull/2592/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia#diff-c25hcHNob3QvcG9saWN5L3BvbGljeV9tYW5hZ2VyLmdv) | `73.20% <0.00%> (-2.27%)` | :arrow_down: |
| [cli/command\_index\_recover.go](https://codecov.io/gh/kopia/kopia/pull/2592/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia#diff-Y2xpL2NvbW1hbmRfaW5kZXhfcmVjb3Zlci5nbw==) | `65.00% <0.00%> (-1.43%)` | :arrow_down: |
| [repo/grpc\_repository\_client.go](https://codecov.io/gh/kopia/kopia/pull/2592/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia#diff-cmVwby9ncnBjX3JlcG9zaXRvcnlfY2xpZW50Lmdv) | `76.27% <0.00%> (-0.69%)` | :arrow_down: |
| [repo/content/index/index\_v1.go](https://codecov.io/gh/kopia/kopia/pull/2592/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia#diff-cmVwby9jb250ZW50L2luZGV4L2luZGV4X3YxLmdv) | `76.17% <0.00%> (+1.17%)` | :arrow_up: |
| [repo/blob/throttling/throttling\_semaphore.go](https://codecov.io/gh/kopia/kopia/pull/2592/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia#diff-cmVwby9ibG9iL3Rocm90dGxpbmcvdGhyb3R0bGluZ19zZW1hcGhvcmUuZ28=) | `100.00% <0.00%> (+2.94%)` | :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/2592?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).

dependabot[bot]: One of your CI runs failed on this pull request, so Dependabot won't merge it.

Dependabot will still automatically merge this pull request if you amend it and your tests pass.

Patch:
diff --git a/go.mod b/go.mod
index d9e028e3..dd10b66b 100644
--- a/go.mod
+++ b/go.mod
@@ -9,7 +9,7 @@ require (
 	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.139
+	github.com/aws/aws-sdk-go v1.44.143
 	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
@@ -30,7 +30,7 @@ require (
 	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.43
+	github.com/minio/minio-go/v7 v7.0.44
 	github.com/minio/sha256-simd v1.0.0 // indirect
 	github.com/natefinch/atomic v1.0.1
 	github.com/pierrec/lz4 v2.6.1+incompatible
@@ -58,7 +58,7 @@ require (
 	golang.org/x/term v0.2.0
 	golang.org/x/text v0.4.0
 	google.golang.org/api v0.103.0
-	google.golang.org/grpc v1.50.1
+	google.golang.org/grpc v1.51.0
 	google.golang.org/protobuf v1.28.1
 	gopkg.in/ini.v1 v1.66.6 // indirect
 	gopkg.in/kothar/go-backblaze.v0 v0.0.0-20210124194846-35409b867216
diff --git a/go.sum b/go.sum
index c30d0105..6023ecb6 100644
--- a/go.sum
+++ b/go.sum
@@ -81,8 +81,8 @@ 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/aws/aws-sdk-go v1.44.139 h1:Mj/OZBy9RTbzJ8pfgK6rOL8xgUEAIn8pfIN6qWFtpAk=
-github.com/aws/aws-sdk-go v1.44.139/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=
+github.com/aws/aws-sdk-go v1.44.143 h1:5xN01uj7zV0VozeE/cxsHsNjSIooqmzVRX0omLxoO0w=
+github.com/aws/aws-sdk-go v1.44.143/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=
 github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
 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=
@@ -310,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.43 h1:14Q4lwblqTdlAmba05oq5xL0VBLHi06zS4yLnIkz6hI=
-github.com/minio/minio-go/v7 v7.0.43/go.mod h1:nCrRzjoSUQh8hgKKtu3Y708OLvRLtuASMg2/nvmbarw=
+github.com/minio/minio-go/v7 v7.0.44 h1:9zUJ7iU7ax2P1jOvTp6nVrgzlZq3AZlFm0XfRFDKstM=
+github.com/minio/minio-go/v7 v7.0.44/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=
@@ -736,8 +736,8 @@ google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3Iji
 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.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
-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 v1.51.0 h1:E1eGv1FTqoLIdnBCZufiSHgKjlqG6fKFf6pPWtMTh8U=
+google.golang.org/grpc v1.51.0/go.mod h1:wgNDFcnuBGmxLKI/qn4T+m5BtEBYXJPvibbUPsAIPww=
 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=
diff --git a/internal/metrics/metrics_aggregation.go b/internal/metrics/metrics_aggregation.go
new file mode 100644
index 00000000..c58d489c
--- /dev/null
+++ b/internal/metrics/metrics_aggregation.go
@@ -0,0 +1,12 @@
+package metrics
+
+// AggregateSnapshots computes aggregate of the provided snapshots.
+func AggregateSnapshots(snapshots []Snapshot) Snapshot {
+	result := createSnapshot()
+
+	for _, s := range snapshots {
+		result.mergeFrom(s)
+	}
+
+	return result
+}
diff --git a/internal/metrics/metrics_distribution.go b/internal/metrics/metrics_distribution.go
index a6cec80a..483c2992 100644
--- a/internal/metrics/metrics_distribution.go
+++ b/internal/metrics/metrics_distribution.go
@@ -19,6 +19,38 @@ type DistributionState[T constraints.Float | constraints.Integer] struct {
 	BucketThresholds []T     `json:"-"`
 }
 
+func (s *DistributionState[T]) mergeFrom(other *DistributionState[T]) {
+	if s.Count == 0 {
+		s.Min = other.Min
+		s.Max = other.Max
+	}
+
+	if other.Max > s.Max {
+		s.Max = other.Max
+	}
+
+	if other.Min < s.Min {
+		s.Min = other.Min
+	}
+
+	s.Sum += other.Sum
+	s.Count += other.Count
+
+	if s.BucketCounters == nil {
+		s.BucketCounters = make([]int64, len(other.BucketCounters))
+	}
+
+	if s.BucketThresholds == nil {
+		s.BucketThresholds = other.BucketThresholds
+	}
+
+	if len(s.BucketCounters) == len(other.BucketCounters) {
+		for i, v := range other.BucketCounters {
+			s.BucketCounters[i] += v
+		}
+	}
+}
+
 // Mean returns arithmetic mean value captured in the distribution.
 func (s *DistributionState[T]) Mean() T {
 	if s.Count == 0 {
@@ -72,13 +104,13 @@ func (d *Distribution[T]) Observe(value T) {
 }
 
 // Snapshot returns a snapshot of the distribution state.
-func (d *Distribution[T]) Snapshot(reset bool) DistributionState[T] {
+func (d *Distribution[T]) Snapshot(reset bool) *DistributionState[T] {
 	if d == nil {
-		return DistributionState[T]{}
+		return &DistributionState[T]{}
 	}
 
 	if reset {
-		return *(d.newState())
+		return d.newState()
 	}
 
 	d.mu.Lock()
@@ -86,7 +118,7 @@ func (d *Distribution[T]) Snapshot(reset bool) DistributionState[T] {
 
 	s := d.state.Load()
 
-	return DistributionState[T]{
+	return &DistributionState[T]{
 		Sum:            s.Sum,
 		Count:          s.Count,
 		Min:            s.Min,
diff --git a/internal/metrics/metrics_gauge.go b/internal/metrics/metrics_gauge.go
deleted file mode 100644
index 753fac65..00000000
--- a/internal/metrics/metrics_gauge.go
+++ /dev/null
@@ -1,72 +0,0 @@
-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(reset bool) int64 {
-	if c == nil {
-		return 0
-	}
-
-	if reset {
-		return c.newState()
-	}
-
-	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
index 96df1b4e..3a3a80de 100644
--- a/internal/metrics/metrics_registry.go
+++ b/internal/metrics/metrics_registry.go
@@ -17,7 +17,6 @@
 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]
@@ -25,29 +24,53 @@ type Registry struct {
 
 // Snapshot captures the state of all metrics.
 type Snapshot struct {
-	Counters              map[string]int64                            `json:"counters"`
-	Gauges                map[string]int64                            `json:"gauges"`
-	DurationDistributions map[string]DistributionState[time.Duration] `json:"durationDistributions"`
-	SizeDistributions     map[string]DistributionState[int64]         `json:"sizeDistributions"`
+	Counters              map[string]int64                             `json:"counters"`
+	DurationDistributions map[string]*DistributionState[time.Duration] `json:"durationDistributions"`
+	SizeDistributions     map[string]*DistributionState[int64]         `json:"sizeDistributions"`
 }
 
-// Snapshot captures the snapshot of all metrics.
-func (r *Registry) Snapshot(reset bool) Snapshot {
-	s := Snapshot{
+func (s *Snapshot) mergeFrom(other Snapshot) {
+	for k, v := range other.Counters {
+		s.Counters[k] += v
+	}
+
+	for k, v := range other.DurationDistributions {
+		target := s.DurationDistributions[k]
+		if target == nil {
+			target = &DistributionState[time.Duration]{}
+			s.DurationDistributions[k] = target
+		}
+
+		target.mergeFrom(v)
+	}
+
+	for k, v := range other.SizeDistributions {
+		target := s.SizeDistributions[k]
+		if target == nil {
+			target = &DistributionState[int64]{}
+			s.SizeDistributions[k] = target
+		}
+
+		target.mergeFrom(v)
+	}
+}
+
+func createSnapshot() Snapshot {
+	return Snapshot{
 		Counters:              map[string]int64{},
-		Gauges:                map[string]int64{},
-		DurationDistributions: map[string]DistributionState[time.Duration]{},
-		SizeDistributions:     map[string]DistributionState[int64]{},
+		DurationDistributions: map[string]*DistributionState[time.Duration]{},
+		SizeDistributions:     map[string]*DistributionState[int64]{},
 	}
+}
+
+// Snapshot captures the snapshot of all metrics.
+func (r *Registry) Snapshot(reset bool) Snapshot {
+	s := createSnapshot()
 
 	for k, c := range r.allCounters {
 		s.Counters[k] = c.Snapshot(reset)
 	}
 
-	for k, c := range r.allGauges {
-		s.Gauges[k] = c.Snapshot(reset)
-	}
-
 	for k, c := range r.allDurationDistributions {
 		s.DurationDistributions[k] = c.Snapshot(reset)
 	}
@@ -71,9 +94,9 @@ func (r *Registry) Close(ctx context.Context) error {
 }
 
 // Log logs all metrics in the registry.
-func (r *Registry) Log(ctx context.Context) error {
+func (r *Registry) Log(ctx context.Context) {
 	if r == nil {
-		return nil
+		return
 	}
 
 	s := r.Snapshot(false)
@@ -82,10 +105,6 @@ func (r *Registry) Log(ctx context.Context) error {
 		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)
 	}
@@ -95,15 +114,12 @@ func (r *Registry) Log(ctx context.Context) error {
 			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 {
 	r := &Registry{
 		allCounters:              map[string]*Counter{},
-		allGauges:                map[string]*Gauge{},
 		allDurationDistributions: map[string]*Distribution[time.Duration]{},
 		allSizeDistributions:     map[string]*Distribution[int64]{},
 		allThroughput:            map[string]*Throughput{},
diff --git a/internal/metrics/prom_cache.go b/internal/metrics/prom_cache.go
index 69a1121a..14f65bef 100644
--- a/internal/metrics/prom_cache.go
+++ b/internal/metrics/prom_cache.go
@@ -17,7 +17,6 @@
 var (
 	promCacheMutex sync.Mutex
 	promCounters   = map[string]*prometheus.CounterVec{}
-	promGauges     = map[string]*prometheus.GaugeVec{}
 	promHistograms = map[string]*prometheus.HistogramVec{}
 )
 
@@ -35,20 +34,6 @@ func getPrometheusCounter(opts prometheus.CounterOpts, labels map[string]string)
 	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()
diff --git a/repo/api_server_repository.go b/repo/api_server_repository.go
index d6c43ea8..7f4c68d3 100644
--- a/repo/api_server_repository.go
+++ b/repo/api_server_repository.go
@@ -10,14 +10,11 @@
 	"github.com/pkg/errors"
 
 	"github.com/kopia/kopia/internal/apiclient"
-	"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"
-	"github.com/kopia/kopia/repo/format"
 	"github.com/kopia/kopia/repo/hashing"
 	"github.com/kopia/kopia/repo/manifest"
 	"github.com/kopia/kopia/repo/object"
@@ -34,17 +31,11 @@ type APIServerInfo struct {
 // API server hosted by `kopia server`, instead of directly manipulating files in the BLOB storage.
 type apiServerRepository struct {
 	cli                              *apiclient.KopiaAPIClient
-	h                                hashing.HashFunc
-	objectFormat                     format.ObjectFormat
 	serverSupportsContentCompression bool
-	cliOpts                          ClientOptions
 	omgr                             *object.Manager
 	wso                              WriteSessionOptions
-	metricsRegistry                  *metrics.Registry
 
-	isSharedReadOnlySession bool
-	contentCache            *cache.PersistentCache
-	*refCountedCloser
+	immutableServerRepositoryParameters // immutable parameters
 }
 
 func (r *apiServerRepository) APIServerURL() string {
@@ -59,10 +50,6 @@ func (r *apiServerRepository) Description() string {
 	return fmt.Sprintf("Repository Server: %v", r.cli.BaseURL)
 }
 
-func (r *apiServerRepository) ClientOptions() ClientOptions {
-	return r.cliOpts
-}
-
 func (r *apiServerRepository) OpenObject(ctx context.Context, id object.ID) (object.Reader, error) {
 	//nolint:wrapcheck
 	return object.Open(ctx, r, id)
@@ -166,7 +153,7 @@ func (r *apiServerRepository) NewWriter(ctx context.Context, opt WriteSessionOpt
 	w.omgr = omgr
 	w.wso = opt
 
-	r.refCountedCloser.addRef()
+	r.addRef()
 
 	return ctx, w, nil
 }
@@ -268,19 +255,14 @@ 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, mr *metrics.Registry) (Repository, error) {
+func openRestAPIRepository(ctx context.Context, si *APIServerInfo, password string, par immutableServerRepositoryParameters) (Repository, error) {
 	cli, err := apiclient.NewKopiaAPIClient(apiclient.Options{
 		BaseURL:                             si.BaseURL,
 		TrustedServerCertificateFingerprint: si.TrustedServerCertificateFingerprint,
-		Username:                            cliOpts.UsernameAtHost(),
+		Username:                            par.cliOpts.UsernameAtHost(),
 		Password:                            password,
 		LogRequests:                         true,
 	})
@@ -288,27 +270,12 @@ func openRestAPIRepository(ctx context.Context, si *APIServerInfo, cliOpts Clien
 		return nil, errors.Wrap(err, "unable to create API client")
 	}
 
-	rcc := newRefCountedCloser(
-		func(ctx context.Context) error {
-			if contentCache != nil {
-				contentCache.Close(ctx)
-			}
-
-			return nil
-		},
-		mr.Close,
-	)
-
 	rr := &apiServerRepository{
-		cli:          cli,
-		cliOpts:      cliOpts,
-		contentCache: contentCache,
+		immutableServerRepositoryParameters: par,
+		cli:                                 cli,
 		wso: WriteSessionOptions{
 			OnUpload: func(i int64) {},
 		},
-		isSharedReadOnlySession: true,
-		metricsRegistry:         mr,
-		refCountedCloser:        rcc,
 	}
 
 	var p remoterepoapi.Parameters
@@ -327,7 +294,7 @@ func(ctx context.Context) error {
 	rr.serverSupportsContentCompression = p.SupportsContentCompression
 
 	// create object manager using rr as contentManager implementation.
-	omgr, err := object.NewObjectManager(ctx, rr, rr.objectFormat, mr)
+	omgr, err := object.NewObjectManager(ctx, rr, rr.objectFormat, par.metricsRegistry)
 	if err != nil {
 		return nil, errors.Wrap(err, "error initializing object manager")
 	}
diff --git a/repo/grpc_repository_client.go b/repo/grpc_repository_client.go
index 71bede5d..f3bdf9a9 100644
--- a/repo/grpc_repository_client.go
+++ b/repo/grpc_repository_client.go
@@ -16,12 +16,10 @@
 	"google.golang.org/grpc"
 	"google.golang.org/grpc/credentials"
 
-	"github.com/kopia/kopia/internal/cache"
 	"github.com/kopia/kopia/internal/clock"
 	"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"
@@ -58,7 +56,6 @@ func errNoSessionResponse() error {
 // grpcRepositoryClient is an implementation of Repository that connects to an instance of
 // GPRC API server hosted by `kopia server`.
 type grpcRepositoryClient struct {
-	*refCountedCloser
 	conn *grpc.ClientConn
 
 	innerSessionMutex sync.Mutex
@@ -76,17 +73,12 @@ type grpcRepositoryClient struct {
 
 	asyncWritesWG *errgroup.Group
 
-	h                                hashing.HashFunc
-	objectFormat                     format.ObjectFormat
+	immutableServerRepositoryParameters
+
 	serverSupportsContentCompression bool
-	cliOpts                          ClientOptions
 	omgr                             *object.Manager
 
-	contentCache *cache.PersistentCache
-
 	recent recentlyRead
-
-	metricsRegistry *metrics.Registry
 }
 
 type grpcInnerSession struct {
@@ -209,10 +201,6 @@ func (r *grpcRepositoryClient) LegacyWriter() RepositoryWriter {
 	return nil
 }
 
-func (r *grpcRepositoryClient) ClientOptions() ClientOptions {
-	return r.cliOpts
-}
-
 func (r *grpcRepositoryClient) OpenObject(ctx context.Context, id object.ID) (object.Reader, error) {
 	//nolint:wrapcheck
 	return object.Open(ctx, r, id)
@@ -460,7 +448,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.refCountedCloser, r.cliOpts, opt, r.contentCache, false, r.metricsRegistry)
+	w, err := newGRPCAPIRepositoryForConnection(ctx, r.conn, opt, false, r.immutableServerRepositoryParameters)
 	if err != nil {
 		return nil, nil, err
 	}
@@ -753,9 +741,9 @@ func (c grpcCreds) RequireTransportSecurity() bool {
 	return true
 }
 
-// OpenGRPCAPIRepository opens the Repository based on remote GRPC server.
+// 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, mr *metrics.Registry) (Repository, error) {
+func openGRPCAPIRepository(ctx context.Context, si *APIServerInfo, password string, par immutableServerRepositoryParameters) (Repository, error) {
 	var transportCreds credentials.TransportCredentials
 
 	if si.TrustedServerCertificateFingerprint != "" {
@@ -775,7 +763,7 @@ func OpenGRPCAPIRepository(ctx context.Context, si *APIServerInfo, cliOpts Clien
 
 	conn, err := grpc.Dial(
 		u.Hostname()+":"+u.Port(),
-		grpc.WithPerRPCCredentials(grpcCreds{cliOpts.Hostname, cliOpts.Username, password}),
+		grpc.WithPerRPCCredentials(grpcCreds{par.cliOpts.Hostname, par.cliOpts.Username, password}),
 		grpc.WithTransportCredentials(transportCreds),
 		grpc.WithDefaultCallOptions(
 			grpc.MaxCallRecvMsgSize(MaxGRPCMessageSize),
@@ -786,18 +774,12 @@ func OpenGRPCAPIRepository(ctx context.Context, si *APIServerInfo, cliOpts Clien
 		return nil, errors.Wrap(err, "dial error")
 	}
 
-	rcc := newRefCountedCloser(
+	par.refCountedCloser.registerEarlyCloseFunc(
 		func(ctx context.Context) error {
 			return errors.Wrap(conn.Close(), "error closing GRPC connection")
-		},
-		func(ctx context.Context) error {
-			contentCache.Close(ctx)
-			return nil
-		},
-		mr.Close,
-	)
+		})
 
-	rep, err := newGRPCAPIRepositoryForConnection(ctx, conn, rcc, cliOpts, WriteSessionOptions{}, contentCache, true, mr)
+	rep, err := newGRPCAPIRepositoryForConnection(ctx, conn, WriteSessionOptions{}, true, par)
 	if err != nil {
 		return nil, err
 	}
@@ -864,36 +846,25 @@ 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,
-	sc *refCountedCloser,
-	cliOpts ClientOptions,
 	opt WriteSessionOptions,
-	contentCache *cache.PersistentCache,
 	transparentRetries bool,
-	mr *metrics.Registry,
+	par immutableServerRepositoryParameters,
 ) (*grpcRepositoryClient, error) {
 	if opt.OnUpload == nil {
 		opt.OnUpload = func(i int64) {}
 	}
 
 	rr := &grpcRepositoryClient{
-		refCountedCloser:   sc,
-		conn:               conn,
-		cliOpts:            cliOpts,
-		transparentRetries: transparentRetries,
-		opt:                opt,
-		isReadOnly:         cliOpts.ReadOnly,
-		contentCache:       contentCache,
-		asyncWritesWG:      new(errgroup.Group),
-		metricsRegistry:    mr,
+		immutableServerRepositoryParameters: par,
+		conn:                                conn,
+		transparentRetries:                  transparentRetries,
+		opt:                                 opt,
+		isReadOnly:                          par.cliOpts.ReadOnly,
+		asyncWritesWG:                       new(errgroup.Group),
 	}
 
 	return inSessionWithoutRetry(ctx, rr, func(ctx context.Context, sess *grpcInnerSession) (*grpcRepositoryClient, error) {
diff --git a/repo/open.go b/repo/open.go
index dc69c373..d44e73cf 100644
--- a/repo/open.go
+++ b/repo/open.go
@@ -161,18 +161,36 @@ 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) {
-	me := metrics.NewRegistry()
+	mr := metrics.NewRegistry()
 
-	contentCache, err := getContentCacheOrNil(ctx, cachingOptions, password, me)
+	contentCache, err := getContentCacheOrNil(ctx, cachingOptions, password, mr)
 	if err != nil {
 		return nil, errors.Wrap(err, "error opening content cache")
 	}
 
+	closer := newRefCountedCloser(
+		func(ctx context.Context) error {
+			if contentCache != nil {
+				contentCache.Close(ctx)
+			}
+
+			return nil
+		},
+		mr.Close,
+	)
+
+	par := immutableServerRepositoryParameters{
+		cliOpts:          cliOpts,
+		contentCache:     contentCache,
+		metricsRegistry:  mr,
+		refCountedCloser: closer,
+	}
+
 	if si.DisableGRPC {
-		return openRestAPIRepository(ctx, si, cliOpts, contentCache, password, me)
+		return openRestAPIRepository(ctx, si, password, par)
 	}
 
-	return OpenGRPCAPIRepository(ctx, si, cliOpts, contentCache, password, me)
+	return openGRPCAPIRepository(ctx, si, password, par)
 }
 
 // openDirect opens the repository that directly manipulates blob storage..
@@ -215,8 +233,8 @@ func openWithConfig(ctx context.Context, st blob.Storage, cliOpts ClientOptions,
 		DisableInternalLog: options.DisableInternalLog,
 	}
 
-	me := metrics.NewRegistry()
-	st = storagemetrics.NewWrapper(st, me)
+	mr := metrics.NewRegistry()
+	st = storagemetrics.NewWrapper(st, mr)
 
 	fmgr, ferr := format.NewManager(ctx, st, cacheOpts.CacheDirectory, cliOpts.FormatBlobCacheDuration, password, cmOpts.TimeNow)
 	if ferr != nil {
@@ -286,7 +304,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, me)
+	scm, ferr := content.NewSharedManager(ctx, st, fmgr, cacheOpts, cmOpts, mr)
 	if ferr != nil {
 		return nil, errors.Wrap(ferr, "unable to create shared content manager")
 	}
@@ -296,19 +314,19 @@ func openWithConfig(ctx context.Context, st blob.Storage, cliOpts ClientOptions,
 		SessionHost: cliOpts.Hostname,
 	}, "")
 
-	om, ferr := object.NewObjectManager(ctx, cm, fmgr.ObjectFormat(), me)
+	om, ferr := object.NewObjectManager(ctx, cm, fmgr.ObjectFormat(), mr)
 	if ferr != nil {
 		return nil, errors.Wrap(ferr, "unable to open object manager")
 	}
 
-	manifests, ferr := manifest.NewManager(ctx, cm, manifest.ManagerOptions{TimeNow: cmOpts.TimeNow}, me)
+	manifests, ferr := manifest.NewManager(ctx, cm, manifest.ManagerOptions{TimeNow: cmOpts.TimeNow}, mr)
 	if ferr != nil {
 		return nil, errors.Wrap(ferr, "unable to open manifests")
 	}
 
-	rcc := newRefCountedCloser(
+	closer := newRefCountedCloser(
 		scm.CloseShared,
-		me.Close,
+		mr.Close,
 	)
 
 	dr := &directRepository{
@@ -317,17 +335,16 @@ func openWithConfig(ctx context.Context, st blob.Storage, cliOpts ClientOptions,
 		blobs: st,
 		mmgr:  manifests,
 		sm:    scm,
-		directRepositoryParameters: directRepositoryParameters{
-			cachingOptions:  *cacheOpts,
-			fmgr:            fmgr,
-			timeNow:         cmOpts.TimeNow,
-			cliOpts:         cliOpts,
-			configFile:      configFile,
-			nextWriterID:    new(int32),
-			throttler:       throttler,
-			metricsRegistry: me,
-
-			refCountedCloser: rcc,
+		immutableDirectRepositoryParameters: immutableDirectRepositoryParameters{
+			cachingOptions:   *cacheOpts,
+			fmgr:             fmgr,
+			timeNow:          cmOpts.TimeNow,
+			cliOpts:          cliOpts,
+			configFile:       configFile,
+			nextWriterID:     new(int32),
+			throttler:        throttler,
+			metricsRegistry:  mr,
+			refCountedCloser: closer,
 		},
 	}
 
diff --git a/repo/refcount_closer.go b/repo/refcount_closer.go
index 8c4c6a71..6cf9598d 100644
--- a/repo/refcount_closer.go
+++ b/repo/refcount_closer.go
@@ -49,6 +49,10 @@ func (c *refCountedCloser) addRef() {
 	c.refCount.Add(1)
 }
 
+func (c *refCountedCloser) registerEarlyCloseFunc(f closeFunc) {
+	c.closers = append(c.closers, append([]closeFunc(nil), f)...)
+}
+
 func newRefCountedCloser(f ...closeFunc) *refCountedCloser {
 	rcc := &refCountedCloser{
 		closers: f,
diff --git a/repo/repository.go b/repo/repository.go
index 556c64e1..ad60b847 100644
--- a/repo/repository.go
+++ b/repo/repository.go
@@ -88,7 +88,7 @@ type DirectRepositoryWriter interface {
 	// RollbackUpgrade(ctx context.Context) error
 }
 
-type directRepositoryParameters struct {
+type immutableDirectRepositoryParameters struct {
 	configFile      string
 	cachingOptions  content.CachingOptions
 	cliOpts         ClientOptions
@@ -103,7 +103,7 @@ type directRepositoryParameters struct {
 
 // directRepository is an implementation of repository that directly manipulates underlying storage.
 type directRepository struct {
-	directRepositoryParameters
+	immutableDirectRepositoryParameters
 
 	blobs blob.Storage
 	cmgr  *content.WriteManager
@@ -262,12 +262,12 @@ func (r *directRepository) NewDirectWriter(ctx context.Context, opt WriteSession
 	}
 
 	w := &directRepository{
-		directRepositoryParameters: r.directRepositoryParameters,
-		blobs:                      r.blobs,
-		cmgr:                       cmgr,
-		omgr:                       omgr,
-		mmgr:                       mmgr,
-		sm:                         r.sm,
+		immutableDirectRepositoryParameters: r.immutableDirectRepositoryParameters,
+		blobs:                               r.blobs,
+		cmgr:                                cmgr,
+		omgr:                                omgr,
+		mmgr:                                mmgr,
+		sm:                                  r.sm,
 	}
 
 	w.addRef()
diff --git a/repo/server_repository_params.go b/repo/server_repository_params.go
new file mode 100644
index 00000000..cd870d37
--- /dev/null
+++ b/repo/server_repository_params.go
@@ -0,0 +1,27 @@
+package repo
+
+import (
+	"github.com/kopia/kopia/internal/cache"
+	"github.com/kopia/kopia/internal/metrics"
+	"github.com/kopia/kopia/repo/format"
+	"github.com/kopia/kopia/repo/hashing"
+)
+
+// immutableServerRepositoryParameters contains immutable parameters shared between HTTP and GRPC clients.
+type immutableServerRepositoryParameters struct {
+	h               hashing.HashFunc
+	objectFormat    format.ObjectFormat
+	cliOpts         ClientOptions
+	metricsRegistry *metrics.Registry
+	contentCache    *cache.PersistentCache
+	*refCountedCloser
+}
+
+// Metrics provides access to the metrics registry.
+func (r *immutableServerRepositoryParameters) Metrics() *metrics.Registry {
+	return r.metricsRegistry
+}
+
+func (r *immutableServerRepositoryParameters) ClientOptions() ClientOptions {
+	return r.cliOpts
+}


Test patch:
diff --git a/internal/metrics/metrics_aggregation_test.go b/internal/metrics/metrics_aggregation_test.go
new file mode 100644
index 00000000..4bb0995e
--- /dev/null
+++ b/internal/metrics/metrics_aggregation_test.go
@@ -0,0 +1,106 @@
+package metrics_test
+
+import (
+	"encoding/json"
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/require"
+
+	"github.com/kopia/kopia/internal/metrics"
+)
+
+func TestAggregation(t *testing.T) {
+	s1 := metrics.Snapshot{
+		Counters: map[string]int64{
+			"counter1": 123,
+			"counter2": 555,
+		},
+		SizeDistributions: map[string]*metrics.DistributionState[int64]{
+			"dist1": {
+				Min:              100,
+				Max:              200,
+				Count:            2,
+				Sum:              300,
+				BucketCounters:   []int64{1, 1, 0, 0},
+				BucketThresholds: []int64{150, 250, 300},
+			},
+		},
+		DurationDistributions: map[string]*metrics.DistributionState[time.Duration]{
+			"dur1": {
+				Min:              100 * time.Second,
+				Max:              200 * time.Second,
+				Count:            2,
+				Sum:              300 * time.Second,
+				BucketCounters:   []int64{1, 1, 0, 0},
+				BucketThresholds: []time.Duration{150 * time.Second, 250 * time.Second, 300 * time.Second},
+			},
+		},
+	}
+
+	s2 := metrics.Snapshot{
+		Counters: map[string]int64{
+			"counter1": 234,
+			"counter3": 666,
+		},
+		SizeDistributions: map[string]*metrics.DistributionState[int64]{
+			"dist1": {
+				Min:              50,
+				Max:              210,
+				Count:            2,
+				Sum:              260,
+				BucketCounters:   []int64{1, 1, 0, 0},
+				BucketThresholds: []int64{150, 250, 300},
+			},
+		},
+		DurationDistributions: map[string]*metrics.DistributionState[time.Duration]{
+			"dur1": {
+				Min:              50 * time.Second,
+				Max:              210 * time.Second,
+				Count:            2,
+				Sum:              260 * time.Second,
+				BucketCounters:   []int64{1, 1, 0, 0},
+				BucketThresholds: []time.Duration{150 * time.Second, 250 * time.Second, 300 * time.Second},
+			},
+		},
+	}
+
+	s3 := metrics.Snapshot{
+		Counters: map[string]int64{
+			"counter1": 3,
+		},
+	}
+
+	s4 := metrics.Snapshot{
+		Counters: map[string]int64{
+			"counter4": 777,
+		},
+	}
+
+	res := metrics.AggregateSnapshots([]metrics.Snapshot{s1, s2, s3, s4})
+
+	// counters are summed
+	require.Equal(t, map[string]int64{
+		"counter1": 360,
+		"counter2": 555,
+		"counter3": 666,
+		"counter4": 777,
+	}, res.Counters)
+
+	require.Equal(t,
+		`{"dist1":{"min":50,"max":210,"sum":560,"count":4,"buckets":[2,2,0,0]}}`,
+		toJSON(res.SizeDistributions))
+
+	require.Equal(t,
+		`{"dur1":{"min":50000000000,"max":210000000000,"sum":560000000000,"count":4,"buckets":[2,2,0,0]}}`,
+		toJSON(res.DurationDistributions))
+}
+
+func toJSON(v any) string {
+	b, err := json.Marshal(v)
+	if err != nil {
+		return "<invalid>"
+	}
+
+	return string(b)
+}
diff --git a/internal/metrics/metrics_duration_distribution_test.go b/internal/metrics/metrics_duration_distribution_test.go
index 085eaafa..5d7e687b 100644
--- a/internal/metrics/metrics_duration_distribution_test.go
+++ b/internal/metrics/metrics_duration_distribution_test.go
@@ -15,7 +15,7 @@ func TestDurationDistribution_Nil(t *testing.T) {
 	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(false))
+	require.Equal(t, &metrics.DistributionState[time.Duration]{}, dist.Snapshot(false))
 }
 
 func TestSizeDistribution_Nil(t *testing.T) {
diff --git a/internal/metrics/metrics_gauge_test.go b/internal/metrics/metrics_gauge_test.go
deleted file mode 100644
index b0bdf4f3..00000000
--- a/internal/metrics/metrics_gauge_test.go
+++ /dev/null
@@ -1,68 +0,0 @@
-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(false))
-}
-
-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(false))
-	require.Equal(t, int64(133), g.Snapshot(true)) // reset
-	require.Equal(t, int64(0), g.Snapshot(false))
-}
-
-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
index 1197ffaf..4f1eba5a 100644
--- a/internal/metrics/metrics_registry_test.go
+++ b/internal/metrics/metrics_registry_test.go
@@ -28,11 +28,10 @@ func TestMetricEmitter_NotNil(t *testing.T) {
 
 	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))
+	r.Log(ctx)
 	require.NoError(t, r.Close(ctx))
 
 	lines := strings.Split(strings.TrimSpace(buf.String()), "\n")
@@ -43,7 +42,6 @@ func TestMetricEmitter_NotNil(t *testing.T) {
 		"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/server/server_test.go b/internal/server/server_test.go
index 753ad5ca..f85d8ea0 100644
--- a/internal/server/server_test.go
+++ b/internal/server/server_test.go
@@ -133,14 +133,14 @@ func testServer(t *testing.T, disableGRPC bool) {
 	remoteRepositoryTest(ctx, t, rep)
 }
 
-func TestGPRServer_AuthenticationError(t *testing.T) {
+func TestGRPCServer_AuthenticationError(t *testing.T) {
 	ctx, env := repotesting.NewEnvironment(t, repotesting.FormatNotImportant)
 	apiServerInfo := startServer(t, env, true)
 
-	if _, err := repo.OpenGRPCAPIRepository(ctx, apiServerInfo, repo.ClientOptions{
+	if _, err := repo.OpenAPIServer(ctx, apiServerInfo, repo.ClientOptions{
 		Username: "bad-username",
 		Hostname: "bad-hostname",
-	}, nil, "bad-password", nil); err == nil {
+	}, nil, "bad-password"); err == nil {
 		t.Fatal("unexpected success when connecting with invalid username")
 	}
 }