Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add OTEL & improve logs in Segment app #1675

Merged
merged 2 commits into from
Jan 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/real-shrimps-hunt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"segment": patch
---

Add OTEL & improve logs in Segment app
13 changes: 13 additions & 0 deletions apps/segment/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,19 @@
},
"dependencies": {
"@hookform/resolvers": "^3.3.1",
"@opentelemetry/api": "../../node_modules/@opentelemetry/api",
"@opentelemetry/api-logs": "../../node_modules/@opentelemetry/api-logs",
"@opentelemetry/core": "../../node_modules/@opentelemetry/core",
"@opentelemetry/exporter-logs-otlp-http": "../../node_modules/@opentelemetry/exporter-logs-otlp-http",
"@opentelemetry/exporter-trace-otlp-http": "../../node_modules/@opentelemetry/exporter-trace-otlp-http",
"@opentelemetry/instrumentation-http": "../../node_modules/@opentelemetry/instrumentation-http",
"@opentelemetry/instrumentation-winston": "../../node_modules/@opentelemetry/instrumentation-winston",
"@opentelemetry/resources": "../../node_modules/@opentelemetry/resources",
"@opentelemetry/sdk-logs": "../../node_modules/@opentelemetry/sdk-logs",
"@opentelemetry/sdk-node": "../../node_modules/@opentelemetry/sdk-node",
"@opentelemetry/sdk-trace-base": "../../node_modules/@opentelemetry/sdk-trace-base",
"@opentelemetry/sdk-trace-node": "../../node_modules/@opentelemetry/sdk-trace-node",
"@opentelemetry/semantic-conventions": "../../node_modules/@opentelemetry/semantic-conventions",
"@saleor/app-sdk": "link:../../node_modules/@saleor/app-sdk",
"@saleor/apps-logger": "workspace:*",
"@saleor/apps-otel": "workspace:*",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,11 @@ export const configurationRouter = router({

const config = await manager.get();

logger.info("Fetched config");
logger.debug("Successfully fetched config");

return config.getConfig();
}),
setConfig: protectedClientProcedure.input(z.string().min(1)).mutation(async ({ input, ctx }) => {
logger.info("Request to set config");

const manager = AppConfigMetadataManager.createFromAuthData({
appId: ctx.appId,
saleorApiUrl: ctx.saleorApiUrl,
Expand All @@ -37,6 +35,6 @@ export const configurationRouter = router({

await manager.set(config);

logger.info("Config set successfully");
logger.debug("Successfully set config");
}),
});
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@ import { AppConfigMetadataManager } from "./configuration/app-config-metadata-ma
import { SegmentClient } from "./segment/segment.client";
import { SegmentEventsTracker } from "./tracking-events/segment-events-tracker";

export const SegmentNotConfiguredError = BaseError.subclass("SegmentNotConfiguredError");
export const SegmentWriteKeyNotFoundError = BaseError.subclass("SegmentNotConfiguredError");

export const createSegmentClientForWebhookContext = async (context: { authData: AuthData }) => {
const config = await AppConfigMetadataManager.createFromAuthData(context.authData).get();

const segmentKey = config.getConfig()?.segmentWriteKey;

if (!segmentKey) {
throw new SegmentNotConfiguredError("Segment write key not found in app config");
throw new SegmentWriteKeyNotFoundError("Segment write key not found in app config");
}

return new SegmentEventsTracker(
Expand Down
18 changes: 7 additions & 11 deletions apps/segment/src/modules/trpc/protected-client-procedure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,9 @@ import { middleware, procedure } from "./trpc-server";

const REQUIRED_SALEOR_PERMISSIONS: Permission[] = ["MANAGE_APPS"];

const logger = createLogger("ProtectedClientProcedure");
const logger = createLogger("protectedClientProcedure");

const attachAppToken = middleware(async ({ ctx, next }) => {
logger.debug("attachAppToken middleware");

if (!ctx.saleorApiUrl) {
logger.debug("ctx.saleorApiUrl not found, throwing");

Expand Down Expand Up @@ -46,12 +44,9 @@ const attachAppToken = middleware(async ({ ctx, next }) => {
});

const validateClientToken = middleware(async ({ ctx, next, meta }) => {
logger.debug(
{
permissions: meta?.requiredClientPermissions,
},
"Calling validateClientToken middleware with permissions required",
);
logger.debug("Calling validateClientToken middleware with permissions required", {
permissions: meta?.requiredClientPermissions,
});

if (!ctx.token) {
throw new TRPCError({
Expand All @@ -77,8 +72,9 @@ const validateClientToken = middleware(async ({ ctx, next, meta }) => {

if (!ctx.ssr) {
try {
logger.debug("trying to verify JWT token from frontend");
logger.debug({ token: ctx.token ? `${ctx.token[0]}...` : undefined });
logger.debug("trying to verify JWT token from frontend", {
token: ctx.token ? `${ctx.token[0]}...` : undefined,
});

await verifyJWT({
appId: ctx.appId,
Expand Down
35 changes: 26 additions & 9 deletions apps/segment/src/pages/api/webhooks/order-cancelled.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { NextWebhookApiHandler } from "@saleor/app-sdk/handlers/next";
import { wrapWithLoggerContext } from "@saleor/apps-logger/node";
import { withOtel } from "@saleor/apps-otel";
import { ObservabilityAttributes } from "@saleor/apps-otel/src/lib/observability-attributes";

import { OrderUpdatedSubscriptionPayloadFragment } from "@/generated/graphql";
import { createLogger } from "@/logger";
import { loggerContext } from "@/logger-context";
import {
createSegmentClientForWebhookContext,
SegmentNotConfiguredError,
SegmentWriteKeyNotFoundError,
} from "@/modules/create-segment-client-for-webhook-context";
import { trackingEventFactory } from "@/modules/tracking-events/tracking-events";
import { orderCancelledAsyncWebhook } from "@/modules/webhooks/definitions/order-cancelled";
Expand All @@ -27,31 +29,46 @@ const handler: NextWebhookApiHandler<OrderUpdatedSubscriptionPayloadFragment> =
const { authData, payload } = context;

if (!payload.order) {
return res.status(200).end();
logger.info("Payload does not contain order data. Skipping.");
return res
.status(200)
.json({ message: "Payload does not contain order data. It will be skipped by app" });
}

loggerContext.set(ObservabilityAttributes.ORDER_ID, payload.order.id);

try {
const segmentEventTracker = await createSegmentClientForWebhookContext({ authData });

logger.info("Sending order cancelled event to Segment");

await segmentEventTracker.trackEvent(
trackingEventFactory.createOrderCancelledEvent(payload.order),
);

return res.status(200).end();
logger.info("Order cancelled event successfully sent to Segment");

return res.status(200).json({ message: "Order cancelled event successfully sent to Segment" });
} catch (e) {
if (e instanceof SegmentNotConfiguredError) {
if (e instanceof SegmentWriteKeyNotFoundError) {
// todo disable webhooks if not configured

return res.status(200).end();
logger.warn(
"Segment write key is not set in app configuration. Contact client to fix the issue.",
);
Comment on lines +54 to +56
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: do we want to treat broken app config as warning for our logs? I think this is fine for now, but imho it's not really scalable and client logs would be better ;)

Copy link
Member Author

@krzysztofzuraw krzysztofzuraw Jan 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In other PR I disable webhooks is there is no key so hopefully there won't be many such situations but I agree - we should let client know (client logs or maybe emails etc.)


return res.status(200).json({
message: "Segment write key is not set in app configuration. Cannot send event to Segment.",
});
}

return res.status(500).end();
logger.error("Error while sending order cancelled event to Segment", { error: e });

return res
.status(500)
.json({ message: "Error while sending order cancelled event to Segment" });
}
};

export default wrapWithLoggerContext(
orderCancelledAsyncWebhook.createHandler(handler),
withOtel(orderCancelledAsyncWebhook.createHandler(handler), "/api/webhooks/order-cancelled"),
loggerContext,
);
36 changes: 26 additions & 10 deletions apps/segment/src/pages/api/webhooks/order-created.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { NextWebhookApiHandler } from "@saleor/app-sdk/handlers/next";
import { wrapWithLoggerContext } from "@saleor/apps-logger/node";
import { withOtel } from "@saleor/apps-otel";
import { ObservabilityAttributes } from "@saleor/apps-otel/src/lib/observability-attributes";

import { OrderCreatedSubscriptionPayloadFragment } from "@/generated/graphql";
import { createLogger } from "@/logger";
import { loggerContext } from "@/logger-context";
import {
createSegmentClientForWebhookContext,
SegmentNotConfiguredError,
SegmentWriteKeyNotFoundError,
} from "@/modules/create-segment-client-for-webhook-context";
import { trackingEventFactory } from "@/modules/tracking-events/tracking-events";
import { orderCreatedAsyncWebhook } from "@/modules/webhooks/definitions/order-created";
Expand All @@ -17,7 +19,7 @@ export const config = {
},
};

const logger = createLogger("orderCreatedSyncWebhook");
const logger = createLogger("orderCreatedAsyncWebhook");

const handler: NextWebhookApiHandler<OrderCreatedSubscriptionPayloadFragment> = async (
req,
Expand All @@ -27,31 +29,45 @@ const handler: NextWebhookApiHandler<OrderCreatedSubscriptionPayloadFragment> =
const { authData, payload } = context;

if (!payload.order) {
return res.status(200).end();
logger.info("Payload does not contain order data. Skipping.");

return res
.status(200)
.json({ message: "Payload does not contain order data. It will be skipped by app" });
}

loggerContext.set(ObservabilityAttributes.ORDER_ID, payload.order.id);

try {
const segmentEventTracker = await createSegmentClientForWebhookContext({ authData });

logger.info("Sending order created event to Segment");

await segmentEventTracker.trackEvent(
trackingEventFactory.createOrderCreatedEvent(payload.order),
);

return res.status(200).end();
logger.info("Order created event successfully sent to Segment");

return res.status(200).json({ message: "Order cancelled event successfully sent to Segment" });
} catch (e) {
if (e instanceof SegmentNotConfiguredError) {
if (e instanceof SegmentWriteKeyNotFoundError) {
// todo disable webhooks if not configured

return res.status(200).end();
logger.warn(
"Segment write key is not set in app configuration. Contact client to fix the issue.",
);

return res.status(200).json({
message: "Segment write key is not set in app configuration. Cannot send event to Segment.",
});
}

return res.status(500).end();
logger.error("Error while sending order created event to Segment", { error: e });

return res.status(500).json({ message: "Error while sending order created event to Segment" });
}
};

export default wrapWithLoggerContext(
orderCreatedAsyncWebhook.createHandler(handler),
withOtel(orderCreatedAsyncWebhook.createHandler(handler), "/api/webhooks/order-created"),
loggerContext,
);
36 changes: 27 additions & 9 deletions apps/segment/src/pages/api/webhooks/order-fully-paid.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { NextWebhookApiHandler } from "@saleor/app-sdk/handlers/next";
import { wrapWithLoggerContext } from "@saleor/apps-logger/node";
import { withOtel } from "@saleor/apps-otel";
import { ObservabilityAttributes } from "@saleor/apps-otel/src/lib/observability-attributes";

import { OrderFullyPaidSubscriptionPayloadFragment } from "@/generated/graphql";
import { createLogger } from "@/logger";
import { loggerContext } from "@/logger-context";
import {
createSegmentClientForWebhookContext,
SegmentNotConfiguredError,
SegmentWriteKeyNotFoundError,
} from "@/modules/create-segment-client-for-webhook-context";
import { trackingEventFactory } from "@/modules/tracking-events/tracking-events";
import { orderFullyPaidAsyncWebhook } from "@/modules/webhooks/definitions/order-fully-paid";
Expand All @@ -27,31 +29,47 @@ const handler: NextWebhookApiHandler<OrderFullyPaidSubscriptionPayloadFragment>
const { authData, payload } = context;

if (!payload.order) {
return res.status(200).end();
logger.info("Payload does not contain order data. Skipping.");

return res
.status(200)
.json({ message: "Payload does not contain order data. It will be skipped by app" });
}

loggerContext.set(ObservabilityAttributes.ORDER_ID, payload.order.id);

try {
const segmentEventTracker = await createSegmentClientForWebhookContext({ authData });

logger.info("Sending order fully paid event to Segment");

await segmentEventTracker.trackEvent(
trackingEventFactory.createOrderCompletedEvent(payload.order),
);

return res.status(200).end();
logger.info("Order fully paid event successfully sent to Segment");

return res.status(200).json({ message: "Order fully paid event successfully sent to Segment" });
} catch (e) {
if (e instanceof SegmentNotConfiguredError) {
if (e instanceof SegmentWriteKeyNotFoundError) {
// todo disable webhooks if not configured

return res.status(200).end();
logger.warn(
"Segment write key is not set in app configuration. Contact client to fix the issue.",
);

return res.status(200).json({
message: "Segment write key is not set in app configuration. Cannot send event to Segment.",
});
}

return res.status(500).end();
logger.error("Error while sending order fully paid event to Segment", { error: e });

return res
.status(500)
.json({ message: "Error while sending order fully paid event to Segment" });
}
};

export default wrapWithLoggerContext(
orderFullyPaidAsyncWebhook.createHandler(handler),
withOtel(orderFullyPaidAsyncWebhook.createHandler(handler), "/api/webhooks/order-fully-paid"),
loggerContext,
);
34 changes: 25 additions & 9 deletions apps/segment/src/pages/api/webhooks/order-refunded.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { NextWebhookApiHandler } from "@saleor/app-sdk/handlers/next";
import { wrapWithLoggerContext } from "@saleor/apps-logger/node";
import { withOtel } from "@saleor/apps-otel";
import { ObservabilityAttributes } from "@saleor/apps-otel/src/lib/observability-attributes";

import { OrderRefundedSubscriptionPayloadFragment } from "@/generated/graphql";
import { createLogger } from "@/logger";
import { loggerContext } from "@/logger-context";
import {
createSegmentClientForWebhookContext,
SegmentNotConfiguredError,
SegmentWriteKeyNotFoundError,
} from "@/modules/create-segment-client-for-webhook-context";
import { trackingEventFactory } from "@/modules/tracking-events/tracking-events";
import { orderRefundedAsyncWebhook } from "@/modules/webhooks/definitions/order-refunded";
Expand All @@ -27,31 +29,45 @@ const handler: NextWebhookApiHandler<OrderRefundedSubscriptionPayloadFragment> =
const { authData, payload } = context;

if (!payload.order) {
return res.status(400).end();
logger.info("Payload does not contain order data. Skipping.");

return res
.status(200)
.json({ message: "Payload does not contain order data. It will be skipped by app" });
}

loggerContext.set(ObservabilityAttributes.ORDER_ID, payload.order.id);

try {
const segmentEventTracker = await createSegmentClientForWebhookContext({ authData });

logger.info("Sending order refunded event to Segment");

await segmentEventTracker.trackEvent(
trackingEventFactory.createOrderRefundedEvent(payload.order),
);

return res.status(200).end();
logger.info("Order refunded event successfully sent to Segment");

return res.status(200).json({ message: "Order refunded event successfully sent to Segment" });
} catch (e) {
if (e instanceof SegmentNotConfiguredError) {
if (e instanceof SegmentWriteKeyNotFoundError) {
// todo disable webhooks if not configured

return res.status(200).end();
logger.warn(
"Segment write key is not set in app configuration. Contact client to fix the issue.",
);

return res.status(200).json({
message: "Segment write key is not set in app configuration. Cannot send event to Segment.",
});
}

return res.status(500).end();
logger.error("Error while sending order refunded event to Segment", { error: e });

return res.status(500).json({ message: "Error while sending order refunded event to Segment" });
}
};

export default wrapWithLoggerContext(
orderRefundedAsyncWebhook.createHandler(handler),
withOtel(orderRefundedAsyncWebhook.createHandler(handler), "/api/webhooks/order-refunded"),
loggerContext,
);
Loading
Loading