mirror of
https://github.com/element-hq/synapse.git
synced 2026-06-04 03:51:28 +00:00
Merge branch 'develop' into anoa/msc4429
This commit is contained in:
@@ -67,7 +67,7 @@ jobs:
|
||||
|
||||
- name: Get team registry token
|
||||
id: import-secrets
|
||||
uses: hashicorp/vault-action@4c06c5ccf5c0761b6029f56cfb1dcf5565918a3b # v3.4.0
|
||||
uses: hashicorp/vault-action@892a26828f195e65540a40b4768ae4571f51ebfc # v4.0.0
|
||||
with:
|
||||
url: https://vault.infra.ci.i.element.dev
|
||||
role: ${{ steps.vault-jwt-role.outputs.role_name }}
|
||||
@@ -164,7 +164,7 @@ jobs:
|
||||
|
||||
- name: Get team registry token
|
||||
id: import-secrets
|
||||
uses: hashicorp/vault-action@4c06c5ccf5c0761b6029f56cfb1dcf5565918a3b # v3.4.0
|
||||
uses: hashicorp/vault-action@892a26828f195e65540a40b4768ae4571f51ebfc # v4.0.0
|
||||
with:
|
||||
url: https://vault.infra.ci.i.element.dev
|
||||
role: ${{ steps.vault-jwt-role.outputs.role_name }}
|
||||
@@ -186,7 +186,7 @@ jobs:
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||
|
||||
- name: Install Cosign
|
||||
uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v4.1.1
|
||||
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
|
||||
|
||||
- name: Calculate docker image tag
|
||||
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
|
||||
|
||||
+37
@@ -1,3 +1,40 @@
|
||||
# Synapse 1.154.0rc1 (2026-05-27)
|
||||
|
||||
## Features
|
||||
|
||||
- Add support for [MSC4452: Preview URL capabilities API](https://github.com/matrix-org/matrix-spec-proposals/pull/4452) which exposes a `io.element.msc4452.preview_url` capability.
|
||||
If `experimental_features.msc4452_enabled` is `true`, the `/_matrix/(client/v1/media|media/v3)/preview_url` endpoint
|
||||
now responds with a 403 status code when the capability is disabled. ([\#19715](https://github.com/element-hq/synapse/issues/19715))
|
||||
|
||||
## Bugfixes
|
||||
|
||||
- Fix a bug in [MSC4186: Simplified Sliding Sync](https://github.com/matrix-org/matrix-spec-proposals/pull/4186) that could prevent user avatars from showing if the room had an empty name. ([\#19468](https://github.com/element-hq/synapse/issues/19468), [\#19791](https://github.com/element-hq/synapse/issues/19791))
|
||||
- Fix access token cache not being invalidated for sessions using refresh tokens. Contributed by @FrenchGithubUser @ Famedly. ([\#19483](https://github.com/element-hq/synapse/issues/19483))
|
||||
- Fix bug where Synapse would return 400 (`M_BAD_JSON`) when sending a message with a `mentions` field and Synapse module `check_event_allowed` callback registered (frozen event). Contributed by @gaetan-sbt. ([\#19634](https://github.com/element-hq/synapse/issues/19634))
|
||||
- Fix long-standing but niche bug with `/sync` where it could attempt to fetch data with flawed invalid future tokens. ([\#19644](https://github.com/element-hq/synapse/issues/19644))
|
||||
- Fix `/sync` failing when [MSC4354 Sticky Events](https://github.com/matrix-org/matrix-spec-proposals/pull/4354) are enabled and the sync request filters out Ephemeral Data Units (EDUs). ([\#19787](https://github.com/element-hq/synapse/issues/19787))
|
||||
- Fix packaging for Fedora and EPEL caused by unnecessary bumping `attrs` minimum version requirement in `pyproject.toml` file. Contributed by Oleg Girko. ([\#19789](https://github.com/element-hq/synapse/issues/19789))
|
||||
- Fix merging signatures when a policy server is running under the same server name as Synapse. The bug was re-introduced in v1.153.0rc1 after being fixed earlier in v1.151.0rc1. Contributed by @tulir @ Beeper. ([\#19797](https://github.com/element-hq/synapse/issues/19797))
|
||||
|
||||
## Improved Documentation
|
||||
|
||||
- Added details about how Synapse syncs the picture claim when `update_profile_information` setting is true. ([\#19508](https://github.com/element-hq/synapse/issues/19508))
|
||||
|
||||
## Internal Changes
|
||||
|
||||
- Port `Event.content` field to Rust. ([\#19725](https://github.com/element-hq/synapse/issues/19725))
|
||||
- Prefer close backfill points (absolute distance). ([\#19748](https://github.com/element-hq/synapse/issues/19748))
|
||||
- Replace unique `quarantined_media` waiting patterns with standard `wait_for_stream_token(...)`. ([\#19764](https://github.com/element-hq/synapse/issues/19764))
|
||||
- Improve Synapse logging around when someone encounters `We can't get valid state history.` so you can correlate everything by `event_id`. ([\#19765](https://github.com/element-hq/synapse/issues/19765))
|
||||
- Tidy up Rust `RoomVersion` structs. ([\#19766](https://github.com/element-hq/synapse/issues/19766))
|
||||
- Update `WorkerLock` tests to better stress the `WORKER_LOCK_MAX_RETRY_INTERVAL`. ([\#19772](https://github.com/element-hq/synapse/issues/19772))
|
||||
- Refactor [MSC4242: State DAG](https://github.com/matrix-org/matrix-spec-proposals/pull/4242) checks behind a single `TypeIs` helper to avoid scattered `isinstance` casts. ([\#19774](https://github.com/element-hq/synapse/issues/19774))
|
||||
- Use `StrCollection` for `prev_state_events`. ([\#19777](https://github.com/element-hq/synapse/issues/19777))
|
||||
- Fix up the construction of events in tests, ahead of the Rust event port. ([\#19781](https://github.com/element-hq/synapse/issues/19781))
|
||||
|
||||
|
||||
|
||||
|
||||
# Synapse 1.153.0 (2026-05-19)
|
||||
|
||||
No significant changes since 1.153.0rc3.
|
||||
|
||||
Generated
+2
-2
@@ -952,9 +952,9 @@ checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.9.2"
|
||||
version = "0.9.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
|
||||
checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea"
|
||||
dependencies = [
|
||||
"rand_chacha",
|
||||
"rand_core",
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
Fix a bug in [MSC4186: Simplified Sliding Sync](https://github.com/matrix-org/matrix-spec-proposals/pull/4186) that could prevent user avatars from showing if the room had an empty name.
|
||||
@@ -1 +0,0 @@
|
||||
Fix access token cache not being invalidated for sessions using refresh tokens. Contributed by @FrenchGithubUser @ Famedly.
|
||||
@@ -1 +0,0 @@
|
||||
Added details about how Synapse syncs the picture claim when `update_profile_information` setting is true.
|
||||
@@ -1 +0,0 @@
|
||||
Fix bug where Synapse would return 400 (`M_BAD_JSON`) when sending a message with `mentions` field and Synapse module `check_event_allowed` callback registered (frozen event). Contributed by @gaetan-sbt.
|
||||
@@ -1 +0,0 @@
|
||||
Fix long-standing but niche bug with sync where it could attempt to fetch data with flawed invalid future tokens.
|
||||
@@ -1,3 +0,0 @@
|
||||
Add support for "MSC4452 Preview URL capabilities API" which exposes a `io.element.msc4452.preview_url` capability.
|
||||
If `experimental_features.msc4452_enabled` is `true`, the `/_matrix/(client/v1/media|media/v3)/preview_url` endpoint
|
||||
now responds with a 403 status code when the capability is disabled.
|
||||
@@ -1 +0,0 @@
|
||||
Port `Event.content` field to Rust.
|
||||
@@ -0,0 +1 @@
|
||||
Work around bug that sometimes breaks joining restricted rooms that require a remote join. Contributed by @tulir @ Beeper.
|
||||
@@ -1 +0,0 @@
|
||||
Prefer close backfill points (absolute distance).
|
||||
@@ -1 +0,0 @@
|
||||
Replace unique `quarantined_media` waiting patterns with standard `wait_for_stream_token(...)`.
|
||||
@@ -1 +0,0 @@
|
||||
Improve Synapse logging around when someone encounters `We can't get valid state history.` so you can correlate everything by `event_id`.
|
||||
@@ -1 +0,0 @@
|
||||
Tidy up Rust `RoomVersion` structs.
|
||||
@@ -1 +0,0 @@
|
||||
Update `WorkerLock` tests to better stress the `WORKER_LOCK_MAX_RETRY_INTERVAL`.
|
||||
@@ -1 +0,0 @@
|
||||
Refactor MSC4242 state DAG checks behind a single `TypeIs` helper to avoid scattered `isinstance` casts.
|
||||
@@ -0,0 +1 @@
|
||||
Add `GcpJsonFormatter` logging formatter for use with Google Cloud Logging and GKE deployments.
|
||||
@@ -1 +0,0 @@
|
||||
Use `StrCollection` for `prev_state_events`.
|
||||
@@ -1 +0,0 @@
|
||||
Fix up event-construction in tests ahead of the Rust event port.
|
||||
@@ -1 +0,0 @@
|
||||
Revert 'Have [MSC4186: Simplified Sliding Sync](https://github.com/matrix-org/matrix-spec-proposals/pull/4186) return a new response immediately if a room subscription has changed and produced a new response. ([\#19714](https://github.com/element-hq/synapse/issues/19714))' due to performance problems.
|
||||
@@ -1 +0,0 @@
|
||||
Fix `/sync` failing when [MSC4354 Sticky Events](https://github.com/matrix-org/matrix-spec-proposals/pull/4354) are enabled and the sync request filters out Ephemeral Data Units (EDUs).
|
||||
@@ -1 +0,0 @@
|
||||
Fix packaging for Fedora and EPEL caused by unnecessary bumping `attrs` minimum version requirement in `pyproject.toml` file. Contributed by Oleg Girko.
|
||||
@@ -1 +0,0 @@
|
||||
Fix a bug in [MSC4186: Simplified Sliding Sync](https://github.com/matrix-org/matrix-spec-proposals/pull/4186) that could prevent user avatars from showing if the room had an empty name.
|
||||
@@ -0,0 +1 @@
|
||||
Add more logging to the to-device message replication stream.
|
||||
+12
-12
@@ -1,8 +1,6 @@
|
||||
module github.com/element-hq/synapse
|
||||
|
||||
go 1.24.1
|
||||
|
||||
toolchain go1.24.4
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/matrix-org/complement v0.0.0-20251120181401-44111a2a8a9d
|
||||
@@ -16,7 +14,7 @@ require (
|
||||
github.com/hashicorp/go-set/v3 v3.0.0 // indirect
|
||||
github.com/moby/sys/atomicwriter v0.1.0 // indirect
|
||||
github.com/oleiade/lane/v2 v2.0.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
|
||||
gotest.tools/v3 v3.4.0 // indirect
|
||||
)
|
||||
@@ -47,13 +45,15 @@ require (
|
||||
github.com/tidwall/sjson v1.2.5 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
|
||||
go.opentelemetry.io/otel v1.41.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.41.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.41.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.7.0 // indirect
|
||||
golang.org/x/crypto v0.45.0 // indirect
|
||||
golang.org/x/sys v0.38.0 // indirect
|
||||
go.opentelemetry.io/otel v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.43.0 // indirect
|
||||
golang.org/x/crypto v0.49.0 // indirect
|
||||
golang.org/x/net v0.52.0 // indirect
|
||||
golang.org/x/sync v0.20.0 // indirect
|
||||
golang.org/x/sys v0.42.0 // indirect
|
||||
golang.org/x/time v0.11.0 // indirect
|
||||
google.golang.org/protobuf v1.36.6 // indirect
|
||||
golang.org/x/tools v0.42.0 // indirect
|
||||
)
|
||||
|
||||
+42
-42
@@ -2,8 +2,8 @@ github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEK
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
||||
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
|
||||
@@ -38,8 +38,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c=
|
||||
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=
|
||||
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI=
|
||||
github.com/hashicorp/go-set/v3 v3.0.0 h1:CaJBQvQCOWoftrBcDt7Nwgo0kdpmrKxar/x2o6pV9JA=
|
||||
@@ -101,55 +101,55 @@ go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
|
||||
go.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c=
|
||||
go.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0 h1:dNzwXjZKpMpE2JhmO+9HsPl42NIXFIFSUSSs0fiqra0=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0/go.mod h1:90PoxvaEB5n6AOdZvi+yWJQoE95U8Dhhw2bSyRqnTD0=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0 h1:BEj3SPM81McUZHYjRS5pEgNgnmzGJ5tRpU5krWnV8Bs=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0/go.mod h1:9cKLGBDzI/F3NoHLQGm4ZrYdIHsvGt6ej6hUowxY0J4=
|
||||
go.opentelemetry.io/otel/metric v1.41.0 h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCno7JlTQ=
|
||||
go.opentelemetry.io/otel/metric v1.41.0/go.mod h1:xPvCwd9pU0VN8tPZYzDZV/BMj9CM9vs00GuBjeKhJps=
|
||||
go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs=
|
||||
go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4=
|
||||
go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0=
|
||||
go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis=
|
||||
go.opentelemetry.io/proto/otlp v1.7.0 h1:jX1VolD6nHuFzOYso2E73H85i92Mv8JQYk0K9vz09os=
|
||||
go.opentelemetry.io/proto/otlp v1.7.0/go.mod h1:fSKjH6YJ7HDlwzltzyMj036AJ3ejJLCgCSHGj4efDDo=
|
||||
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
|
||||
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 h1:3iZJKlCZufyRzPzlQhUIWVmfltrXuGyfjREgGP3UUjc=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0/go.mod h1:/G+nUPfhq2e+qiXMGxMwumDrP5jtzU+mWN7/sjT2rak=
|
||||
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
|
||||
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
|
||||
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
|
||||
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
|
||||
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
|
||||
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
|
||||
go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g=
|
||||
go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
|
||||
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
|
||||
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
|
||||
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
|
||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
|
||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
|
||||
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
|
||||
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
|
||||
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
|
||||
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
||||
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
||||
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
||||
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
|
||||
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
@@ -157,20 +157,20 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
|
||||
golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
|
||||
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
|
||||
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
|
||||
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a h1:SGktgSolFCo75dnHJF2yMvnns6jCmHFJ0vE4Vn2JKvQ=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a/go.mod h1:a77HrdMjoeKbnd2jmgcWdaS++ZLZAEq3orIOAEIKiVw=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a h1:v2PbRU4K3llS09c7zodFpNePeamkAwG3mPrAery9VeE=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
|
||||
google.golang.org/grpc v1.72.2 h1:TdbGzwb82ty4OusHWepvFWGLgIbNo1/SUynEN0ssqv8=
|
||||
google.golang.org/grpc v1.72.2/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
|
||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 h1:m8qni9SQFH0tJc1X0vmnpw/0t+AImlSvp30sEupozUg=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM=
|
||||
google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/h2non/gock.v1 v1.1.2 h1:jBbHXgGBK/AoPVfJh5x4r/WxIrElvbLel8TCZkkZJoY=
|
||||
gopkg.in/h2non/gock.v1 v1.1.2/go.mod h1:n7UGz/ckNChHiK05rDoiC4MYSunEC/lyaUm2WWaDva0=
|
||||
|
||||
Vendored
+6
@@ -1,3 +1,9 @@
|
||||
matrix-synapse-py3 (1.154.0~rc1) stable; urgency=medium
|
||||
|
||||
* New Synapse release 1.154.0rc1.
|
||||
|
||||
-- Synapse Packaging team <packages@matrix.org> Wed, 27 May 2026 12:23:54 +0100
|
||||
|
||||
matrix-synapse-py3 (1.153.0) stable; urgency=medium
|
||||
|
||||
* New Synapse release 1.153.0.
|
||||
|
||||
@@ -78,3 +78,44 @@ loggers:
|
||||
The above logging config will set Synapse as 'INFO' logging level by default,
|
||||
with the SQL layer at 'WARNING', and will log JSON formatted messages to a
|
||||
remote endpoint at 10.1.2.3:9999.
|
||||
|
||||
## Google Cloud Logging (GKE)
|
||||
|
||||
When running Synapse on GKE, use `synapse.logging.GcpJsonFormatter`. It outputs
|
||||
JSON to stdout with a `severity` field that Google Cloud Logging maps to the
|
||||
correct per-entry severity. Without this, GKE assigns `ERROR` to everything
|
||||
written to stderr regardless of the actual Python log level.
|
||||
|
||||
Example output:
|
||||
|
||||
```json
|
||||
{"severity":"INFO","message":"Processed request: 3.481sec 200 GET /sync","logger":"synapse.access.http.8008","time":"2026-05-12T13:40:37.829Z"}
|
||||
```
|
||||
|
||||
Configuration:
|
||||
|
||||
```yaml
|
||||
version: 1
|
||||
disable_existing_loggers: false
|
||||
|
||||
formatters:
|
||||
gcp_json:
|
||||
class: synapse.logging.GcpJsonFormatter
|
||||
|
||||
handlers:
|
||||
console:
|
||||
class: logging.StreamHandler
|
||||
formatter: gcp_json
|
||||
stream: ext://sys.stdout
|
||||
|
||||
loggers:
|
||||
synapse.storage.SQL:
|
||||
level: WARNING
|
||||
twisted:
|
||||
handlers: [console]
|
||||
propagate: false
|
||||
|
||||
root:
|
||||
level: INFO
|
||||
handlers: [console]
|
||||
```
|
||||
|
||||
Generated
+4
-4
@@ -752,18 +752,18 @@ test = ["coverage[toml]", "pretend", "pytest", "pytest-cov"]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.11"
|
||||
version = "3.15"
|
||||
description = "Internationalized Domain Names in Applications (IDNA)"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main", "dev"]
|
||||
files = [
|
||||
{file = "idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea"},
|
||||
{file = "idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902"},
|
||||
{file = "idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8"},
|
||||
{file = "idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"]
|
||||
all = ["mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"]
|
||||
|
||||
[[package]]
|
||||
name = "ijson"
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "matrix-synapse"
|
||||
version = "1.153.0"
|
||||
version = "1.154.0rc1"
|
||||
description = "Homeserver for the Matrix decentralised comms protocol"
|
||||
readme = "README.rst"
|
||||
authors = [
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
$schema: https://element-hq.github.io/synapse/latest/schema/v1/meta.schema.json
|
||||
$id: https://element-hq.github.io/synapse/schema/synapse/v1.153/synapse-config.schema.json
|
||||
$id: https://element-hq.github.io/synapse/schema/synapse/v1.154/synapse-config.schema.json
|
||||
type: object
|
||||
properties:
|
||||
modules:
|
||||
|
||||
@@ -1355,7 +1355,15 @@ class RoomMemberHandler(metaclass=abc.ABCMeta):
|
||||
current_state = {
|
||||
state_key: event_map[event_id]
|
||||
for state_key, event_id in state_before_join.items()
|
||||
# TODO figure out why events present in state_before_join are sometimes not found in event_map
|
||||
# See https://github.com/element-hq/synapse/issues/19465
|
||||
if event_id in event_map
|
||||
}
|
||||
if len(current_state) < len(state_before_join):
|
||||
logger.warning(
|
||||
"Some events from state_before_join were not found in event_map: %s",
|
||||
set(state_before_join.values()) - set(event_map.keys()),
|
||||
)
|
||||
servers_that_can_issue_invite = get_servers_from_users(
|
||||
get_users_which_can_issue_invite(current_state)
|
||||
)
|
||||
|
||||
@@ -251,8 +251,8 @@ class RoomPolicyHandler:
|
||||
# Note: if the policy server and event sender are the same server, the sender
|
||||
# might not have added policy server signatures to the event for whatever reason.
|
||||
# When this happens, we don't want to obliterate the event's existing signatures
|
||||
# because the event will fail authorization. This is why we add defaults rather
|
||||
# than simply `update` the signatures on the event.
|
||||
# because the event will fail authorization. This is why we add items individually
|
||||
# rather than simply `update` the signatures on the event.
|
||||
#
|
||||
# This situation can happen if the homeserver and policy server parts are
|
||||
# logically the same server, but run by different software. For example, Synapse
|
||||
@@ -261,7 +261,9 @@ class RoomPolicyHandler:
|
||||
# servers need to manually fetch signatures for. This is the code that allows
|
||||
# those events to continue working (because they're legally sent, even if missing
|
||||
# the policy server signature).
|
||||
event.signatures.update(signature)
|
||||
signatures = signature.get(policy_server.server_name, {})
|
||||
for key_id, sig in signatures.items():
|
||||
event.signatures.add_signature(policy_server.server_name, key_id, sig)
|
||||
except HttpResponseException as ex:
|
||||
# re-wrap HTTP errors as `SynapseError` so they can be proxied to clients directly
|
||||
raise ex.to_synapse_error() from ex
|
||||
|
||||
@@ -22,10 +22,14 @@
|
||||
import logging
|
||||
|
||||
from synapse.logging._remote import RemoteHandler
|
||||
from synapse.logging._terse_json import JsonFormatter, TerseJsonFormatter
|
||||
from synapse.logging._terse_json import (
|
||||
GcpJsonFormatter,
|
||||
JsonFormatter,
|
||||
TerseJsonFormatter,
|
||||
)
|
||||
|
||||
# These are imported to allow for nicer logging configuration files.
|
||||
__all__ = ["RemoteHandler", "JsonFormatter", "TerseJsonFormatter"]
|
||||
__all__ = ["RemoteHandler", "JsonFormatter", "TerseJsonFormatter", "GcpJsonFormatter"]
|
||||
|
||||
# Debug logger for https://github.com/matrix-org/synapse/issues/9533 etc
|
||||
issue9533_logger = logging.getLogger("synapse.9533_debug")
|
||||
|
||||
@@ -25,6 +25,7 @@ Log formatters that output terse JSON.
|
||||
|
||||
import json
|
||||
import logging
|
||||
from datetime import datetime, timezone
|
||||
|
||||
_encoder = json.JSONEncoder(ensure_ascii=False, separators=(",", ":"))
|
||||
|
||||
@@ -93,3 +94,31 @@ class TerseJsonFormatter(JsonFormatter):
|
||||
}
|
||||
|
||||
return self._format(record, event)
|
||||
|
||||
|
||||
class GcpJsonFormatter(logging.Formatter):
|
||||
"""JSON formatter compatible with Google Cloud Logging structured logging.
|
||||
|
||||
Outputs `severity` (not `level`) so GCL correctly maps each log record to
|
||||
the right severity instead of inheriting ERROR from stderr.
|
||||
"""
|
||||
|
||||
def format(self, record: logging.LogRecord) -> str:
|
||||
msg = record.getMessage()
|
||||
if record.exc_info:
|
||||
if not record.exc_text:
|
||||
record.exc_text = self.formatException(record.exc_info)
|
||||
if record.exc_text:
|
||||
msg = f"{msg}\n{record.exc_text}"
|
||||
|
||||
event = {
|
||||
"severity": record.levelname,
|
||||
"message": msg,
|
||||
"logger": record.name,
|
||||
"time": datetime.fromtimestamp(record.created, tz=timezone.utc).strftime(
|
||||
"%Y-%m-%dT%H:%M:%S.%f"
|
||||
)[:-3]
|
||||
+ "Z",
|
||||
}
|
||||
|
||||
return _encoder.encode(event)
|
||||
|
||||
@@ -182,13 +182,10 @@ class ReplicationStreamer:
|
||||
self.command_handler.will_announce_positions()
|
||||
|
||||
for stream in all_streams:
|
||||
self.command_handler.send_command(
|
||||
PositionCommand(
|
||||
stream.NAME,
|
||||
self._instance_name,
|
||||
stream.last_token,
|
||||
stream.last_token,
|
||||
)
|
||||
self._send_position_command(
|
||||
stream_name=stream.NAME,
|
||||
prev_token=stream.last_token,
|
||||
new_token=stream.last_token,
|
||||
)
|
||||
|
||||
for stream in all_streams:
|
||||
@@ -205,9 +202,9 @@ class ReplicationStreamer:
|
||||
last_token = stream.last_token
|
||||
|
||||
logger.debug(
|
||||
"Getting stream: %s: %s -> %s",
|
||||
"Getting stream updates for %s: %s -> %s",
|
||||
stream.NAME,
|
||||
stream.last_token,
|
||||
last_token,
|
||||
stream.current_token(self._instance_name),
|
||||
)
|
||||
try:
|
||||
@@ -217,26 +214,7 @@ class ReplicationStreamer:
|
||||
logger.info("Failed to handle stream %s", stream.NAME)
|
||||
raise
|
||||
|
||||
logger.debug(
|
||||
"Sending %d updates",
|
||||
len(updates),
|
||||
)
|
||||
|
||||
if updates:
|
||||
logger.info(
|
||||
"Streaming: %s -> %s (limited: %s, updates: %s, max token: %s)",
|
||||
stream.NAME,
|
||||
updates[-1][0],
|
||||
limited,
|
||||
len(updates),
|
||||
current_token,
|
||||
)
|
||||
stream_updates_counter.labels(
|
||||
stream_name=stream.NAME,
|
||||
**{SERVER_NAME_LABEL: self.server_name},
|
||||
).inc(len(updates))
|
||||
|
||||
else:
|
||||
if not updates:
|
||||
# The token has advanced but there is no data to
|
||||
# send, so we send a `POSITION` to inform other
|
||||
# workers of the updated position.
|
||||
@@ -266,21 +244,28 @@ class ReplicationStreamer:
|
||||
# POSITION with last token of X+1, which will
|
||||
# cause them to check if there were any missing
|
||||
# updates between X and X+1.
|
||||
logger.info(
|
||||
"Sending position: %s -> %s",
|
||||
stream.NAME,
|
||||
current_token,
|
||||
)
|
||||
self.command_handler.send_command(
|
||||
PositionCommand(
|
||||
stream.NAME,
|
||||
self._instance_name,
|
||||
last_token,
|
||||
current_token,
|
||||
)
|
||||
self._send_position_command(
|
||||
stream_name=stream.NAME,
|
||||
prev_token=last_token,
|
||||
new_token=current_token,
|
||||
)
|
||||
continue
|
||||
|
||||
logger.info(
|
||||
"Sending update for %s: %s -> %s (limited: %s, updates: %s, max token: %s)",
|
||||
stream.NAME,
|
||||
last_token,
|
||||
updates[-1][0],
|
||||
limited,
|
||||
len(updates),
|
||||
current_token,
|
||||
)
|
||||
|
||||
stream_updates_counter.labels(
|
||||
stream_name=stream.NAME,
|
||||
**{SERVER_NAME_LABEL: self.server_name},
|
||||
).inc(len(updates))
|
||||
|
||||
# Some streams return multiple rows with the same stream IDs,
|
||||
# we need to make sure they get sent out in batches. We do
|
||||
# this by setting the current token to all but the last of
|
||||
@@ -300,18 +285,10 @@ class ReplicationStreamer:
|
||||
# token, in which case we want to send out a `POSITION`
|
||||
# to tell other workers the actual current position.
|
||||
if updates[-1][0] < current_token:
|
||||
logger.info(
|
||||
"Sending position: %s -> %s",
|
||||
stream.NAME,
|
||||
current_token,
|
||||
)
|
||||
self.command_handler.send_command(
|
||||
PositionCommand(
|
||||
stream.NAME,
|
||||
self._instance_name,
|
||||
updates[-1][0],
|
||||
current_token,
|
||||
)
|
||||
self._send_position_command(
|
||||
stream_name=stream.NAME,
|
||||
prev_token=updates[-1][0],
|
||||
new_token=current_token,
|
||||
)
|
||||
|
||||
logger.debug("No more pending updates, breaking poke loop")
|
||||
@@ -319,6 +296,25 @@ class ReplicationStreamer:
|
||||
self.pending_updates = False
|
||||
self.is_looping = False
|
||||
|
||||
def _send_position_command(
|
||||
self, *, stream_name: str, prev_token: int, new_token: int
|
||||
) -> None:
|
||||
"""Send a POSITION command over replication"""
|
||||
logger.info(
|
||||
"Sending position for %s: %s -> %s",
|
||||
stream_name,
|
||||
prev_token,
|
||||
new_token,
|
||||
)
|
||||
self.command_handler.send_command(
|
||||
PositionCommand(
|
||||
stream_name,
|
||||
self._instance_name,
|
||||
prev_token,
|
||||
new_token,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _batch_updates(
|
||||
updates: list[tuple[Token, StreamRow]],
|
||||
|
||||
@@ -897,10 +897,20 @@ class DeviceInboxWorkerStore(SQLBaseStore):
|
||||
) -> None:
|
||||
assert self._can_write_to_device
|
||||
|
||||
local_by_user_then_device = {}
|
||||
# A map from user id, to device id, to a pair of (serialized message, msgid).
|
||||
local_by_user_then_device: dict[str, dict[str, tuple[str, str]]] = {}
|
||||
|
||||
for user_id, messages_by_device in messages_by_user_then_device.items():
|
||||
messages_json_for_user = {}
|
||||
# Mesages to send to this specific user. A map
|
||||
# from device id, to a pair of (serialized message, msgid).
|
||||
messages_json_for_user: dict[str, tuple[str, str]] = {}
|
||||
|
||||
devices = list(messages_by_device.keys())
|
||||
if not devices:
|
||||
# No to-device messages for this user. (For example, someone has
|
||||
# hit `/sendToDevice` with an empty {device: message} dict.)
|
||||
continue
|
||||
|
||||
if len(devices) == 1 and devices[0] == "*":
|
||||
# Handle wildcard device_ids.
|
||||
# We exclude hidden devices (such as cross-signing keys) here as they are
|
||||
@@ -912,15 +922,28 @@ class DeviceInboxWorkerStore(SQLBaseStore):
|
||||
retcol="device_id",
|
||||
)
|
||||
|
||||
message_json = json_encoder.encode(messages_by_device["*"])
|
||||
# Don't bother to serialize if there are no devices for this user
|
||||
if not devices:
|
||||
if issue9533_logger.isEnabledFor(logging.DEBUG):
|
||||
msgid = _get_msgid_for_message(messages_by_device["*"])
|
||||
issue9533_logger.debug(
|
||||
"Dropping wildcard to-device message for user %s with no devices (msgid %s)",
|
||||
user_id,
|
||||
msgid,
|
||||
)
|
||||
continue
|
||||
|
||||
message_json, msgid = _serialize_to_device_message(
|
||||
user_id=user_id, device_id="*", msg=messages_by_device["*"]
|
||||
)
|
||||
for device_id in devices:
|
||||
# Add the message for all devices for this user on this
|
||||
# server.
|
||||
messages_json_for_user[device_id] = message_json
|
||||
messages_json_for_user[device_id] = (message_json, msgid)
|
||||
else:
|
||||
if not devices:
|
||||
continue
|
||||
|
||||
# Query the database to determine which of the target devices actually
|
||||
# exist.
|
||||
#
|
||||
# We exclude hidden devices (such as cross-signing keys) here as they are
|
||||
# not expected to receive to-device messages.
|
||||
rows = cast(
|
||||
@@ -938,19 +961,25 @@ class DeviceInboxWorkerStore(SQLBaseStore):
|
||||
for (device_id,) in rows:
|
||||
# Only insert into the local inbox if the device exists on
|
||||
# this server
|
||||
with start_active_span("serialise_to_device_message"):
|
||||
msg = messages_by_device[device_id]
|
||||
set_tag(SynapseTags.TO_DEVICE_TYPE, msg["type"])
|
||||
set_tag(SynapseTags.TO_DEVICE_SENDER, msg["sender"])
|
||||
set_tag(SynapseTags.TO_DEVICE_RECIPIENT, user_id)
|
||||
set_tag(SynapseTags.TO_DEVICE_RECIPIENT_DEVICE, device_id)
|
||||
set_tag(
|
||||
SynapseTags.TO_DEVICE_MSGID,
|
||||
msg["content"].get(EventContentFields.TO_DEVICE_MSGID),
|
||||
)
|
||||
message_json = json_encoder.encode(msg)
|
||||
msg = messages_by_device[device_id]
|
||||
message_json, msgid = _serialize_to_device_message(
|
||||
user_id=user_id, device_id=device_id, msg=msg
|
||||
)
|
||||
messages_json_for_user[device_id] = (message_json, msgid)
|
||||
|
||||
messages_json_for_user[device_id] = message_json
|
||||
if issue9533_logger.isEnabledFor(logging.DEBUG):
|
||||
# Log any messages we are dropping
|
||||
unmapped_devices = (
|
||||
messages_by_device.keys() - messages_json_for_user.keys()
|
||||
)
|
||||
if unmapped_devices:
|
||||
issue9533_logger.debug(
|
||||
"Dropping to-device messages for unknown devices: %s",
|
||||
[
|
||||
f"{user_id}/{device_id} (msgid {_get_msgid_for_message(messages_by_device[device_id])})"
|
||||
for device_id in unmapped_devices
|
||||
],
|
||||
)
|
||||
|
||||
if messages_json_for_user:
|
||||
local_by_user_then_device[user_id] = messages_json_for_user
|
||||
@@ -965,22 +994,21 @@ class DeviceInboxWorkerStore(SQLBaseStore):
|
||||
values=[
|
||||
(user_id, device_id, stream_id, message_json, self._instance_name)
|
||||
for user_id, messages_by_device in local_by_user_then_device.items()
|
||||
for device_id, message_json in messages_by_device.items()
|
||||
for device_id, (message_json, _msgid) in messages_by_device.items()
|
||||
],
|
||||
)
|
||||
|
||||
if issue9533_logger.isEnabledFor(logging.DEBUG):
|
||||
issue9533_logger.debug(
|
||||
"Stored to-device messages with stream_id %i: %s",
|
||||
"Storing to-device messages with stream_id %i: %s",
|
||||
stream_id,
|
||||
[
|
||||
f"{user_id}/{device_id} (msgid "
|
||||
f"{msg['content'].get(EventContentFields.TO_DEVICE_MSGID)})"
|
||||
f"{user_id}/{device_id} (msgid {msgid})"
|
||||
for (
|
||||
user_id,
|
||||
messages_by_device,
|
||||
) in messages_by_user_then_device.items()
|
||||
for (device_id, msg) in messages_by_device.items()
|
||||
) in local_by_user_then_device.items()
|
||||
for (device_id, (_msg, msgid)) in messages_by_device.items()
|
||||
],
|
||||
)
|
||||
|
||||
@@ -1066,6 +1094,29 @@ class DeviceInboxWorkerStore(SQLBaseStore):
|
||||
return results
|
||||
|
||||
|
||||
def _serialize_to_device_message(
|
||||
*, user_id: str, device_id: str, msg: JsonDict
|
||||
) -> tuple[str, str]:
|
||||
"""Serialiize a to-device message, ready to add to the device_inbox table.
|
||||
|
||||
Returns a tuple (message_json, msgid).
|
||||
"""
|
||||
with start_active_span("serialise_to_device_message"):
|
||||
msgid = _get_msgid_for_message(msg)
|
||||
set_tag(SynapseTags.TO_DEVICE_TYPE, msg["type"])
|
||||
set_tag(SynapseTags.TO_DEVICE_SENDER, msg["sender"])
|
||||
set_tag(SynapseTags.TO_DEVICE_RECIPIENT, user_id)
|
||||
set_tag(SynapseTags.TO_DEVICE_RECIPIENT_DEVICE, device_id)
|
||||
set_tag(SynapseTags.TO_DEVICE_MSGID, msgid)
|
||||
message_json = json_encoder.encode(msg)
|
||||
return message_json, msgid
|
||||
|
||||
|
||||
def _get_msgid_for_message(msg: JsonDict) -> str:
|
||||
"""Extract the message ID from a to-device message."""
|
||||
return str(msg["content"].get(EventContentFields.TO_DEVICE_MSGID, ""))
|
||||
|
||||
|
||||
class DeviceInboxBackgroundUpdateStore(SQLBaseStore):
|
||||
DEVICE_INBOX_STREAM_ID = "device_inbox_stream_drop"
|
||||
REMOVE_DEAD_DEVICES_FROM_INBOX = "remove_dead_devices_from_device_inbox"
|
||||
|
||||
@@ -38,6 +38,7 @@ from typing import (
|
||||
import attr
|
||||
from sortedcontainers import SortedList, SortedSet
|
||||
|
||||
from synapse.logging import issue9533_logger
|
||||
from synapse.metrics.background_process_metrics import run_as_background_process
|
||||
from synapse.storage.database import (
|
||||
DatabasePool,
|
||||
@@ -774,6 +775,14 @@ class MultiWriterIdGenerator(AbstractStreamIdGenerator):
|
||||
# We move the current min position up if the minimum current positions
|
||||
# of all instances is higher (since by definition all positions less
|
||||
# that that have been persisted).
|
||||
#
|
||||
# If we are one of several writers, then we don't need to factor our own
|
||||
# `_current_position` into `_persisted_upto_position` unless we have unfinished
|
||||
# writes (since we know that any future write that happens locally will have
|
||||
# a higher stream ID than any of the other writers' current positions). In other
|
||||
# words, when we have no outstanding writes, then the new `_persisted_upto_position`
|
||||
# can be the minimum of all *other* writers' current positions,
|
||||
#
|
||||
our_current_position = self._current_positions.get(self._instance_name, 0)
|
||||
min_curr = min(
|
||||
(
|
||||
@@ -783,7 +792,6 @@ class MultiWriterIdGenerator(AbstractStreamIdGenerator):
|
||||
),
|
||||
default=our_current_position,
|
||||
)
|
||||
|
||||
if our_current_position and (self._unfinished_ids or self._in_flight_fetches):
|
||||
min_curr = min(min_curr, our_current_position)
|
||||
|
||||
@@ -820,6 +828,22 @@ class MultiWriterIdGenerator(AbstractStreamIdGenerator):
|
||||
# do.
|
||||
break
|
||||
|
||||
# Hacky debug logging to attempt to trace https://github.com/element-hq/synapse/issues/19795
|
||||
if (
|
||||
issue9533_logger.isEnabledFor(logging.DEBUG)
|
||||
and self._stream_name == "to_device"
|
||||
):
|
||||
issue9533_logger.debug(
|
||||
"stream_id=%i now persisted for stream=%s; _current_positions=%s _unfinished_ids=%s, _known_persisted_positions=%s _persisted_upto_position=%i min_curr=%i",
|
||||
new_id,
|
||||
self._stream_name,
|
||||
self._current_positions,
|
||||
self._unfinished_ids,
|
||||
self._known_persisted_positions,
|
||||
self._persisted_upto_position,
|
||||
min_curr,
|
||||
)
|
||||
|
||||
def _update_stream_positions_table_txn(self, txn: Cursor) -> None:
|
||||
"""Update the `stream_positions` table with newly persisted position."""
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ from synapse.handlers.room_policy import POLICY_SERVER_KEY_ID
|
||||
from synapse.rest import admin
|
||||
from synapse.rest.client import filter, login, room, sync
|
||||
from synapse.server import HomeServer
|
||||
from synapse.synapse_rust.events import Signatures
|
||||
from synapse.types import JsonDict, UserID
|
||||
from synapse.util.clock import Clock
|
||||
|
||||
@@ -113,7 +114,15 @@ class RoomPolicyTestCase(unittest.FederatingHomeserverTestCase):
|
||||
self.OTHER_SERVER_NAME,
|
||||
self.signing_key,
|
||||
)
|
||||
return sigs
|
||||
# Only return the new signature like the policy server spec says,
|
||||
# not any others that were already in the event
|
||||
return {
|
||||
self.OTHER_SERVER_NAME: {
|
||||
POLICY_SERVER_KEY_ID: sigs[self.OTHER_SERVER_NAME][
|
||||
POLICY_SERVER_KEY_ID
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
async def policy_server_signs_event_with_wrong_key(
|
||||
destination: str, pdu: EventBase, timeout: int | None = None
|
||||
@@ -169,6 +178,19 @@ class RoomPolicyTestCase(unittest.FederatingHomeserverTestCase):
|
||||
state_key="",
|
||||
)
|
||||
|
||||
def _sign_with_random_key(self, server_name: str, event: EventBase) -> None:
|
||||
non_policyserver_key = signedjson.key.generate_signing_key(
|
||||
"non_policyserver_key"
|
||||
)
|
||||
event.signatures = Signatures(
|
||||
compute_event_signature(
|
||||
event.room_version,
|
||||
event.get_dict(),
|
||||
server_name,
|
||||
non_policyserver_key,
|
||||
)
|
||||
)
|
||||
|
||||
def test_no_policy_event_set(self) -> None:
|
||||
# We don't need to modify the room state at all - we're testing the default
|
||||
# case where a room doesn't use a policy server.
|
||||
@@ -316,11 +338,69 @@ class RoomPolicyTestCase(unittest.FederatingHomeserverTestCase):
|
||||
},
|
||||
},
|
||||
)
|
||||
# Sign the event as the origin server first, since that's what events passed to
|
||||
# ask_policy_server_to_sign_event will generally look like. The exact key used
|
||||
# here isn't important.
|
||||
self._sign_with_random_key("example.org", event)
|
||||
self.mock_federation_transport_client.ask_policy_server_to_sign_event.side_effect = self.policy_server_signs_event
|
||||
self.get_success(
|
||||
self.handler.ask_policy_server_to_sign_event(event, verify=True)
|
||||
)
|
||||
self.assertEqual(len(event.signatures), 1)
|
||||
# Standard success case: event has signatures from the origin and the policy server
|
||||
self.assertEqual(
|
||||
{
|
||||
server: len(signatures)
|
||||
for server, signatures in event.signatures.as_dict().items()
|
||||
},
|
||||
{"example.org": 1, self.OTHER_SERVER_NAME: 1},
|
||||
f"Expected signatures for the origin homeserver (example.org) and policy server ({self.OTHER_SERVER_NAME})",
|
||||
)
|
||||
|
||||
def test_ask_origin_server_to_sign_event_doesnt_replace_signatures(self) -> None:
|
||||
"""
|
||||
``ask_policy_server_to_sign_event`` has had bugs where it accidentally overwrote
|
||||
the origin server's signature in the case where the origin server has the same
|
||||
server name as the policy server (each have their own signing key). This test is
|
||||
otherwise equivalent to the success case test above, but the server name for
|
||||
origin event sending server and the policy server are the same and we want to
|
||||
ensure both signatures are preserved.
|
||||
"""
|
||||
verify_key_str = encode_verify_key_base64(get_verify_key(self.signing_key))
|
||||
self._add_policy_server_to_room(public_key=verify_key_str)
|
||||
event = make_test_event(
|
||||
room_version=self.room_version,
|
||||
internal_metadata_dict={},
|
||||
event_dict={
|
||||
"room_id": self.room_id,
|
||||
"type": "m.room.message",
|
||||
"sender": "@spammy:" + self.OTHER_SERVER_NAME,
|
||||
"content": {
|
||||
"msgtype": "m.text",
|
||||
"body": "This is another signed event.",
|
||||
},
|
||||
},
|
||||
)
|
||||
# Sign the event as the origin server that sent the event, which in this case
|
||||
# has the same server name as the policy server. We're using a different key
|
||||
# than `self.signing_key` (for the policy server), as the ed25519:policy_server
|
||||
# key is only used for policy server signatures, not any other federation traffic
|
||||
# even when the origin server and policy are logically the same server.
|
||||
self._sign_with_random_key(self.OTHER_SERVER_NAME, event)
|
||||
self.mock_federation_transport_client.ask_policy_server_to_sign_event.side_effect = self.policy_server_signs_event
|
||||
self.get_success(
|
||||
self.handler.ask_policy_server_to_sign_event(event, verify=True)
|
||||
)
|
||||
# Less common success case: the event origin server is logically the same as
|
||||
# the policy server, so there will be two signatures from one server name.
|
||||
# It's important to make sure both signatures are preserved.
|
||||
self.assertEqual(
|
||||
{
|
||||
server: len(signatures)
|
||||
for server, signatures in event.signatures.as_dict().items()
|
||||
},
|
||||
{self.OTHER_SERVER_NAME: 2},
|
||||
f"Expected 2 signatures for the origin server and policy server under the same server name ({self.OTHER_SERVER_NAME}) but with different keys",
|
||||
)
|
||||
|
||||
def test_ask_policy_server_to_sign_event_refuses(self) -> None:
|
||||
verify_key_str = encode_verify_key_base64(get_verify_key(self.signing_key))
|
||||
|
||||
@@ -28,7 +28,11 @@ from twisted.web.http import HTTPChannel
|
||||
from twisted.web.server import Request
|
||||
|
||||
from synapse.http.site import SynapseRequest
|
||||
from synapse.logging._terse_json import JsonFormatter, TerseJsonFormatter
|
||||
from synapse.logging._terse_json import (
|
||||
GcpJsonFormatter,
|
||||
JsonFormatter,
|
||||
TerseJsonFormatter,
|
||||
)
|
||||
from synapse.logging.context import LoggingContext, LoggingContextFilter
|
||||
from synapse.types import JsonDict
|
||||
|
||||
@@ -251,3 +255,77 @@ class TerseJsonTestCase(LoggerCleanupMixin, TestCase):
|
||||
self.assertEqual(log["log"], "Hello there, wally!")
|
||||
self.assertEqual(log["exc_type"], "ValueError")
|
||||
self.assertEqual(log["exc_value"], "That's wrong, you wally!")
|
||||
|
||||
|
||||
class GcpJsonFormatterTestCase(LoggerCleanupMixin, TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.output = StringIO()
|
||||
|
||||
def get_log_line(self) -> JsonDict:
|
||||
data = self.output.getvalue()
|
||||
logs = data.splitlines()
|
||||
self.assertEqual(len(logs), 1)
|
||||
self.assertEqual(data.count("\n"), 1)
|
||||
return json.loads(logs[0])
|
||||
|
||||
def test_gcp_json_output(self) -> None:
|
||||
"""
|
||||
GcpJsonFormatter produces exactly the four fields GCL expects.
|
||||
"""
|
||||
handler = logging.StreamHandler(self.output)
|
||||
handler.setFormatter(GcpJsonFormatter())
|
||||
logger = self.get_logger(handler)
|
||||
|
||||
logger.info("Hello there, %s!", "wally")
|
||||
|
||||
log = self.get_log_line()
|
||||
|
||||
self.assertIncludes(
|
||||
log.keys(), {"severity", "message", "logger", "time"}, exact=True
|
||||
)
|
||||
self.assertEqual(log["message"], "Hello there, wally!")
|
||||
self.assertEqual(log["severity"], "INFO")
|
||||
self.assertTrue(log["time"].endswith("Z"))
|
||||
|
||||
def test_severity_levels(self) -> None:
|
||||
"""
|
||||
Python log levels are mapped to their GCL severity equivalents.
|
||||
"""
|
||||
cases = [
|
||||
(logging.DEBUG, "DEBUG"),
|
||||
(logging.INFO, "INFO"),
|
||||
(logging.WARNING, "WARNING"),
|
||||
(logging.ERROR, "ERROR"),
|
||||
(logging.CRITICAL, "CRITICAL"),
|
||||
]
|
||||
for level, expected_severity in cases:
|
||||
self.output = StringIO()
|
||||
handler = logging.StreamHandler(self.output)
|
||||
handler.setFormatter(GcpJsonFormatter())
|
||||
logger = self.get_logger(handler)
|
||||
logger.setLevel(level)
|
||||
logger.log(level, "test")
|
||||
log = self.get_log_line()
|
||||
self.assertEqual(log["severity"], expected_severity, f"level={level}")
|
||||
|
||||
def test_gcp_json_with_exception(self) -> None:
|
||||
"""
|
||||
Exception info is appended to the message field, not separate keys.
|
||||
"""
|
||||
handler = logging.StreamHandler(self.output)
|
||||
handler.setFormatter(GcpJsonFormatter())
|
||||
logger = self.get_logger(handler)
|
||||
|
||||
try:
|
||||
raise ValueError("That's wrong, you wally!")
|
||||
except ValueError:
|
||||
logger.exception("Hello there, %s!", "wally")
|
||||
|
||||
log = self.get_log_line()
|
||||
|
||||
self.assertIncludes(
|
||||
log.keys(), {"severity", "message", "logger", "time"}, exact=True
|
||||
)
|
||||
self.assertIn("Hello there, wally!", log["message"])
|
||||
self.assertIn("ValueError", log["message"])
|
||||
self.assertIn("That's wrong, you wally!", log["message"])
|
||||
|
||||
Reference in New Issue
Block a user