Skip to content

Commit

Permalink
Fix visibility of logs send to OTEL (#1577)
Browse files Browse the repository at this point in the history
  • Loading branch information
krzysztofzuraw authored Sep 24, 2024
1 parent 9efbb98 commit 93969b2
Show file tree
Hide file tree
Showing 22 changed files with 275 additions and 66 deletions.
5 changes: 5 additions & 0 deletions .changeset/many-tigers-crash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@saleor/apps-otel": patch
---

Patch OTEL dependency - it should handle NaN & Infinity values. It will ensure that logs are properly parsed and send to log service.
5 changes: 5 additions & 0 deletions .changeset/thick-poets-invite.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@saleor/apps-logger": patch
---

Changed how we serialize errors in logs for OTEL. After this change they will be JSON stringified instead on modern error serialize function which didn't work with nested objects.
4 changes: 3 additions & 1 deletion apps/avatax/src/lib/app-config-extractor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,9 @@ export class AppConfigExtractor implements IAppConfigExtractor {
);

if (appConfigResult.isErr()) {
this.logger.error("Failed to resolve app configuration", { error: appConfigResult.error });
this.logger.error("Failed to resolve app configuration", {
error: appConfigResult.error,
});

return err(
new AppConfigExtractor.CantResolveAppConfigError("Failed to extract config from metadata", {
Expand Down
4 changes: 2 additions & 2 deletions apps/avatax/src/lib/app-configuration-logger.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,13 +81,13 @@ describe("AppConfigurationLogger", () => {
);

expect(mockInfo).toHaveBeenCalledWith("Received configuration", {
address: {
address: JSON.stringify({
city: "test",
country: "test",
zip: "10111",
state: "NY",
street: "test",
},
}),
appConfigName: "config",
channelSlug: "default-channel",
companyCode: "test",
Expand Down
2 changes: 1 addition & 1 deletion apps/avatax/src/lib/app-configuration-logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export class AppConfigurationLogger {
appConfigName: resolvedAvataxConfig.config.name,
shippingTaxCode: resolvedAvataxConfig.config.shippingTaxCode,
companyCode: resolvedAvataxConfig.config.companyCode,
address: resolvedAvataxConfig.config.address,
address: JSON.stringify(resolvedAvataxConfig.config.address),
isSandbox: resolvedAvataxConfig.config.isSandbox,
isAutocommit: resolvedAvataxConfig.config.isAutocommit,
isDocumentRecordingEnabled: resolvedAvataxConfig.config.isDocumentRecordingEnabled,
Expand Down
18 changes: 10 additions & 8 deletions apps/avatax/src/modules/app/order-metadata-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,15 +51,17 @@ export class OrderMetadataManager {
const errorToReport = error ?? gqlErrors[0] ?? null;

if (errorToReport) {
this.logger.error("Failed to update metadata", {
error: new OrderMetadataManager.MutationError(
errorToReport.message ?? "Failed to update metadata",
{
props: {
error: errorToReport,
},
const error = new OrderMetadataManager.MutationError(
errorToReport.message ?? "Failed to update metadata",
{
props: {
error: errorToReport,
},
),
},
);

this.logger.error("Failed to update metadata", {
error,
});

throw new OrderMetadataManager.MutationError("Failed to update metadata", {
Expand Down
12 changes: 7 additions & 5 deletions apps/avatax/src/modules/avatax/avatax-client-tax-code.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,13 @@ export class AvataxClientTaxCodeService {
...(filter ? { filter: `taxCode contains "${filter}"` } : {}),
top: 50,
})
.catch((err) => {
this.logger.error("Failed to call listTaxCodes on Avatax client", { error: err });
.catch((error) => {
this.logger.error("Failed to call listTaxCodes on Avatax client", {
error,
});

try {
const parsedError = AvataxErrorShape.parse(err);
const parsedError = AvataxErrorShape.parse(error);

/*
* Catch specific client error so it's returned to the frontend
Expand All @@ -52,13 +54,13 @@ export class AvataxClientTaxCodeService {
throw new AvataxClientTaxCodeService.ForbiddenAccessError(
"PermissionRequired error was returned from Avatax",
{
cause: err,
cause: error,
},
);
}

// Throw other errors like usual
throw err;
throw error;
} catch (outerError) {
throw outerError;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export const extractTransactionRedactedLogProperties = (model: CreateTransaction
description: line.description,
discounted: line.discounted,
})),
date: model.date,
date: model.date.toISOString(),
isTaxIncluded: model.lines[0]?.taxIncluded ?? false,
discountAmount: model.discount,
});
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ export const avataxTaxCodesRouter = router({
),
);

const connection = await connectionService.getById(input.connectionId).catch((err) => {
logger.error("Failed to resolve connection from settings", { error: err });
const connection = await connectionService.getById(input.connectionId).catch((error) => {
logger.error("Failed to resolve connection from settings", { error: error });

throw new TRPCError({
message: "Failed to resolve configuration, please try again",
Expand All @@ -61,10 +61,10 @@ export const avataxTaxCodesRouter = router({

logger.debug("Returning tax codes");

return taxCodesService.getAllFiltered({ filter: input.filter }).catch((err) => {
logger.error("Failed to fetch tax codes from AvaTax", { error: err });
return taxCodesService.getAllFiltered({ filter: input.filter }).catch((error) => {
logger.error("Failed to fetch tax codes from AvaTax", { error: error });

if (err instanceof AvataxClientTaxCodeService.ForbiddenAccessError) {
if (error instanceof AvataxClientTaxCodeService.ForbiddenAccessError) {
throw new TRPCError({
code: "FORBIDDEN",
cause: "AvaTax PermissionRequired",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@ export class AvataxTaxCodesService {
}

async getAllFiltered({ filter }: { filter: string | null }): Promise<TaxCode[]> {
const response = await this.client.getFilteredTaxCodes({ filter }).catch((err) => {
this.logger.error("Failed to fetch filtered tax codes", { error: err });
const response = await this.client.getFilteredTaxCodes({ filter }).catch((error) => {
this.logger.error("Failed to fetch filtered tax codes", { error });

throw err;
throw error;
});

return this.adapt(response);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,9 @@ export class CalculateTaxesUseCase {
const config = this.extractConfig(appMetadata, channelSlug);

if (config.isErr()) {
this.logger.warn("Failed to extract app config from metadata", { error: config.error });
this.logger.warn("Failed to extract app config from metadata", {
error: config.error,
});

ClientLogStoreRequest.create({
level: "error",
Expand Down Expand Up @@ -153,7 +155,7 @@ export class CalculateTaxesUseCase {
}
default: {
Sentry.captureException(innerError);
this.logger.fatal("Unhandled error", { error: err });
this.logger.fatal("Unhandled error", { error: innerError });

return err(
new CalculateTaxesUseCase.UnhandledError("Unhandled error", { errors: [innerError] }),
Expand Down Expand Up @@ -225,7 +227,7 @@ export class CalculateTaxesUseCase {
});
},
).map((results) => {
this.logger.info("Taxes calculated", { calculatedTaxes: results });
this.logger.info("Taxes calculated", { calculatedTaxes: JSON.stringify(results) });

ClientLogStoreRequest.create({
level: "info",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export class CrudSettingsManager {

if (!validation.success) {
this.logger.error("Error while validating metadata", {
error: validation.error,
error: JSON.stringify(validation.error),
metadataKey: this.params.metadataKey,
});
throw new Error("Error while validating metadata");
Expand Down
4 changes: 2 additions & 2 deletions apps/avatax/src/modules/tax-classes/tax-classes.router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ export const taxClassesRouter = router({

logger.debug("Returning tax classes");

return taxClassesFetcher.fetch().catch((err) => {
logger.error("Failed to fetch tax classes", { error: err });
return taxClassesFetcher.fetch().catch((error) => {
logger.error("Failed to fetch tax classes", { error: error });

// TODO: Map errors from Saleor and return proper response
throw new TRPCError({
Expand Down
16 changes: 8 additions & 8 deletions apps/avatax/src/pages/api/webhooks/checkout-calculate-taxes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export default wrapWithLoggerContext(
subscriptionErrorChecker.checkPayload(payload);

logger.info("Tax base payload for checkout calculate taxes", {
payload: payload.taxBase,
payload: JSON.stringify(payload.taxBase),
});

loggerContext.set(ObservabilityAttributes.CHANNEL_SLUG, ctx.payload.taxBase.channel.slug);
Expand Down Expand Up @@ -88,24 +88,24 @@ export default wrapWithLoggerContext(
});

if (config.isErr()) {
logger.warn("Failed to extract app config from metadata", { error: config.error });
logger.warn("Failed to extract app config from metadata", {
error: config.error,
});

return res.status(400).json({
message: `App configuration is broken for checkout: ${payload.taxBase.sourceObject.id}`,
});
}

metadataCache.setMetadata(appMetadata);

return useCase.calculateTaxes(payload, authData).then((result) => {
return result.match(
(value) => {
return res.status(200).json(ctx.buildResponse(value));
},
(err) => {
logger.warn("Error calculating taxes", { error: err });
(error) => {
logger.warn("Error calculating taxes", { error });

switch (err.constructor) {
switch (error.constructor) {
case CalculateTaxesUseCase.FailedCalculatingTaxesError: {
return res.status(500).json({
message: `Failed to calculate taxes for checkout: ${payload.taxBase.sourceObject.id}`,
Expand All @@ -122,7 +122,7 @@ export default wrapWithLoggerContext(
});
}
case CalculateTaxesUseCase.UnhandledError: {
captureException(err);
captureException(error);

return res.status(500).json({
message: `Failed to calculate taxes (Unhandled error) for checkout: ${payload.taxBase.sourceObject.id}`,
Expand Down
10 changes: 5 additions & 5 deletions apps/avatax/src/pages/api/webhooks/order-calculate-taxes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,9 @@ export default wrapWithLoggerContext(
});

if (config.isErr()) {
logger.warn("Failed to extract app config from metadata", { error: config.error });
logger.warn("Failed to extract app config from metadata", {
error: config.error,
});

ClientLogStoreRequest.create({
level: "error",
Expand All @@ -122,8 +124,6 @@ export default wrapWithLoggerContext(
});
}

metadataCache.setMetadata(appMetadata);

const AvataxWebhookServiceFactory = await import(
"../../../modules/taxes/avatax-webhook-service-factory"
).then((m) => m.AvataxWebhookServiceFactory);
Expand Down Expand Up @@ -161,7 +161,7 @@ export default wrapWithLoggerContext(
);

// eslint-disable-next-line @saleor/saleor-app/logger-leak
logger.info("Taxes calculated", { calculatedTaxes });
logger.info("Taxes calculated", { calculatedTaxes: JSON.stringify(calculatedTaxes) });

ClientLogStoreRequest.create({
level: "info",
Expand Down Expand Up @@ -202,7 +202,7 @@ export default wrapWithLoggerContext(
}
default: {
Sentry.captureException(avataxWebhookServiceResult.error);
logger.fatal("Unhandled error", { error: err });
logger.error("Unhandled error", { error: err });

ClientLogStoreRequest.create({
level: "error",
Expand Down
3 changes: 2 additions & 1 deletion cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@
"**/*.bru",
"**/.eslintrc*",
".github/**",
"Dockerfile*"
"Dockerfile*",
"patches/**"
]
}
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@
"pnpm": {
"overrides": {
"crypto-js@<4.2.0": ">=4.2.0"
},
"patchedDependencies": {
"@opentelemetry/[email protected]": "patches/@[email protected]",
"@opentelemetry/[email protected]": "patches/@[email protected]"
}
},
"private": true,
Expand Down
11 changes: 2 additions & 9 deletions packages/logger/src/logger-otel-transport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { context } from "@opentelemetry/api";
import { LogAttributeValue, logs } from "@opentelemetry/api-logs";
import { SemanticResourceAttributes } from "@opentelemetry/semantic-conventions";
import { ILogObj, Logger } from "tslog";

import { LoggerContext } from "./logger-context";

export const attachLoggerOtelTransport = (
Expand Down Expand Up @@ -45,17 +46,9 @@ export const attachLoggerOtelTransport = (
* Try to serialize Error. Modern-errors has plugin to serialize
* https://github.com/ehmicky/modern-errors-serialize
*
* It add "serialize" method that converts class to plain object, working for OTEL.
*
* This is not perfect, doesn't work for nested object. We probably need to introduce some abstraction
* on logger error?
*/
try {
const errorAttribute = serializedAttributes.error;
const ErrorConstructor = errorAttribute["constructor"];

// @ts-expect-error - ErrorConstructor is a class that could have serialize method. If not, safely throw and ignore
serializedAttributes.error = ErrorConstructor.serialize(serializedAttributes.error);
serializedAttributes.error = JSON.stringify(serializedAttributes.error);
// @ts-expect-error - Additional mapping for Datadog
serializedAttributes.error.type = serializedAttributes.error.name;
} catch (e) {}
Expand Down
9 changes: 5 additions & 4 deletions packages/logger/src/logger.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,20 +96,21 @@ describe("Logger", () => {
});

it("Calls Open Telemetry logger emit() function, passing there error attribute", () => {
expect.assertions(3);
expect.assertions(2);

const logger = createLogger("Test Logger", {
rootScopePrimitiveArg: 1,
rootScopeObjectArg: {
objectKey: "objectValue",
},
error: new Error("Error Message"),
});

const mockOtelEmit = vi.fn().mockImplementation((log) => {
const error = log.attributes.error;

expect(error.message).toBe("Error Message");
expect(error.cause).toBe("Error cause");
// Error is serialized to JSON
expect(error).toBe("{}");
});

vi.spyOn(logs, "getLogger").mockImplementation(() => {
Expand Down Expand Up @@ -139,7 +140,7 @@ describe("Logger", () => {
context: expect.anything(), // Unique otel context
body: "[Test Logger] Test Message",
attributes: {
error: expect.any(Error),
error: expect.any(String),
rootScopePrimitiveArg: 1,
rootScopeObjectArg: {
objectKey: "objectValue",
Expand Down
Loading

0 comments on commit 93969b2

Please sign in to comment.