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

PoC: Add a logging abstraction and wrap remix build output to track errors #941

Closed
wants to merge 4 commits into from

Conversation

blittle
Copy link
Contributor

@blittle blittle commented May 24, 2023

Summary

This PR does two things:

  1. It wraps the Remix module build output. This means that every loader and action method defined in routes now gets automatically wrapped where we call the method before Remix, and catch any errors passing them to a logging abstraction.
  2. Add a lite logging abstraction with a default implementation:
import {Logger} from '@shopify/hydrogen';

// Default log implementation
const log = new Logger(executionContext.waitUntil, request);

// Override specific log options
const log = new Logger(executionContext.waitUntil, request, {
  // Each logger method (trace, debug, warn, error, fatal) is passed a context object,
  // which includes the original request. These methods can also be async functions,
  // which could call a third party API to ingest logs.
  error: ({request}, ...args) => {
    console.log(request.url, ...args);
  }
});

How to test?

  1. Add an error within a loader or action within both the demo store and the skeleton app.
  2. Notice that the skeleton app passes in a Logger instance and the demo store does not. This just verifies that it works with and without the logger passed in. If it isn't passed in, just default to console.error().

@github-actions
Copy link
Contributor

We detected some changes in packages/*/package.json or packages/*/src, and there are no updates in the .changeset.
If the changes are user-facing and should cause a version bump, run npm run changeset add to track your changes and include them in the next release CHANGELOG.
If you are making simple updates to examples or documentation, you do not need to add a changeset.

type AppLoadContext,
type ServerBuild,
} from '@remix-run/server-runtime';
import type {Logger} from '@shopify/hydrogen';
Copy link
Contributor Author

Choose a reason for hiding this comment

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

CI is failing because of this. I think our build should be more robust, so that our packages can rely on shared types across the monorepo.

Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe need to update turbo.json to ensure that hydrogen package is built before the remix-oxygen package is built?

Copy link
Contributor

Choose a reason for hiding this comment

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

Can we just copy the type? I think it would be better if remix-oxygen doesn't depend on Hydrogen... since it's just a generic adapter for Remix <> Oxygen 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hmm, it might be best to keep them separate

const handleRequest = createRemixRequestHandler(build, mode);
const routes: typeof build.routes = {};

for (const [id, route] of Object.entries(build.routes)) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is where remix modules loader and actions are overwritten

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not sure if it's the job of remix-oxygen to wrap the Remix build and enhance it with logs.
Maybe we could also consider exporting this as a standalone function from Hydrogen itself?
Perhaps as part of createLogger (as an option, you pass the Remix build to enhance its logs), or as a separate utility?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, I was only hoping to automatically add logging for existing merchants without having them update their server.ts. The only way to do that AFAIK is to do it in the oxygen handler. Though maybe that's not a fair tradeoff considering no one outside Oxygen would get it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@frandiox are you suggesting:

 const log = new Logger({ request, waitUntil, remixBuild });

And then having the logger mutate remixBuild.routes?

Copy link
Contributor

Choose a reason for hiding this comment

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

Either that or as a separate utility, yes. Looking at it now it doesn't feel like it's clear what it does... so maybe a more verbose function name would be better?

Comment on lines 92 to 96
if (error instanceof Error) {
console.error(red(`Error processing route:${url}\n${error.stack}`));
} else {
console.error(red(`Error:${url} ${error}`));
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I copied this logic from H1, though not sure if we want to keep it 🤷🏼

Copy link
Contributor

Choose a reason for hiding this comment

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

I wonder if this is where we implement the logging format that we discussed in #876?

Copy link
Contributor

Choose a reason for hiding this comment

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

I copied this logic from H1, though not sure if we want to keep it 🤷🏼

Maybe we can remove it for now?

I wonder if this is where we implement the logging format that we discussed in 876

I think that would be a different place since that should only be for Hydrogen logs. This logger here is supposed to be used manually by the user as well, right?

@@ -31,6 +47,23 @@ export function createRequestHandler<Context = unknown>({
};
}

type RemixModule = ServerBuild['routes'][0]['module'];

function fill(module: RemixModule, name: 'loader' | 'action', log?: Logger) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Should it also support handle and meta exports?

Copy link
Contributor

@frehner frehner left a comment

Choose a reason for hiding this comment

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

Notice that the skeleton app passes in a Logger instance and the demo store does not. This just verifies that it works with and without the logger passed in. If it isn't passed in, just default to console.error().

I feel like it should be the other way around - implemented in demo-store but not in skeleton? Not a big deal though.


export class Logger {
#waitUntil: (p: Promise<unknown>) => void;
#request: Request;
Copy link
Contributor

Choose a reason for hiding this comment

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

oh baby private fields on a class. We fancy now.

type LoggerMethod = (
context: {request: Request},
...args: Array<any>
) => void | Promise<any>;
Copy link
Contributor

Choose a reason for hiding this comment

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

Promise<void> or do we actually expect to get something out of the promise here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Probably void is more correct

);
}
} catch (e) {
const message = e instanceof Error ? e.stack : e;
Copy link
Contributor

Choose a reason for hiding this comment

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

the message is from e.stack? or should it be from e.message?

Copy link
Contributor

Choose a reason for hiding this comment

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

I think the stack normally contains the same message, so this is just printing the whole thing. Might be wrong tho.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, the stack contains the message.

Comment on lines 92 to 96
if (error instanceof Error) {
console.error(red(`Error processing route:${url}\n${error.stack}`));
} else {
console.error(red(`Error:${url} ${error}`));
}
Copy link
Contributor

Choose a reason for hiding this comment

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

I wonder if this is where we implement the logging format that we discussed in #876?

type AppLoadContext,
type ServerBuild,
} from '@remix-run/server-runtime';
import type {Logger} from '@shopify/hydrogen';
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe need to update turbo.json to ensure that hydrogen package is built before the remix-oxygen package is built?

@@ -0,0 +1,103 @@
import {yellow, red} from 'kolorist';
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd prefer not to bundle kolorist or any library like that in Hydrogen itself. We can just add colors from the CLI in dev by recognizing patterns 🤔

Comment on lines +21 to +22
waitUntil: (p: Promise<unknown>) => void,
request: Request,
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe request should be the first argument? waitUntil could be optional in some environments like Node.js

);
}
} catch (e) {
const message = e instanceof Error ? e.stack : e;
Copy link
Contributor

Choose a reason for hiding this comment

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

I think the stack normally contains the same message, so this is just printing the whole thing. Might be wrong tho.

Comment on lines +77 to +83
trace: (context, ...args) => {
console.log(...args);
},

debug: (context, ...args) => {
console.log(...args);
},
Copy link
Contributor

Choose a reason for hiding this comment

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

Should these default to console.trace and console.debug respectively?

Comment on lines 92 to 96
if (error instanceof Error) {
console.error(red(`Error processing route:${url}\n${error.stack}`));
} else {
console.error(red(`Error:${url} ${error}`));
}
Copy link
Contributor

Choose a reason for hiding this comment

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

I copied this logic from H1, though not sure if we want to keep it 🤷🏼

Maybe we can remove it for now?

I wonder if this is where we implement the logging format that we discussed in 876

I think that would be a different place since that should only be for Hydrogen logs. This logger here is supposed to be used manually by the user as well, right?

type AppLoadContext,
type ServerBuild,
} from '@remix-run/server-runtime';
import type {Logger} from '@shopify/hydrogen';
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we just copy the type? I think it would be better if remix-oxygen doesn't depend on Hydrogen... since it's just a generic adapter for Remix <> Oxygen 🤔

const handleRequest = createRemixRequestHandler(build, mode);
const routes: typeof build.routes = {};

for (const [id, route] of Object.entries(build.routes)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not sure if it's the job of remix-oxygen to wrap the Remix build and enhance it with logs.
Maybe we could also consider exporting this as a standalone function from Hydrogen itself?
Perhaps as part of createLogger (as an option, you pass the Remix build to enhance its logs), or as a separate utility?

@frandiox
Copy link
Contributor

frandiox commented May 26, 2023

@blittle one more thing to keep in mind: the remixBuild is shared across requests for the same isolate (or same Node.js process). Therefore, we shouldn't modify/wrap it on every request (like new Logger({..., remixBuild}) would do). We'll need to check if it's been already modified and skip it, or create a whole new build object (like I think you are doing in remix-oxygen/src/server.ts).
Therefore, maybe something like const enhancedRemixBuild = addLogsToBuild(remixBuild, logger) would be better? 🤔

@frandiox
Copy link
Contributor

@blittle I've pushed a few commits mostly to show that we should also wrap deferred data because Remix also swallows that.

I've also removed type and colors dependencies.

Still not sure if this should happen in remix-oxygen, though.

@davidhousedev
Copy link
Contributor

Given that the only extensibility point is the logger method, it requires a more advanced composition pattern (we used higher-order functions) if you want to e.g. send any log statement to an external source.

Have you considered a more generic extension point that would be called for all logger methods?

Also, have you considered introducing a logger.info? It's been confusing for folks on our end adjusting to the idea that "if you want to info log, use debug"

@blittle blittle changed the title Add a logging abstraction and wrap remix build output to track errors PoC: Add a logging abstraction and wrap remix build output to track errors May 26, 2023
@blittle
Copy link
Contributor Author

blittle commented May 26, 2023

@davidhousedev this is an early PoC, and we'd love some ideas. I'm happy to add a .info method.

@blittle
Copy link
Contributor Author

blittle commented May 26, 2023

@frandiox we might be able to simplify this given this change to Remix: remix-run/remix#6495

@michenly
Copy link
Contributor

@blittle closing this PR due to inactivity. @frandiox unless you have any idea how remix-run/remix#6495 play with our current main and if this PR is still needed?

@michenly michenly closed this Feb 14, 2024
@frandiox
Copy link
Contributor

I don't think we wanted to move forward with this 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants