Repository: kopia/kopia
Pull request #4237 (2024-11-11 23:47:43)

build(deps): bump the telemetry-dependencies group with 4 updates
Bumps the telemetry-dependencies group with 4 updates: [go.opentelemetry.io/otel](https://github.com/open-telemetry/opentelemetry-go), [go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc](https://github.com/open-telemetry/opentelemetry-go), [go.opentelemetry.io/otel/sdk](https://github.com/open-telemetry/opentelemetry-go) and [go.opentelemetry.io/otel/trace](https://github.com/open-telemetry/opentelemetry-go).

Updates `go.opentelemetry.io/otel` from 1.31.0 to 1.32.0
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a href="https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md">go.opentelemetry.io/otel's changelog</a>.</em></p>
<blockquote>
<h2>[1.32.0/0.54.0/0.8.0/0.0.11] 2024-11-08</h2>
<h3>Added</h3>
<ul>
<li>Add <code>go.opentelemetry.io/otel/sdk/metric/exemplar.AlwaysOffFilter</code>, which can be used to disable exemplar recording. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5850">#5850</a>)</li>
<li>Add <code>go.opentelemetry.io/otel/sdk/metric.WithExemplarFilter</code>, which can be used to configure the exemplar filter used by the metrics SDK. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5850">#5850</a>)</li>
<li>Add <code>ExemplarReservoirProviderSelector</code> and <code>DefaultExemplarReservoirProviderSelector</code> to <code>go.opentelemetry.io/otel/sdk/metric</code>, which defines the exemplar reservoir to use based on the aggregation of the metric. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5861">#5861</a>)</li>
<li>Add <code>ExemplarReservoirProviderSelector</code> to <code>go.opentelemetry.io/otel/sdk/metric.Stream</code> to allow using views to configure the exemplar reservoir to use for a metric. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5861">#5861</a>)</li>
<li>Add <code>ReservoirProvider</code>, <code>HistogramReservoirProvider</code> and <code>FixedSizeReservoirProvider</code> to <code>go.opentelemetry.io/otel/sdk/metric/exemplar</code> to make it convenient to use providers of Reservoirs. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5861">#5861</a>)</li>
<li>The <code>go.opentelemetry.io/otel/semconv/v1.27.0</code> package.
The package contains semantic conventions from the <code>v1.27.0</code> version of the OpenTelemetry Semantic Conventions. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5894">#5894</a>)</li>
<li>Add <code>Attributes attribute.Set</code> field to <code>Scope</code> in <code>go.opentelemetry.io/otel/sdk/instrumentation</code>. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5903">#5903</a>)</li>
<li>Add <code>Attributes attribute.Set</code> field to <code>ScopeRecords</code> in <code>go.opentelemetry.io/otel/log/logtest</code>. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5927">#5927</a>)</li>
<li><code>go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc</code> adds instrumentation scope attributes. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5934">#5934</a>)</li>
<li><code>go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp</code> adds instrumentation scope attributes. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5934">#5934</a>)</li>
<li><code>go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc</code> adds instrumentation scope attributes. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5935">#5935</a>)</li>
<li><code>go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp</code> adds instrumentation scope attributes. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5935">#5935</a>)</li>
<li><code>go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc</code> adds instrumentation scope attributes. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5933">#5933</a>)</li>
<li><code>go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp</code> adds instrumentation scope attributes. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5933">#5933</a>)</li>
<li><code>go.opentelemetry.io/otel/exporters/prometheus</code> adds instrumentation scope attributes in <code>otel_scope_info</code> metric as labels. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5932">#5932</a>)</li>
</ul>
<h3>Changed</h3>
<ul>
<li>Support scope attributes and make them as identifying for <code>Tracer</code> in <code>go.opentelemetry.io/otel</code> and <code>go.opentelemetry.io/otel/sdk/trace</code>. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5924">#5924</a>)</li>
<li>Support scope attributes and make them as identifying for <code>Meter</code> in <code>go.opentelemetry.io/otel</code> and <code>go.opentelemetry.io/otel/sdk/metric</code>. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5926">#5926</a>)</li>
<li>Support scope attributes and make them as identifying for <code>Logger</code> in <code>go.opentelemetry.io/otel</code> and <code>go.opentelemetry.io/otel/sdk/log</code>. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5925">#5925</a>)</li>
<li>Make schema URL and scope attributes as identifying for <code>Tracer</code> in <code>go.opentelemetry.io/otel/bridge/opentracing</code>. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5931">#5931</a>)</li>
<li>Clear unneeded slice elements to allow GC to collect the objects in <code>go.opentelemetry.io/otel/sdk/metric</code> and <code>go.opentelemetry.io/otel/sdk/trace</code>. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5804">#5804</a>)</li>
</ul>
<h3>Fixed</h3>
<ul>
<li>Global MeterProvider registration unwraps global instrument Observers, the undocumented Unwrap() methods are now private. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5881">#5881</a>)</li>
<li><code>go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc</code> now keeps the metadata already present in the context when <code>WithHeaders</code> is used. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5892">#5892</a>)</li>
<li><code>go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc</code> now keeps the metadata already present in the context when <code>WithHeaders</code> is used. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5911">#5911</a>)</li>
<li><code>go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc</code> now keeps the metadata already present in the context when <code>WithHeaders</code> is used. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5915">#5915</a>)</li>
<li>Fix <code>go.opentelemetry.io/otel/exporters/prometheus</code> trying to add exemplars to Gauge metrics, which is unsupported. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5912">#5912</a>)</li>
<li>Fix <code>WithEndpointURL</code> to always use a secure connection when an https URL is passed in <code>go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc</code>. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5944">#5944</a>)</li>
<li>Fix <code>WithEndpointURL</code> to always use a secure connection when an https URL is passed in <code>go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp</code>. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5944">#5944</a>)</li>
<li>Fix <code>WithEndpointURL</code> to always use a secure connection when an https URL is passed in <code>go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc</code>. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5944">#5944</a>)</li>
<li>Fix <code>WithEndpointURL</code> to always use a secure connection when an https URL is passed in <code>go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp</code>. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5944">#5944</a>)</li>
<li>Fix incorrect metrics generated from callbacks when multiple readers are used in <code>go.opentelemetry.io/otel/sdk/metric</code>. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5900">#5900</a>)</li>
</ul>
<h3>Removed</h3>
<ul>
<li>Remove all examples under <code>go.opentelemetry.io/otel/example</code> as they are moved to <a href="https://github.com/open-telemetry/opentelemetry-go-contrib/tree/main/examples">Contrib repository</a>. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5930">#5930</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/open-telemetry/opentelemetry-go/commit/7cfbd86a605c85e598eca9a899f6176b17076f4f"><code>7cfbd86</code></a> Release v1.32.0/v0.54.0/v0.8.0/v0.0.11 (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5960">#5960</a>)</li>
<li><a href="https://github.com/open-telemetry/opentelemetry-go/commit/2be617e08003601514a5ec573063f749f868e629"><code>2be617e</code></a> fix(deps): update github.com/opentracing-contrib/go-grpc/test digest to 51a56...</li>
<li><a href="https://github.com/open-telemetry/opentelemetry-go/commit/6db18df3e2662c323287e1992e4eef34d63968a3"><code>6db18df</code></a> fix(deps): update module github.com/opentracing-contrib/go-grpc to v0.1.0 (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5">#5</a>...</li>
<li><a href="https://github.com/open-telemetry/opentelemetry-go/commit/ef12bf858da5421901185b3a90c343444eadcdd0"><code>ef12bf8</code></a> chore(deps): update golang.org/x (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5957">#5957</a>)</li>
<li><a href="https://github.com/open-telemetry/opentelemetry-go/commit/85eb76f2fd8ef474b7cbb4e5f8dc307b872cf3c6"><code>85eb76f</code></a> Allow GC to collect unneeded slice elements (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5804">#5804</a>)</li>
<li><a href="https://github.com/open-telemetry/opentelemetry-go/commit/1492efa719ac935cfbb88a8248ff18667d688b30"><code>1492efa</code></a> Fix incorrect metrics getting generated from multiple readers (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5900">#5900</a>)</li>
<li><a href="https://github.com/open-telemetry/opentelemetry-go/commit/d2b066346b213ca5cbc8a78ce7667dc9cbcbfc8e"><code>d2b0663</code></a> fix(deps): update module go.opentelemetry.io/build-tools/multimod to v0.15.0 ...</li>
<li><a href="https://github.com/open-telemetry/opentelemetry-go/commit/394cbd28909ef7628c525efec15f090533c14f2d"><code>394cbd2</code></a> chore(deps): update lycheeverse/lychee-action action to v2.1.0 (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5950">#5950</a>)</li>
<li><a href="https://github.com/open-telemetry/opentelemetry-go/commit/37b2537692f1b76c686b3ab651570f57b6f89415"><code>37b2537</code></a> fix(deps): update github.com/opentracing-contrib/go-grpc digest to e3cbcab (#...</li>
<li><a href="https://github.com/open-telemetry/opentelemetry-go/commit/7f68356a7def1429099008275e4ce0d3ce7e5067"><code>7f68356</code></a> fix(deps): update module go.opentelemetry.io/build-tools/semconvgen to v0.15....</li>
<li>Additional commits viewable in <a href="https://github.com/open-telemetry/opentelemetry-go/compare/v1.31.0...v1.32.0">compare view</a></li>
</ul>
</details>
<br />

Updates `go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc` from 1.31.0 to 1.32.0
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a href="https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md">go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc's changelog</a>.</em></p>
<blockquote>
<h2>[1.32.0/0.54.0/0.8.0/0.0.11] 2024-11-08</h2>
<h3>Added</h3>
<ul>
<li>Add <code>go.opentelemetry.io/otel/sdk/metric/exemplar.AlwaysOffFilter</code>, which can be used to disable exemplar recording. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5850">#5850</a>)</li>
<li>Add <code>go.opentelemetry.io/otel/sdk/metric.WithExemplarFilter</code>, which can be used to configure the exemplar filter used by the metrics SDK. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5850">#5850</a>)</li>
<li>Add <code>ExemplarReservoirProviderSelector</code> and <code>DefaultExemplarReservoirProviderSelector</code> to <code>go.opentelemetry.io/otel/sdk/metric</code>, which defines the exemplar reservoir to use based on the aggregation of the metric. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5861">#5861</a>)</li>
<li>Add <code>ExemplarReservoirProviderSelector</code> to <code>go.opentelemetry.io/otel/sdk/metric.Stream</code> to allow using views to configure the exemplar reservoir to use for a metric. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5861">#5861</a>)</li>
<li>Add <code>ReservoirProvider</code>, <code>HistogramReservoirProvider</code> and <code>FixedSizeReservoirProvider</code> to <code>go.opentelemetry.io/otel/sdk/metric/exemplar</code> to make it convenient to use providers of Reservoirs. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5861">#5861</a>)</li>
<li>The <code>go.opentelemetry.io/otel/semconv/v1.27.0</code> package.
The package contains semantic conventions from the <code>v1.27.0</code> version of the OpenTelemetry Semantic Conventions. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5894">#5894</a>)</li>
<li>Add <code>Attributes attribute.Set</code> field to <code>Scope</code> in <code>go.opentelemetry.io/otel/sdk/instrumentation</code>. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5903">#5903</a>)</li>
<li>Add <code>Attributes attribute.Set</code> field to <code>ScopeRecords</code> in <code>go.opentelemetry.io/otel/log/logtest</code>. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5927">#5927</a>)</li>
<li><code>go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc</code> adds instrumentation scope attributes. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5934">#5934</a>)</li>
<li><code>go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp</code> adds instrumentation scope attributes. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5934">#5934</a>)</li>
<li><code>go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc</code> adds instrumentation scope attributes. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5935">#5935</a>)</li>
<li><code>go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp</code> adds instrumentation scope attributes. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5935">#5935</a>)</li>
<li><code>go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc</code> adds instrumentation scope attributes. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5933">#5933</a>)</li>
<li><code>go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp</code> adds instrumentation scope attributes. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5933">#5933</a>)</li>
<li><code>go.opentelemetry.io/otel/exporters/prometheus</code> adds instrumentation scope attributes in <code>otel_scope_info</code> metric as labels. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5932">#5932</a>)</li>
</ul>
<h3>Changed</h3>
<ul>
<li>Support scope attributes and make them as identifying for <code>Tracer</code> in <code>go.opentelemetry.io/otel</code> and <code>go.opentelemetry.io/otel/sdk/trace</code>. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5924">#5924</a>)</li>
<li>Support scope attributes and make them as identifying for <code>Meter</code> in <code>go.opentelemetry.io/otel</code> and <code>go.opentelemetry.io/otel/sdk/metric</code>. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5926">#5926</a>)</li>
<li>Support scope attributes and make them as identifying for <code>Logger</code> in <code>go.opentelemetry.io/otel</code> and <code>go.opentelemetry.io/otel/sdk/log</code>. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5925">#5925</a>)</li>
<li>Make schema URL and scope attributes as identifying for <code>Tracer</code> in <code>go.opentelemetry.io/otel/bridge/opentracing</code>. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5931">#5931</a>)</li>
<li>Clear unneeded slice elements to allow GC to collect the objects in <code>go.opentelemetry.io/otel/sdk/metric</code> and <code>go.opentelemetry.io/otel/sdk/trace</code>. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5804">#5804</a>)</li>
</ul>
<h3>Fixed</h3>
<ul>
<li>Global MeterProvider registration unwraps global instrument Observers, the undocumented Unwrap() methods are now private. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5881">#5881</a>)</li>
<li><code>go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc</code> now keeps the metadata already present in the context when <code>WithHeaders</code> is used. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5892">#5892</a>)</li>
<li><code>go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc</code> now keeps the metadata already present in the context when <code>WithHeaders</code> is used. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5911">#5911</a>)</li>
<li><code>go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc</code> now keeps the metadata already present in the context when <code>WithHeaders</code> is used. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5915">#5915</a>)</li>
<li>Fix <code>go.opentelemetry.io/otel/exporters/prometheus</code> trying to add exemplars to Gauge metrics, which is unsupported. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5912">#5912</a>)</li>
<li>Fix <code>WithEndpointURL</code> to always use a secure connection when an https URL is passed in <code>go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc</code>. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5944">#5944</a>)</li>
<li>Fix <code>WithEndpointURL</code> to always use a secure connection when an https URL is passed in <code>go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp</code>. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5944">#5944</a>)</li>
<li>Fix <code>WithEndpointURL</code> to always use a secure connection when an https URL is passed in <code>go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc</code>. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5944">#5944</a>)</li>
<li>Fix <code>WithEndpointURL</code> to always use a secure connection when an https URL is passed in <code>go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp</code>. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5944">#5944</a>)</li>
<li>Fix incorrect metrics generated from callbacks when multiple readers are used in <code>go.opentelemetry.io/otel/sdk/metric</code>. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5900">#5900</a>)</li>
</ul>
<h3>Removed</h3>
<ul>
<li>Remove all examples under <code>go.opentelemetry.io/otel/example</code> as they are moved to <a href="https://github.com/open-telemetry/opentelemetry-go-contrib/tree/main/examples">Contrib repository</a>. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5930">#5930</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/open-telemetry/opentelemetry-go/commit/7cfbd86a605c85e598eca9a899f6176b17076f4f"><code>7cfbd86</code></a> Release v1.32.0/v0.54.0/v0.8.0/v0.0.11 (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5960">#5960</a>)</li>
<li><a href="https://github.com/open-telemetry/opentelemetry-go/commit/2be617e08003601514a5ec573063f749f868e629"><code>2be617e</code></a> fix(deps): update github.com/opentracing-contrib/go-grpc/test digest to 51a56...</li>
<li><a href="https://github.com/open-telemetry/opentelemetry-go/commit/6db18df3e2662c323287e1992e4eef34d63968a3"><code>6db18df</code></a> fix(deps): update module github.com/opentracing-contrib/go-grpc to v0.1.0 (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5">#5</a>...</li>
<li><a href="https://github.com/open-telemetry/opentelemetry-go/commit/ef12bf858da5421901185b3a90c343444eadcdd0"><code>ef12bf8</code></a> chore(deps): update golang.org/x (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5957">#5957</a>)</li>
<li><a href="https://github.com/open-telemetry/opentelemetry-go/commit/85eb76f2fd8ef474b7cbb4e5f8dc307b872cf3c6"><code>85eb76f</code></a> Allow GC to collect unneeded slice elements (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5804">#5804</a>)</li>
<li><a href="https://github.com/open-telemetry/opentelemetry-go/commit/1492efa719ac935cfbb88a8248ff18667d688b30"><code>1492efa</code></a> Fix incorrect metrics getting generated from multiple readers (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5900">#5900</a>)</li>
<li><a href="https://github.com/open-telemetry/opentelemetry-go/commit/d2b066346b213ca5cbc8a78ce7667dc9cbcbfc8e"><code>d2b0663</code></a> fix(deps): update module go.opentelemetry.io/build-tools/multimod to v0.15.0 ...</li>
<li><a href="https://github.com/open-telemetry/opentelemetry-go/commit/394cbd28909ef7628c525efec15f090533c14f2d"><code>394cbd2</code></a> chore(deps): update lycheeverse/lychee-action action to v2.1.0 (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5950">#5950</a>)</li>
<li><a href="https://github.com/open-telemetry/opentelemetry-go/commit/37b2537692f1b76c686b3ab651570f57b6f89415"><code>37b2537</code></a> fix(deps): update github.com/opentracing-contrib/go-grpc digest to e3cbcab (#...</li>
<li><a href="https://github.com/open-telemetry/opentelemetry-go/commit/7f68356a7def1429099008275e4ce0d3ce7e5067"><code>7f68356</code></a> fix(deps): update module go.opentelemetry.io/build-tools/semconvgen to v0.15....</li>
<li>Additional commits viewable in <a href="https://github.com/open-telemetry/opentelemetry-go/compare/v1.31.0...v1.32.0">compare view</a></li>
</ul>
</details>
<br />

Updates `go.opentelemetry.io/otel/sdk` from 1.31.0 to 1.32.0
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a href="https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md">go.opentelemetry.io/otel/sdk's changelog</a>.</em></p>
<blockquote>
<h2>[1.32.0/0.54.0/0.8.0/0.0.11] 2024-11-08</h2>
<h3>Added</h3>
<ul>
<li>Add <code>go.opentelemetry.io/otel/sdk/metric/exemplar.AlwaysOffFilter</code>, which can be used to disable exemplar recording. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5850">#5850</a>)</li>
<li>Add <code>go.opentelemetry.io/otel/sdk/metric.WithExemplarFilter</code>, which can be used to configure the exemplar filter used by the metrics SDK. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5850">#5850</a>)</li>
<li>Add <code>ExemplarReservoirProviderSelector</code> and <code>DefaultExemplarReservoirProviderSelector</code> to <code>go.opentelemetry.io/otel/sdk/metric</code>, which defines the exemplar reservoir to use based on the aggregation of the metric. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5861">#5861</a>)</li>
<li>Add <code>ExemplarReservoirProviderSelector</code> to <code>go.opentelemetry.io/otel/sdk/metric.Stream</code> to allow using views to configure the exemplar reservoir to use for a metric. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5861">#5861</a>)</li>
<li>Add <code>ReservoirProvider</code>, <code>HistogramReservoirProvider</code> and <code>FixedSizeReservoirProvider</code> to <code>go.opentelemetry.io/otel/sdk/metric/exemplar</code> to make it convenient to use providers of Reservoirs. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5861">#5861</a>)</li>
<li>The <code>go.opentelemetry.io/otel/semconv/v1.27.0</code> package.
The package contains semantic conventions from the <code>v1.27.0</code> version of the OpenTelemetry Semantic Conventions. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5894">#5894</a>)</li>
<li>Add <code>Attributes attribute.Set</code> field to <code>Scope</code> in <code>go.opentelemetry.io/otel/sdk/instrumentation</code>. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5903">#5903</a>)</li>
<li>Add <code>Attributes attribute.Set</code> field to <code>ScopeRecords</code> in <code>go.opentelemetry.io/otel/log/logtest</code>. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5927">#5927</a>)</li>
<li><code>go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc</code> adds instrumentation scope attributes. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5934">#5934</a>)</li>
<li><code>go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp</code> adds instrumentation scope attributes. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5934">#5934</a>)</li>
<li><code>go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc</code> adds instrumentation scope attributes. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5935">#5935</a>)</li>
<li><code>go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp</code> adds instrumentation scope attributes. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5935">#5935</a>)</li>
<li><code>go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc</code> adds instrumentation scope attributes. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5933">#5933</a>)</li>
<li><code>go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp</code> adds instrumentation scope attributes. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5933">#5933</a>)</li>
<li><code>go.opentelemetry.io/otel/exporters/prometheus</code> adds instrumentation scope attributes in <code>otel_scope_info</code> metric as labels. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5932">#5932</a>)</li>
</ul>
<h3>Changed</h3>
<ul>
<li>Support scope attributes and make them as identifying for <code>Tracer</code> in <code>go.opentelemetry.io/otel</code> and <code>go.opentelemetry.io/otel/sdk/trace</code>. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5924">#5924</a>)</li>
<li>Support scope attributes and make them as identifying for <code>Meter</code> in <code>go.opentelemetry.io/otel</code> and <code>go.opentelemetry.io/otel/sdk/metric</code>. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5926">#5926</a>)</li>
<li>Support scope attributes and make them as identifying for <code>Logger</code> in <code>go.opentelemetry.io/otel</code> and <code>go.opentelemetry.io/otel/sdk/log</code>. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5925">#5925</a>)</li>
<li>Make schema URL and scope attributes as identifying for <code>Tracer</code> in <code>go.opentelemetry.io/otel/bridge/opentracing</code>. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5931">#5931</a>)</li>
<li>Clear unneeded slice elements to allow GC to collect the objects in <code>go.opentelemetry.io/otel/sdk/metric</code> and <code>go.opentelemetry.io/otel/sdk/trace</code>. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5804">#5804</a>)</li>
</ul>
<h3>Fixed</h3>
<ul>
<li>Global MeterProvider registration unwraps global instrument Observers, the undocumented Unwrap() methods are now private. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5881">#5881</a>)</li>
<li><code>go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc</code> now keeps the metadata already present in the context when <code>WithHeaders</code> is used. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5892">#5892</a>)</li>
<li><code>go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc</code> now keeps the metadata already present in the context when <code>WithHeaders</code> is used. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5911">#5911</a>)</li>
<li><code>go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc</code> now keeps the metadata already present in the context when <code>WithHeaders</code> is used. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5915">#5915</a>)</li>
<li>Fix <code>go.opentelemetry.io/otel/exporters/prometheus</code> trying to add exemplars to Gauge metrics, which is unsupported. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5912">#5912</a>)</li>
<li>Fix <code>WithEndpointURL</code> to always use a secure connection when an https URL is passed in <code>go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc</code>. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5944">#5944</a>)</li>
<li>Fix <code>WithEndpointURL</code> to always use a secure connection when an https URL is passed in <code>go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp</code>. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5944">#5944</a>)</li>
<li>Fix <code>WithEndpointURL</code> to always use a secure connection when an https URL is passed in <code>go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc</code>. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5944">#5944</a>)</li>
<li>Fix <code>WithEndpointURL</code> to always use a secure connection when an https URL is passed in <code>go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp</code>. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5944">#5944</a>)</li>
<li>Fix incorrect metrics generated from callbacks when multiple readers are used in <code>go.opentelemetry.io/otel/sdk/metric</code>. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5900">#5900</a>)</li>
</ul>
<h3>Removed</h3>
<ul>
<li>Remove all examples under <code>go.opentelemetry.io/otel/example</code> as they are moved to <a href="https://github.com/open-telemetry/opentelemetry-go-contrib/tree/main/examples">Contrib repository</a>. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5930">#5930</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/open-telemetry/opentelemetry-go/commit/7cfbd86a605c85e598eca9a899f6176b17076f4f"><code>7cfbd86</code></a> Release v1.32.0/v0.54.0/v0.8.0/v0.0.11 (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5960">#5960</a>)</li>
<li><a href="https://github.com/open-telemetry/opentelemetry-go/commit/2be617e08003601514a5ec573063f749f868e629"><code>2be617e</code></a> fix(deps): update github.com/opentracing-contrib/go-grpc/test digest to 51a56...</li>
<li><a href="https://github.com/open-telemetry/opentelemetry-go/commit/6db18df3e2662c323287e1992e4eef34d63968a3"><code>6db18df</code></a> fix(deps): update module github.com/opentracing-contrib/go-grpc to v0.1.0 (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5">#5</a>...</li>
<li><a href="https://github.com/open-telemetry/opentelemetry-go/commit/ef12bf858da5421901185b3a90c343444eadcdd0"><code>ef12bf8</code></a> chore(deps): update golang.org/x (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5957">#5957</a>)</li>
<li><a href="https://github.com/open-telemetry/opentelemetry-go/commit/85eb76f2fd8ef474b7cbb4e5f8dc307b872cf3c6"><code>85eb76f</code></a> Allow GC to collect unneeded slice elements (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5804">#5804</a>)</li>
<li><a href="https://github.com/open-telemetry/opentelemetry-go/commit/1492efa719ac935cfbb88a8248ff18667d688b30"><code>1492efa</code></a> Fix incorrect metrics getting generated from multiple readers (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5900">#5900</a>)</li>
<li><a href="https://github.com/open-telemetry/opentelemetry-go/commit/d2b066346b213ca5cbc8a78ce7667dc9cbcbfc8e"><code>d2b0663</code></a> fix(deps): update module go.opentelemetry.io/build-tools/multimod to v0.15.0 ...</li>
<li><a href="https://github.com/open-telemetry/opentelemetry-go/commit/394cbd28909ef7628c525efec15f090533c14f2d"><code>394cbd2</code></a> chore(deps): update lycheeverse/lychee-action action to v2.1.0 (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5950">#5950</a>)</li>
<li><a href="https://github.com/open-telemetry/opentelemetry-go/commit/37b2537692f1b76c686b3ab651570f57b6f89415"><code>37b2537</code></a> fix(deps): update github.com/opentracing-contrib/go-grpc digest to e3cbcab (#...</li>
<li><a href="https://github.com/open-telemetry/opentelemetry-go/commit/7f68356a7def1429099008275e4ce0d3ce7e5067"><code>7f68356</code></a> fix(deps): update module go.opentelemetry.io/build-tools/semconvgen to v0.15....</li>
<li>Additional commits viewable in <a href="https://github.com/open-telemetry/opentelemetry-go/compare/v1.31.0...v1.32.0">compare view</a></li>
</ul>
</details>
<br />

Updates `go.opentelemetry.io/otel/trace` from 1.31.0 to 1.32.0
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a href="https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md">go.opentelemetry.io/otel/trace's changelog</a>.</em></p>
<blockquote>
<h2>[1.32.0/0.54.0/0.8.0/0.0.11] 2024-11-08</h2>
<h3>Added</h3>
<ul>
<li>Add <code>go.opentelemetry.io/otel/sdk/metric/exemplar.AlwaysOffFilter</code>, which can be used to disable exemplar recording. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5850">#5850</a>)</li>
<li>Add <code>go.opentelemetry.io/otel/sdk/metric.WithExemplarFilter</code>, which can be used to configure the exemplar filter used by the metrics SDK. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5850">#5850</a>)</li>
<li>Add <code>ExemplarReservoirProviderSelector</code> and <code>DefaultExemplarReservoirProviderSelector</code> to <code>go.opentelemetry.io/otel/sdk/metric</code>, which defines the exemplar reservoir to use based on the aggregation of the metric. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5861">#5861</a>)</li>
<li>Add <code>ExemplarReservoirProviderSelector</code> to <code>go.opentelemetry.io/otel/sdk/metric.Stream</code> to allow using views to configure the exemplar reservoir to use for a metric. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5861">#5861</a>)</li>
<li>Add <code>ReservoirProvider</code>, <code>HistogramReservoirProvider</code> and <code>FixedSizeReservoirProvider</code> to <code>go.opentelemetry.io/otel/sdk/metric/exemplar</code> to make it convenient to use providers of Reservoirs. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5861">#5861</a>)</li>
<li>The <code>go.opentelemetry.io/otel/semconv/v1.27.0</code> package.
The package contains semantic conventions from the <code>v1.27.0</code> version of the OpenTelemetry Semantic Conventions. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5894">#5894</a>)</li>
<li>Add <code>Attributes attribute.Set</code> field to <code>Scope</code> in <code>go.opentelemetry.io/otel/sdk/instrumentation</code>. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5903">#5903</a>)</li>
<li>Add <code>Attributes attribute.Set</code> field to <code>ScopeRecords</code> in <code>go.opentelemetry.io/otel/log/logtest</code>. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5927">#5927</a>)</li>
<li><code>go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc</code> adds instrumentation scope attributes. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5934">#5934</a>)</li>
<li><code>go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp</code> adds instrumentation scope attributes. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5934">#5934</a>)</li>
<li><code>go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc</code> adds instrumentation scope attributes. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5935">#5935</a>)</li>
<li><code>go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp</code> adds instrumentation scope attributes. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5935">#5935</a>)</li>
<li><code>go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc</code> adds instrumentation scope attributes. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5933">#5933</a>)</li>
<li><code>go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp</code> adds instrumentation scope attributes. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5933">#5933</a>)</li>
<li><code>go.opentelemetry.io/otel/exporters/prometheus</code> adds instrumentation scope attributes in <code>otel_scope_info</code> metric as labels. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5932">#5932</a>)</li>
</ul>
<h3>Changed</h3>
<ul>
<li>Support scope attributes and make them as identifying for <code>Tracer</code> in <code>go.opentelemetry.io/otel</code> and <code>go.opentelemetry.io/otel/sdk/trace</code>. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5924">#5924</a>)</li>
<li>Support scope attributes and make them as identifying for <code>Meter</code> in <code>go.opentelemetry.io/otel</code> and <code>go.opentelemetry.io/otel/sdk/metric</code>. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5926">#5926</a>)</li>
<li>Support scope attributes and make them as identifying for <code>Logger</code> in <code>go.opentelemetry.io/otel</code> and <code>go.opentelemetry.io/otel/sdk/log</code>. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5925">#5925</a>)</li>
<li>Make schema URL and scope attributes as identifying for <code>Tracer</code> in <code>go.opentelemetry.io/otel/bridge/opentracing</code>. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5931">#5931</a>)</li>
<li>Clear unneeded slice elements to allow GC to collect the objects in <code>go.opentelemetry.io/otel/sdk/metric</code> and <code>go.opentelemetry.io/otel/sdk/trace</code>. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5804">#5804</a>)</li>
</ul>
<h3>Fixed</h3>
<ul>
<li>Global MeterProvider registration unwraps global instrument Observers, the undocumented Unwrap() methods are now private. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5881">#5881</a>)</li>
<li><code>go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc</code> now keeps the metadata already present in the context when <code>WithHeaders</code> is used. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5892">#5892</a>)</li>
<li><code>go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc</code> now keeps the metadata already present in the context when <code>WithHeaders</code> is used. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5911">#5911</a>)</li>
<li><code>go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc</code> now keeps the metadata already present in the context when <code>WithHeaders</code> is used. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5915">#5915</a>)</li>
<li>Fix <code>go.opentelemetry.io/otel/exporters/prometheus</code> trying to add exemplars to Gauge metrics, which is unsupported. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5912">#5912</a>)</li>
<li>Fix <code>WithEndpointURL</code> to always use a secure connection when an https URL is passed in <code>go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc</code>. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5944">#5944</a>)</li>
<li>Fix <code>WithEndpointURL</code> to always use a secure connection when an https URL is passed in <code>go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp</code>. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5944">#5944</a>)</li>
<li>Fix <code>WithEndpointURL</code> to always use a secure connection when an https URL is passed in <code>go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc</code>. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5944">#5944</a>)</li>
<li>Fix <code>WithEndpointURL</code> to always use a secure connection when an https URL is passed in <code>go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp</code>. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5944">#5944</a>)</li>
<li>Fix incorrect metrics generated from callbacks when multiple readers are used in <code>go.opentelemetry.io/otel/sdk/metric</code>. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5900">#5900</a>)</li>
</ul>
<h3>Removed</h3>
<ul>
<li>Remove all examples under <code>go.opentelemetry.io/otel/example</code> as they are moved to <a href="https://github.com/open-telemetry/opentelemetry-go-contrib/tree/main/examples">Contrib repository</a>. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5930">#5930</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/open-telemetry/opentelemetry-go/commit/7cfbd86a605c85e598eca9a899f6176b17076f4f"><code>7cfbd86</code></a> Release v1.32.0/v0.54.0/v0.8.0/v0.0.11 (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5960">#5960</a>)</li>
<li><a href="https://github.com/open-telemetry/opentelemetry-go/commit/2be617e08003601514a5ec573063f749f868e629"><code>2be617e</code></a> fix(deps): update github.com/opentracing-contrib/go-grpc/test digest to 51a56...</li>
<li><a href="https://github.com/open-telemetry/opentelemetry-go/commit/6db18df3e2662c323287e1992e4eef34d63968a3"><code>6db18df</code></a> fix(deps): update module github.com/opentracing-contrib/go-grpc to v0.1.0 (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5">#5</a>...</li>
<li><a href="https://github.com/open-telemetry/opentelemetry-go/commit/ef12bf858da5421901185b3a90c343444eadcdd0"><code>ef12bf8</code></a> chore(deps): update golang.org/x (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5957">#5957</a>)</li>
<li><a href="https://github.com/open-telemetry/opentelemetry-go/commit/85eb76f2fd8ef474b7cbb4e5f8dc307b872cf3c6"><code>85eb76f</code></a> Allow GC to collect unneeded slice elements (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5804">#5804</a>)</li>
<li><a href="https://github.com/open-telemetry/opentelemetry-go/commit/1492efa719ac935cfbb88a8248ff18667d688b30"><code>1492efa</code></a> Fix incorrect metrics getting generated from multiple readers (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5900">#5900</a>)</li>
<li><a href="https://github.com/open-telemetry/opentelemetry-go/commit/d2b066346b213ca5cbc8a78ce7667dc9cbcbfc8e"><code>d2b0663</code></a> fix(deps): update module go.opentelemetry.io/build-tools/multimod to v0.15.0 ...</li>
<li><a href="https://github.com/open-telemetry/opentelemetry-go/commit/394cbd28909ef7628c525efec15f090533c14f2d"><code>394cbd2</code></a> chore(deps): update lycheeverse/lychee-action action to v2.1.0 (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5950">#5950</a>)</li>
<li><a href="https://github.com/open-telemetry/opentelemetry-go/commit/37b2537692f1b76c686b3ab651570f57b6f89415"><code>37b2537</code></a> fix(deps): update github.com/opentracing-contrib/go-grpc digest to e3cbcab (#...</li>
<li><a href="https://github.com/open-telemetry/opentelemetry-go/commit/7f68356a7def1429099008275e4ce0d3ce7e5067"><code>7f68356</code></a> fix(deps): update module go.opentelemetry.io/build-tools/semconvgen to v0.15....</li>
<li>Additional commits viewable in <a href="https://github.com/open-telemetry/opentelemetry-go/compare/v1.31.0...v1.32.0">compare view</a></li>
</ul>
</details>
<br />


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 show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will remove the ignore condition of the specified dependency and ignore conditions


</details>


Patch:
diff --git a/cli/app.go b/cli/app.go
index 43083575..b9bf1fe1 100644
--- a/cli/app.go
+++ b/cli/app.go
@@ -16,9 +16,13 @@
 	"go.opentelemetry.io/otel/trace"
 
 	"github.com/kopia/kopia/internal/apiclient"
+	"github.com/kopia/kopia/internal/clock"
 	"github.com/kopia/kopia/internal/gather"
 	"github.com/kopia/kopia/internal/passwordpersist"
 	"github.com/kopia/kopia/internal/releasable"
+	"github.com/kopia/kopia/notification"
+	"github.com/kopia/kopia/notification/notifydata"
+	"github.com/kopia/kopia/notification/notifytemplate"
 	"github.com/kopia/kopia/repo"
 	"github.com/kopia/kopia/repo/blob"
 	"github.com/kopia/kopia/repo/logging"
@@ -88,6 +92,7 @@ type appServices interface {
 	repositoryConfigFileName() string
 	getProgress() *cliProgress
 	getRestoreProgress() RestoreProgress
+	notificationTemplateOptions() notifytemplate.Options
 
 	stdout() io.Writer
 	Stderr() io.Writer
@@ -112,6 +117,7 @@ type advancedAppServices interface {
 	getPasswordFromFlags(ctx context.Context, isCreate, allowPersistent bool) (string, error)
 	optionsFromFlags(ctx context.Context) *repo.Options
 	runAppWithContext(command *kingpin.CmdClause, callback func(ctx context.Context) error) error
+	enableErrorNotifications() bool
 }
 
 // App contains per-invocation flags and state of Kopia CLI.
@@ -139,6 +145,8 @@ type App struct {
 	upgradeOwnerID      string
 	doNotWaitForUpgrade bool
 
+	errorNotifications bool
+
 	currentAction         string
 	onExitCallbacks       []func()
 	onFatalErrorCallbacks []func(err error)
@@ -240,6 +248,10 @@ func (c *App) passwordPersistenceStrategy() passwordpersist.Strategy {
 	return passwordpersist.File()
 }
 
+func (c *App) enableErrorNotifications() bool {
+	return c.errorNotifications
+}
+
 func (c *App) setup(app *kingpin.Application) {
 	app.PreAction(func(pc *kingpin.ParseContext) error {
 		if sc := pc.SelectedCommand; sc != nil {
@@ -274,6 +286,7 @@ func (c *App) setup(app *kingpin.Application) {
 	app.Flag("dump-allocator-stats", "Dump allocator stats at the end of execution.").Hidden().Envar(c.EnvName("KOPIA_DUMP_ALLOCATOR_STATS")).BoolVar(&c.dumpAllocatorStats)
 	app.Flag("upgrade-owner-id", "Repository format upgrade owner-id.").Hidden().Envar(c.EnvName("KOPIA_REPO_UPGRADE_OWNER_ID")).StringVar(&c.upgradeOwnerID)
 	app.Flag("upgrade-no-block", "Do not block when repository format upgrade is in progress, instead exit with a message.").Hidden().Default("false").Envar(c.EnvName("KOPIA_REPO_UPGRADE_NO_BLOCK")).BoolVar(&c.doNotWaitForUpgrade)
+	app.Flag("error-notifications", "Send notification on errors").Hidden().Envar(c.EnvName("KOPIA_SEND_ERROR_NOTIFICATIONS")).Default("true").BoolVar(&c.errorNotifications)
 
 	if c.enableTestOnlyFlags() {
 		app.Flag("ignore-missing-required-features", "Open repository despite missing features (VERY DANGEROUS, ONLY FOR TESTING)").Hidden().BoolVar(&c.testonlyIgnoreMissingRequiredFeatures)
@@ -562,6 +575,8 @@ func (c *App) maybeRepositoryAction(act func(ctx context.Context, rep repo.Repos
 			return errors.Wrap(err, "open repository")
 		}
 
+		t0 := clock.Now()
+
 		err = act(ctx, rep)
 
 		if rep != nil && err == nil && !mode.disableMaintenance {
@@ -570,6 +585,17 @@ func (c *App) maybeRepositoryAction(act func(ctx context.Context, rep repo.Repos
 			}
 		}
 
+		if err != nil && rep != nil && c.errorNotifications {
+			notification.Send(ctx, rep, "generic-error", notifydata.NewErrorInfo(
+				c.currentActionName(),
+				c.currentActionName(),
+				t0,
+				clock.Now(),
+				err), notification.SeverityError,
+				c.notificationTemplateOptions(),
+			)
+		}
+
 		if rep != nil && mode.mustBeConnected {
 			if cerr := rep.Close(ctx); cerr != nil {
 				return errors.Wrap(cerr, "unable to close repository")
@@ -647,6 +673,11 @@ func (c *App) advancedCommand(ctx context.Context) {
 	}
 }
 
+func (c *App) notificationTemplateOptions() notifytemplate.Options {
+	// perhaps make this configurable in the future
+	return notifytemplate.DefaultOptions
+}
+
 func init() {
 	kingpin.EnableFileExpansion = false
 }
diff --git a/cli/command_notification_template_set.go b/cli/command_notification_template_set.go
index 82d5b6b1..5c1ba85b 100644
--- a/cli/command_notification_template_set.go
+++ b/cli/command_notification_template_set.go
@@ -77,7 +77,7 @@ func (c *commandNotificationTemplateSet) launchEditor(ctx context.Context, rep r
 	var lastUpdated string
 
 	if err := editor.EditLoop(ctx, "template.md", s, false, func(updated string) error {
-		_, err := notifytemplate.ParseTemplate(updated)
+		_, err := notifytemplate.ParseTemplate(updated, notifytemplate.DefaultOptions)
 		if err == nil {
 			lastUpdated = updated
 			return nil
diff --git a/cli/command_server_start.go b/cli/command_server_start.go
index 98ff726a..cb509490 100644
--- a/cli/command_server_start.go
+++ b/cli/command_server_start.go
@@ -161,6 +161,9 @@ func (c *commandServerStart) serverStartOptions(ctx context.Context) (*server.Op
 		DebugScheduler:         c.debugScheduler,
 		MinMaintenanceInterval: c.minMaintenanceInterval,
 		DisableCSRFTokenChecks: c.disableCSRFTokenChecks,
+
+		EnableErrorNotifications: c.svc.enableErrorNotifications(),
+		NotifyTemplateOptions:    c.svc.notificationTemplateOptions(),
 	}, nil
 }
 
diff --git a/cli/command_snapshot_create.go b/cli/command_snapshot_create.go
index 26700cbe..7f34127e 100644
--- a/cli/command_snapshot_create.go
+++ b/cli/command_snapshot_create.go
@@ -12,6 +12,8 @@
 
 	"github.com/kopia/kopia/fs"
 	"github.com/kopia/kopia/fs/virtualfs"
+	"github.com/kopia/kopia/notification"
+	"github.com/kopia/kopia/notification/notifydata"
 	"github.com/kopia/kopia/repo"
 	"github.com/kopia/kopia/snapshot"
 	"github.com/kopia/kopia/snapshot/policy"
@@ -127,6 +129,8 @@ func (c *commandSnapshotCreate) run(ctx context.Context, rep repo.RepositoryWrit
 		return err
 	}
 
+	var st notifydata.MultiSnapshotStatus
+
 	for _, snapshotDir := range sources {
 		if u.IsCanceled() {
 			log(ctx).Info("Upload canceled")
@@ -138,11 +142,13 @@ func (c *commandSnapshotCreate) run(ctx context.Context, rep repo.RepositoryWrit
 			finalErrors = append(finalErrors, fmt.Sprintf("failed to prepare source: %s", err))
 		}
 
-		if err := c.snapshotSingleSource(ctx, fsEntry, setManual, rep, u, sourceInfo, tags); err != nil {
+		if err := c.snapshotSingleSource(ctx, fsEntry, setManual, rep, u, sourceInfo, tags, &st); err != nil {
 			finalErrors = append(finalErrors, err.Error())
 		}
 	}
 
+	notification.Send(ctx, rep, "snapshot-report", st, notification.SeverityReport, c.svc.notificationTemplateOptions())
+
 	// ensure we flush at least once in the session to properly close all pending buffers,
 	// otherwise the session will be reported as memory leak.
 	// by default the wrapper function does not flush on errors, which is what we want to do always.
@@ -259,27 +265,48 @@ func startTimeAfterEndTime(startTime, endTime time.Time) bool {
 		startTime.After(endTime)
 }
 
-//nolint:gocyclo
-func (c *commandSnapshotCreate) snapshotSingleSource(ctx context.Context, fsEntry fs.Entry, setManual bool, rep repo.RepositoryWriter, u *snapshotfs.Uploader, sourceInfo snapshot.SourceInfo, tags map[string]string) error {
+//nolint:gocyclo,funlen
+func (c *commandSnapshotCreate) snapshotSingleSource(
+	ctx context.Context,
+	fsEntry fs.Entry,
+	setManual bool,
+	rep repo.RepositoryWriter,
+	u *snapshotfs.Uploader,
+	sourceInfo snapshot.SourceInfo,
+	tags map[string]string,
+	st *notifydata.MultiSnapshotStatus,
+) (finalErr error) {
 	log(ctx).Infof("Snapshotting %v ...", sourceInfo)
 
-	var err error
+	var mwe notifydata.ManifestWithError
 
-	previous, err := findPreviousSnapshotManifest(ctx, rep, sourceInfo, nil)
-	if err != nil {
-		return err
+	mwe.Source = sourceInfo
+
+	st.Snapshots = append(st.Snapshots, &mwe)
+
+	defer func() {
+		if finalErr != nil {
+			mwe.Error = finalErr.Error()
+		}
+	}()
+
+	var previous []*snapshot.Manifest
+
+	previous, finalErr = findPreviousSnapshotManifest(ctx, rep, sourceInfo, nil)
+	if finalErr != nil {
+		return finalErr
 	}
 
-	policyTree, err := policy.TreeForSource(ctx, rep, sourceInfo)
-	if err != nil {
-		return errors.Wrap(err, "unable to get policy tree")
+	policyTree, finalErr := policy.TreeForSource(ctx, rep, sourceInfo)
+	if finalErr != nil {
+		return errors.Wrap(finalErr, "unable to get policy tree")
 	}
 
-	manifest, err := u.Upload(ctx, fsEntry, policyTree, sourceInfo, previous...)
-	if err != nil {
+	manifest, finalErr := u.Upload(ctx, fsEntry, policyTree, sourceInfo, previous...)
+	if finalErr != nil {
 		// fail-fast uploads will fail here without recording a manifest, other uploads will
 		// possibly fail later.
-		return errors.Wrap(err, "upload error")
+		return errors.Wrap(finalErr, "upload error")
 	}
 
 	manifest.Description = c.snapshotCreateDescription
@@ -308,6 +335,8 @@ func (c *commandSnapshotCreate) snapshotSingleSource(ctx context.Context, fsEntr
 		manifest.EndTime = fs.UTCTimestampFromTime(endTimeOverride)
 	}
 
+	mwe.Manifest = *manifest
+
 	ignoreIdenticalSnapshot := policyTree.EffectivePolicy().RetentionPolicy.IgnoreIdenticalSnapshots.OrDefault(false)
 	if ignoreIdenticalSnapshot && len(previous) > 0 {
 		if previous[0].RootObjectID() == manifest.RootObjectID() {
@@ -316,17 +345,17 @@ func (c *commandSnapshotCreate) snapshotSingleSource(ctx context.Context, fsEntr
 		}
 	}
 
-	if _, err = snapshot.SaveSnapshot(ctx, rep, manifest); err != nil {
-		return errors.Wrap(err, "cannot save manifest")
+	if _, finalErr = snapshot.SaveSnapshot(ctx, rep, manifest); finalErr != nil {
+		return errors.Wrap(finalErr, "cannot save manifest")
 	}
 
-	if _, err = policy.ApplyRetentionPolicy(ctx, rep, sourceInfo, true); err != nil {
-		return errors.Wrap(err, "unable to apply retention policy")
+	if _, finalErr = policy.ApplyRetentionPolicy(ctx, rep, sourceInfo, true); finalErr != nil {
+		return errors.Wrap(finalErr, "unable to apply retention policy")
 	}
 
 	if setManual {
-		if err = policy.SetManual(ctx, rep, sourceInfo); err != nil {
-			return errors.Wrap(err, "unable to set manual field in scheduling policy for source")
+		if finalErr = policy.SetManual(ctx, rep, sourceInfo); finalErr != nil {
+			return errors.Wrap(finalErr, "unable to set manual field in scheduling policy for source")
 		}
 	}
 
diff --git a/go.mod b/go.mod
index e8df41ba..629475d0 100644
--- a/go.mod
+++ b/go.mod
@@ -50,10 +50,10 @@ require (
 	github.com/tg123/go-htpasswd v1.2.3
 	github.com/zalando/go-keyring v0.2.6
 	github.com/zeebo/blake3 v0.2.4
-	go.opentelemetry.io/otel v1.31.0
-	go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.31.0
-	go.opentelemetry.io/otel/sdk v1.31.0
-	go.opentelemetry.io/otel/trace v1.31.0
+	go.opentelemetry.io/otel v1.32.0
+	go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.32.0
+	go.opentelemetry.io/otel/sdk v1.32.0
+	go.opentelemetry.io/otel/trace v1.32.0
 	go.uber.org/multierr v1.11.0
 	go.uber.org/zap v1.27.0
 	golang.org/x/crypto v0.29.0
@@ -117,7 +117,7 @@ require (
 	github.com/google/s2a-go v0.1.8 // indirect
 	github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect
 	github.com/googleapis/gax-go/v2 v2.13.0 // indirect
-	github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 // indirect
+	github.com/grpc-ecosystem/grpc-gateway/v2 v2.23.0 // indirect
 	github.com/josharian/intern v1.0.0 // indirect
 	github.com/klauspost/cpuid/v2 v2.2.8 // indirect
 	github.com/kr/fs v0.1.0 // indirect
@@ -136,14 +136,14 @@ require (
 	go.opentelemetry.io/contrib/detectors/gcp v1.29.0 // indirect
 	go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 // indirect
 	go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect
-	go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.31.0 // indirect
-	go.opentelemetry.io/otel/metric v1.31.0 // indirect
+	go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.32.0 // indirect
+	go.opentelemetry.io/otel/metric v1.32.0 // indirect
 	go.opentelemetry.io/otel/sdk/metric v1.29.0 // indirect
 	go.opentelemetry.io/proto/otlp v1.3.1 // indirect
 	golang.org/x/time v0.7.0 // indirect
 	google.golang.org/genproto v0.0.0-20241021214115-324edc3d5d38 // indirect
-	google.golang.org/genproto/googleapis/api v0.0.0-20241015192408-796eee8c2d53 // indirect
-	google.golang.org/genproto/googleapis/rpc v0.0.0-20241021214115-324edc3d5d38 // indirect
+	google.golang.org/genproto/googleapis/api v0.0.0-20241104194629-dd2ea8efbc28 // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20241104194629-dd2ea8efbc28 // indirect
 	google.golang.org/grpc/stats/opentelemetry v0.0.0-20240907200651-3ffb98b2c93a // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
 )
diff --git a/go.sum b/go.sum
index 2f90b847..ef06e7aa 100644
--- a/go.sum
+++ b/go.sum
@@ -189,8 +189,8 @@ github.com/googleapis/gax-go/v2 v2.13.0 h1:yitjD5f7jQHhyDsnhKEBU52NdvvdSeGzlAnDP
 github.com/googleapis/gax-go/v2 v2.13.0/go.mod h1:Z/fvTZXF8/uw7Xu5GuslPw+bplx6SS338j1Is2S+B7A=
 github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
 github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
-github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys=
-github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.23.0 h1:ad0vkEBuk23VJzZR9nkLVG0YAoN9coASF1GusYX6AlU=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.23.0/go.mod h1:igFoXX2ELCW06bol23DWPB5BEWfZISOzSP5K2sbLea0=
 github.com/hanwen/go-fuse/v2 v2.6.3 h1:tDcEkLRx93lXu4XyN1/j8Z74VWvhHDl6qU1kNnvFUqI=
 github.com/hanwen/go-fuse/v2 v2.6.3/go.mod h1:ugNaD/iv5JYyS1Rcvi57Wz7/vrLQJo10mmketmoef48=
 github.com/hashicorp/cronexpr v1.1.2 h1:wG/ZYIKT+RT3QkOdgYc+xsKWVRgnxJ1OJtjjy84fJ9A=
@@ -323,20 +323,20 @@ go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.5
 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0/go.mod h1:B9yO6b04uB80CzjedvewuqDhxJxi11s7/GtiGa8bAjI=
 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk=
 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8=
-go.opentelemetry.io/otel v1.31.0 h1:NsJcKPIW0D0H3NgzPDHmo0WW6SptzPdqg/L1zsIm2hY=
-go.opentelemetry.io/otel v1.31.0/go.mod h1:O0C14Yl9FgkjqcCZAsE053C13OaddMYr/hz6clDkEJE=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.31.0 h1:K0XaT3DwHAcV4nKLzcQvwAgSyisUghWoY20I7huthMk=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.31.0/go.mod h1:B5Ki776z/MBnVha1Nzwp5arlzBbE3+1jk+pGmaP5HME=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.31.0 h1:FFeLy03iVTXP6ffeN2iXrxfGsZGCjVx0/4KlizjyBwU=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.31.0/go.mod h1:TMu73/k1CP8nBUpDLc71Wj/Kf7ZS9FK5b53VapRsP9o=
-go.opentelemetry.io/otel/metric v1.31.0 h1:FSErL0ATQAmYHUIzSezZibnyVlft1ybhy4ozRPcF2fE=
-go.opentelemetry.io/otel/metric v1.31.0/go.mod h1:C3dEloVbLuYoX41KpmAhOqNriGbA+qqH6PQ5E5mUfnY=
-go.opentelemetry.io/otel/sdk v1.31.0 h1:xLY3abVHYZ5HSfOg3l2E5LUj2Cwva5Y7yGxnSW9H5Gk=
-go.opentelemetry.io/otel/sdk v1.31.0/go.mod h1:TfRbMdhvxIIr/B2N2LQW2S5v9m3gOQ/08KsbbO5BPT0=
+go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U=
+go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.32.0 h1:IJFEoHiytixx8cMiVAO+GmHR6Frwu+u5Ur8njpFO6Ac=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.32.0/go.mod h1:3rHrKNtLIoS0oZwkY2vxi+oJcwFRWdtUyRII+so45p8=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.32.0 h1:9kV11HXBHZAvuPUZxmMWrH8hZn/6UnHX4K0mu36vNsU=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.32.0/go.mod h1:JyA0FHXe22E1NeNiHmVp7kFHglnexDQ7uRWDiiJ1hKQ=
+go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M=
+go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8=
+go.opentelemetry.io/otel/sdk v1.32.0 h1:RNxepc9vK59A8XsgZQouW8ue8Gkb4jpWtJm9ge5lEG4=
+go.opentelemetry.io/otel/sdk v1.32.0/go.mod h1:LqgegDBjKMmb2GC6/PrTnteJG39I8/vJCAP9LlJXEjU=
 go.opentelemetry.io/otel/sdk/metric v1.29.0 h1:K2CfmJohnRgvZ9UAj2/FhIf/okdWcNdBwe1m8xFXiSY=
 go.opentelemetry.io/otel/sdk/metric v1.29.0/go.mod h1:6zZLdCl2fkauYoZIOn/soQIDSWFmNSRcICarHfuhNJQ=
-go.opentelemetry.io/otel/trace v1.31.0 h1:ffjsj1aRouKewfr85U2aGagJ46+MvodynlQ1HYdmJys=
-go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A=
+go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM=
+go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8=
 go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0=
 go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
 go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
@@ -439,10 +439,10 @@ google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98
 google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
 google.golang.org/genproto v0.0.0-20241021214115-324edc3d5d38 h1:Q3nlH8iSQSRUwOskjbcSMcF2jiYMNiQYZ0c2KEJLKKU=
 google.golang.org/genproto v0.0.0-20241021214115-324edc3d5d38/go.mod h1:xBI+tzfqGGN2JBeSebfKXFSdBpWVQ7sLW40PTupVRm4=
-google.golang.org/genproto/googleapis/api v0.0.0-20241015192408-796eee8c2d53 h1:fVoAXEKA4+yufmbdVYv+SE73+cPZbbbe8paLsHfkK+U=
-google.golang.org/genproto/googleapis/api v0.0.0-20241015192408-796eee8c2d53/go.mod h1:riSXTwQ4+nqmPGtobMFyW5FqVAmIs0St6VPp4Ug7CE4=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20241021214115-324edc3d5d38 h1:zciRKQ4kBpFgpfC5QQCVtnnNAcLIqweL7plyZRQHVpI=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20241021214115-324edc3d5d38/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI=
+google.golang.org/genproto/googleapis/api v0.0.0-20241104194629-dd2ea8efbc28 h1:M0KvPgPmDZHPlbRbaNU1APr28TvwvvdUPlSv7PUvy8g=
+google.golang.org/genproto/googleapis/api v0.0.0-20241104194629-dd2ea8efbc28/go.mod h1:dguCy7UOdZhTvLzDyt15+rOrawrpM4q7DD9dQ1P11P4=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20241104194629-dd2ea8efbc28 h1:XVhgTWWV3kGQlwJHR3upFWZeTsei6Oks1apkZSeonIE=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20241104194629-dd2ea8efbc28/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
 google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
diff --git a/internal/server/grpc_session.go b/internal/server/grpc_session.go
index 3779ea26..e532b288 100644
--- a/internal/server/grpc_session.go
+++ b/internal/server/grpc_session.go
@@ -132,7 +132,7 @@ func (s *Server) Session(srv grpcapi.KopiaRepository_SessionServer) error {
 			go func() {
 				defer s.grpcServerState.sem.Release(1)
 
-				handleSessionRequest(ctx, dw, authz, usernameAtHostname, req, func(resp *grpcapi.SessionResponse) {
+				s.handleSessionRequest(ctx, dw, authz, usernameAtHostname, req, func(resp *grpcapi.SessionResponse) {
 					if err := s.send(srv, req.GetRequestId(), resp); err != nil {
 						select {
 						case lastErr <- err:
@@ -149,7 +149,7 @@ func (s *Server) Session(srv grpcapi.KopiaRepository_SessionServer) error {
 
 var tracer = otel.Tracer("kopia/grpc")
 
-func handleSessionRequest(ctx context.Context, dw repo.DirectRepositoryWriter, authz auth.AuthorizationInfo, usernameAtHostname string, req *grpcapi.SessionRequest, respond func(*grpcapi.SessionResponse)) {
+func (s *Server) handleSessionRequest(ctx context.Context, dw repo.DirectRepositoryWriter, authz auth.AuthorizationInfo, usernameAtHostname string, req *grpcapi.SessionRequest, respond func(*grpcapi.SessionResponse)) {
 	if req.GetTraceContext() != nil {
 		var tc propagation.TraceContext
 		ctx = tc.Extract(ctx, propagation.MapCarrier(req.GetTraceContext()))
@@ -187,7 +187,7 @@ func handleSessionRequest(ctx context.Context, dw repo.DirectRepositoryWriter, a
 		respond(handleApplyRetentionPolicyRequest(ctx, dw, authz, usernameAtHostname, inner.ApplyRetentionPolicy))
 
 	case *grpcapi.SessionRequest_SendNotification:
-		respond(handleSendNotificationRequest(ctx, dw, authz, inner.SendNotification))
+		respond(s.handleSendNotificationRequest(ctx, dw, authz, inner.SendNotification))
 
 	case *grpcapi.SessionRequest_InitializeSession:
 		respond(errorResponse(errors.New("InitializeSession must be the first request in a session")))
@@ -488,7 +488,7 @@ func handleApplyRetentionPolicyRequest(ctx context.Context, rep repo.RepositoryW
 	}
 }
 
-func handleSendNotificationRequest(ctx context.Context, rep repo.RepositoryWriter, authz auth.AuthorizationInfo, req *grpcapi.SendNotificationRequest) *grpcapi.SessionResponse {
+func (s *Server) handleSendNotificationRequest(ctx context.Context, rep repo.RepositoryWriter, authz auth.AuthorizationInfo, req *grpcapi.SendNotificationRequest) *grpcapi.SessionResponse {
 	ctx, span := tracer.Start(ctx, "GRPCSession.SendNotification")
 	defer span.End()
 
@@ -499,7 +499,8 @@ func handleSendNotificationRequest(ctx context.Context, rep repo.RepositoryWrite
 	if err := notification.SendInternal(ctx, rep,
 		req.GetTemplateName(),
 		json.RawMessage(req.GetEventArgs()),
-		notification.Severity(req.GetSeverity())); err != nil {
+		notification.Severity(req.GetSeverity()),
+		s.options.NotifyTemplateOptions); err != nil {
 		return errorResponse(err)
 	}
 
diff --git a/internal/server/server.go b/internal/server/server.go
index a0e4e516..208c5048 100644
--- a/internal/server/server.go
+++ b/internal/server/server.go
@@ -26,6 +26,7 @@
 	"github.com/kopia/kopia/internal/scheduler"
 	"github.com/kopia/kopia/internal/serverapi"
 	"github.com/kopia/kopia/internal/uitask"
+	"github.com/kopia/kopia/notification/notifytemplate"
 	"github.com/kopia/kopia/repo"
 	"github.com/kopia/kopia/repo/logging"
 	"github.com/kopia/kopia/repo/maintenance"
@@ -207,6 +208,9 @@ func isAuthenticated(rc requestContext) bool {
 		rc.w.Header().Set("WWW-Authenticate", `Basic realm="Kopia"`)
 		http.Error(rc.w, "Access denied.\n", http.StatusUnauthorized)
 
+		// Log failed authentication attempt
+		log(rc.req.Context()).Warnf("failed login attempt by client %s for user %s", rc.req.RemoteAddr, username)
+
 		return false
 	}
 
@@ -222,6 +226,9 @@ func isAuthenticated(rc requestContext) bool {
 			Expires: now.Add(kopiaAuthCookieTTL),
 			Path:    "/",
 		})
+
+		// Log successful authentication
+		log(rc.req.Context()).Infof("successful login by client %s for user %s", rc.req.RemoteAddr, username)
 	}
 
 	return true
@@ -535,6 +542,14 @@ func (s *Server) endUpload(ctx context.Context, src snapshot.SourceInfo) {
 	s.parallelSnapshotsChanged.Signal()
 }
 
+func (s *Server) enableErrorNotifications() bool {
+	return s.options.EnableErrorNotifications
+}
+
+func (s *Server) notificationTemplateOptions() notifytemplate.Options {
+	return s.options.NotifyTemplateOptions
+}
+
 // SetRepository sets the repository (nil is allowed and indicates server that is not
 // connected to the repository).
 func (s *Server) SetRepository(ctx context.Context, rep repo.Repository) error {
@@ -785,23 +800,25 @@ func (s *Server) ServeStaticFiles(m *mux.Router, fs http.FileSystem) {
 
 // Options encompasses all API server options.
 type Options struct {
-	ConfigFile             string
-	ConnectOptions         *repo.ConnectOptions
-	RefreshInterval        time.Duration
-	MaxConcurrency         int
-	Authenticator          auth.Authenticator
-	Authorizer             auth.Authorizer
-	PasswordPersist        passwordpersist.Strategy
-	AuthCookieSigningKey   string
-	LogRequests            bool
-	UIUser                 string // name of the user allowed to access the UI API
-	UIPreferencesFile      string // name of the JSON file storing UI preferences
-	ServerControlUser      string // name of the user allowed to access the server control API
-	DisableCSRFTokenChecks bool
-	PersistentLogs         bool
-	UITitlePrefix          string
-	DebugScheduler         bool
-	MinMaintenanceInterval time.Duration
+	ConfigFile               string
+	ConnectOptions           *repo.ConnectOptions
+	RefreshInterval          time.Duration
+	MaxConcurrency           int
+	Authenticator            auth.Authenticator
+	Authorizer               auth.Authorizer
+	PasswordPersist          passwordpersist.Strategy
+	AuthCookieSigningKey     string
+	LogRequests              bool
+	UIUser                   string // name of the user allowed to access the UI API
+	UIPreferencesFile        string // name of the JSON file storing UI preferences
+	ServerControlUser        string // name of the user allowed to access the server control API
+	DisableCSRFTokenChecks   bool
+	PersistentLogs           bool
+	UITitlePrefix            string
+	DebugScheduler           bool
+	MinMaintenanceInterval   time.Duration
+	EnableErrorNotifications bool
+	NotifyTemplateOptions    notifytemplate.Options
 }
 
 // InitRepositoryFunc is a function that attempts to connect to/open repository.
diff --git a/internal/server/server_maintenance.go b/internal/server/server_maintenance.go
index 8faedbcd..1703d1e8 100644
--- a/internal/server/server_maintenance.go
+++ b/internal/server/server_maintenance.go
@@ -8,6 +8,9 @@
 	"github.com/pkg/errors"
 
 	"github.com/kopia/kopia/internal/clock"
+	"github.com/kopia/kopia/notification"
+	"github.com/kopia/kopia/notification/notifydata"
+	"github.com/kopia/kopia/notification/notifytemplate"
 	"github.com/kopia/kopia/repo"
 	"github.com/kopia/kopia/repo/maintenance"
 )
@@ -32,6 +35,8 @@ type srvMaintenance struct {
 type maintenanceManagerServerInterface interface {
 	runMaintenanceTask(ctx context.Context, dr repo.DirectRepository) error
 	refreshScheduler(reason string)
+	enableErrorNotifications() bool
+	notificationTemplateOptions() notifytemplate.Options
 }
 
 func (s *srvMaintenance) trigger() {
@@ -138,9 +143,21 @@ func startMaintenanceManager(
 
 				m.beforeRun()
 
+				t0 := clock.Now()
+
 				if err := srv.runMaintenanceTask(mctx, rep); err != nil {
 					log(ctx).Debugw("maintenance task failed", "err", err)
 					m.afterFailedRun()
+
+					if srv.enableErrorNotifications() {
+						notification.Send(ctx,
+							rep,
+							"generic-error",
+							notifydata.NewErrorInfo("Maintenance", "Scheduled Maintenance", t0, clock.Now(), err),
+							notification.SeverityError,
+							srv.notificationTemplateOptions(),
+						)
+					}
 				}
 
 				m.refresh(mctx, true)
diff --git a/notification/notification_send.go b/notification/notification_send.go
index 1a4c2b92..200f34d1 100644
--- a/notification/notification_send.go
+++ b/notification/notification_send.go
@@ -105,7 +105,7 @@ func notificationSendersFromRepo(ctx context.Context, rep repo.Repository, sever
 
 // Send sends a notification for the given event.
 // Any errors encountered during the process are logged.
-func Send(ctx context.Context, rep repo.Repository, templateName string, eventArgs any, sev Severity) {
+func Send(ctx context.Context, rep repo.Repository, templateName string, eventArgs any, sev Severity, opt notifytemplate.Options) {
 	// if we're connected to a repository server, send the notification there.
 	if rem, ok := rep.(repo.RemoteNotifications); ok {
 		jsonData, err := json.Marshal(eventArgs)
@@ -122,13 +122,13 @@ func Send(ctx context.Context, rep repo.Repository, templateName string, eventAr
 		return
 	}
 
-	if err := SendInternal(ctx, rep, templateName, eventArgs, sev); err != nil {
+	if err := SendInternal(ctx, rep, templateName, eventArgs, sev, opt); err != nil {
 		log(ctx).Warnw("unable to send notification", "err", err)
 	}
 }
 
 // SendInternal sends a notification for the given event and returns an error.
-func SendInternal(ctx context.Context, rep repo.Repository, templateName string, eventArgs any, sev Severity) error {
+func SendInternal(ctx context.Context, rep repo.Repository, templateName string, eventArgs any, sev Severity, opt notifytemplate.Options) error {
 	senders, err := notificationSendersFromRepo(ctx, rep, sev)
 	if err != nil {
 		return errors.Wrap(err, "unable to get notification senders")
@@ -137,7 +137,7 @@ func SendInternal(ctx context.Context, rep repo.Repository, templateName string,
 	var resultErr error
 
 	for _, s := range senders {
-		if err := SendTo(ctx, rep, s, templateName, eventArgs, sev); err != nil {
+		if err := SendTo(ctx, rep, s, templateName, eventArgs, sev, opt); err != nil {
 			resultErr = multierr.Append(resultErr, err)
 		}
 	}
@@ -166,7 +166,7 @@ func MakeTemplateArgs(eventArgs any) TemplateArgs {
 }
 
 // SendTo sends a notification to the given sender.
-func SendTo(ctx context.Context, rep repo.Repository, s sender.Sender, templateName string, eventArgs any, sev Severity) error {
+func SendTo(ctx context.Context, rep repo.Repository, s sender.Sender, templateName string, eventArgs any, sev Severity, opt notifytemplate.Options) error {
 	// execute template
 	var bodyBuf bytes.Buffer
 
@@ -175,7 +175,7 @@ func SendTo(ctx context.Context, rep repo.Repository, s sender.Sender, templateN
 		return errors.Wrap(err, "unable to resolve notification template")
 	}
 
-	t, err := notifytemplate.ParseTemplate(tmpl)
+	t, err := notifytemplate.ParseTemplate(tmpl, opt)
 	if err != nil {
 		return errors.Wrap(err, "unable to parse notification template")
 	}
@@ -205,5 +205,5 @@ func SendTo(ctx context.Context, rep repo.Repository, s sender.Sender, templateN
 func SendTestNotification(ctx context.Context, rep repo.Repository, s sender.Sender) error {
 	log(ctx).Infof("Sending test notification to %v", s.Summary())
 
-	return SendTo(ctx, rep, s, notifytemplate.TestNotification, struct{}{}, SeveritySuccess)
+	return SendTo(ctx, rep, s, notifytemplate.TestNotification, struct{}{}, SeveritySuccess, notifytemplate.DefaultOptions)
 }
diff --git a/notification/notifydata/doc.go b/notification/notifydata/doc.go
new file mode 100644
index 00000000..26449422
--- /dev/null
+++ b/notification/notifydata/doc.go
@@ -0,0 +1,2 @@
+// Package notifydata contains the data structures used by the notification package.
+package notifydata
diff --git a/notification/notifydata/error_info.go b/notification/notifydata/error_info.go
new file mode 100644
index 00000000..4002a061
--- /dev/null
+++ b/notification/notifydata/error_info.go
@@ -0,0 +1,43 @@
+package notifydata
+
+import (
+	"fmt"
+	"time"
+)
+
+// ErrorInfo represents information about errors.
+type ErrorInfo struct {
+	Operation        string    `json:"operation"`
+	OperationDetails string    `json:"operationDetails"`
+	StartTime        time.Time `json:"start"`
+	EndTime          time.Time `json:"end"`
+	ErrorMessage     string    `json:"error"`
+	ErrorDetails     string    `json:"errorDetails"`
+}
+
+// StartTimestamp returns the start time of the operation that caused the error.
+func (e *ErrorInfo) StartTimestamp() time.Time {
+	return e.StartTime.Truncate(time.Second)
+}
+
+// EndTimestamp returns the end time of the operation that caused the error.
+func (e *ErrorInfo) EndTimestamp() time.Time {
+	return e.EndTime.Truncate(time.Second)
+}
+
+// Duration returns the duration of the operation.
+func (e *ErrorInfo) Duration() time.Duration {
+	return e.EndTimestamp().Sub(e.StartTimestamp())
+}
+
+// NewErrorInfo creates a new ErrorInfo.
+func NewErrorInfo(operation, operationDetails string, startTime, endTime time.Time, err error) *ErrorInfo {
+	return &ErrorInfo{
+		Operation:        operation,
+		OperationDetails: operationDetails,
+		StartTime:        startTime,
+		EndTime:          endTime,
+		ErrorMessage:     fmt.Sprintf("%v", err),
+		ErrorDetails:     fmt.Sprintf("%+v", err),
+	}
+}
diff --git a/notification/notifydata/multi_snapshot_status.go b/notification/notifydata/multi_snapshot_status.go
new file mode 100644
index 00000000..4543ea89
--- /dev/null
+++ b/notification/notifydata/multi_snapshot_status.go
@@ -0,0 +1,96 @@
+package notifydata
+
+import (
+	"time"
+
+	"github.com/kopia/kopia/snapshot"
+)
+
+// ManifestWithError represents information about the snapshot manifest with optional error.
+type ManifestWithError struct {
+	snapshot.Manifest `json:"manifest"` // may not be filled out if there was an error, Manifst.Source is always set.
+
+	Error string `json:"error"` // will be present if there was an error
+}
+
+// StartTimestamp returns the start time of the snapshot.
+func (m *ManifestWithError) StartTimestamp() time.Time {
+	return m.StartTime.ToTime().UTC().Truncate(time.Second)
+}
+
+// EndTimestamp returns the end time of the snapshot.
+func (m *ManifestWithError) EndTimestamp() time.Time {
+	return m.EndTime.ToTime().UTC().Truncate(time.Second)
+}
+
+// TotalSize returns the total size of the snapshot in bytes.
+func (m *ManifestWithError) TotalSize() int64 {
+	if m.RootEntry == nil {
+		return 0
+	}
+
+	if m.RootEntry.DirSummary != nil {
+		return m.RootEntry.DirSummary.TotalFileSize
+	}
+
+	return m.RootEntry.FileSize
+}
+
+// TotalFiles returns the total number of files in the snapshot.
+func (m *ManifestWithError) TotalFiles() int64 {
+	if m.RootEntry == nil {
+		return 0
+	}
+
+	if m.RootEntry.DirSummary != nil {
+		return m.RootEntry.DirSummary.TotalFileCount
+	}
+
+	return 1
+}
+
+// TotalDirs returns the total number of directories in the snapshot.
+func (m *ManifestWithError) TotalDirs() int64 {
+	if m.RootEntry == nil {
+		return 0
+	}
+
+	if m.RootEntry.DirSummary != nil {
+		return m.RootEntry.DirSummary.TotalDirCount
+	}
+
+	return 0
+}
+
+// Duration returns the duration of the snapshot.
+func (m *ManifestWithError) Duration() time.Duration {
+	return time.Duration(m.EndTime - m.StartTime)
+}
+
+// StatusCode returns the status code of the manifest.
+func (m *ManifestWithError) StatusCode() string {
+	if m.Error != "" {
+		return "fatal"
+	}
+
+	if m.Manifest.IncompleteReason != "" {
+		return "incomplete"
+	}
+
+	if m.Manifest.RootEntry != nil && m.Manifest.RootEntry.DirSummary != nil {
+		if m.Manifest.RootEntry.DirSummary.FatalErrorCount > 0 {
+			return "fatal"
+		}
+
+		if m.Manifest.RootEntry.DirSummary.IgnoredErrorCount > 0 {
+			return "error"
+		}
+	}
+
+	return "ok"
+}
+
+// MultiSnapshotStatus represents the status of multiple snapshots.
+type MultiSnapshotStatus struct {
+	Snapshots []*ManifestWithError `json:"snapshots"`
+}
diff --git a/notification/notifytemplate/embeddedtemplate.go b/notification/notifytemplate/embeddedtemplate.go
index 9e3841f8..0c24cb12 100644
--- a/notification/notifytemplate/embeddedtemplate.go
+++ b/notification/notifytemplate/embeddedtemplate.go
@@ -3,12 +3,15 @@
 
 import (
 	"embed"
+	"slices"
+	"sort"
 	"text/template"
 	"time"
 
 	"github.com/pkg/errors"
 
-	"github.com/kopia/kopia/fs"
+	"github.com/kopia/kopia/internal/units"
+	"github.com/kopia/kopia/notification/notifydata"
 )
 
 //go:embed "*.html"
@@ -20,22 +23,41 @@
 	TestNotification = "test-notification"
 )
 
-// Functions is a map of functions that can be used in templates.
+// Options provides options for template rendering.
+type Options struct {
+	Timezone   *time.Location
+	TimeFormat string
+}
+
+// functions is a map of functions that can be used in templates.
+func functions(opt Options) template.FuncMap {
+	if opt.Timezone == nil {
+		opt.Timezone = time.Local
+	}
+
+	if opt.TimeFormat == "" {
+		opt.TimeFormat = time.RFC1123Z
+	}
+
+	return template.FuncMap{
+		"bytes": units.BytesString[int64],
+		"sortSnapshotManifestsByName": func(man []*notifydata.ManifestWithError) []*notifydata.ManifestWithError {
+			res := slices.Clone(man)
+			sort.Slice(res, func(i, j int) bool {
+				return res[i].Source.String() < res[j].Source.String()
+			})
+			return res
+		},
+		"formatTime": func(t time.Time) string {
+			return t.In(opt.Timezone).Format(opt.TimeFormat)
+		},
+	}
+}
+
+// DefaultOptions is the default set of options.
 //
 //nolint:gochecknoglobals
-var Functions = template.FuncMap{
-	"toTime": func(t any) time.Time {
-		if t, ok := t.(time.Time); ok {
-			return t
-		}
-
-		if t, ok := t.(fs.UTCTimestamp); ok {
-			return t.ToTime()
-		}
-
-		return time.Time{}
-	},
-}
+var DefaultOptions = Options{}
 
 // GetEmbeddedTemplate returns embedded template by name.
 func GetEmbeddedTemplate(templateName string) (string, error) {
@@ -61,7 +83,7 @@ func SupportedTemplates() []string {
 }
 
 // ParseTemplate parses a named template.
-func ParseTemplate(tmpl string) (*template.Template, error) {
+func ParseTemplate(tmpl string, opt Options) (*template.Template, error) {
 	//nolint:wrapcheck
-	return template.New("template").Funcs(Functions).Parse(tmpl)
+	return template.New("template").Funcs(functions(opt)).Parse(tmpl)
 }
diff --git a/notification/notifytemplate/generic-error.html b/notification/notifytemplate/generic-error.html
new file mode 100644
index 00000000..7287c11a
--- /dev/null
+++ b/notification/notifytemplate/generic-error.html
@@ -0,0 +1,20 @@
+Subject: Kopia has encountered an error during {{ .EventArgs.Operation }} on {{.Hostname}}
+
+<!doctype html>
+<html>
+<head>
+</head>
+<body>
+
+<p><b>Operation:</b> {{ .EventArgs.OperationDetails }}</p>
+<p><b>Started:</b> {{ .EventArgs.StartTimestamp | formatTime }}</p>
+<p><b>Finished:</b> {{ .EventArgs.EndTimestamp | formatTime }} ({{ .EventArgs.Duration }})</p>
+
+<p><b>Message:</b> {{ .EventArgs.ErrorMessage }}</p>
+
+<pre>{{ .EventArgs.ErrorDetails }}</pre>
+
+<p>Generated at {{ .EventTime | formatTime }} by <a href="https://kopia.io">Kopia {{ .KopiaBuildVersion }}</a>.</p>
+
+</body>
+</html>
diff --git a/notification/notifytemplate/generic-error.txt b/notification/notifytemplate/generic-error.txt
new file mode 100644
index 00000000..eea8ace7
--- /dev/null
+++ b/notification/notifytemplate/generic-error.txt
@@ -0,0 +1,11 @@
+Subject: Kopia has encountered an error during {{ .EventArgs.Operation }} on {{.Hostname}}
+
+Operation: {{ .EventArgs.OperationDetails }}
+Started:   {{ .EventArgs.StartTimestamp | formatTime }}
+Finished:  {{ .EventArgs.EndTimestamp | formatTime }} ({{ .EventArgs.Duration }})
+
+{{ .EventArgs.ErrorDetails }}
+
+Generated at {{ .EventTime | formatTime }} by Kopia {{ .KopiaBuildVersion }}.
+
+https://kopia.io/
\ No newline at end of file
diff --git a/notification/notifytemplate/snapshot-report.html b/notification/notifytemplate/snapshot-report.html
new file mode 100644
index 00000000..272d4330
--- /dev/null
+++ b/notification/notifytemplate/snapshot-report.html
@@ -0,0 +1,103 @@
+Subject: Kopia created {{ len .EventArgs.Snapshots }} snapshot{{ if gt (len .EventArgs.Snapshots) 1 }}s{{end}} on {{.Hostname}}
+
+<!doctype html>
+<html>
+<head>
+<style type="text/css">
+    table {
+        width: 100%;
+        border-collapse: collapse;
+    }
+
+    thead tr {
+        background-color: #f2f2f2;
+    }
+
+    th, td {
+        border: 1px solid black;
+        padding: 2px;
+        text-align: left;
+    }
+
+    th {
+        background-color: #f2f2f2;
+    }
+
+    code {
+        background-color: #f2f2f2;
+        padding: 2px;
+    }
+
+    span.path {
+        font-family: monospace;
+        color: #344652;
+        font-weight: bold;
+    }
+
+    tr.snapshotstatus-fatal {
+        background-color: #fde9e4;
+    }
+
+    tr.snapshotstatus-error {
+        background-color: #fcffba;
+    }
+
+    tr.snapshotstatus-incomplete {
+        background-color: #8a8c7e;
+    }
+</style>
+</head>
+<body>
+<table border="1">
+<thead>
+    <tr>
+        <th>Source</th>
+        <th>Started</th>
+        <th>Duration</th>
+        <th>Total Size</th>
+        <th>Total Files</th>
+        <th>Total Directories</th>
+    </tr>
+</thead>
+{{ range .EventArgs.Snapshots | sortSnapshotManifestsByName}}
+<tr class="snapshotstatus-{{ .StatusCode }}">
+<td><span class="path">{{ .Source.Path }}</span></td>
+<td>{{ .StartTimestamp | formatTime }}</td>
+<td>{{ .Duration }}</td>
+<td>{{ .TotalSize | bytes }}</td>
+<td>{{ .TotalFiles }}</td>
+<td>{{ .TotalDirs }}</td>
+</tr>
+
+{{ if .Error }}
+<tr class="snapshotstatus-{{ .StatusCode }}">
+    <td colspan="6">
+        <b style="color:red">Error:</b> {{ .Error }}
+    </td>
+</tr>
+{{ end }}
+
+{{ if .RootEntry }}
+{{ if .RootEntry.DirSummary }}
+{{ if .RootEntry.DirSummary.FailedEntries }}
+<tr class="snapshotstatus-{{ .StatusCode }}">
+    <td colspan="6">
+        <b style="color:red">Failed Entries:</b>
+        <ul>
+        {{ range .RootEntry.DirSummary.FailedEntries }}
+        <li><span class="path">{{.EntryPath}}</span>: {{.Error}}</li>
+        {{ end }}
+        </ul>
+    </td>
+</tr>
+{{ end }}
+{{ end }}
+{{ end }}
+
+{{ end }}
+</table>
+
+<p>Generated at {{ .EventTime | formatTime }} by <a href="https://kopia.io">Kopia {{ .KopiaBuildVersion }}</a>.</p>
+
+</body>
+</html>
diff --git a/notification/notifytemplate/snapshot-report.txt b/notification/notifytemplate/snapshot-report.txt
new file mode 100644
index 00000000..212c85d0
--- /dev/null
+++ b/notification/notifytemplate/snapshot-report.txt
@@ -0,0 +1,16 @@
+Subject: Kopia created {{ len .EventArgs.Snapshots }} snapshot{{ if gt (len .EventArgs.Snapshots) 1 }}s{{end}} on {{.Hostname}}
+
+{{ range .EventArgs.Snapshots | sortSnapshotManifestsByName}}Path: {{ .Source.Path }}
+
+  Status: {{ .StatusCode }}
+  Start: {{ .StartTimestamp | formatTime  }} Duration: {{ .Duration }}
+  Size: {{ .TotalSize | bytes }}, {{ .TotalFiles }} files, {{ .TotalDirs }} directories.
+{{ if .Error }}  Error: {{ .Error }}
+{{ end }}{{ if .RootEntry }}{{ if .RootEntry.DirSummary }}{{ if .RootEntry.DirSummary.FailedEntries }}
+  Failed Entries:
+{{ range .RootEntry.DirSummary.FailedEntries }}
+  - {{.EntryPath}}: {{.Error}}{{ end }}{{ end }}{{ end }}
+{{ end }}
+{{ end }}Generated at {{ .EventTime | formatTime }} by Kopia {{ .KopiaBuildVersion }}.
+
+https://kopia.io/
\ No newline at end of file


Test patch:
diff --git a/internal/server/server_maintenance_test.go b/internal/server/server_maintenance_test.go
index cade0639..1d478549 100644
--- a/internal/server/server_maintenance_test.go
+++ b/internal/server/server_maintenance_test.go
@@ -12,6 +12,7 @@
 
 	"github.com/kopia/kopia/internal/clock"
 	"github.com/kopia/kopia/internal/repotesting"
+	"github.com/kopia/kopia/notification/notifytemplate"
 	"github.com/kopia/kopia/repo"
 	"github.com/kopia/kopia/repo/maintenance"
 )
@@ -47,6 +48,14 @@ func (s *testServer) refreshScheduler(reason string) {
 	s.refreshSchedulerCount.Add(1)
 }
 
+func (s *testServer) enableErrorNotifications() bool {
+	return false
+}
+
+func (s *testServer) notificationTemplateOptions() notifytemplate.Options {
+	return notifytemplate.DefaultOptions
+}
+
 func TestServerMaintenance(t *testing.T) {
 	ctx, env := repotesting.NewEnvironment(t, repotesting.FormatNotImportant)
 
diff --git a/internal/server/server_test.go b/internal/server/server_test.go
index 36182e99..4a6a8402 100644
--- a/internal/server/server_test.go
+++ b/internal/server/server_test.go
@@ -290,7 +290,7 @@ func remoteRepositoryNotificationTest(t *testing.T, ctx context.Context, rep rep
 	}))
 	require.NoError(t, rw.Flush(ctx))
 
-	notification.Send(ctx, rep, notifytemplate.TestNotification, nil, notification.SeverityError)
+	notification.Send(ctx, rep, notifytemplate.TestNotification, nil, notification.SeverityError, notifytemplate.DefaultOptions)
 	require.Equal(t, int32(1), numRequestsReceived.Load())
 
 	// another webhook which fails
@@ -307,7 +307,7 @@ func remoteRepositoryNotificationTest(t *testing.T, ctx context.Context, rep rep
 	}))
 
 	require.NoError(t, rw.Flush(ctx))
-	notification.Send(ctx, rep, notifytemplate.TestNotification, nil, notification.SeverityError)
+	notification.Send(ctx, rep, notifytemplate.TestNotification, nil, notification.SeverityError, notifytemplate.DefaultOptions)
 	require.Equal(t, int32(1), numRequestsReceived.Load())
 }
 
diff --git a/notification/notifytemplate/notifytemplate_test.go b/notification/notifytemplate/notifytemplate_test.go
new file mode 100644
index 00000000..182c98fb
--- /dev/null
+++ b/notification/notifytemplate/notifytemplate_test.go
@@ -0,0 +1,118 @@
+package notifytemplate_test
+
+import (
+	"bytes"
+	"os"
+	"path/filepath"
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/require"
+
+	"github.com/kopia/kopia/fs"
+	"github.com/kopia/kopia/notification"
+	"github.com/kopia/kopia/notification/notifydata"
+	"github.com/kopia/kopia/notification/notifytemplate"
+	"github.com/kopia/kopia/snapshot"
+)
+
+var defaultTestOptions = notifytemplate.Options{
+	Timezone: time.UTC,
+}
+
+var altTestOptions = notifytemplate.Options{
+	Timezone:   time.FixedZone("PST", -8*60*60),
+	TimeFormat: time.RFC1123,
+}
+
+func TestNotifyTemplate_generic_error(t *testing.T) {
+	args := notification.MakeTemplateArgs(&notifydata.ErrorInfo{
+		Operation:        "Some Operation",
+		OperationDetails: "Some Operation Details",
+		ErrorMessage:     "error message",
+		ErrorDetails:     "error details",
+		StartTime:        time.Date(2020, 1, 2, 3, 4, 5, 6, time.UTC),
+		EndTime:          time.Date(2020, 1, 2, 3, 4, 6, 7, time.UTC),
+	})
+
+	args.EventTime = time.Date(2020, 1, 2, 3, 4, 5, 6, time.UTC)
+	args.Hostname = "some-host"
+
+	verifyTemplate(t, "generic-error.txt", ".default", args, defaultTestOptions)
+	verifyTemplate(t, "generic-error.html", ".default", args, defaultTestOptions)
+	verifyTemplate(t, "generic-error.txt", ".alt", args, altTestOptions)
+	verifyTemplate(t, "generic-error.html", ".alt", args, altTestOptions)
+}
+
+func TestNotifyTemplate_snapshot_report(t *testing.T) {
+	args := notification.MakeTemplateArgs(&notifydata.MultiSnapshotStatus{
+		Snapshots: []*notifydata.ManifestWithError{
+			{
+				Manifest: snapshot.Manifest{
+					Source:    snapshot.SourceInfo{Host: "some-host", UserName: "some-user", Path: "/some/path"},
+					StartTime: fs.UTCTimestamp(time.Date(2020, 1, 2, 3, 4, 5, 6, time.UTC).UnixNano()),
+					EndTime:   fs.UTCTimestamp(time.Date(2020, 1, 2, 3, 4, 6, 7, time.UTC).UnixNano()),
+					RootEntry: &snapshot.DirEntry{
+						DirSummary: &fs.DirectorySummary{
+							TotalFileCount: 123,
+							TotalFileSize:  456,
+							TotalDirCount:  33,
+							FailedEntries: []*fs.EntryWithError{
+								{
+									EntryPath: "/some/path",
+									Error:     "some error",
+								},
+								{
+									EntryPath: "/some/path2",
+									Error:     "some error",
+								},
+							},
+						},
+					},
+				},
+			},
+			{
+				Error: "some top-level error",
+				Manifest: snapshot.Manifest{
+					Source: snapshot.SourceInfo{Host: "some-host", UserName: "some-user", Path: "/some/other/path"},
+				},
+			},
+		},
+	})
+
+	args.EventTime = time.Date(2020, 1, 2, 3, 4, 5, 6, time.UTC)
+	args.Hostname = "some-host"
+
+	verifyTemplate(t, "snapshot-report.txt", ".default", args, defaultTestOptions)
+	verifyTemplate(t, "snapshot-report.html", ".default", args, defaultTestOptions)
+	verifyTemplate(t, "snapshot-report.txt", ".alt", args, altTestOptions)
+	verifyTemplate(t, "snapshot-report.html", ".alt", args, altTestOptions)
+}
+
+func verifyTemplate(t *testing.T, embeddedTemplateName, expectedSuffix string, args interface{}, opt notifytemplate.Options) {
+	t.Helper()
+
+	tmpl, err := notifytemplate.GetEmbeddedTemplate(embeddedTemplateName)
+	require.NoError(t, err)
+
+	tt, err := notifytemplate.ParseTemplate(tmpl, opt)
+	require.NoError(t, err)
+
+	var buf bytes.Buffer
+
+	require.NoError(t, tt.Execute(&buf, args))
+
+	actualFileName := filepath.Join("testdata", embeddedTemplateName+expectedSuffix+".actual")
+	require.NoError(t, os.WriteFile(actualFileName, buf.Bytes(), 0o644))
+
+	expectedFileName := filepath.Join("testdata", embeddedTemplateName+expectedSuffix+".expected")
+
+	wantBytes, err := os.ReadFile(expectedFileName)
+	require.NoError(t, err)
+
+	want := string(wantBytes)
+
+	require.Equal(t, want, buf.String())
+
+	require.NoError(t, os.Remove(actualFileName))
+}
diff --git a/notification/notifytemplate/test-notification.html b/notification/notifytemplate/test-notification.html
index d8823c65..a9aaf6ea 100644
--- a/notification/notifytemplate/test-notification.html
+++ b/notification/notifytemplate/test-notification.html
@@ -1,4 +1,4 @@
-Subject: Test notification from Kopia at {{ .EventTime }}
+Subject: Test notification from Kopia at {{ .EventTime | formatTime }}
 
 <p>This is a test notification from Kopia.</p>
 
diff --git a/notification/notifytemplate/test-notification.txt b/notification/notifytemplate/test-notification.txt
index e364e269..6624b03e 100644
--- a/notification/notifytemplate/test-notification.txt
+++ b/notification/notifytemplate/test-notification.txt
@@ -1,4 +1,4 @@
-Subject: Test notification from Kopia at {{ .EventTime }}
+Subject: Test notification from Kopia at {{ .EventTime | formatTime }}
 
 This is a test notification from Kopia.
 
diff --git a/notification/notifytemplate/testdata/.gitignore b/notification/notifytemplate/testdata/.gitignore
new file mode 100644
index 00000000..e69de29b
diff --git a/notification/notifytemplate/testdata/generic-error.html.alt.expected b/notification/notifytemplate/testdata/generic-error.html.alt.expected
new file mode 100644
index 00000000..0d8f0013
--- /dev/null
+++ b/notification/notifytemplate/testdata/generic-error.html.alt.expected
@@ -0,0 +1,20 @@
+Subject: Kopia has encountered an error during Some Operation on some-host
+
+<!doctype html>
+<html>
+<head>
+</head>
+<body>
+
+<p><b>Operation:</b> Some Operation Details</p>
+<p><b>Started:</b> Wed, 01 Jan 2020 19:04:05 PST</p>
+<p><b>Finished:</b> Wed, 01 Jan 2020 19:04:06 PST (1s)</p>
+
+<p><b>Message:</b> error message</p>
+
+<pre>error details</pre>
+
+<p>Generated at Wed, 01 Jan 2020 19:04:05 PST by <a href="https://kopia.io">Kopia v0-unofficial</a>.</p>
+
+</body>
+</html>
diff --git a/notification/notifytemplate/testdata/generic-error.html.default.expected b/notification/notifytemplate/testdata/generic-error.html.default.expected
new file mode 100644
index 00000000..9faf8d9d
--- /dev/null
+++ b/notification/notifytemplate/testdata/generic-error.html.default.expected
@@ -0,0 +1,20 @@
+Subject: Kopia has encountered an error during Some Operation on some-host
+
+<!doctype html>
+<html>
+<head>
+</head>
+<body>
+
+<p><b>Operation:</b> Some Operation Details</p>
+<p><b>Started:</b> Thu, 02 Jan 2020 03:04:05 +0000</p>
+<p><b>Finished:</b> Thu, 02 Jan 2020 03:04:06 +0000 (1s)</p>
+
+<p><b>Message:</b> error message</p>
+
+<pre>error details</pre>
+
+<p>Generated at Thu, 02 Jan 2020 03:04:05 +0000 by <a href="https://kopia.io">Kopia v0-unofficial</a>.</p>
+
+</body>
+</html>
diff --git a/notification/notifytemplate/testdata/generic-error.txt.alt.expected b/notification/notifytemplate/testdata/generic-error.txt.alt.expected
new file mode 100644
index 00000000..170cdcdc
--- /dev/null
+++ b/notification/notifytemplate/testdata/generic-error.txt.alt.expected
@@ -0,0 +1,11 @@
+Subject: Kopia has encountered an error during Some Operation on some-host
+
+Operation: Some Operation Details
+Started:   Wed, 01 Jan 2020 19:04:05 PST
+Finished:  Wed, 01 Jan 2020 19:04:06 PST (1s)
+
+error details
+
+Generated at Wed, 01 Jan 2020 19:04:05 PST by Kopia v0-unofficial.
+
+https://kopia.io/
\ No newline at end of file
diff --git a/notification/notifytemplate/testdata/generic-error.txt.default.expected b/notification/notifytemplate/testdata/generic-error.txt.default.expected
new file mode 100644
index 00000000..4f03e75b
--- /dev/null
+++ b/notification/notifytemplate/testdata/generic-error.txt.default.expected
@@ -0,0 +1,11 @@
+Subject: Kopia has encountered an error during Some Operation on some-host
+
+Operation: Some Operation Details
+Started:   Thu, 02 Jan 2020 03:04:05 +0000
+Finished:  Thu, 02 Jan 2020 03:04:06 +0000 (1s)
+
+error details
+
+Generated at Thu, 02 Jan 2020 03:04:05 +0000 by Kopia v0-unofficial.
+
+https://kopia.io/
\ No newline at end of file
diff --git a/notification/notifytemplate/testdata/snapshot-report.html.alt.expected b/notification/notifytemplate/testdata/snapshot-report.html.alt.expected
new file mode 100644
index 00000000..11647e19
--- /dev/null
+++ b/notification/notifytemplate/testdata/snapshot-report.html.alt.expected
@@ -0,0 +1,119 @@
+Subject: Kopia created 2 snapshots on some-host
+
+<!doctype html>
+<html>
+<head>
+<style type="text/css">
+    table {
+        width: 100%;
+        border-collapse: collapse;
+    }
+
+    thead tr {
+        background-color: #f2f2f2;
+    }
+
+    th, td {
+        border: 1px solid black;
+        padding: 2px;
+        text-align: left;
+    }
+
+    th {
+        background-color: #f2f2f2;
+    }
+
+    code {
+        background-color: #f2f2f2;
+        padding: 2px;
+    }
+
+    span.path {
+        font-family: monospace;
+        color: #344652;
+        font-weight: bold;
+    }
+
+    tr.snapshotstatus-fatal {
+        background-color: #fde9e4;
+    }
+
+    tr.snapshotstatus-error {
+        background-color: #fcffba;
+    }
+
+    tr.snapshotstatus-incomplete {
+        background-color: #8a8c7e;
+    }
+</style>
+</head>
+<body>
+<table border="1">
+<thead>
+    <tr>
+        <th>Source</th>
+        <th>Started</th>
+        <th>Duration</th>
+        <th>Total Size</th>
+        <th>Total Files</th>
+        <th>Total Directories</th>
+    </tr>
+</thead>
+
+<tr class="snapshotstatus-fatal">
+<td><span class="path">/some/other/path</span></td>
+<td>Wed, 31 Dec 1969 16:00:00 PST</td>
+<td>0s</td>
+<td>0 B</td>
+<td>0</td>
+<td>0</td>
+</tr>
+
+
+<tr class="snapshotstatus-fatal">
+    <td colspan="6">
+        <b style="color:red">Error:</b> some top-level error
+    </td>
+</tr>
+
+
+
+
+
+<tr class="snapshotstatus-ok">
+<td><span class="path">/some/path</span></td>
+<td>Wed, 01 Jan 2020 19:04:05 PST</td>
+<td>1.000000001s</td>
+<td>456 B</td>
+<td>123</td>
+<td>33</td>
+</tr>
+
+
+
+
+
+
+<tr class="snapshotstatus-ok">
+    <td colspan="6">
+        <b style="color:red">Failed Entries:</b>
+        <ul>
+        
+        <li><span class="path">/some/path</span>: some error</li>
+        
+        <li><span class="path">/some/path2</span>: some error</li>
+        
+        </ul>
+    </td>
+</tr>
+
+
+
+
+
+</table>
+
+<p>Generated at Wed, 01 Jan 2020 19:04:05 PST by <a href="https://kopia.io">Kopia v0-unofficial</a>.</p>
+
+</body>
+</html>
diff --git a/notification/notifytemplate/testdata/snapshot-report.html.default.expected b/notification/notifytemplate/testdata/snapshot-report.html.default.expected
new file mode 100644
index 00000000..5cf75fb2
--- /dev/null
+++ b/notification/notifytemplate/testdata/snapshot-report.html.default.expected
@@ -0,0 +1,119 @@
+Subject: Kopia created 2 snapshots on some-host
+
+<!doctype html>
+<html>
+<head>
+<style type="text/css">
+    table {
+        width: 100%;
+        border-collapse: collapse;
+    }
+
+    thead tr {
+        background-color: #f2f2f2;
+    }
+
+    th, td {
+        border: 1px solid black;
+        padding: 2px;
+        text-align: left;
+    }
+
+    th {
+        background-color: #f2f2f2;
+    }
+
+    code {
+        background-color: #f2f2f2;
+        padding: 2px;
+    }
+
+    span.path {
+        font-family: monospace;
+        color: #344652;
+        font-weight: bold;
+    }
+
+    tr.snapshotstatus-fatal {
+        background-color: #fde9e4;
+    }
+
+    tr.snapshotstatus-error {
+        background-color: #fcffba;
+    }
+
+    tr.snapshotstatus-incomplete {
+        background-color: #8a8c7e;
+    }
+</style>
+</head>
+<body>
+<table border="1">
+<thead>
+    <tr>
+        <th>Source</th>
+        <th>Started</th>
+        <th>Duration</th>
+        <th>Total Size</th>
+        <th>Total Files</th>
+        <th>Total Directories</th>
+    </tr>
+</thead>
+
+<tr class="snapshotstatus-fatal">
+<td><span class="path">/some/other/path</span></td>
+<td>Thu, 01 Jan 1970 00:00:00 +0000</td>
+<td>0s</td>
+<td>0 B</td>
+<td>0</td>
+<td>0</td>
+</tr>
+
+
+<tr class="snapshotstatus-fatal">
+    <td colspan="6">
+        <b style="color:red">Error:</b> some top-level error
+    </td>
+</tr>
+
+
+
+
+
+<tr class="snapshotstatus-ok">
+<td><span class="path">/some/path</span></td>
+<td>Thu, 02 Jan 2020 03:04:05 +0000</td>
+<td>1.000000001s</td>
+<td>456 B</td>
+<td>123</td>
+<td>33</td>
+</tr>
+
+
+
+
+
+
+<tr class="snapshotstatus-ok">
+    <td colspan="6">
+        <b style="color:red">Failed Entries:</b>
+        <ul>
+        
+        <li><span class="path">/some/path</span>: some error</li>
+        
+        <li><span class="path">/some/path2</span>: some error</li>
+        
+        </ul>
+    </td>
+</tr>
+
+
+
+
+
+</table>
+
+<p>Generated at Thu, 02 Jan 2020 03:04:05 +0000 by <a href="https://kopia.io">Kopia v0-unofficial</a>.</p>
+
+</body>
+</html>
diff --git a/notification/notifytemplate/testdata/snapshot-report.txt.alt.expected b/notification/notifytemplate/testdata/snapshot-report.txt.alt.expected
new file mode 100644
index 00000000..46de1240
--- /dev/null
+++ b/notification/notifytemplate/testdata/snapshot-report.txt.alt.expected
@@ -0,0 +1,23 @@
+Subject: Kopia created 2 snapshots on some-host
+
+Path: /some/other/path
+
+  Status: fatal
+  Start: Wed, 31 Dec 1969 16:00:00 PST Duration: 0s
+  Size: 0 B, 0 files, 0 directories.
+  Error: some top-level error
+
+Path: /some/path
+
+  Status: ok
+  Start: Wed, 01 Jan 2020 19:04:05 PST Duration: 1.000000001s
+  Size: 456 B, 123 files, 33 directories.
+
+  Failed Entries:
+
+  - /some/path: some error
+  - /some/path2: some error
+
+Generated at Wed, 01 Jan 2020 19:04:05 PST by Kopia v0-unofficial.
+
+https://kopia.io/
\ No newline at end of file
diff --git a/notification/notifytemplate/testdata/snapshot-report.txt.default.expected b/notification/notifytemplate/testdata/snapshot-report.txt.default.expected
new file mode 100644
index 00000000..b11a4e06
--- /dev/null
+++ b/notification/notifytemplate/testdata/snapshot-report.txt.default.expected
@@ -0,0 +1,23 @@
+Subject: Kopia created 2 snapshots on some-host
+
+Path: /some/other/path
+
+  Status: fatal
+  Start: Thu, 01 Jan 1970 00:00:00 +0000 Duration: 0s
+  Size: 0 B, 0 files, 0 directories.
+  Error: some top-level error
+
+Path: /some/path
+
+  Status: ok
+  Start: Thu, 02 Jan 2020 03:04:05 +0000 Duration: 1.000000001s
+  Size: 456 B, 123 files, 33 directories.
+
+  Failed Entries:
+
+  - /some/path: some error
+  - /some/path2: some error
+
+Generated at Thu, 02 Jan 2020 03:04:05 +0000 by Kopia v0-unofficial.
+
+https://kopia.io/
\ No newline at end of file