From 2b58fcec69522b2613bb5061a6464d652dff2bf5 Mon Sep 17 00:00:00 2001 From: Lukasz Ostrowski Date: Tue, 28 May 2024 10:13:04 +0200 Subject: [PATCH 01/36] interface --- .../public-log-drain.service.ts | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 apps/avatax/src/modules/public-log-drain/public-log-drain.service.ts diff --git a/apps/avatax/src/modules/public-log-drain/public-log-drain.service.ts b/apps/avatax/src/modules/public-log-drain/public-log-drain.service.ts new file mode 100644 index 000000000..792d6f3ca --- /dev/null +++ b/apps/avatax/src/modules/public-log-drain/public-log-drain.service.ts @@ -0,0 +1,34 @@ +export interface PublicLog = {}> { + message: string; + timestamp: Date; + level: string; // todo enum, maybe otel level + attributes: T; +} + +export interface PublicLogDrain { + emitLog(log: PublicLog): Promise; +} + +export interface LogDrainTransporter { + emit(log: PublicLog): Promise; +} + +export class PublicLogDrainService implements PublicLogDrain { + constructor(private transporter: LogDrainTransporter) {} + + emitLog(log: PublicLog): Promise { + return this.transporter.emit(log); + } +} + +export class LogDrainJsonTransporter implements LogDrainTransporter { + async emit(log: PublicLog): Promise { + throw new Error("Not implemented"); + } +} + +export class LogDrainOtelTransporter implements LogDrainTransporter { + async emit(log: PublicLog): Promise { + throw new Error("Not implemented"); + } +} From 77d334e5c77c35c00659f72a80333b1d087da54b Mon Sep 17 00:00:00 2001 From: Lukasz Ostrowski Date: Tue, 28 May 2024 10:19:39 +0200 Subject: [PATCH 02/36] wip --- .../public-log-drain.service.ts | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/apps/avatax/src/modules/public-log-drain/public-log-drain.service.ts b/apps/avatax/src/modules/public-log-drain/public-log-drain.service.ts index 792d6f3ca..a492b477c 100644 --- a/apps/avatax/src/modules/public-log-drain/public-log-drain.service.ts +++ b/apps/avatax/src/modules/public-log-drain/public-log-drain.service.ts @@ -1,10 +1,19 @@ export interface PublicLog = {}> { message: string; + eventType: string; // enum timestamp: Date; level: string; // todo enum, maybe otel level attributes: T; } +export class TaxesCalculatedLog implements PublicLog { + message = "Taxes calculated"; + eventType = "TAXES_CALCULATED" as const; + timestamp = new Date(); + level = "info"; + attributes = {}; +} + export interface PublicLogDrain { emitLog(log: PublicLog): Promise; } @@ -22,9 +31,20 @@ export class PublicLogDrainService implements PublicLogDrain { } export class LogDrainJsonTransporter implements LogDrainTransporter { + private endpoint: string | null = null; + async emit(log: PublicLog): Promise { - throw new Error("Not implemented"); + if (!this.endpoint) { + throw new Error("Endpoint is not set, call setSettings first"); + } + + return fetch(this.endpoint, { + method: "POST", + body: JSON.stringify(log), + }); } + + setSettings() {} } export class LogDrainOtelTransporter implements LogDrainTransporter { From 4d85cf577a559622248a9a668893453e75af44c3 Mon Sep 17 00:00:00 2001 From: Lukasz Ostrowski Date: Tue, 28 May 2024 10:37:33 +0200 Subject: [PATCH 03/36] wip --- .../public-log-drain.service.ts | 59 ++++++++++++++++++- packages/otel/src/otel-logs-setup.ts | 6 +- 2 files changed, 62 insertions(+), 3 deletions(-) diff --git a/apps/avatax/src/modules/public-log-drain/public-log-drain.service.ts b/apps/avatax/src/modules/public-log-drain/public-log-drain.service.ts index a492b477c..69f4ecac4 100644 --- a/apps/avatax/src/modules/public-log-drain/public-log-drain.service.ts +++ b/apps/avatax/src/modules/public-log-drain/public-log-drain.service.ts @@ -1,3 +1,7 @@ +import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-http"; +import { sharedOtelConfig } from "@saleor/apps-otel/src/shared-config"; +import { IResource } from "@opentelemetry/resources"; + export interface PublicLog = {}> { message: string; eventType: string; // enum @@ -28,6 +32,10 @@ export class PublicLogDrainService implements PublicLogDrain { emitLog(log: PublicLog): Promise { return this.transporter.emit(log); } + + getTransport() { + return this.transporter; + } } export class LogDrainJsonTransporter implements LogDrainTransporter { @@ -41,14 +49,61 @@ export class LogDrainJsonTransporter implements LogDrainTransporter { return fetch(this.endpoint, { method: "POST", body: JSON.stringify(log), - }); + }) + .then((r) => r.json()) + .then((res) => { + console.log(res); // todo + + return; + }); } setSettings() {} } export class LogDrainOtelTransporter implements LogDrainTransporter { + private otelExporter: OTLPLogExporter | null = null; + async emit(log: PublicLog): Promise { - throw new Error("Not implemented"); + return new Promise((res, rej) => { + if (!this.otelExporter) { + throw new Error("Call setSettings first"); + } + + return this.otelExporter.export( + [ + { + body: log.message, + attributes: log.attributes, + severityText: log.level, + hrTimeObserved: [0, 0], + hrTime: [0, 0], + resource: { + attributes: {}, + merge(other: IResource | null): IResource { + return other as IResource; // todo + }, + }, + droppedAttributesCount: 0, + instrumentationScope: { name: "LogDrainOtelTransporter" }, + }, + ], + (cb) => { + if (cb.error) { + rej(cb.error); + } else { + res(); + } + }, + ); + }); + } + + setSettings(settings: { url: string }) { + this.otelExporter = new OTLPLogExporter({ + headers: sharedOtelConfig.exporterHeaders, + url: settings.url, + timeoutMillis: 2000, + }); } } diff --git a/packages/otel/src/otel-logs-setup.ts b/packages/otel/src/otel-logs-setup.ts index 7685cfb3c..0a9e954f9 100644 --- a/packages/otel/src/otel-logs-setup.ts +++ b/packages/otel/src/otel-logs-setup.ts @@ -1,5 +1,9 @@ import { logs } from "@opentelemetry/api-logs"; -import { BatchLogRecordProcessor, LoggerProvider } from "@opentelemetry/sdk-logs"; +import { + BatchLogRecordProcessor, + LoggerProvider, + SimpleLogRecordProcessor, +} from "@opentelemetry/sdk-logs"; import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-http"; import { detectResourcesSync, From 63a972dd6119b0fdbf43c16723d453df19b9e10a Mon Sep 17 00:00:00 2001 From: Lukasz Ostrowski Date: Tue, 28 May 2024 10:49:21 +0200 Subject: [PATCH 04/36] wip --- .../pages/api/webhooks/order-calculate-taxes.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/apps/avatax/src/pages/api/webhooks/order-calculate-taxes.ts b/apps/avatax/src/pages/api/webhooks/order-calculate-taxes.ts index eeb3a43e7..b1506aa9d 100644 --- a/apps/avatax/src/pages/api/webhooks/order-calculate-taxes.ts +++ b/apps/avatax/src/pages/api/webhooks/order-calculate-taxes.ts @@ -12,6 +12,11 @@ import { loggerContext } from "../../../logger-context"; import { AvataxInvalidAddressError } from "../../../modules/taxes/tax-error"; import { orderCalculateTaxesSyncWebhook } from "../../../modules/webhooks/definitions/order-calculate-taxes"; import { verifyCalculateTaxesPayload } from "../../../modules/webhooks/validate-webhook-payload"; +import { + LogDrainOtelTransporter, + PublicLogDrainService, + TaxesCalculatedLog, +} from "../../../modules/public-log-drain/public-log-drain.service"; export const config = { api: { @@ -25,6 +30,14 @@ const withMetadataCache = wrapWithMetadataCache(metadataCache); const subscriptionErrorChecker = new SubscriptionPayloadErrorChecker(logger, captureException); +const otelLogDrainTransporter = new LogDrainOtelTransporter(); + +otelLogDrainTransporter.setSettings({ + url: "http://192.168.1.108:4318/v1/logs", +}); + +const publicLoggerOtel = new PublicLogDrainService(otelLogDrainTransporter); + export default wrapWithLoggerContext( withOtel( withMetadataCache( @@ -111,6 +124,8 @@ export default wrapWithLoggerContext( logger.info("Taxes calculated", { calculatedTaxes }); + await publicLoggerOtel.emitLog(new TaxesCalculatedLog()); + return res.status(200).json(ctx.buildResponse(calculatedTaxes)); } else if (avataxWebhookServiceResult.isErr()) { const err = avataxWebhookServiceResult.error; From 154f6eb869d8659cbdaecce3d5ba307dbc8979f7 Mon Sep 17 00:00:00 2001 From: Jonatan Witoszek Date: Tue, 28 May 2024 11:01:21 +0200 Subject: [PATCH 05/36] Validate attributes, fix hrTime mapping --- .../public-log-drain.service.ts | 56 ++++++++++++++++--- 1 file changed, 47 insertions(+), 9 deletions(-) diff --git a/apps/avatax/src/modules/public-log-drain/public-log-drain.service.ts b/apps/avatax/src/modules/public-log-drain/public-log-drain.service.ts index 69f4ecac4..981afc896 100644 --- a/apps/avatax/src/modules/public-log-drain/public-log-drain.service.ts +++ b/apps/avatax/src/modules/public-log-drain/public-log-drain.service.ts @@ -1,6 +1,10 @@ import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-http"; import { sharedOtelConfig } from "@saleor/apps-otel/src/shared-config"; import { IResource } from "@opentelemetry/resources"; +import { timeInputToHrTime, isAttributeValue, InstrumentationScope } from "@opentelemetry/core"; +import { LogAttributes } from "@opentelemetry/api-logs"; +import { Attributes } from "@opentelemetry/api"; +import * as packageJson from "../../../package.json"; export interface PublicLog = {}> { message: string; @@ -70,20 +74,54 @@ export class LogDrainOtelTransporter implements LogDrainTransporter { throw new Error("Call setSettings first"); } + const isValidAttribute = (value: unknown) => { + return ( + value === null || + value === undefined || + (typeof value === "number" ? !Number.isFinite(value) : true) + ); + }; + + /* + * We must filter out non-serializable values + * https://opentelemetry.io/docs/specs/otel/common/#attribute + */ + const filteredAttributes = Object.fromEntries( + Object.entries(log.attributes).filter((_, value) => { + if (Array.isArray(value)) { + return value.every((item) => + typeof value === "number" ? !Number.isFinite(value) : true, + ); + } + return isValidAttribute(value); + }), + ); + + const resourceAttributes: Attributes = { + "service.name": "saleor-app-avatax", + "service.version": packageJson.version, + }; + + const resource: IResource = { + attributes: resourceAttributes, + merge(other: IResource | null): IResource { + return this; + }, + }; + return this.otelExporter.export( [ { body: log.message, - attributes: log.attributes, + attributes: filteredAttributes as LogAttributes, severityText: log.level, - hrTimeObserved: [0, 0], - hrTime: [0, 0], - resource: { - attributes: {}, - merge(other: IResource | null): IResource { - return other as IResource; // todo - }, - }, + hrTimeObserved: timeInputToHrTime(log.timestamp), + hrTime: timeInputToHrTime(log.timestamp), + /* + * TODO: Pass traceId to logs + * spanContext: ... + */ + resource, droppedAttributesCount: 0, instrumentationScope: { name: "LogDrainOtelTransporter" }, }, From 3faaec1bf9722143c356333fc8d42603afb325c0 Mon Sep 17 00:00:00 2001 From: Jonatan Witoszek Date: Tue, 28 May 2024 11:57:26 +0200 Subject: [PATCH 06/36] Add validation of attributes as in OTEL spec --- .../public-log-drain.service.ts | 126 ++++++++++++++---- 1 file changed, 98 insertions(+), 28 deletions(-) diff --git a/apps/avatax/src/modules/public-log-drain/public-log-drain.service.ts b/apps/avatax/src/modules/public-log-drain/public-log-drain.service.ts index 981afc896..3a74b0620 100644 --- a/apps/avatax/src/modules/public-log-drain/public-log-drain.service.ts +++ b/apps/avatax/src/modules/public-log-drain/public-log-drain.service.ts @@ -1,8 +1,7 @@ import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-http"; import { sharedOtelConfig } from "@saleor/apps-otel/src/shared-config"; import { IResource } from "@opentelemetry/resources"; -import { timeInputToHrTime, isAttributeValue, InstrumentationScope } from "@opentelemetry/core"; -import { LogAttributes } from "@opentelemetry/api-logs"; +import { timeInputToHrTime, isAttributeValue } from "@opentelemetry/core"; import { Attributes } from "@opentelemetry/api"; import * as packageJson from "../../../package.json"; @@ -65,8 +64,87 @@ export class LogDrainJsonTransporter implements LogDrainTransporter { setSettings() {} } +export interface LogRecordLimits { + /** attributeValueLengthLimit is maximum allowed attribute value size */ + attributeValueLengthLimit?: number; + + /** attributeCountLimit is number of attributes per LogRecord */ + attributeCountLimit?: number; +} + export class LogDrainOtelTransporter implements LogDrainTransporter { private otelExporter: OTLPLogExporter | null = null; + private logRecordLimit: Required = { + /* + * Default values used by OTEL spec + * https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/#attribute-limits + */ + attributeValueLengthLimit: Infinity, + attributeCountLimit: 128, + }; + + private _truncateSize(value: unknown) { + const limit = this.logRecordLimit.attributeValueLengthLimit; + + const truncateToLimit = (value: string) => { + if (value.length <= limit) { + return value; + } + return value.substring(0, limit); + }; + + if (typeof value === "string") { + return truncateToLimit(value); + } + + if (Array.isArray(value)) { + return (value as []).map((val) => (typeof val === "string" ? truncateToLimit(val) : val)); + } + + // If value is of another type, we can safely return it as is + return value; + } + + private _filterAndTruncateAttributes(attributes: Record) { + /* + * We must filter out non-serializable values and truncate ones that exceed limits + * https://opentelemetry.io/docs/specs/otel/common/#attribute + */ + const filteredAttributesEntries = Object.entries(attributes).filter(([key, value]) => { + if (value === null) { + return false; + } + if (key.length === 0) { + return false; + } + if ( + !isAttributeValue(value) && + !(typeof value === "object" && !Array.isArray(value) && Object.keys(value).length > 0) + ) { + return false; + } + }); + + return filteredAttributesEntries + .slice(0, this.logRecordLimit.attributeCountLimit) + .reduce((acc, [key, value]) => { + if (Object.keys(acc).length >= this.logRecordLimit.attributeCountLimit) { + return acc; + } + + if (isAttributeValue(value)) { + return { + ...acc, + [key]: this._truncateSize(value), + }; + } else { + return { + ...acc, + [key]: value, + }; + } + }, {}); + } async emit(log: PublicLog): Promise { return new Promise((res, rej) => { @@ -74,29 +152,6 @@ export class LogDrainOtelTransporter implements LogDrainTransporter { throw new Error("Call setSettings first"); } - const isValidAttribute = (value: unknown) => { - return ( - value === null || - value === undefined || - (typeof value === "number" ? !Number.isFinite(value) : true) - ); - }; - - /* - * We must filter out non-serializable values - * https://opentelemetry.io/docs/specs/otel/common/#attribute - */ - const filteredAttributes = Object.fromEntries( - Object.entries(log.attributes).filter((_, value) => { - if (Array.isArray(value)) { - return value.every((item) => - typeof value === "number" ? !Number.isFinite(value) : true, - ); - } - return isValidAttribute(value); - }), - ); - const resourceAttributes: Attributes = { "service.name": "saleor-app-avatax", "service.version": packageJson.version, @@ -104,7 +159,8 @@ export class LogDrainOtelTransporter implements LogDrainTransporter { const resource: IResource = { attributes: resourceAttributes, - merge(other: IResource | null): IResource { + // This is a workaround to support OTEL SDK types + merge(): IResource { return this; }, }; @@ -113,8 +169,13 @@ export class LogDrainOtelTransporter implements LogDrainTransporter { [ { body: log.message, - attributes: filteredAttributes as LogAttributes, + attributes: this._filterAndTruncateAttributes(log.attributes), severityText: log.level, + /* + * TODO: Map severity to OTEL levels + * severityNumber: "", + */ + severityNumber: 0, // todo hrTimeObserved: timeInputToHrTime(log.timestamp), hrTime: timeInputToHrTime(log.timestamp), /* @@ -137,11 +198,20 @@ export class LogDrainOtelTransporter implements LogDrainTransporter { }); } - setSettings(settings: { url: string }) { + setSettings(settings: { url: string; logRecordLimit: Required }) { this.otelExporter = new OTLPLogExporter({ headers: sharedOtelConfig.exporterHeaders, url: settings.url, timeoutMillis: 2000, }); + if (this.logRecordLimit) { + if (this.logRecordLimit.attributeValueLengthLimit <= 0) { + throw new Error("attributeValueLengthLimit cannot be less than 0"); + } + if (this.logRecordLimit.attributeCountLimit <= 0) { + throw new Error("attributeCountLimit cannot be less than 0"); + } + this.logRecordLimit = settings.logRecordLimit; + } } } From f50edbfb0637133929a2bfd0ad58d186ec47e511 Mon Sep 17 00:00:00 2001 From: Jonatan Witoszek Date: Tue, 28 May 2024 12:00:59 +0200 Subject: [PATCH 07/36] Check dropped attributes length --- .../modules/public-log-drain/public-log-drain.service.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/apps/avatax/src/modules/public-log-drain/public-log-drain.service.ts b/apps/avatax/src/modules/public-log-drain/public-log-drain.service.ts index 3a74b0620..f716196b4 100644 --- a/apps/avatax/src/modules/public-log-drain/public-log-drain.service.ts +++ b/apps/avatax/src/modules/public-log-drain/public-log-drain.service.ts @@ -165,11 +165,13 @@ export class LogDrainOtelTransporter implements LogDrainTransporter { }, }; + const attributes = this._filterAndTruncateAttributes(log.attributes); + return this.otelExporter.export( [ { body: log.message, - attributes: this._filterAndTruncateAttributes(log.attributes), + attributes, severityText: log.level, /* * TODO: Map severity to OTEL levels @@ -183,7 +185,8 @@ export class LogDrainOtelTransporter implements LogDrainTransporter { * spanContext: ... */ resource, - droppedAttributesCount: 0, + droppedAttributesCount: + Object.keys(log.attributes).length - Object.keys(attributes).length, instrumentationScope: { name: "LogDrainOtelTransporter" }, }, ], From 530c8eb774f54f9f4ad8415a249ef3cccf8c52b6 Mon Sep 17 00:00:00 2001 From: Jonatan Witoszek Date: Tue, 28 May 2024 12:04:26 +0200 Subject: [PATCH 08/36] Add NaN validation --- .../modules/public-log-drain/public-log-drain.service.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/apps/avatax/src/modules/public-log-drain/public-log-drain.service.ts b/apps/avatax/src/modules/public-log-drain/public-log-drain.service.ts index f716196b4..194bf6fe7 100644 --- a/apps/avatax/src/modules/public-log-drain/public-log-drain.service.ts +++ b/apps/avatax/src/modules/public-log-drain/public-log-drain.service.ts @@ -123,6 +123,13 @@ export class LogDrainOtelTransporter implements LogDrainTransporter { ) { return false; } + /* + * Additional validation that is missing in OTEL SDK, but causes crashes on otel-collector + * when sending non-finite numbers (e.g. NaN, Infinity) + */ + if (typeof value === "number" && !Number.isFinite(value)) { + return false; + } }); return filteredAttributesEntries From c5fcd5756ea32362061ec07b2f839a487bfbea13 Mon Sep 17 00:00:00 2001 From: Lukasz Ostrowski Date: Tue, 28 May 2024 12:07:24 +0200 Subject: [PATCH 09/36] wip --- .gitignore | 3 ++ .../public-log-drain.service.ts | 31 +++++++++++++++---- .../api/webhooks/order-calculate-taxes.ts | 9 +++--- 3 files changed, 33 insertions(+), 10 deletions(-) diff --git a/.gitignore b/.gitignore index 0bd4a1372..f99f2decb 100644 --- a/.gitignore +++ b/.gitignore @@ -49,3 +49,6 @@ apps/**/generated # bruno cloud.bru + + +analyze \ No newline at end of file diff --git a/apps/avatax/src/modules/public-log-drain/public-log-drain.service.ts b/apps/avatax/src/modules/public-log-drain/public-log-drain.service.ts index 194bf6fe7..27a18e69d 100644 --- a/apps/avatax/src/modules/public-log-drain/public-log-drain.service.ts +++ b/apps/avatax/src/modules/public-log-drain/public-log-drain.service.ts @@ -1,9 +1,12 @@ import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-http"; -import { sharedOtelConfig } from "@saleor/apps-otel/src/shared-config"; import { IResource } from "@opentelemetry/resources"; import { timeInputToHrTime, isAttributeValue } from "@opentelemetry/core"; import { Attributes } from "@opentelemetry/api"; import * as packageJson from "../../../package.json"; +import { + SEMRESATTRS_SERVICE_NAME, + SEMRESATTRS_SERVICE_VERSION, +} from "@opentelemetry/semantic-conventions"; export interface PublicLog = {}> { message: string; @@ -153,6 +156,18 @@ export class LogDrainOtelTransporter implements LogDrainTransporter { }, {}); } + constructor(settings?: { url: string; headers: Record }) { + if (!settings) { + return; + } + + this.otelExporter = new OTLPLogExporter({ + url: settings.url, + timeoutMillis: 2000, + headers: settings.headers, + }); + } + async emit(log: PublicLog): Promise { return new Promise((res, rej) => { if (!this.otelExporter) { @@ -160,8 +175,8 @@ export class LogDrainOtelTransporter implements LogDrainTransporter { } const resourceAttributes: Attributes = { - "service.name": "saleor-app-avatax", - "service.version": packageJson.version, + [SEMRESATTRS_SERVICE_NAME]: "saleor-app-avatax", + [SEMRESATTRS_SERVICE_VERSION]: packageJson.version, }; const resource: IResource = { @@ -208,13 +223,17 @@ export class LogDrainOtelTransporter implements LogDrainTransporter { }); } - setSettings(settings: { url: string; logRecordLimit: Required }) { + setSettings(settings: { + url: string; + headers: Record; + logRecordLimit?: Required; + }) { this.otelExporter = new OTLPLogExporter({ - headers: sharedOtelConfig.exporterHeaders, url: settings.url, timeoutMillis: 2000, + headers: settings.headers, }); - if (this.logRecordLimit) { + if (settings.logRecordLimit) { if (this.logRecordLimit.attributeValueLengthLimit <= 0) { throw new Error("attributeValueLengthLimit cannot be less than 0"); } diff --git a/apps/avatax/src/pages/api/webhooks/order-calculate-taxes.ts b/apps/avatax/src/pages/api/webhooks/order-calculate-taxes.ts index b1506aa9d..7a7ed3ea8 100644 --- a/apps/avatax/src/pages/api/webhooks/order-calculate-taxes.ts +++ b/apps/avatax/src/pages/api/webhooks/order-calculate-taxes.ts @@ -32,10 +32,6 @@ const subscriptionErrorChecker = new SubscriptionPayloadErrorChecker(logger, cap const otelLogDrainTransporter = new LogDrainOtelTransporter(); -otelLogDrainTransporter.setSettings({ - url: "http://192.168.1.108:4318/v1/logs", -}); - const publicLoggerOtel = new PublicLogDrainService(otelLogDrainTransporter); export default wrapWithLoggerContext( @@ -124,6 +120,11 @@ export default wrapWithLoggerContext( logger.info("Taxes calculated", { calculatedTaxes }); + otelLogDrainTransporter.setSettings({ + headers: {}, + url: "TODO", + }); + await publicLoggerOtel.emitLog(new TaxesCalculatedLog()); return res.status(200).json(ctx.buildResponse(calculatedTaxes)); From c74768656093a76244850bf4668dd3418002b3cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20=C5=BBuraw?= <9116238+krzysztofzuraw@users.noreply.github.com> Date: Tue, 28 May 2024 12:13:06 +0200 Subject: [PATCH 10/36] Basic otel section --- apps/avatax/src/modules/ui/otel-section.tsx | 42 +++++++++++++++++++++ apps/avatax/src/pages/configuration.tsx | 4 +- 2 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 apps/avatax/src/modules/ui/otel-section.tsx diff --git a/apps/avatax/src/modules/ui/otel-section.tsx b/apps/avatax/src/modules/ui/otel-section.tsx new file mode 100644 index 000000000..262dbaff0 --- /dev/null +++ b/apps/avatax/src/modules/ui/otel-section.tsx @@ -0,0 +1,42 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import { Input } from "@saleor/react-hook-form-macaw"; +import { FormProvider, useForm } from "react-hook-form"; +import { z } from "zod"; +import { AppCard } from "./app-card"; +import { Section } from "./app-section"; + +const otelSectionSchema = z.object({ + url: z.string().url(), +}); + +type OtelConfig = z.infer; + +const defaultOtelConfig: OtelConfig = { + url: "", +}; + +export const OTELSection = () => { + const formMethods = useForm({ + defaultValues: defaultOtelConfig, + resolver: zodResolver(otelSectionSchema), + }); + + const { control } = formMethods; + + return ( + <> + Lorem ipsum dolor sit amet consectetur adipisicing elit. Obcaecati, minima quam? + } + /> + + + + + + + ); +}; diff --git a/apps/avatax/src/pages/configuration.tsx b/apps/avatax/src/pages/configuration.tsx index e5b899b4c..03a4ea650 100644 --- a/apps/avatax/src/pages/configuration.tsx +++ b/apps/avatax/src/pages/configuration.tsx @@ -1,10 +1,11 @@ import { useAppBridge } from "@saleor/app-sdk/app-bridge"; +import { Text } from "@saleor/macaw-ui"; import { ChannelSection } from "../modules/channel-configuration/ui/channel-section"; import { ProvidersSection } from "../modules/provider-connections/ui/providers-section"; import { AppPageLayout } from "../modules/ui/app-page-layout"; import { Section } from "../modules/ui/app-section"; import { MatcherSection } from "../modules/ui/matcher-section"; -import { Text } from "@saleor/macaw-ui"; +import { OTELSection } from "../modules/ui/otel-section"; const Header = () => { return ( @@ -38,6 +39,7 @@ const ConfigurationPage = () => { + ); }; From 09a9b67d814207994813110b4523a366001738c7 Mon Sep 17 00:00:00 2001 From: Lukasz Ostrowski Date: Tue, 28 May 2024 12:18:58 +0200 Subject: [PATCH 11/36] change to multiple tansporters --- .../use-case/calculate-taxes.use-case.ts | 18 ++++++++++++++++++ .../public-log-drain.service.ts | 18 ++++++++++++------ .../api/webhooks/checkout-calculate-taxes.ts | 6 ++++++ .../api/webhooks/order-calculate-taxes.ts | 3 ++- 4 files changed, 38 insertions(+), 7 deletions(-) diff --git a/apps/avatax/src/modules/calculate-taxes/use-case/calculate-taxes.use-case.ts b/apps/avatax/src/modules/calculate-taxes/use-case/calculate-taxes.use-case.ts index 14d599d2c..65b38c11d 100644 --- a/apps/avatax/src/modules/calculate-taxes/use-case/calculate-taxes.use-case.ts +++ b/apps/avatax/src/modules/calculate-taxes/use-case/calculate-taxes.use-case.ts @@ -11,6 +11,11 @@ import * as Sentry from "@sentry/nextjs"; import { captureException } from "@sentry/nextjs"; import { AvataxCalculateTaxesResponse } from "../../avatax/calculate-taxes/avatax-calculate-taxes-adapter"; import { MetadataItem } from "../../../../generated/graphql"; +import { + LogDrainOtelTransporter, + PublicLogDrain, + TaxesCalculatedLog, +} from "../../public-log-drain/public-log-drain.service"; export class CalculateTaxesUseCase { private logger = createLogger("CalculateTaxesUseCase"); @@ -28,6 +33,7 @@ export class CalculateTaxesUseCase { constructor( private deps: { configExtractor: IAppConfigExtractor; + publicLogDrain: PublicLogDrain; }, ) {} @@ -162,6 +168,16 @@ export class CalculateTaxesUseCase { ); } + this.deps.publicLogDrain + .getTransporters() + .filter((t) => t instanceof LogDrainOtelTransporter) + .forEach((t) => { + (t as LogDrainOtelTransporter).setSettings({ + headers: {}, + url: "", // TODO Krzysiek + }); + }); + return fromPromise( taxProvider.calculateTaxes(payload, providerConfig.value.avataxConfig.config, authData), (err) => @@ -171,6 +187,8 @@ export class CalculateTaxesUseCase { ).map((results) => { this.logger.info("Taxes calculated", { calculatedTaxes: results }); + this.deps.publicLogDrain.emitLog(new TaxesCalculatedLog()); + return results; }); } diff --git a/apps/avatax/src/modules/public-log-drain/public-log-drain.service.ts b/apps/avatax/src/modules/public-log-drain/public-log-drain.service.ts index 27a18e69d..1c624e207 100644 --- a/apps/avatax/src/modules/public-log-drain/public-log-drain.service.ts +++ b/apps/avatax/src/modules/public-log-drain/public-log-drain.service.ts @@ -25,7 +25,9 @@ export class TaxesCalculatedLog implements PublicLog { } export interface PublicLogDrain { - emitLog(log: PublicLog): Promise; + emitLog(log: PublicLog): Promise; + getTransporters(): LogDrainTransporter[]; + addTransporter(transporter: LogDrainTransporter): void; } export interface LogDrainTransporter { @@ -33,14 +35,18 @@ export interface LogDrainTransporter { } export class PublicLogDrainService implements PublicLogDrain { - constructor(private transporter: LogDrainTransporter) {} + constructor(private transporters: [LogDrainTransporter]) {} - emitLog(log: PublicLog): Promise { - return this.transporter.emit(log); + addTransporter(transporter: LogDrainTransporter) { + this.transporters.push(transporter); } - getTransport() { - return this.transporter; + emitLog(log: PublicLog): Promise { + return Promise.all(this.transporters.map((t) => t.emit(log))); + } + + getTransporters() { + return this.transporters; } } diff --git a/apps/avatax/src/pages/api/webhooks/checkout-calculate-taxes.ts b/apps/avatax/src/pages/api/webhooks/checkout-calculate-taxes.ts index a2ede64ee..56d7deb51 100644 --- a/apps/avatax/src/pages/api/webhooks/checkout-calculate-taxes.ts +++ b/apps/avatax/src/pages/api/webhooks/checkout-calculate-taxes.ts @@ -13,6 +13,10 @@ import { loggerContext } from "../../../logger-context"; import { CalculateTaxesUseCase } from "../../../modules/calculate-taxes/use-case/calculate-taxes.use-case"; import { AvataxInvalidAddressError } from "../../../modules/taxes/tax-error"; import { checkoutCalculateTaxesSyncWebhook } from "../../../modules/webhooks/definitions/checkout-calculate-taxes"; +import { + LogDrainOtelTransporter, + PublicLogDrainService, +} from "../../../modules/public-log-drain/public-log-drain.service"; export const config = { api: { @@ -27,6 +31,8 @@ const withMetadataCache = wrapWithMetadataCache(metadataCache); const subscriptionErrorChecker = new SubscriptionPayloadErrorChecker(logger, captureException); const useCase = new CalculateTaxesUseCase({ configExtractor: new AppConfigExtractor(), + // TODO: Use addTransporter dynamically once we resolve config + publicLogDrain: new PublicLogDrainService([new LogDrainOtelTransporter()]), }); /** diff --git a/apps/avatax/src/pages/api/webhooks/order-calculate-taxes.ts b/apps/avatax/src/pages/api/webhooks/order-calculate-taxes.ts index 7a7ed3ea8..6f98bc9da 100644 --- a/apps/avatax/src/pages/api/webhooks/order-calculate-taxes.ts +++ b/apps/avatax/src/pages/api/webhooks/order-calculate-taxes.ts @@ -32,7 +32,7 @@ const subscriptionErrorChecker = new SubscriptionPayloadErrorChecker(logger, cap const otelLogDrainTransporter = new LogDrainOtelTransporter(); -const publicLoggerOtel = new PublicLogDrainService(otelLogDrainTransporter); +const publicLoggerOtel = new PublicLogDrainService([otelLogDrainTransporter]); export default wrapWithLoggerContext( withOtel( @@ -120,6 +120,7 @@ export default wrapWithLoggerContext( logger.info("Taxes calculated", { calculatedTaxes }); + // TODO: Krzysiek add metadata fetching here otelLogDrainTransporter.setSettings({ headers: {}, url: "TODO", From af79cbc31e3fd8e61af3c6e00307469ff32c1651 Mon Sep 17 00:00:00 2001 From: Jonatan Witoszek Date: Tue, 28 May 2024 12:20:41 +0200 Subject: [PATCH 12/36] Map log severity to a number --- .../public-log-drain.service.ts | 42 +++++++++++++++---- 1 file changed, 35 insertions(+), 7 deletions(-) diff --git a/apps/avatax/src/modules/public-log-drain/public-log-drain.service.ts b/apps/avatax/src/modules/public-log-drain/public-log-drain.service.ts index 1c624e207..11b7b1fd4 100644 --- a/apps/avatax/src/modules/public-log-drain/public-log-drain.service.ts +++ b/apps/avatax/src/modules/public-log-drain/public-log-drain.service.ts @@ -8,11 +8,22 @@ import { SEMRESATTRS_SERVICE_VERSION, } from "@opentelemetry/semantic-conventions"; +export const LogSeverityLevel = { + TRACE: "TRACE", + DEBUG: "DEBUG", + INFO: "INFO", + WARN: "WARN", + ERROR: "ERROR", + FATAL: "FATAL", +} as const; + +export type LogSeverityLevelType = keyof typeof LogSeverityLevel; + export interface PublicLog = {}> { message: string; eventType: string; // enum timestamp: Date; - level: string; // todo enum, maybe otel level + level: LogSeverityLevelType; attributes: T; } @@ -20,7 +31,7 @@ export class TaxesCalculatedLog implements PublicLog { message = "Taxes calculated"; eventType = "TAXES_CALCULATED" as const; timestamp = new Date(); - level = "info"; + level = LogSeverityLevel.INFO; attributes = {}; } @@ -92,6 +103,27 @@ export class LogDrainOtelTransporter implements LogDrainTransporter { attributeCountLimit: 128, }; + /* + * Maps seveity level to a matching number in OTEL specification range + * https://opentelemetry.io/docs/specs/otel/logs/data-model/#field-severitynumber + */ + private _mapSeverityToOtelNumber(severityLevel: LogSeverityLevelType) { + switch (severityLevel) { + case "TRACE": + return 1; + case "DEBUG": + return 5; + case "INFO": + return 9; + case "WARN": + return 13; + case "ERROR": + return 17; + case "FATAL": + return 21; + } + } + private _truncateSize(value: unknown) { const limit = this.logRecordLimit.attributeValueLengthLimit; @@ -201,11 +233,7 @@ export class LogDrainOtelTransporter implements LogDrainTransporter { body: log.message, attributes, severityText: log.level, - /* - * TODO: Map severity to OTEL levels - * severityNumber: "", - */ - severityNumber: 0, // todo + severityNumber: this._mapSeverityToOtelNumber(log.level), hrTimeObserved: timeInputToHrTime(log.timestamp), hrTime: timeInputToHrTime(log.timestamp), /* From d6da7b7a30dd5fafd0bf40cf6772ff8edbd340bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20=C5=BBuraw?= <9116238+krzysztofzuraw@users.noreply.github.com> Date: Tue, 28 May 2024 12:34:51 +0200 Subject: [PATCH 13/36] full UI --- apps/avatax/src/modules/ui/otel-section.tsx | 25 +++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/apps/avatax/src/modules/ui/otel-section.tsx b/apps/avatax/src/modules/ui/otel-section.tsx index 262dbaff0..a0f01c142 100644 --- a/apps/avatax/src/modules/ui/otel-section.tsx +++ b/apps/avatax/src/modules/ui/otel-section.tsx @@ -1,18 +1,21 @@ import { zodResolver } from "@hookform/resolvers/zod"; +import { Box, Button } from "@saleor/macaw-ui"; import { Input } from "@saleor/react-hook-form-macaw"; -import { FormProvider, useForm } from "react-hook-form"; +import { FormProvider, useFieldArray, useForm } from "react-hook-form"; import { z } from "zod"; import { AppCard } from "./app-card"; import { Section } from "./app-section"; const otelSectionSchema = z.object({ url: z.string().url(), + headers: z.array(z.object({ key: z.string(), value: z.string() })), }); type OtelConfig = z.infer; const defaultOtelConfig: OtelConfig = { url: "", + headers: [], }; export const OTELSection = () => { @@ -23,6 +26,11 @@ export const OTELSection = () => { const { control } = formMethods; + const { fields, append, remove } = useFieldArray({ + control, + name: "headers", + }); + return ( <> { <>Lorem ipsum dolor sit amet consectetur adipisicing elit. Obcaecati, minima quam? } /> - + + + Add additional headers required by your OTEL collector. + + {fields.map((field, index) => ( + + + + + + ))} + + + From 3a2a770f87384a757f4cabe8cf203ec0961de8a2 Mon Sep 17 00:00:00 2001 From: Lukasz Ostrowski Date: Tue, 28 May 2024 12:52:57 +0200 Subject: [PATCH 14/36] refactor --- .../modules/public-log-drain/public-events.ts | 26 ++ .../public-log-drain.service.ts | 266 +----------------- .../public-log-drain/public-log-drain.ts | 11 + .../public-log-drain-json-transporter.ts | 25 ++ .../public-log-drain-otel-transporter.ts | 206 ++++++++++++++ .../api/webhooks/checkout-calculate-taxes.ts | 6 +- 6 files changed, 272 insertions(+), 268 deletions(-) create mode 100644 apps/avatax/src/modules/public-log-drain/public-events.ts create mode 100644 apps/avatax/src/modules/public-log-drain/public-log-drain.ts create mode 100644 apps/avatax/src/modules/public-log-drain/transporters/public-log-drain-json-transporter.ts create mode 100644 apps/avatax/src/modules/public-log-drain/transporters/public-log-drain-otel-transporter.ts diff --git a/apps/avatax/src/modules/public-log-drain/public-events.ts b/apps/avatax/src/modules/public-log-drain/public-events.ts new file mode 100644 index 000000000..2811d2b02 --- /dev/null +++ b/apps/avatax/src/modules/public-log-drain/public-events.ts @@ -0,0 +1,26 @@ +export const LogSeverityLevel = { + TRACE: "TRACE", + DEBUG: "DEBUG", + INFO: "INFO", + WARN: "WARN", + ERROR: "ERROR", + FATAL: "FATAL", +} as const; + +export type LogSeverityLevelType = keyof typeof LogSeverityLevel; + +export interface PublicLog = {}> { + message: string; + eventType: string; // enum + timestamp: Date; + level: LogSeverityLevelType; + attributes: T; +} + +export class TaxesCalculatedLog implements PublicLog { + message = "Taxes calculated"; + eventType = "TAXES_CALCULATED" as const; + timestamp = new Date(); + level = LogSeverityLevel.INFO; + attributes = {}; +} diff --git a/apps/avatax/src/modules/public-log-drain/public-log-drain.service.ts b/apps/avatax/src/modules/public-log-drain/public-log-drain.service.ts index 11b7b1fd4..221ac5250 100644 --- a/apps/avatax/src/modules/public-log-drain/public-log-drain.service.ts +++ b/apps/avatax/src/modules/public-log-drain/public-log-drain.service.ts @@ -1,49 +1,5 @@ -import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-http"; -import { IResource } from "@opentelemetry/resources"; -import { timeInputToHrTime, isAttributeValue } from "@opentelemetry/core"; -import { Attributes } from "@opentelemetry/api"; -import * as packageJson from "../../../package.json"; -import { - SEMRESATTRS_SERVICE_NAME, - SEMRESATTRS_SERVICE_VERSION, -} from "@opentelemetry/semantic-conventions"; - -export const LogSeverityLevel = { - TRACE: "TRACE", - DEBUG: "DEBUG", - INFO: "INFO", - WARN: "WARN", - ERROR: "ERROR", - FATAL: "FATAL", -} as const; - -export type LogSeverityLevelType = keyof typeof LogSeverityLevel; - -export interface PublicLog = {}> { - message: string; - eventType: string; // enum - timestamp: Date; - level: LogSeverityLevelType; - attributes: T; -} - -export class TaxesCalculatedLog implements PublicLog { - message = "Taxes calculated"; - eventType = "TAXES_CALCULATED" as const; - timestamp = new Date(); - level = LogSeverityLevel.INFO; - attributes = {}; -} - -export interface PublicLogDrain { - emitLog(log: PublicLog): Promise; - getTransporters(): LogDrainTransporter[]; - addTransporter(transporter: LogDrainTransporter): void; -} - -export interface LogDrainTransporter { - emit(log: PublicLog): Promise; -} +import { LogDrainTransporter, PublicLogDrain } from "./public-log-drain"; +import { PublicLog } from "./public-events"; export class PublicLogDrainService implements PublicLogDrain { constructor(private transporters: [LogDrainTransporter]) {} @@ -60,221 +16,3 @@ export class PublicLogDrainService implements PublicLogDrain { return this.transporters; } } - -export class LogDrainJsonTransporter implements LogDrainTransporter { - private endpoint: string | null = null; - - async emit(log: PublicLog): Promise { - if (!this.endpoint) { - throw new Error("Endpoint is not set, call setSettings first"); - } - - return fetch(this.endpoint, { - method: "POST", - body: JSON.stringify(log), - }) - .then((r) => r.json()) - .then((res) => { - console.log(res); // todo - - return; - }); - } - - setSettings() {} -} - -export interface LogRecordLimits { - /** attributeValueLengthLimit is maximum allowed attribute value size */ - attributeValueLengthLimit?: number; - - /** attributeCountLimit is number of attributes per LogRecord */ - attributeCountLimit?: number; -} - -export class LogDrainOtelTransporter implements LogDrainTransporter { - private otelExporter: OTLPLogExporter | null = null; - private logRecordLimit: Required = { - /* - * Default values used by OTEL spec - * https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/#attribute-limits - */ - attributeValueLengthLimit: Infinity, - attributeCountLimit: 128, - }; - - /* - * Maps seveity level to a matching number in OTEL specification range - * https://opentelemetry.io/docs/specs/otel/logs/data-model/#field-severitynumber - */ - private _mapSeverityToOtelNumber(severityLevel: LogSeverityLevelType) { - switch (severityLevel) { - case "TRACE": - return 1; - case "DEBUG": - return 5; - case "INFO": - return 9; - case "WARN": - return 13; - case "ERROR": - return 17; - case "FATAL": - return 21; - } - } - - private _truncateSize(value: unknown) { - const limit = this.logRecordLimit.attributeValueLengthLimit; - - const truncateToLimit = (value: string) => { - if (value.length <= limit) { - return value; - } - return value.substring(0, limit); - }; - - if (typeof value === "string") { - return truncateToLimit(value); - } - - if (Array.isArray(value)) { - return (value as []).map((val) => (typeof val === "string" ? truncateToLimit(val) : val)); - } - - // If value is of another type, we can safely return it as is - return value; - } - - private _filterAndTruncateAttributes(attributes: Record) { - /* - * We must filter out non-serializable values and truncate ones that exceed limits - * https://opentelemetry.io/docs/specs/otel/common/#attribute - */ - const filteredAttributesEntries = Object.entries(attributes).filter(([key, value]) => { - if (value === null) { - return false; - } - if (key.length === 0) { - return false; - } - if ( - !isAttributeValue(value) && - !(typeof value === "object" && !Array.isArray(value) && Object.keys(value).length > 0) - ) { - return false; - } - /* - * Additional validation that is missing in OTEL SDK, but causes crashes on otel-collector - * when sending non-finite numbers (e.g. NaN, Infinity) - */ - if (typeof value === "number" && !Number.isFinite(value)) { - return false; - } - }); - - return filteredAttributesEntries - .slice(0, this.logRecordLimit.attributeCountLimit) - .reduce((acc, [key, value]) => { - if (Object.keys(acc).length >= this.logRecordLimit.attributeCountLimit) { - return acc; - } - - if (isAttributeValue(value)) { - return { - ...acc, - [key]: this._truncateSize(value), - }; - } else { - return { - ...acc, - [key]: value, - }; - } - }, {}); - } - - constructor(settings?: { url: string; headers: Record }) { - if (!settings) { - return; - } - - this.otelExporter = new OTLPLogExporter({ - url: settings.url, - timeoutMillis: 2000, - headers: settings.headers, - }); - } - - async emit(log: PublicLog): Promise { - return new Promise((res, rej) => { - if (!this.otelExporter) { - throw new Error("Call setSettings first"); - } - - const resourceAttributes: Attributes = { - [SEMRESATTRS_SERVICE_NAME]: "saleor-app-avatax", - [SEMRESATTRS_SERVICE_VERSION]: packageJson.version, - }; - - const resource: IResource = { - attributes: resourceAttributes, - // This is a workaround to support OTEL SDK types - merge(): IResource { - return this; - }, - }; - - const attributes = this._filterAndTruncateAttributes(log.attributes); - - return this.otelExporter.export( - [ - { - body: log.message, - attributes, - severityText: log.level, - severityNumber: this._mapSeverityToOtelNumber(log.level), - hrTimeObserved: timeInputToHrTime(log.timestamp), - hrTime: timeInputToHrTime(log.timestamp), - /* - * TODO: Pass traceId to logs - * spanContext: ... - */ - resource, - droppedAttributesCount: - Object.keys(log.attributes).length - Object.keys(attributes).length, - instrumentationScope: { name: "LogDrainOtelTransporter" }, - }, - ], - (cb) => { - if (cb.error) { - rej(cb.error); - } else { - res(); - } - }, - ); - }); - } - - setSettings(settings: { - url: string; - headers: Record; - logRecordLimit?: Required; - }) { - this.otelExporter = new OTLPLogExporter({ - url: settings.url, - timeoutMillis: 2000, - headers: settings.headers, - }); - if (settings.logRecordLimit) { - if (this.logRecordLimit.attributeValueLengthLimit <= 0) { - throw new Error("attributeValueLengthLimit cannot be less than 0"); - } - if (this.logRecordLimit.attributeCountLimit <= 0) { - throw new Error("attributeCountLimit cannot be less than 0"); - } - this.logRecordLimit = settings.logRecordLimit; - } - } -} diff --git a/apps/avatax/src/modules/public-log-drain/public-log-drain.ts b/apps/avatax/src/modules/public-log-drain/public-log-drain.ts new file mode 100644 index 000000000..6b799bb72 --- /dev/null +++ b/apps/avatax/src/modules/public-log-drain/public-log-drain.ts @@ -0,0 +1,11 @@ +import { PublicLog } from "./public-events"; + +export interface PublicLogDrain { + emitLog(log: PublicLog): Promise; + getTransporters(): LogDrainTransporter[]; + addTransporter(transporter: LogDrainTransporter): void; +} + +export interface LogDrainTransporter { + emit(log: PublicLog): Promise; +} diff --git a/apps/avatax/src/modules/public-log-drain/transporters/public-log-drain-json-transporter.ts b/apps/avatax/src/modules/public-log-drain/transporters/public-log-drain-json-transporter.ts new file mode 100644 index 000000000..d8543ae3d --- /dev/null +++ b/apps/avatax/src/modules/public-log-drain/transporters/public-log-drain-json-transporter.ts @@ -0,0 +1,25 @@ +import { LogDrainTransporter } from "../public-log-drain"; +import { PublicLog } from "../public-events"; + +export class LogDrainJsonTransporter implements LogDrainTransporter { + private endpoint: string | null = null; + + async emit(log: PublicLog): Promise { + if (!this.endpoint) { + throw new Error("Endpoint is not set, call setSettings first"); + } + + return fetch(this.endpoint, { + method: "POST", + body: JSON.stringify(log), + }) + .then((r) => r.json()) + .then((res) => { + console.log(res); // todo + + return; + }); + } + + setSettings() {} +} diff --git a/apps/avatax/src/modules/public-log-drain/transporters/public-log-drain-otel-transporter.ts b/apps/avatax/src/modules/public-log-drain/transporters/public-log-drain-otel-transporter.ts new file mode 100644 index 000000000..03b9cac83 --- /dev/null +++ b/apps/avatax/src/modules/public-log-drain/transporters/public-log-drain-otel-transporter.ts @@ -0,0 +1,206 @@ +import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-http"; +import { isAttributeValue, timeInputToHrTime } from "@opentelemetry/core"; +import { Attributes } from "@opentelemetry/api"; +import { + SEMRESATTRS_SERVICE_NAME, + SEMRESATTRS_SERVICE_VERSION, +} from "@opentelemetry/semantic-conventions"; +import * as packageJson from "../../../../package.json"; +import { IResource } from "@opentelemetry/resources"; +import { LogSeverityLevelType, PublicLog } from "../public-events"; +import { LogDrainTransporter } from "../public-log-drain"; + +export interface LogRecordLimits { + /** attributeValueLengthLimit is maximum allowed attribute value size */ + attributeValueLengthLimit?: number; + + /** attributeCountLimit is number of attributes per LogRecord */ + attributeCountLimit?: number; +} + +export class LogDrainOtelTransporter implements LogDrainTransporter { + private otelExporter: OTLPLogExporter | null = null; + private logRecordLimit: Required = { + /* + * Default values used by OTEL spec + * https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/#attribute-limits + */ + attributeValueLengthLimit: Infinity, + attributeCountLimit: 128, + }; + + /* + * Maps seveity level to a matching number in OTEL specification range + * https://opentelemetry.io/docs/specs/otel/logs/data-model/#field-severitynumber + */ + private _mapSeverityToOtelNumber(severityLevel: LogSeverityLevelType) { + switch (severityLevel) { + case "TRACE": + return 1; + case "DEBUG": + return 5; + case "INFO": + return 9; + case "WARN": + return 13; + case "ERROR": + return 17; + case "FATAL": + return 21; + } + } + + private _truncateSize(value: unknown) { + const limit = this.logRecordLimit.attributeValueLengthLimit; + + const truncateToLimit = (value: string) => { + if (value.length <= limit) { + return value; + } + return value.substring(0, limit); + }; + + if (typeof value === "string") { + return truncateToLimit(value); + } + + if (Array.isArray(value)) { + return (value as []).map((val) => (typeof val === "string" ? truncateToLimit(val) : val)); + } + + // If value is of another type, we can safely return it as is + return value; + } + + private _filterAndTruncateAttributes(attributes: Record) { + /* + * We must filter out non-serializable values and truncate ones that exceed limits + * https://opentelemetry.io/docs/specs/otel/common/#attribute + */ + const filteredAttributesEntries = Object.entries(attributes).filter(([key, value]) => { + if (value === null) { + return false; + } + if (key.length === 0) { + return false; + } + if ( + !isAttributeValue(value) && + !(typeof value === "object" && !Array.isArray(value) && Object.keys(value).length > 0) + ) { + return false; + } + /* + * Additional validation that is missing in OTEL SDK, but causes crashes on otel-collector + * when sending non-finite numbers (e.g. NaN, Infinity) + */ + if (typeof value === "number" && !Number.isFinite(value)) { + return false; + } + }); + + return filteredAttributesEntries + .slice(0, this.logRecordLimit.attributeCountLimit) + .reduce((acc, [key, value]) => { + if (Object.keys(acc).length >= this.logRecordLimit.attributeCountLimit) { + return acc; + } + + if (isAttributeValue(value)) { + return { + ...acc, + [key]: this._truncateSize(value), + }; + } else { + return { + ...acc, + [key]: value, + }; + } + }, {}); + } + + constructor(settings?: { url: string; headers: Record }) { + if (!settings) { + return; + } + + this.otelExporter = new OTLPLogExporter({ + url: settings.url, + timeoutMillis: 2000, + headers: settings.headers, + }); + } + + async emit(log: PublicLog): Promise { + return new Promise((res, rej) => { + if (!this.otelExporter) { + throw new Error("Call setSettings first"); + } + + const resourceAttributes: Attributes = { + [SEMRESATTRS_SERVICE_NAME]: "saleor-app-avatax", + [SEMRESATTRS_SERVICE_VERSION]: packageJson.version, + }; + + const resource: IResource = { + attributes: resourceAttributes, + // This is a workaround to support OTEL SDK types + merge(): IResource { + return this; + }, + }; + + const attributes = this._filterAndTruncateAttributes(log.attributes); + + return this.otelExporter.export( + [ + { + body: log.message, + attributes, + severityText: log.level, + severityNumber: this._mapSeverityToOtelNumber(log.level), + hrTimeObserved: timeInputToHrTime(log.timestamp), + hrTime: timeInputToHrTime(log.timestamp), + /* + * TODO: Pass traceId to logs + * spanContext: ... + */ + resource, + droppedAttributesCount: + Object.keys(log.attributes).length - Object.keys(attributes).length, + instrumentationScope: { name: "LogDrainOtelTransporter" }, + }, + ], + (cb) => { + if (cb.error) { + rej(cb.error); + } else { + res(); + } + }, + ); + }); + } + + setSettings(settings: { + url: string; + headers: Record; + logRecordLimit?: Required; + }) { + this.otelExporter = new OTLPLogExporter({ + url: settings.url, + timeoutMillis: 2000, + headers: settings.headers, + }); + if (settings.logRecordLimit) { + if (this.logRecordLimit.attributeValueLengthLimit <= 0) { + throw new Error("attributeValueLengthLimit cannot be less than 0"); + } + if (this.logRecordLimit.attributeCountLimit <= 0) { + throw new Error("attributeCountLimit cannot be less than 0"); + } + this.logRecordLimit = settings.logRecordLimit; + } + } +} diff --git a/apps/avatax/src/pages/api/webhooks/checkout-calculate-taxes.ts b/apps/avatax/src/pages/api/webhooks/checkout-calculate-taxes.ts index 56d7deb51..c359c5728 100644 --- a/apps/avatax/src/pages/api/webhooks/checkout-calculate-taxes.ts +++ b/apps/avatax/src/pages/api/webhooks/checkout-calculate-taxes.ts @@ -13,10 +13,8 @@ import { loggerContext } from "../../../logger-context"; import { CalculateTaxesUseCase } from "../../../modules/calculate-taxes/use-case/calculate-taxes.use-case"; import { AvataxInvalidAddressError } from "../../../modules/taxes/tax-error"; import { checkoutCalculateTaxesSyncWebhook } from "../../../modules/webhooks/definitions/checkout-calculate-taxes"; -import { - LogDrainOtelTransporter, - PublicLogDrainService, -} from "../../../modules/public-log-drain/public-log-drain.service"; +import { PublicLogDrainService } from "../../../modules/public-log-drain/public-log-drain.service"; +import { LogDrainOtelTransporter } from "../../../modules/public-log-drain/transporters/public-log-drain-otel-transporter"; export const config = { api: { From 2d27c2a6ed3c32a3113817decf2a26ad44dbe07e Mon Sep 17 00:00:00 2001 From: Jonatan Witoszek Date: Tue, 28 May 2024 12:58:10 +0200 Subject: [PATCH 15/36] Fix use case --- .../calculate-taxes/use-case/calculate-taxes.use-case.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/apps/avatax/src/modules/calculate-taxes/use-case/calculate-taxes.use-case.ts b/apps/avatax/src/modules/calculate-taxes/use-case/calculate-taxes.use-case.ts index 65b38c11d..b116429aa 100644 --- a/apps/avatax/src/modules/calculate-taxes/use-case/calculate-taxes.use-case.ts +++ b/apps/avatax/src/modules/calculate-taxes/use-case/calculate-taxes.use-case.ts @@ -11,11 +11,9 @@ import * as Sentry from "@sentry/nextjs"; import { captureException } from "@sentry/nextjs"; import { AvataxCalculateTaxesResponse } from "../../avatax/calculate-taxes/avatax-calculate-taxes-adapter"; import { MetadataItem } from "../../../../generated/graphql"; -import { - LogDrainOtelTransporter, - PublicLogDrain, - TaxesCalculatedLog, -} from "../../public-log-drain/public-log-drain.service"; +import { LogDrainOtelTransporter } from "../../public-log-drain/transporters/public-log-drain-otel-transporter"; +import { PublicLogDrain } from "../../public-log-drain/public-log-drain"; +import { TaxesCalculatedLog } from "../../public-log-drain/public-events"; export class CalculateTaxesUseCase { private logger = createLogger("CalculateTaxesUseCase"); From 7fab4ad0362342bc6f3e6338ed4f6ca738775784 Mon Sep 17 00:00:00 2001 From: Lukasz Ostrowski Date: Tue, 28 May 2024 12:58:20 +0200 Subject: [PATCH 16/36] fix test --- .../use-case/calculate-taxes.use-case.test.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/apps/avatax/src/modules/calculate-taxes/use-case/calculate-taxes.use-case.test.ts b/apps/avatax/src/modules/calculate-taxes/use-case/calculate-taxes.use-case.test.ts index f811b6669..7493f03be 100644 --- a/apps/avatax/src/modules/calculate-taxes/use-case/calculate-taxes.use-case.test.ts +++ b/apps/avatax/src/modules/calculate-taxes/use-case/calculate-taxes.use-case.test.ts @@ -7,6 +7,8 @@ import { AppConfigExtractor, IAppConfigExtractor } from "../../../lib/app-config import { AvataxWebhookServiceFactory } from "../../taxes/avatax-webhook-service-factory"; import { CalculateTaxesPayload } from "../../webhooks/payloads/calculate-taxes-payload"; import { CalculateTaxesUseCase } from "./calculate-taxes.use-case"; +import { PublicLogDrainService } from "../../public-log-drain/public-log-drain.service"; +import { PublicLog } from "../../public-log-drain/public-events"; const mockGetAppConfig = vi.fn>(); @@ -127,6 +129,11 @@ describe("CalculateTaxesUseCase", () => { instance = new CalculateTaxesUseCase({ configExtractor: MockConfigExtractor, + publicLogDrain: new PublicLogDrainService([ + { + async emit(log: PublicLog): Promise {}, + }, + ]), }); }); From dd5f5058141ddb1c754eade4165750d615a753f7 Mon Sep 17 00:00:00 2001 From: Lukasz Ostrowski Date: Tue, 28 May 2024 13:11:55 +0200 Subject: [PATCH 17/36] fix deps --- .../transporters/public-log-drain-otel-transporter.ts | 9 +++------ .../src/pages/api/webhooks/order-calculate-taxes.ts | 8 +++----- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/apps/avatax/src/modules/public-log-drain/transporters/public-log-drain-otel-transporter.ts b/apps/avatax/src/modules/public-log-drain/transporters/public-log-drain-otel-transporter.ts index 03b9cac83..d99e7804d 100644 --- a/apps/avatax/src/modules/public-log-drain/transporters/public-log-drain-otel-transporter.ts +++ b/apps/avatax/src/modules/public-log-drain/transporters/public-log-drain-otel-transporter.ts @@ -1,10 +1,7 @@ import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-http"; import { isAttributeValue, timeInputToHrTime } from "@opentelemetry/core"; import { Attributes } from "@opentelemetry/api"; -import { - SEMRESATTRS_SERVICE_NAME, - SEMRESATTRS_SERVICE_VERSION, -} from "@opentelemetry/semantic-conventions"; +import { SemanticResourceAttributes } from "@opentelemetry/semantic-conventions"; import * as packageJson from "../../../../package.json"; import { IResource } from "@opentelemetry/resources"; import { LogSeverityLevelType, PublicLog } from "../public-events"; @@ -139,8 +136,8 @@ export class LogDrainOtelTransporter implements LogDrainTransporter { } const resourceAttributes: Attributes = { - [SEMRESATTRS_SERVICE_NAME]: "saleor-app-avatax", - [SEMRESATTRS_SERVICE_VERSION]: packageJson.version, + [SemanticResourceAttributes.SERVICE_NAME]: "saleor-app-avatax", + [SemanticResourceAttributes.SERVICE_VERSION]: packageJson.version, }; const resource: IResource = { diff --git a/apps/avatax/src/pages/api/webhooks/order-calculate-taxes.ts b/apps/avatax/src/pages/api/webhooks/order-calculate-taxes.ts index 6f98bc9da..cf9ad6997 100644 --- a/apps/avatax/src/pages/api/webhooks/order-calculate-taxes.ts +++ b/apps/avatax/src/pages/api/webhooks/order-calculate-taxes.ts @@ -12,11 +12,9 @@ import { loggerContext } from "../../../logger-context"; import { AvataxInvalidAddressError } from "../../../modules/taxes/tax-error"; import { orderCalculateTaxesSyncWebhook } from "../../../modules/webhooks/definitions/order-calculate-taxes"; import { verifyCalculateTaxesPayload } from "../../../modules/webhooks/validate-webhook-payload"; -import { - LogDrainOtelTransporter, - PublicLogDrainService, - TaxesCalculatedLog, -} from "../../../modules/public-log-drain/public-log-drain.service"; +import { PublicLogDrainService } from "../../../modules/public-log-drain/public-log-drain.service"; +import { LogDrainOtelTransporter } from "../../../modules/public-log-drain/transporters/public-log-drain-otel-transporter"; +import { TaxesCalculatedLog } from "../../../modules/public-log-drain/public-events"; export const config = { api: { From fd5fb8d53f8e5a08836f330c3a894366fd78f33f Mon Sep 17 00:00:00 2001 From: Jonatan Witoszek Date: Tue, 28 May 2024 13:44:54 +0200 Subject: [PATCH 18/36] Add spanContext --- .../transporters/public-log-drain-otel-transporter.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/apps/avatax/src/modules/public-log-drain/transporters/public-log-drain-otel-transporter.ts b/apps/avatax/src/modules/public-log-drain/transporters/public-log-drain-otel-transporter.ts index d99e7804d..74536c7bf 100644 --- a/apps/avatax/src/modules/public-log-drain/transporters/public-log-drain-otel-transporter.ts +++ b/apps/avatax/src/modules/public-log-drain/transporters/public-log-drain-otel-transporter.ts @@ -1,6 +1,6 @@ import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-http"; import { isAttributeValue, timeInputToHrTime } from "@opentelemetry/core"; -import { Attributes } from "@opentelemetry/api"; +import { Attributes, trace } from "@opentelemetry/api"; import { SemanticResourceAttributes } from "@opentelemetry/semantic-conventions"; import * as packageJson from "../../../../package.json"; import { IResource } from "@opentelemetry/resources"; @@ -135,6 +135,8 @@ export class LogDrainOtelTransporter implements LogDrainTransporter { throw new Error("Call setSettings first"); } + const spanContext = trace.getActiveSpan()?.spanContext(); + const resourceAttributes: Attributes = { [SemanticResourceAttributes.SERVICE_NAME]: "saleor-app-avatax", [SemanticResourceAttributes.SERVICE_VERSION]: packageJson.version, @@ -159,10 +161,7 @@ export class LogDrainOtelTransporter implements LogDrainTransporter { severityNumber: this._mapSeverityToOtelNumber(log.level), hrTimeObserved: timeInputToHrTime(log.timestamp), hrTime: timeInputToHrTime(log.timestamp), - /* - * TODO: Pass traceId to logs - * spanContext: ... - */ + spanContext, resource, droppedAttributesCount: Object.keys(log.attributes).length - Object.keys(attributes).length, From 780a05b10bc5c182f421cd64e6118a2f0bed9fbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20=C5=BBuraw?= <9116238+krzysztofzuraw@users.noreply.github.com> Date: Tue, 28 May 2024 13:54:29 +0200 Subject: [PATCH 19/36] Save logs settings into metdata --- .../avatax/avatax-connection-schema.ts | 10 ++++ .../avatax-connection.service.ts | 1 + .../avatax/ui/avatax-configuration-form.tsx | 3 ++ .../avatax/ui/logs-settings-fragment.tsx | 51 +++++++++++++++++++ .../channel-configuration-merger.ts | 2 + 5 files changed, 67 insertions(+) create mode 100644 apps/avatax/src/modules/avatax/ui/logs-settings-fragment.tsx diff --git a/apps/avatax/src/modules/avatax/avatax-connection-schema.ts b/apps/avatax/src/modules/avatax/avatax-connection-schema.ts index 3583c4880..2c647909c 100644 --- a/apps/avatax/src/modules/avatax/avatax-connection-schema.ts +++ b/apps/avatax/src/modules/avatax/avatax-connection-schema.ts @@ -29,6 +29,15 @@ export const avataxConfigSchema = z shippingTaxCode: z.string().optional(), isDocumentRecordingEnabled: z.boolean().default(true), address: addressSchema, + logsSettings: z + .object({ + otel: z.object({ + url: z.string().url(), + headers: z.array(z.object({ key: z.string(), value: z.string() })), + }), + }) + .or(z.null()) + .default(null), }) .merge(baseAvataxConfigSchema); @@ -52,6 +61,7 @@ export const defaultAvataxConfig: AvataxConfig = { street: "", zip: "", }, + logsSettings: null, }; export const avataxConnectionSchema = z.object({ diff --git a/apps/avatax/src/modules/avatax/configuration/avatax-connection.service.ts b/apps/avatax/src/modules/avatax/configuration/avatax-connection.service.ts index d6d3cf3f2..88350f074 100644 --- a/apps/avatax/src/modules/avatax/configuration/avatax-connection.service.ts +++ b/apps/avatax/src/modules/avatax/configuration/avatax-connection.service.ts @@ -78,6 +78,7 @@ export class AvataxConnectionService { ...prevConfig.address, ...nextConfigPartial.address, }, + logsSettings: null, }; await this.checkIfAuthorized(input); diff --git a/apps/avatax/src/modules/avatax/ui/avatax-configuration-form.tsx b/apps/avatax/src/modules/avatax/ui/avatax-configuration-form.tsx index 35f5a4e6a..4208a1b04 100644 --- a/apps/avatax/src/modules/avatax/ui/avatax-configuration-form.tsx +++ b/apps/avatax/src/modules/avatax/ui/avatax-configuration-form.tsx @@ -17,6 +17,7 @@ import { AvataxConfigurationCredentialsFragment } from "./avatax-configuration-c import { AvataxConfigurationSettingsFragment } from "./avatax-configuration-settings-fragment"; import { useAvataxConfigurationStatus } from "./configuration-status"; import { HelperText } from "./form-helper-text"; +import { LogsSettingsFragment } from "./logs-settings-fragment"; type AvataxConfigurationFormProps = { submit: { @@ -83,6 +84,8 @@ export const AvataxConfigurationForm = (props: AvataxConfigurationFormProps) => isLoading={props.validateAddress.isLoading} /> + + {props.leftButton} diff --git a/apps/avatax/src/modules/avatax/ui/logs-settings-fragment.tsx b/apps/avatax/src/modules/avatax/ui/logs-settings-fragment.tsx new file mode 100644 index 000000000..ecf2f1651 --- /dev/null +++ b/apps/avatax/src/modules/avatax/ui/logs-settings-fragment.tsx @@ -0,0 +1,51 @@ +import { Box, Button } from "@saleor/macaw-ui"; +import { Input } from "@saleor/react-hook-form-macaw"; +import { useFieldArray, useFormContext } from "react-hook-form"; +import { AvataxConfig } from "../avatax-connection-schema"; +import { FormSection } from "./form-section"; + +export const LogsSettingsFragment = () => { + const { control } = useFormContext(); + + const { fields, append, remove } = useFieldArray({ + control, + name: "logsSettings.otel.headers", + }); + + return ( + <> + + + + + {fields.map((field, index) => ( + + + + + + ))} + + + + + + ); +}; diff --git a/apps/avatax/src/modules/channel-configuration/channel-configuration-merger.ts b/apps/avatax/src/modules/channel-configuration/channel-configuration-merger.ts index 2d21b6c35..30bb1b25c 100644 --- a/apps/avatax/src/modules/channel-configuration/channel-configuration-merger.ts +++ b/apps/avatax/src/modules/channel-configuration/channel-configuration-merger.ts @@ -13,6 +13,7 @@ export class ChannelConfigurationMerger { config: { providerConnectionId: null, slug: channel.slug, + orderSettings: null, }, }; } @@ -22,6 +23,7 @@ export class ChannelConfigurationMerger { config: { providerConnectionId: channelConfig.config.providerConnectionId, slug: channel.slug, + orderSettings: null, }, }; }); From 5be49e0c2de8e61d037ef270847f32cd18300629 Mon Sep 17 00:00:00 2001 From: Lukasz Ostrowski Date: Tue, 28 May 2024 13:59:07 +0200 Subject: [PATCH 20/36] add waituntil --- apps/avatax/package.json | 1 + .../use-case/calculate-taxes.use-case.ts | 3 ++- .../api/webhooks/order-calculate-taxes.ts | 3 ++- pnpm-lock.yaml | 19 ++++++++++--------- 4 files changed, 15 insertions(+), 11 deletions(-) diff --git a/apps/avatax/package.json b/apps/avatax/package.json index ffa678582..c7aa4c635 100644 --- a/apps/avatax/package.json +++ b/apps/avatax/package.json @@ -52,6 +52,7 @@ "@trpc/react-query": "10.43.1", "@trpc/server": "10.43.1", "@urql/exchange-auth": "^2.1.4", + "@vercel/functions": "1.0.2", "avatax": "^23.7.0", "dotenv": "^16.3.1", "graphql": "16.7.1", diff --git a/apps/avatax/src/modules/calculate-taxes/use-case/calculate-taxes.use-case.ts b/apps/avatax/src/modules/calculate-taxes/use-case/calculate-taxes.use-case.ts index b116429aa..8eec5dfb8 100644 --- a/apps/avatax/src/modules/calculate-taxes/use-case/calculate-taxes.use-case.ts +++ b/apps/avatax/src/modules/calculate-taxes/use-case/calculate-taxes.use-case.ts @@ -14,6 +14,7 @@ import { MetadataItem } from "../../../../generated/graphql"; import { LogDrainOtelTransporter } from "../../public-log-drain/transporters/public-log-drain-otel-transporter"; import { PublicLogDrain } from "../../public-log-drain/public-log-drain"; import { TaxesCalculatedLog } from "../../public-log-drain/public-events"; +import { waitUntil } from "@vercel/functions"; export class CalculateTaxesUseCase { private logger = createLogger("CalculateTaxesUseCase"); @@ -185,7 +186,7 @@ export class CalculateTaxesUseCase { ).map((results) => { this.logger.info("Taxes calculated", { calculatedTaxes: results }); - this.deps.publicLogDrain.emitLog(new TaxesCalculatedLog()); + waitUntil(this.deps.publicLogDrain.emitLog(new TaxesCalculatedLog())); return results; }); diff --git a/apps/avatax/src/pages/api/webhooks/order-calculate-taxes.ts b/apps/avatax/src/pages/api/webhooks/order-calculate-taxes.ts index cf9ad6997..63d7309a8 100644 --- a/apps/avatax/src/pages/api/webhooks/order-calculate-taxes.ts +++ b/apps/avatax/src/pages/api/webhooks/order-calculate-taxes.ts @@ -15,6 +15,7 @@ import { verifyCalculateTaxesPayload } from "../../../modules/webhooks/validate- import { PublicLogDrainService } from "../../../modules/public-log-drain/public-log-drain.service"; import { LogDrainOtelTransporter } from "../../../modules/public-log-drain/transporters/public-log-drain-otel-transporter"; import { TaxesCalculatedLog } from "../../../modules/public-log-drain/public-events"; +import { waitUntil } from "@vercel/functions"; export const config = { api: { @@ -124,7 +125,7 @@ export default wrapWithLoggerContext( url: "TODO", }); - await publicLoggerOtel.emitLog(new TaxesCalculatedLog()); + waitUntil(publicLoggerOtel.emitLog(new TaxesCalculatedLog())); return res.status(200).json(ctx.buildResponse(calculatedTaxes)); } else if (avataxWebhookServiceResult.isErr()) { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index be5e0e22a..d605751c2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -101,13 +101,13 @@ importers: version: 13.5.4 '@opentelemetry/api': specifier: ../../node_modules/@opentelemetry/api - version: link:../../node_modules/@opentelemetry/api + version: 1.8.0 '@opentelemetry/api-logs': specifier: ../../node_modules/@opentelemetry/api-logs version: link:../../node_modules/@opentelemetry/api-logs '@opentelemetry/core': specifier: ../../node_modules/@opentelemetry/core - version: link:../../node_modules/@opentelemetry/core + version: 1.24.1(@opentelemetry/api@1.8.0) '@opentelemetry/exporter-logs-otlp-http': specifier: ../../node_modules/@opentelemetry/exporter-logs-otlp-http version: link:../../node_modules/@opentelemetry/exporter-logs-otlp-http @@ -131,13 +131,13 @@ importers: version: link:../../node_modules/@opentelemetry/sdk-node '@opentelemetry/sdk-trace-base': specifier: ../../node_modules/@opentelemetry/sdk-trace-base - version: link:../../node_modules/@opentelemetry/sdk-trace-base + version: 1.24.1(@opentelemetry/api@1.8.0) '@opentelemetry/sdk-trace-node': specifier: ../../node_modules/@opentelemetry/sdk-trace-node version: link:../../node_modules/@opentelemetry/sdk-trace-node '@opentelemetry/semantic-conventions': specifier: ../../node_modules/@opentelemetry/semantic-conventions - version: link:../../node_modules/@opentelemetry/semantic-conventions + version: 1.24.1 '@saleor/app-sdk': specifier: link:../../node_modules/@saleor/app-sdk version: link:../../node_modules/@saleor/app-sdk @@ -167,7 +167,7 @@ importers: version: 2.31.2 '@sentry/nextjs': specifier: 8.0.0 - version: 8.0.0(@opentelemetry/api@node_modules+@opentelemetry+api)(@opentelemetry/core@node_modules+@opentelemetry+core)(@opentelemetry/instrumentation@0.51.1)(@opentelemetry/sdk-trace-base@node_modules+@opentelemetry+sdk-trace-base)(@opentelemetry/semantic-conventions@node_modules+@opentelemetry+semantic-conventions)(next@14.2.3)(react@18.2.0)(webpack@5.82.1) + version: 8.0.0(@opentelemetry/api@1.8.0)(@opentelemetry/core@1.24.1)(@opentelemetry/instrumentation@0.51.1)(@opentelemetry/sdk-trace-base@1.24.1)(@opentelemetry/semantic-conventions@1.24.1)(next@14.2.3)(react@18.2.0)(webpack@5.82.1) '@tanstack/react-query': specifier: 4.29.19 version: 4.29.19(react-dom@18.2.0)(react@18.2.0) @@ -186,6 +186,9 @@ importers: '@urql/exchange-auth': specifier: ^2.1.4 version: 2.1.4(graphql@16.7.1) + '@vercel/functions': + specifier: 1.0.2 + version: 1.0.2 avatax: specifier: ^23.7.0 version: 23.7.0 @@ -215,7 +218,7 @@ importers: version: 6.1.0 next: specifier: 14.2.3 - version: 14.2.3(@babel/core@7.24.3)(@opentelemetry/api@node_modules+@opentelemetry+api)(react-dom@18.2.0)(react@18.2.0) + version: 14.2.3(@babel/core@7.24.3)(@opentelemetry/api@1.8.0)(react-dom@18.2.0)(react@18.2.0) pino: specifier: ^8.14.1 version: 8.14.1 @@ -12255,7 +12258,6 @@ packages: - '@opentelemetry/semantic-conventions' - encoding - supports-color - dev: true /@sentry/nextjs@8.0.0(@opentelemetry/api@node_modules+@opentelemetry+api)(@opentelemetry/core@node_modules+@opentelemetry+core)(@opentelemetry/instrumentation@0.51.1)(@opentelemetry/sdk-trace-base@node_modules+@opentelemetry+sdk-trace-base)(@opentelemetry/semantic-conventions@node_modules+@opentelemetry+semantic-conventions)(next@14.2.3)(react@18.2.0)(webpack@5.82.1): resolution: {integrity: sha512-FJEW0w1WJHMV9NWHZHOXNQPOSg9pxjAq5y7XcP88rCqa08bmVUNVyg5UbHKBMXG1BjZukVr/8in/4sbxBOv3VQ==} @@ -13518,7 +13520,7 @@ packages: '@trpc/client': 10.43.1(@trpc/server@10.43.1) '@trpc/react-query': 10.43.1(@tanstack/react-query@4.29.19)(@trpc/client@10.43.1)(@trpc/server@10.43.1)(react-dom@18.2.0)(react@18.2.0) '@trpc/server': 10.43.1 - next: 14.2.3(@babel/core@7.24.3)(@opentelemetry/api@node_modules+@opentelemetry+api)(react-dom@18.2.0)(react@18.2.0) + next: 14.2.3(@babel/core@7.24.3)(@opentelemetry/api@1.8.0)(react-dom@18.2.0)(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) react-ssr-prepass: 1.5.0(react@18.2.0) @@ -21978,7 +21980,6 @@ packages: transitivePeerDependencies: - '@babel/core' - babel-plugin-macros - dev: true /next@14.2.3(@babel/core@7.24.3)(@opentelemetry/api@node_modules+@opentelemetry+api)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-dowFkFTR8v79NPJO4QsBUtxv0g9BrS/phluVpMAt2ku7H+cbcBJlopXjkWlwxrk/xGqMemr7JkGPGemPrLLX7A==} From 0bf9fee5218f22994bb39c4dc87a6a46c9c95473 Mon Sep 17 00:00:00 2001 From: Lukasz Ostrowski Date: Tue, 28 May 2024 14:06:51 +0200 Subject: [PATCH 21/36] add checkout id --- .../calculate-taxes/use-case/calculate-taxes.use-case.ts | 8 +++++++- apps/avatax/src/modules/public-log-drain/public-events.ts | 8 ++++++-- .../src/pages/api/webhooks/order-calculate-taxes.ts | 8 +++++++- 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/apps/avatax/src/modules/calculate-taxes/use-case/calculate-taxes.use-case.ts b/apps/avatax/src/modules/calculate-taxes/use-case/calculate-taxes.use-case.ts index 8eec5dfb8..bea139c35 100644 --- a/apps/avatax/src/modules/calculate-taxes/use-case/calculate-taxes.use-case.ts +++ b/apps/avatax/src/modules/calculate-taxes/use-case/calculate-taxes.use-case.ts @@ -186,7 +186,13 @@ export class CalculateTaxesUseCase { ).map((results) => { this.logger.info("Taxes calculated", { calculatedTaxes: results }); - waitUntil(this.deps.publicLogDrain.emitLog(new TaxesCalculatedLog())); + waitUntil( + this.deps.publicLogDrain.emitLog( + new TaxesCalculatedLog({ + orderOrCheckoutId: payload.taxBase.sourceObject.id, + }), + ), + ); return results; }); diff --git a/apps/avatax/src/modules/public-log-drain/public-events.ts b/apps/avatax/src/modules/public-log-drain/public-events.ts index 2811d2b02..6eab292a4 100644 --- a/apps/avatax/src/modules/public-log-drain/public-events.ts +++ b/apps/avatax/src/modules/public-log-drain/public-events.ts @@ -17,10 +17,14 @@ export interface PublicLog = {}> { attributes: T; } -export class TaxesCalculatedLog implements PublicLog { +export class TaxesCalculatedLog implements PublicLog<{ orderOrCheckoutId: string }> { message = "Taxes calculated"; eventType = "TAXES_CALCULATED" as const; timestamp = new Date(); level = LogSeverityLevel.INFO; - attributes = {}; + attributes = { orderOrCheckoutId: "" }; + + constructor(params: { orderOrCheckoutId: string }) { + this.attributes.orderOrCheckoutId = params.orderOrCheckoutId; + } } diff --git a/apps/avatax/src/pages/api/webhooks/order-calculate-taxes.ts b/apps/avatax/src/pages/api/webhooks/order-calculate-taxes.ts index 63d7309a8..2c8f9be34 100644 --- a/apps/avatax/src/pages/api/webhooks/order-calculate-taxes.ts +++ b/apps/avatax/src/pages/api/webhooks/order-calculate-taxes.ts @@ -125,7 +125,13 @@ export default wrapWithLoggerContext( url: "TODO", }); - waitUntil(publicLoggerOtel.emitLog(new TaxesCalculatedLog())); + waitUntil( + publicLoggerOtel.emitLog( + new TaxesCalculatedLog({ + orderOrCheckoutId: payload.taxBase?.sourceObject.id, + }), + ), + ); return res.status(200).json(ctx.buildResponse(calculatedTaxes)); } else if (avataxWebhookServiceResult.isErr()) { From 53b2791bd23d69b2b524fb452276524316daa6e4 Mon Sep 17 00:00:00 2001 From: Lukasz Ostrowski Date: Tue, 28 May 2024 14:16:34 +0200 Subject: [PATCH 22/36] wip --- .../transporters/public-log-drain-otel-transporter.ts | 2 ++ apps/avatax/src/pages/api/webhooks/order-calculate-taxes.ts | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/avatax/src/modules/public-log-drain/transporters/public-log-drain-otel-transporter.ts b/apps/avatax/src/modules/public-log-drain/transporters/public-log-drain-otel-transporter.ts index 74536c7bf..7979f7935 100644 --- a/apps/avatax/src/modules/public-log-drain/transporters/public-log-drain-otel-transporter.ts +++ b/apps/avatax/src/modules/public-log-drain/transporters/public-log-drain-otel-transporter.ts @@ -94,6 +94,8 @@ export class LogDrainOtelTransporter implements LogDrainTransporter { if (typeof value === "number" && !Number.isFinite(value)) { return false; } + + return true; }); return filteredAttributesEntries diff --git a/apps/avatax/src/pages/api/webhooks/order-calculate-taxes.ts b/apps/avatax/src/pages/api/webhooks/order-calculate-taxes.ts index 2c8f9be34..b278034d4 100644 --- a/apps/avatax/src/pages/api/webhooks/order-calculate-taxes.ts +++ b/apps/avatax/src/pages/api/webhooks/order-calculate-taxes.ts @@ -122,7 +122,7 @@ export default wrapWithLoggerContext( // TODO: Krzysiek add metadata fetching here otelLogDrainTransporter.setSettings({ headers: {}, - url: "TODO", + url: "http://192.168.1.108:4318/v1/logs", }); waitUntil( From 97f8f451374868bc6354df2df07cd698ef001002 Mon Sep 17 00:00:00 2001 From: Lukasz Ostrowski Date: Tue, 28 May 2024 15:11:16 +0200 Subject: [PATCH 23/36] refactor attrs --- .../use-case/calculate-taxes.use-case.ts | 1 + .../src/modules/public-log-drain/public-events.ts | 13 ++++++++----- .../src/pages/api/webhooks/order-calculate-taxes.ts | 1 + 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/apps/avatax/src/modules/calculate-taxes/use-case/calculate-taxes.use-case.ts b/apps/avatax/src/modules/calculate-taxes/use-case/calculate-taxes.use-case.ts index bea139c35..91a1c5285 100644 --- a/apps/avatax/src/modules/calculate-taxes/use-case/calculate-taxes.use-case.ts +++ b/apps/avatax/src/modules/calculate-taxes/use-case/calculate-taxes.use-case.ts @@ -190,6 +190,7 @@ export class CalculateTaxesUseCase { this.deps.publicLogDrain.emitLog( new TaxesCalculatedLog({ orderOrCheckoutId: payload.taxBase.sourceObject.id, + saleorApiUrl: authData.saleorApiUrl, }), ), ); diff --git a/apps/avatax/src/modules/public-log-drain/public-events.ts b/apps/avatax/src/modules/public-log-drain/public-events.ts index 6eab292a4..7502ea222 100644 --- a/apps/avatax/src/modules/public-log-drain/public-events.ts +++ b/apps/avatax/src/modules/public-log-drain/public-events.ts @@ -11,20 +11,23 @@ export type LogSeverityLevelType = keyof typeof LogSeverityLevel; export interface PublicLog = {}> { message: string; - eventType: string; // enum + timestamp: Date; level: LogSeverityLevelType; - attributes: T; + attributes: T & { + saleorApiUrl: string; + eventType: string; + }; } export class TaxesCalculatedLog implements PublicLog<{ orderOrCheckoutId: string }> { message = "Taxes calculated"; - eventType = "TAXES_CALCULATED" as const; timestamp = new Date(); level = LogSeverityLevel.INFO; - attributes = { orderOrCheckoutId: "" }; + attributes = { orderOrCheckoutId: "", saleorApiUrl: "", eventType: "TAXES_CALCULATED" }; - constructor(params: { orderOrCheckoutId: string }) { + constructor(params: { orderOrCheckoutId: string; saleorApiUrl: string }) { this.attributes.orderOrCheckoutId = params.orderOrCheckoutId; + this.attributes.saleorApiUrl = params.saleorApiUrl; } } diff --git a/apps/avatax/src/pages/api/webhooks/order-calculate-taxes.ts b/apps/avatax/src/pages/api/webhooks/order-calculate-taxes.ts index b278034d4..2c39467f3 100644 --- a/apps/avatax/src/pages/api/webhooks/order-calculate-taxes.ts +++ b/apps/avatax/src/pages/api/webhooks/order-calculate-taxes.ts @@ -129,6 +129,7 @@ export default wrapWithLoggerContext( publicLoggerOtel.emitLog( new TaxesCalculatedLog({ orderOrCheckoutId: payload.taxBase?.sourceObject.id, + saleorApiUrl: ctx.authData.saleorApiUrl, }), ), ); From 793d2f269639f5f422918e42126e441b852844a3 Mon Sep 17 00:00:00 2001 From: Jonatan Witoszek Date: Tue, 28 May 2024 15:52:12 +0200 Subject: [PATCH 24/36] Fix error handling in log drain --- .../public-log-drain-json-transporter.ts | 56 +++++++++++++++---- 1 file changed, 45 insertions(+), 11 deletions(-) diff --git a/apps/avatax/src/modules/public-log-drain/transporters/public-log-drain-json-transporter.ts b/apps/avatax/src/modules/public-log-drain/transporters/public-log-drain-json-transporter.ts index d8543ae3d..dadb01959 100644 --- a/apps/avatax/src/modules/public-log-drain/transporters/public-log-drain-json-transporter.ts +++ b/apps/avatax/src/modules/public-log-drain/transporters/public-log-drain-json-transporter.ts @@ -1,25 +1,59 @@ import { LogDrainTransporter } from "../public-log-drain"; import { PublicLog } from "../public-events"; +import { trace } from "@opentelemetry/api"; +import { createLogger } from "@saleor/apps-logger"; +import { ResultAsync, err, ok } from "neverthrow"; +import { BaseError } from "../../../error"; export class LogDrainJsonTransporter implements LogDrainTransporter { + static TransporterError = BaseError.subclass("TransporterError"); + + static ConfigError = this.TransporterError.subclass("TransporterConfigError"); + static FetchError = this.TransporterError.subclass("TransporterFetchError"); + private endpoint: string | null = null; async emit(log: PublicLog): Promise { + const logger = createLogger("LogDrainJsonTransporter.emit"); + if (!this.endpoint) { - throw new Error("Endpoint is not set, call setSettings first"); + logger.error("Endpoint is not set, call setSettings first"); + throw new LogDrainJsonTransporter.ConfigError("Endpoint is not set, call setSettings first"); } - return fetch(this.endpoint, { - method: "POST", - body: JSON.stringify(log), - }) - .then((r) => r.json()) - .then((res) => { - console.log(res); // todo + const spanContext = trace.getActiveSpan()?.spanContext(); + + const payload = { + ...log, + traceId: spanContext?.traceId, + spanId: spanContext?.spanId, + isRemote: spanContext?.isRemote, + traceFlags: spanContext?.traceFlags, + traceState: spanContext?.traceState?.serialize(), + }; - return; - }); + const result = await ResultAsync.fromPromise( + fetch(this.endpoint, { + method: "POST", + body: JSON.stringify(payload), + }), + (err) => + new LogDrainJsonTransporter.FetchError("Failed to make request to log drain", { + cause: err, + }), + ).andThen((response) => + response.ok + ? ok(undefined) + : err(new LogDrainJsonTransporter.FetchError("Response from log drain is not HTTP 200")), + ); + + if (result.isErr()) { + // Silently ignore errors caused by making request to log drain + logger.debug("Error while making request to log drain"); + } } - setSettings() {} + setSettings({ endpoint }: { endpoint: string }) { + this.endpoint = endpoint; + } } From 9665e5c07dd846af7b7d004b5235d09a72ba1f9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20=C5=BBuraw?= <9116238+krzysztofzuraw@users.noreply.github.com> Date: Tue, 28 May 2024 16:53:30 +0200 Subject: [PATCH 25/36] Add UI for otel settings --- apps/avatax/src/lib/app-config.test.ts | 8 ++- .../src/lib/app-configuration-logger.test.ts | 9 ++- .../src/modules/app/get-app-config.test.ts | 7 +- .../avatax/avatax-config-mock-generator.ts | 3 + .../avatax/avatax-connection-schema.ts | 22 +++--- .../avatax-connection.service.ts | 5 +- .../avatax/ui/avatax-configuration-form.tsx | 2 + .../avatax/ui/edit-avatax-configuration.tsx | 4 +- .../avatax/ui/logs-settings-fragment.tsx | 69 +++++++++---------- .../use-case/calculate-taxes.use-case.test.ts | 10 ++- .../use-case/calculate-taxes.use-case.ts | 39 +++++++---- .../avatax-webhook-service-factory.test.ts | 8 ++- apps/avatax/src/modules/ui/otel-section.tsx | 63 ----------------- .../api/webhooks/order-calculate-taxes.ts | 22 +++--- apps/avatax/src/pages/configuration.tsx | 2 - 15 files changed, 125 insertions(+), 148 deletions(-) delete mode 100644 apps/avatax/src/modules/ui/otel-section.tsx diff --git a/apps/avatax/src/lib/app-config.test.ts b/apps/avatax/src/lib/app-config.test.ts index c1b6dfda1..c4a8ce324 100644 --- a/apps/avatax/src/lib/app-config.test.ts +++ b/apps/avatax/src/lib/app-config.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from "vitest"; +import { describe, expect, it } from "vitest"; import { AppConfig } from "./app-config"; describe("AppConfig", () => { @@ -50,6 +50,9 @@ describe("AppConfig", () => { isAutocommit: false, isDocumentRecordingEnabled: false, shippingTaxCode: "123", + logsSettings: { + otel: {}, + }, }, }, ], @@ -118,6 +121,9 @@ describe("AppConfig", () => { isAutocommit: false, isDocumentRecordingEnabled: false, shippingTaxCode: "123", + logsSettings: { + otel: {}, + }, }, }, ], diff --git a/apps/avatax/src/lib/app-configuration-logger.test.ts b/apps/avatax/src/lib/app-configuration-logger.test.ts index 4eaca4691..2ab01f63a 100644 --- a/apps/avatax/src/lib/app-configuration-logger.test.ts +++ b/apps/avatax/src/lib/app-configuration-logger.test.ts @@ -1,7 +1,7 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; -import { AppConfigurationLogger } from "./app-configuration-logger"; -import { AppConfig } from "./app-config"; import { ObservabilityAttributes } from "@saleor/apps-otel/src/lib/observability-attributes"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { AppConfig } from "./app-config"; +import { AppConfigurationLogger } from "./app-configuration-logger"; describe("AppConfigurationLogger", () => { const mockWarn = vi.fn(); @@ -72,6 +72,9 @@ describe("AppConfigurationLogger", () => { isAutocommit: false, isDocumentRecordingEnabled: false, shippingTaxCode: "123", + logsSettings: { + otel: {}, + }, }, }, ], diff --git a/apps/avatax/src/modules/app/get-app-config.test.ts b/apps/avatax/src/modules/app/get-app-config.test.ts index 47443f5e4..ed5063983 100644 --- a/apps/avatax/src/modules/app/get-app-config.test.ts +++ b/apps/avatax/src/modules/app/get-app-config.test.ts @@ -1,9 +1,9 @@ import { encrypt } from "@saleor/app-sdk/settings-manager"; -import { getAppConfig } from "./get-app-config"; import { describe, expect, it, vi } from "vitest"; -import { ProviderConnections } from "../provider-connections/provider-connections"; import { MetadataItem } from "../../../generated/graphql"; import { ChannelsConfig } from "../channel-configuration/channel-config"; +import { ProviderConnections } from "../provider-connections/provider-connections"; +import { getAppConfig } from "./get-app-config"; const mockedSecretKey = "test_secret_key"; const mockedProviders: ProviderConnections = [ @@ -28,6 +28,9 @@ const mockedProviders: ProviderConnections = [ street: "123 Main St", zip: "10001", }, + logsSettings: { + otel: {}, + }, }, }, ]; diff --git a/apps/avatax/src/modules/avatax/avatax-config-mock-generator.ts b/apps/avatax/src/modules/avatax/avatax-config-mock-generator.ts index 934a5bdb0..8517bbb4d 100644 --- a/apps/avatax/src/modules/avatax/avatax-config-mock-generator.ts +++ b/apps/avatax/src/modules/avatax/avatax-config-mock-generator.ts @@ -18,6 +18,9 @@ const defaultAvataxConfig: AvataxConfig = { password: "password", username: "username", }, + logsSettings: { + otel: {}, + }, }; const testingScenariosMap = { diff --git a/apps/avatax/src/modules/avatax/avatax-connection-schema.ts b/apps/avatax/src/modules/avatax/avatax-connection-schema.ts index 2c647909c..d8f655041 100644 --- a/apps/avatax/src/modules/avatax/avatax-connection-schema.ts +++ b/apps/avatax/src/modules/avatax/avatax-connection-schema.ts @@ -29,15 +29,12 @@ export const avataxConfigSchema = z shippingTaxCode: z.string().optional(), isDocumentRecordingEnabled: z.boolean().default(true), address: addressSchema, - logsSettings: z - .object({ - otel: z.object({ - url: z.string().url(), - headers: z.array(z.object({ key: z.string(), value: z.string() })), - }), - }) - .or(z.null()) - .default(null), + logsSettings: z.object({ + otel: z.object({ + url: z.string().optional(), + headers: z.string().optional(), + }), + }), }) .merge(baseAvataxConfigSchema); @@ -61,7 +58,12 @@ export const defaultAvataxConfig: AvataxConfig = { street: "", zip: "", }, - logsSettings: null, + logsSettings: { + otel: { + url: "", + headers: "", + }, + }, }; export const avataxConnectionSchema = z.object({ diff --git a/apps/avatax/src/modules/avatax/configuration/avatax-connection.service.ts b/apps/avatax/src/modules/avatax/configuration/avatax-connection.service.ts index 88350f074..48c586f53 100644 --- a/apps/avatax/src/modules/avatax/configuration/avatax-connection.service.ts +++ b/apps/avatax/src/modules/avatax/configuration/avatax-connection.service.ts @@ -78,7 +78,10 @@ export class AvataxConnectionService { ...prevConfig.address, ...nextConfigPartial.address, }, - logsSettings: null, + logsSettings: { + ...prevConfig.logsSettings, + ...nextConfigPartial.logsSettings, + }, }; await this.checkIfAuthorized(input); diff --git a/apps/avatax/src/modules/avatax/ui/avatax-configuration-form.tsx b/apps/avatax/src/modules/avatax/ui/avatax-configuration-form.tsx index 4208a1b04..cc6352598 100644 --- a/apps/avatax/src/modules/avatax/ui/avatax-configuration-form.tsx +++ b/apps/avatax/src/modules/avatax/ui/avatax-configuration-form.tsx @@ -56,6 +56,8 @@ export const AvataxConfigurationForm = (props: AvataxConfigurationFormProps) => [props], ); + console.log(formState); + return ( diff --git a/apps/avatax/src/modules/avatax/ui/edit-avatax-configuration.tsx b/apps/avatax/src/modules/avatax/ui/edit-avatax-configuration.tsx index 429109230..8bd048507 100644 --- a/apps/avatax/src/modules/avatax/ui/edit-avatax-configuration.tsx +++ b/apps/avatax/src/modules/avatax/ui/edit-avatax-configuration.tsx @@ -4,8 +4,8 @@ import { useRouter } from "next/router"; import React from "react"; import { z } from "zod"; import { trpcClient } from "../../trpc/trpc-client"; -import { AvataxObfuscator } from "../avatax-obfuscator"; import { AvataxConfig, BaseAvataxConfig } from "../avatax-connection-schema"; +import { AvataxObfuscator } from "../avatax-obfuscator"; import { AvataxConfigurationForm } from "./avatax-configuration-form"; import { useAvataxConfigurationStatus } from "./configuration-status"; @@ -141,6 +141,8 @@ export const EditAvataxConfiguration = () => { ); } + console.log("config", data.config); + return ( { - const { control } = useFormContext(); - - const { fields, append, remove } = useFieldArray({ - control, - name: "logsSettings.otel.headers", - }); + const { control, formState } = useFormContext(); return ( <> - - - - - {fields.map((field, index) => ( - - - + + Logs settings + + + + Configure where AvaTax should emit logs. This is useful for debugging and + troubleshooting connection issues. + + + + ( +