diff options
Diffstat (limited to 'packages/taler-util/src/observability.ts')
-rw-r--r-- | packages/taler-util/src/observability.ts | 98 |
1 files changed, 98 insertions, 0 deletions
diff --git a/packages/taler-util/src/observability.ts b/packages/taler-util/src/observability.ts new file mode 100644 index 000000000..0171142c8 --- /dev/null +++ b/packages/taler-util/src/observability.ts @@ -0,0 +1,98 @@ +/* + This file is part of GNU Taler + (C) 2024 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +import { + AbsoluteTime, + CancellationToken, + ObservabilityEvent, +} from "./index.js"; +import { + HttpRequestLibrary, + HttpRequestOptions, + HttpResponse, +} from "./http-common.js"; +import { ObservabilityEventType } from "./notifications.js"; +import { getErrorDetailFromException } from "./errors.js"; + +/** + * Observability sink can be passed into various operations (HTTP requests, DB access) + * to do structured logging within a particular context (task, request, ...). + */ +export interface ObservabilityContext { + observe(evt: ObservabilityEvent): void; +} + +let seqId = 1000; + +export class ObservableHttpClientLibrary implements HttpRequestLibrary { + private readonly cancelatorById = new Map<string, CancellationToken.Source>(); + constructor( + private impl: HttpRequestLibrary, + private oc: ObservabilityContext, + ) {} + + public cancelRequest(id: string): void { + const cancelator = this.cancelatorById.get(id); + if (!cancelator) return; + cancelator.cancel(); + } + + async fetch( + url: string, + opt?: HttpRequestOptions | undefined, + ): Promise<HttpResponse> { + const id = `req-${seqId}`; + seqId = seqId + 1; + + const cancelator = CancellationToken.create(); + if (opt?.cancellationToken) { + opt.cancellationToken.onCancelled(cancelator.cancel); + } + this.cancelatorById.set(id, cancelator); + + this.oc.observe({ + id, + when: AbsoluteTime.now(), + type: ObservabilityEventType.HttpFetchStart, + url: url, + }); + + const optsWithCancel = opt ?? {}; + optsWithCancel.cancellationToken = cancelator.token; + try { + const res = await this.impl.fetch(url, optsWithCancel); + this.oc.observe({ + id, + when: AbsoluteTime.now(), + type: ObservabilityEventType.HttpFetchFinishSuccess, + url, + status: res.status, + }); + return res; + } catch (e) { + this.oc.observe({ + id, + when: AbsoluteTime.now(), + type: ObservabilityEventType.HttpFetchFinishError, + url, + error: getErrorDetailFromException(e), + }); + throw e; + } finally { + this.cancelatorById.delete(id); + } + } +} |