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

attach request info to log events #28

Merged
merged 20 commits into from Aug 10, 2022
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
26 changes: 19 additions & 7 deletions README.md
Expand Up @@ -31,26 +31,25 @@ export { reportWebVitals } from 'next-axiom'

## Sending Logs

1. Import Axiom's logger
```js
import { log } from 'next-axiom';
```
2. If you want to log from a function, wrap it using `withAxiom` like this:


1. Wrap your functions using `withAxiom`, then you can use `req.log` like this:
```js
// serverless function
async function handler(req, res) {
log.info("hello from function")
req.log.info("hello from function")
res.status(200).text('hi')
}

export default withAxiom(handler)
```

```js
// middleware function
import { NextResponse } from 'next/server'

async function handler(req, ev) {
log.info("hello from middleware")
req.log.info("hello from middleware")
return NextResponse.next()
}

Expand Down Expand Up @@ -85,6 +84,19 @@ logger.info('User logged in', { userId: 42 })
// }
```

In the frontend pages you can import `log` directly from `next-axiom`

```js
import { log } from `next-axiom`;

// pages/index.js
function home() {
...
log.debug('User logged in', { userId: 42 })
...
}
```

4. Deploy your site and watch data coming into your Axiom dataset.

> **Note**: Logs are only sent to Axiom from production deployments.
Expand Down
6 changes: 3 additions & 3 deletions examples/logger/pages/api/edge.js
@@ -1,11 +1,11 @@
import { log, withAxiom } from 'next-axiom'
import { withAxiom } from 'next-axiom'

export const config = {
runtime: 'experimental-edge',
};

function handler() {
log.debug("message from edge", { foo: 'bar' })
function handler(req) {
req.log.debug("message from edge", { foo: 'bar' })

return new Response(
JSON.stringify({
Expand Down
4 changes: 2 additions & 2 deletions examples/logger/pages/api/hello.js
@@ -1,8 +1,8 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import { log, withAxiom } from 'next-axiom'
import { withAxiom } from 'next-axiom'

async function handler(req, res) {
log.info('Hello from function', { url: req.url });
req.log.info('Hello from function', { url: req.url });
res.status(200).json({ name: 'John Doe' })
}

Expand Down
2 changes: 1 addition & 1 deletion src/index.ts
@@ -1,3 +1,3 @@
export { reportWebVitals } from './webVitals';
export { log } from './logger';
export { log, Logger } from './logger';
export { withAxiom } from './withAxiom';
185 changes: 122 additions & 63 deletions src/logger.ts
@@ -1,86 +1,140 @@
import { proxyPath, isBrowser, EndpointType, getIngestURL, isEnvVarsSet, isNoPrettyPrint } from './shared';
import {
proxyPath,
isBrowser,
EndpointType,
getIngestURL,
isEnvVarsSet,
isNoPrettyPrint,
vercelEnv,
vercelRegion,
} from './shared';
import { throttle } from './shared';

const url = isBrowser ? `${proxyPath}/logs` : getIngestURL(EndpointType.logs);
const throttledSendLogs = throttle(sendLogs, 1000);
let logEvents: any[] = [];

function _log(level: string, message: string, args: any = {}) {
if (!isEnvVarsSet) {
// if AXIOM ingesting url is not set, fallback to printing to console
// to avoid network errors in development environments
prettyPrint(level, message, args);
return;
}

const logEvent = { level, message, _time: new Date(Date.now()).toISOString() };
if (Object.keys(args).length > 0) {
logEvent['fields'] = args;
}
interface LogEvent {
level: string;
message: string;
fields: {};
_time: string;
request?: RequestReport;
vercel?: VercelData;
}

logEvents.push(logEvent);
throttledSendLogs();
export interface RequestReport {
startTime: number;
statusCode?: number;
ip?: string;
region?: string;
path: string;
host: string;
method: string;
scheme: string;
userAgent?: string | null;
}

export const log = {
debug: (message: string, args: any = {}) => _log('debug', message, args),
info: (message: string, args: any = {}) => _log('info', message, args),
warn: (message: string, args: any = {}) => _log('warn', message, args),
error: (message: string, args: any = {}) => _log('error', message, args),
with: (args: any) => new Logger(args),
flush: sendLogs,
};
interface VercelData {
environment?: string;
region?: string;
route?: string;
}

class Logger {
args: any = {};
export class Logger {
public logEvents: LogEvent[] = [];
throttledSendLogs = throttle(this.sendLogs, 1000);

constructor(args: any = {}) {
this.args = args;
}
constructor(private args: any = {}, private req: RequestReport | null = null, private autoFlush: Boolean = true) {}

debug(message: string, args: any = {}) {
_log('debug', message, { ...this.args, ...args });
this._log('debug', message, { ...this.args, ...args });
}
info(message: string, args: any = {}) {
_log('info', message, { ...this.args, ...args });
this._log('info', message, { ...this.args, ...args });
}
warn(message: string, args: any = {}) {
_log('warn', message, { ...this.args, ...args });
this._log('warn', message, { ...this.args, ...args });
}
error(message: string, args: any = {}) {
_log('error', message, { ...this.args, ...args });
this._log('error', message, { ...this.args, ...args });
}

with(args: any) {
return new Logger({ ...this.args, ...args });
return new Logger({ ...this.args, ...args }, this.req, this.autoFlush);
}
}

async function sendLogs() {
if (!logEvents.length) {
return;
withRequest(req: RequestReport) {
return new Logger({ ...this.args }, req, this.autoFlush);
}

_log(level: string, message: string, args: any = {}) {
const logEvent: LogEvent = { level, message, _time: new Date(Date.now()).toISOString(), fields: {} };
if (Object.keys(args).length > 0) {
logEvent.fields = args;
}

logEvent.vercel = {
environment: vercelEnv,
region: vercelRegion,
};

if (this.req != null) {
logEvent.request = this.req;
logEvent.vercel.route = this.req.path;
}

this.logEvents.push(logEvent);
if (this.autoFlush) {
this.throttledSendLogs();
}
}

const method = 'POST';
const keepalive = true;
const body = JSON.stringify(logEvents);
// clear pending logs
logEvents = [];

try {
if (typeof fetch === 'undefined') {
const fetch = await require('whatwg-fetch');
await fetch(url, { body, method, keepalive });
} else if (isBrowser && navigator.sendBeacon) {
navigator.sendBeacon(url, body);
} else {
await fetch(url, { body, method, keepalive });
attachResponseStatus(statusCode: number) {
this.logEvents = this.logEvents.map((log) => {
if (log.request) {
log.request.statusCode = statusCode;
}
return log;
});
}

async sendLogs() {
if (!this.logEvents.length) {
return;
}

if (!isEnvVarsSet) {
// if AXIOM ingesting url is not set, fallback to printing to console
// to avoid network errors in development environments
this.logEvents.forEach((ev) => prettyPrint(ev));
this.logEvents = [];
return;
}

const method = 'POST';
const keepalive = true;
const body = JSON.stringify(this.logEvents);
// clear pending logs
this.logEvents = [];

try {
if (typeof fetch === 'undefined') {
const fetch = await require('whatwg-fetch');
await fetch(url, { body, method, keepalive });
} else if (isBrowser && navigator.sendBeacon) {
navigator.sendBeacon(url, body);
} else {
await fetch(url, { body, method, keepalive });
}
} catch (e) {
console.error(`Failed to send logs to Axiom: ${e}`);
}
} catch (e) {
console.error(`Failed to send logs to Axiom: ${e}`);
}

flush = this.sendLogs;
}

export const log = new Logger();

const levelColors = {
info: {
terminal: '32',
Expand All @@ -100,13 +154,13 @@ const levelColors = {
},
};

export function prettyPrint(level: string, message: string, fields: any = {}) {
const hasFields = Object.keys(fields).length > 0;
export function prettyPrint(ev: LogEvent) {
const hasFields = Object.keys(ev.fields).length > 0;
// check whether pretty print is disabled
if (isNoPrettyPrint) {
let msg = `${level} - ${message}`;
let msg = `${ev.level} - ${ev.message}`;
if (hasFields) {
msg += ' ' + JSON.stringify(fields);
msg += ' ' + JSON.stringify(ev.fields);
}
console.log(msg);
return;
Expand All @@ -116,19 +170,24 @@ export function prettyPrint(level: string, message: string, fields: any = {}) {
// object as normal text, it loses all the functionality the browser gives for viewing
// objects in the console, such as expanding and collapsing the object.
let msgString = '';
let args = [level, message];
let args: any[] = [ev.level, ev.message];

if (isBrowser) {
msgString = '%c%s - %s';
args = [`color: ${levelColors[level].browser};`, ...args];
args = [`color: ${levelColors[ev.level].browser};`, ...args];
} else {
msgString = `\x1b[${levelColors[level].terminal}m%s\x1b[0m - %s`;
msgString = `\x1b[${levelColors[ev.level].terminal}m%s\x1b[0m - %s`;
}
// we check if the fields object is not empty, otherwise its printed as <empty string>
// or just "".
if (hasFields) {
msgString += ' %o';
args.push(fields);
args.push(ev.fields);
}

if (ev.request) {
msgString += ' %o';
args.push(ev.request);
}

console.log.apply(console, [msgString, ...args]);
Expand Down
2 changes: 2 additions & 0 deletions src/shared.ts
Expand Up @@ -3,6 +3,8 @@ export const proxyPath = '/_axiom';
export const isBrowser = typeof window !== 'undefined';
export const isEnvVarsSet = process.env.AXIOM_INGEST_ENDPOINT || process.env.NEXT_PUBLIC_AXIOM_INGEST_ENDPOINT;
export const isNoPrettyPrint = process.env.AXIOM_NO_PRETTY_PRINT == 'true' ? true : false;
export const vercelRegion = process.env.VERCEL_REGION;
export const vercelEnv = process.env.VERCEL_ENV;

export enum EndpointType {
webVitals = 'web-vitals',
Expand Down
2 changes: 0 additions & 2 deletions src/webVitals.ts
@@ -1,8 +1,6 @@
import { NextWebVitalsMetric } from 'next/app';
import { isBrowser, proxyPath, isEnvVarsSet, throttle } from './shared';

export { log } from './logger';

const url = `${proxyPath}/web-vitals`;

export declare type WebVitalsMetric = NextWebVitalsMetric & { route: string };
Expand Down