| author | |
| committer | |
| log | 53a477c2e91d206057adc5ac4c28cbdaddfbc07c |
| tree | e8819f30f603bd4334426068dbe774e5b648ec31 |
| parent | d84ebe831922167d556e16cb40a238a904e0f14a |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: codex <codex@users.noreply.github.com>2 files changed, 98 insertions(+), 15 deletions(-)
apps/desktop/src/backend/DesktopBackendConfiguration.test.ts+62| ... | @@ -3,6 +3,8 @@ import { assert, describe, it } from "@effect/vitest"; | ... | @@ -3,6 +3,8 @@ import { assert, describe, it } from "@effect/vitest"; |
| 3 | import * as Effect from "effect/Effect"; | 3 | import * as Effect from "effect/Effect"; |
| 4 | import * as FileSystem from "effect/FileSystem"; | 4 | import * as FileSystem from "effect/FileSystem"; |
| 5 | import * as Layer from "effect/Layer"; | 5 | import * as Layer from "effect/Layer"; |
| 6 | import * as Logger from "effect/Logger"; | ||
| 7 | import * as PlatformError from "effect/PlatformError"; | ||
| 6 | import * as Schema from "effect/Schema"; | 8 | import * as Schema from "effect/Schema"; |
| 7 | 9 | ||
| 8 | import * as DesktopEnvironment from "../app/DesktopEnvironment.ts"; | 10 | import * as DesktopEnvironment from "../app/DesktopEnvironment.ts"; |
| ... | @@ -21,6 +23,10 @@ const encodePersistedServerObservabilitySettingsDocument = Schema.encodeEffect( | ... | @@ -21,6 +23,10 @@ const encodePersistedServerObservabilitySettingsDocument = Schema.encodeEffect( |
| 21 | Schema.fromJsonString(PersistedServerObservabilitySettingsDocument), | 23 | Schema.fromJsonString(PersistedServerObservabilitySettingsDocument), |
| 22 | ); | 24 | ); |
| 23 | 25 | ||
| 26 | const isDesktopBackendObservabilitySettingsReadError = Schema.is( | ||
| 27 | DesktopBackendConfiguration.DesktopBackendObservabilitySettingsReadError, | ||
| 28 | ); | ||
| 29 | |||
| 24 | const serverExposureLayer = Layer.succeed(DesktopServerExposure.DesktopServerExposure, { | 30 | const serverExposureLayer = Layer.succeed(DesktopServerExposure.DesktopServerExposure, { |
| 25 | getState: Effect.die("unexpected getState"), | 31 | getState: Effect.die("unexpected getState"), |
| 26 | backendConfig: Effect.succeed({ | 32 | backendConfig: Effect.succeed({ |
| ... | @@ -166,6 +172,62 @@ describe("DesktopBackendConfiguration", () => { | ... | @@ -166,6 +172,62 @@ describe("DesktopBackendConfiguration", () => { |
| 166 | ), | 172 | ), |
| 167 | ); | 173 | ); |
| 168 | 174 | ||
| 175 | it.effect("logs structured context when persisted observability settings cannot be read", () => | ||
| 176 | Effect.gen(function* () { | ||
| 177 | const fileSystem = yield* FileSystem.FileSystem; | ||
| 178 | const baseDir = yield* fileSystem.makeTempDirectoryScoped({ | ||
| 179 | prefix: "t3-desktop-backend-config-test-", | ||
| 180 | }); | ||
| 181 | const settingsPath = `${baseDir}/userdata/settings.json`; | ||
| 182 | const cause = PlatformError.systemError({ | ||
| 183 | _tag: "PermissionDenied", | ||
| 184 | module: "FileSystem", | ||
| 185 | method: "readFileString", | ||
| 186 | pathOrDescriptor: settingsPath, | ||
| 187 | }); | ||
| 188 | const messages: Array<unknown> = []; | ||
| 189 | const logger = Logger.make(({ message }) => { | ||
| 190 | messages.push(message); | ||
| 191 | }); | ||
| 192 | const failingFileSystemLayer = Layer.succeed( | ||
| 193 | FileSystem.FileSystem, | ||
| 194 | FileSystem.makeNoop({ | ||
| 195 | readFileString: () => Effect.fail(cause), | ||
| 196 | }), | ||
| 197 | ); | ||
| 198 | |||
| 199 | const config = yield* Effect.gen(function* () { | ||
| 200 | const configuration = yield* DesktopBackendConfiguration.DesktopBackendConfiguration; | ||
| 201 | return yield* configuration.resolve; | ||
| 202 | }).pipe( | ||
| 203 | Effect.provide( | ||
| 204 | Layer.mergeAll( | ||
| 205 | DesktopBackendConfiguration.layer.pipe( | ||
| 206 | Layer.provideMerge(serverExposureLayer), | ||
| 207 | Layer.provideMerge(makeEnvironmentLayer(baseDir)), | ||
| 208 | Layer.provideMerge(failingFileSystemLayer), | ||
| 209 | ), | ||
| 210 | Logger.layer([logger], { mergeWithExisting: false }), | ||
| 211 | ), | ||
| 212 | ), | ||
| 213 | ); | ||
| 214 | |||
| 215 | assert.isUndefined(config.bootstrap.otlpTracesUrl); | ||
| 216 | assert.isUndefined(config.bootstrap.otlpMetricsUrl); | ||
| 217 | |||
| 218 | const error = messages | ||
| 219 | .flatMap((message) => (Array.isArray(message) ? message : [message])) | ||
| 220 | .find(isDesktopBackendObservabilitySettingsReadError); | ||
| 221 | assert.isDefined(error); | ||
| 222 | assert.equal(error.settingsPath, settingsPath); | ||
| 223 | assert.equal(error.cause, cause); | ||
| 224 | assert.equal( | ||
| 225 | error.message, | ||
| 226 | `Failed to read persisted backend observability settings at ${settingsPath}.`, | ||
| 227 | ); | ||
| 228 | }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)), | ||
| 229 | ); | ||
| 230 | |||
| 169 | it.effect("captures backend output in development so child process logs can be persisted", () => | 231 | it.effect("captures backend output in development so child process logs can be persisted", () => |
| 170 | Effect.gen(function* () { | 232 | Effect.gen(function* () { |
| 171 | const fileSystem = yield* FileSystem.FileSystem; | 233 | const fileSystem = yield* FileSystem.FileSystem; |
apps/desktop/src/backend/DesktopBackendConfiguration.ts+36-15| ... | @@ -8,12 +8,24 @@ import * as Layer from "effect/Layer"; | ... | @@ -8,12 +8,24 @@ import * as Layer from "effect/Layer"; |
| 8 | import * as Option from "effect/Option"; | 8 | import * as Option from "effect/Option"; |
| 9 | import * as PlatformError from "effect/PlatformError"; | 9 | import * as PlatformError from "effect/PlatformError"; |
| 10 | import * as Ref from "effect/Ref"; | 10 | import * as Ref from "effect/Ref"; |
| 11 | import * as Schema from "effect/Schema"; | ||
| 11 | 12 | ||
| 12 | import * as DesktopBackendManager from "./DesktopBackendManager.ts"; | 13 | import * as DesktopBackendManager from "./DesktopBackendManager.ts"; |
| 13 | import * as DesktopEnvironment from "../app/DesktopEnvironment.ts"; | 14 | import * as DesktopEnvironment from "../app/DesktopEnvironment.ts"; |
| 14 | import * as DesktopObservability from "../app/DesktopObservability.ts"; | ||
| 15 | import * as DesktopServerExposure from "./DesktopServerExposure.ts"; | 15 | import * as DesktopServerExposure from "./DesktopServerExposure.ts"; |
| 16 | 16 | ||
| 17 | export class DesktopBackendObservabilitySettingsReadError extends Schema.TaggedErrorClass<DesktopBackendObservabilitySettingsReadError>()( | ||
| 18 | "DesktopBackendObservabilitySettingsReadError", | ||
| 19 | { | ||
| 20 | settingsPath: Schema.String, | ||
| 21 | cause: Schema.Defect(), | ||
| 22 | }, | ||
| 23 | ) { | ||
| 24 | override get message(): string { | ||
| 25 | return `Failed to read persisted backend observability settings at ${this.settingsPath}.`; | ||
| 26 | } | ||
| 27 | } | ||
| 28 | |||
| 17 | export class DesktopBackendConfiguration extends Context.Service< | 29 | export class DesktopBackendConfiguration extends Context.Service< |
| 18 | DesktopBackendConfiguration, | 30 | DesktopBackendConfiguration, |
| 19 | { | 31 | { |
| ... | @@ -50,25 +62,34 @@ const DESKTOP_BACKEND_ENV_NAMES = [ | ... | @@ -50,25 +62,34 @@ const DESKTOP_BACKEND_ENV_NAMES = [ |
| 50 | const backendChildEnvPatch = (): Record<string, string | undefined> => | 62 | const backendChildEnvPatch = (): Record<string, string | undefined> => |
| 51 | Object.fromEntries(DESKTOP_BACKEND_ENV_NAMES.map((name) => [name, undefined])); | 63 | Object.fromEntries(DESKTOP_BACKEND_ENV_NAMES.map((name) => [name, undefined])); |
| 52 | 64 | ||
| 53 | const { logWarning: logBackendConfigurationWarning } = DesktopObservability.makeComponentLogger( | 65 | const logBackendObservabilitySettingsReadFailure = ( |
| 54 | "desktop-backend-configuration", | 66 | settingsPath: string, |
| 55 | ); | 67 | cause: PlatformError.PlatformError, |
| 68 | ) => { | ||
| 69 | const error = new DesktopBackendObservabilitySettingsReadError({ settingsPath, cause }); | ||
| 70 | return Effect.logWarning(error).pipe( | ||
| 71 | Effect.annotateLogs({ | ||
| 72 | component: "desktop-backend-configuration", | ||
| 73 | error, | ||
| 74 | }), | ||
| 75 | ); | ||
| 76 | }; | ||
| 56 | 77 | ||
| 57 | const readPersistedBackendObservabilitySettings = Effect.gen(function* () { | 78 | const readPersistedBackendObservabilitySettings = Effect.gen(function* () { |
| 58 | const fileSystem = yield* FileSystem.FileSystem; | 79 | const fileSystem = yield* FileSystem.FileSystem; |
| 59 | const environment = yield* DesktopEnvironment.DesktopEnvironment; | 80 | const environment = yield* DesktopEnvironment.DesktopEnvironment; |
| 60 | const exists = yield* fileSystem | 81 | const raw = yield* fileSystem.readFileString(environment.serverSettingsPath).pipe( |
| 61 | .exists(environment.serverSettingsPath) | 82 | Effect.map(Option.some), |
| 62 | .pipe(Effect.orElseSucceed(() => false)); | 83 | Effect.catchTags({ |
| 63 | if (!exists) { | 84 | PlatformError: (cause) => |
| 64 | return emptyBackendObservabilitySettings; | 85 | cause.reason._tag === "NotFound" |
| 65 | } | 86 | ? Effect.succeed(Option.none()) |
| 66 | 87 | : logBackendObservabilitySettingsReadFailure(environment.serverSettingsPath, cause).pipe( | |
| 67 | const raw = yield* fileSystem.readFileString(environment.serverSettingsPath).pipe(Effect.option); | 88 | Effect.as(Option.none()), |
| 89 | ), | ||
| 90 | }), | ||
| 91 | ); | ||
| 68 | if (Option.isNone(raw)) { | 92 | if (Option.isNone(raw)) { |
| 69 | yield* logBackendConfigurationWarning( | ||
| 70 | "failed to read persisted backend observability settings", | ||
| 71 | ); | ||
| 72 | return emptyBackendObservabilitySettings; | 93 | return emptyBackendObservabilitySettings; |
| 73 | } | 94 | } |
| 74 | 95 |