From 667101a778343a4e8378f40a2422929105689e61 Mon Sep 17 00:00:00 2001 From: Islam Shehata Date: Tue, 14 May 2024 15:38:15 +0300 Subject: [PATCH 01/13] enhance(logger): imporve traffic and error capturing --- README.md | 79 ++++++++++++++++++++++++++++++++++- examples/logger/error.tsx | 24 +++++++++++ examples/logger/middleware.ts | 30 +++++++++++++ src/config.ts | 7 +++- src/logger.ts | 44 +++++++++++++++---- src/platform/generic.ts | 35 ++++++++++++---- src/platform/netlify.ts | 2 +- src/webVitals/webVitals.ts | 4 +- src/withAxiom.ts | 24 +++++++---- 9 files changed, 219 insertions(+), 30 deletions(-) create mode 100644 examples/logger/error.tsx create mode 100644 examples/logger/middleware.ts diff --git a/README.md b/README.md index f24f3190..14b8fad2 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,47 @@ no further configuration is required. Otherwise create a dataset and an API token in [Axiom settings](https://cloud.axiom.co/settings/profile), then export them as environment variables `NEXT_PUBLIC_AXIOM_DATASET` and `NEXT_PUBLIC_AXIOM_TOKEN`. -## Usage + +## Capture traffic requests + +Create a `middleware.ts` in the root dir of your app: + +```typescript +import { Logger } from 'next-axiom' +import { NextResponse } from 'next/server' +import type { NextFetchEvent, NextRequest } from 'next/server' + +const logger = new Logger({ + source: 'traffic' +}); + +// This function can be marked `async` if using `await` inside +export async function middleware(request: NextRequest, event: NextFetchEvent) { + const req = { + ip: request.ip, + region: request.geo?.region, + method: request.method, + host: request.nextUrl.host, + path: request.nextUrl.pathname, + scheme: request.nextUrl.protocol.split(":")[0], + referer: request.headers.get('Referer'), + userAgent: request.headers.get('user-agent'), + statusCode: 0, + } + + + const message = `[${request.method}] [middleware: "middleware"] ${request.nextUrl.pathname}` + + logger.logHttpRequest(message, req, {}) + event.waitUntil(logger.flush()) + + return NextResponse.next() +} + +// See "Matching Paths" below to learn more +export const config = { +} +``` ### Web Vitals @@ -146,6 +186,43 @@ export NEXT_PUBLIC_AXIOM_LOG_LEVEL=info You can also disable logging completely by setting the log level to `off`. + +### Capturing Errors + +To capture routing errors we can use the [Error Handling](https://nextjs.org/docs/app/building-your-application/routing/error-handling) mechanism of Next. + +Create a new file named `error.ts` under your `/app` directory. Inside your component function use the logger to ingest the error to Axiom. e.g: + +```typescript +"use client"; + +import NavTable from "@/components/NavTable"; +import { useLogger } from "next-axiom"; + +export default function Error({ + error, +}: { + error: Error & { digest?: string }; +}) { + const log = useLogger(); + log.error(error.message, { + error: error.name, + cause: error.cause, + stack: error.stack, + digest: error.digest, + }); + + return
+ Ops! An Error has occurred:

`{ error.message }`

+ +
+ +
+ +
; +} +``` + ## Upgrade to the App Router next-axiom switched to support the App Router starting with version 1.0. If you are upgrading a Pages Router app with next-axiom v0.x to the App Router, you will need to make the following changes: diff --git a/examples/logger/error.tsx b/examples/logger/error.tsx new file mode 100644 index 00000000..eb20ff39 --- /dev/null +++ b/examples/logger/error.tsx @@ -0,0 +1,24 @@ +'use client'; + +import { useLogger } from 'next-axiom'; + +export default function Error({ + error, +}: { + error: Error & { digest?: string }; +}) { + const log = useLogger(); + log.error(error.message, { + error: error.name, + cause: error.cause, + stack: error.stack, + digest: error.digest, + }); + + return ( +
+ Ops! An Error has occurred:{' '} +

`{error.message}`

+
+ ); +} diff --git a/examples/logger/middleware.ts b/examples/logger/middleware.ts new file mode 100644 index 00000000..af857be6 --- /dev/null +++ b/examples/logger/middleware.ts @@ -0,0 +1,30 @@ +import { Logger } from 'next-axiom' +import { NextResponse } from 'next/server' +import type { NextFetchEvent, NextRequest } from 'next/server' + +const logger = new Logger({ source: 'traffic' }); + +export async function middleware(request: NextRequest, event: NextFetchEvent) { + const req = { + ip: request.ip, + region: request.geo?.region, + method: request.method, + host: request.nextUrl.hostname, + path: request.nextUrl.pathname, + scheme: request.nextUrl.protocol.split(":")[0], + referer: request.headers.get('Referer'), + userAgent: request.headers.get('user-agent'), + } + + const message = `[${request.method}] [middleware: "middleware"] ${request.nextUrl.pathname}` + + logger.logHttpRequest(message, req, {}) + + + event.waitUntil(logger.flush()) + return NextResponse.next() +} + +// See "Matching Paths" below to learn more +export const config = { +} \ No newline at end of file diff --git a/src/config.ts b/src/config.ts index 7e64d0d3..162c4ae7 100644 --- a/src/config.ts +++ b/src/config.ts @@ -8,7 +8,10 @@ declare global { } export const Version = require('../package.json').version; -export const isVercel = process.env.NEXT_PUBLIC_AXIOM_INGEST_ENDPOINT || process.env.AXIOM_INGEST_ENDPOINT; +// detect if Vercel integration & logdrain is enabled +export const isVercelIntegration = process.env.NEXT_PUBLIC_AXIOM_INGEST_ENDPOINT || process.env.AXIOM_INGEST_ENDPOINT; +// detect if app is running on the Vercel platform +export const isVercel = process.env.NEXT_PUBLIC_VERCEL || process.env.VERCEL; export const isNetlify = process.env.NETLIFY == 'true'; export const isWebWorker = typeof self !== 'undefined' && @@ -20,7 +23,7 @@ export const isEdgeRuntime = globalThis.EdgeRuntime ? true : false; // Detect the platform provider, and return the appropriate config // fallback to generic config if no provider is detected let config = new GenericConfig(); -if (isVercel) { +if (isVercelIntegration) { config = new VercelConfig(); } else if (isNetlify) { config = new NetlifyConfig(); diff --git a/src/logger.ts b/src/logger.ts index 45ac0758..8a00b1f8 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -1,4 +1,4 @@ -import { config, isBrowser, isVercel, Version } from './config'; +import { config, isBrowser, isVercelIntegration, Version } from './config'; import { NetlifyInfo } from './platform/netlify'; import { isNoPrettyPrint, throttle } from './shared'; @@ -12,9 +12,13 @@ export interface LogEvent { fields: any; _time: string; request?: RequestReport; + git?: any, platform?: PlatformInfo; vercel?: PlatformInfo; netlify?: NetlifyInfo; + "@axiom": { + "next-axiom": string, + } } export enum LogLevel { @@ -42,6 +46,12 @@ export interface PlatformInfo { region?: string; route?: string; source?: string; + deploymentId?: string; + deploymentUrl?: string; + commit?: string; + project?: string; + repo?: string; + ref?: string; } export type LoggerConfig = { @@ -59,7 +69,7 @@ export class Logger { public logLevel: LogLevel = LogLevel.debug; public config: LoggerConfig = { autoFlush: true, - source: 'frontend', + source: 'frontend-log', }; constructor(public initConfig: LoggerConfig = {}) { @@ -96,15 +106,15 @@ export class Logger { return new Logger({ ...this.config, req: { ...this.config.req, ...req } }); }; - _log = (level: LogLevel, message: string, args: { [key: string]: any } = {}) => { - if (level < this.logLevel) { - return; - } + private _transformEvent = (level: LogLevel, message: string, args: { [key: string]: any } = {}) => { const logEvent: LogEvent = { level: LogLevel[level].toString(), message, _time: new Date(Date.now()).toISOString(), fields: this.config.args || {}, + "@axiom": { + "next-axiom": Version, + } }; // check if passed args is an object, if its not an object, add it to fields.args @@ -128,6 +138,24 @@ export class Logger { } } + return logEvent; + } + + logHttpRequest(message: string, request: any, args: any) { + const logEvent = this._transformEvent(LogLevel.info, message, args); + logEvent.request = request; + this.logEvents.push(logEvent); + if (this.config.autoFlush) { + this.throttledSendLogs(); + } + } + + private _log = (level: LogLevel, message: string, args: { [key: string]: any } = {}) => { + if (level < this.logLevel) { + return; + } + const logEvent = this._transformEvent(level, message, args) + this.logEvents.push(logEvent); if (this.config.autoFlush) { this.throttledSendLogs(); @@ -159,7 +187,7 @@ export class Logger { // if vercel integration is enabled, we can utilize the log drain // to send logs to Axiom without HTTP. // This saves resources and time on lambda and edge functions - if (isVercel && (this.config.source === 'edge' || this.config.source === 'lambda')) { + if (isVercelIntegration && (this.config.source === 'edge-log' || this.config.source === 'lambda-log')) { this.logEvents.forEach((ev) => console.log(JSON.stringify(ev))); this.logEvents = []; return; @@ -188,7 +216,7 @@ export class Logger { if (typeof fetch === 'undefined') { const fetch = await require('whatwg-fetch'); return fetch(url, reqOptions).catch(console.error); - } else if (isBrowser && isVercel && navigator.sendBeacon) { + } else if (isBrowser && isVercelIntegration && navigator.sendBeacon) { // sendBeacon fails if message size is greater than 64kb, so // we fall back to fetch. // Navigator has to be bound to ensure it does not error in some browsers diff --git a/src/platform/generic.ts b/src/platform/generic.ts index 60053d30..856d0551 100644 --- a/src/platform/generic.ts +++ b/src/platform/generic.ts @@ -2,7 +2,7 @@ import { GetServerSidePropsContext, NextApiRequest } from "next"; import { LogEvent, RequestReport } from "../logger"; import { EndpointType } from "../shared"; import type Provider from "./base"; -import { isBrowser } from "../config"; +import { isBrowser, isVercel } from "../config"; // This is the generic config class for all platforms that doesn't have a special // implementation (e.g: vercel, netlify). All config classes extends this one. @@ -33,21 +33,38 @@ export default class GenericConfig implements Provider { wrapWebVitalsObject(metrics: any[]): any { return metrics.map(m => ({ - webVital: m, - _time: new Date().getTime(), - platform: { - environment: this.environment, - source: 'web-vital', - }, + webVital: m, + _time: new Date().getTime(), + platform: { + environment: this.environment, + source: 'web-vital', + }, })) } injectPlatformMetadata(logEvent: LogEvent, source: string) { - logEvent.platform = { + let key: "platform" | "vercel" | "netlify" = "platform" + if (isVercel) { + key = "vercel" + } + + logEvent[key] = { environment: this.environment, region: this.region, - source: source + '-log', + source: source, }; + + if (isVercel) { + logEvent[key]!.region = process.env.VERCEL_REGION; + logEvent[key]!.deploymentId = process.env.VERCEL_DEPLOYMENT_ID; + logEvent[key]!.deploymentUrl = process.env.NEXT_PUBLIC_VERCEL_URL; + logEvent[key]!.project = process.env.NEXT_PUBLIC_VERCEL_PROJECT_PRODUCTION_URL; + logEvent.git = { + commit: process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA, + repo: process.env.NEXT_PUBLIC_VERCEL_GIT_REPO_SLUG, + ref: process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_REF, + } + } } generateRequestMeta(req: NextApiRequest | GetServerSidePropsContext['req']): RequestReport { diff --git a/src/platform/netlify.ts b/src/platform/netlify.ts index 4da69f80..2123099b 100644 --- a/src/platform/netlify.ts +++ b/src/platform/netlify.ts @@ -38,7 +38,7 @@ export default class NetlifyConfig extends GenericConfig implements Provider { logEvent.netlify = { environment: this.environment, region: source === 'edge' ? process.env.DENO_REGION : process.env.AWS_REGION, - source: source + '-log', + source: source, siteId: netlifySiteId, buildId: netlifyBuildId, context: netlifyContext, diff --git a/src/webVitals/webVitals.ts b/src/webVitals/webVitals.ts index 0ea4c97b..a2587e0a 100644 --- a/src/webVitals/webVitals.ts +++ b/src/webVitals/webVitals.ts @@ -1,5 +1,5 @@ import { NextWebVitalsMetric } from 'next/app'; -import { config, isBrowser, isVercel, Version } from '../config'; +import { config, isBrowser, isVercelIntegration, Version } from '../config'; import { throttle } from '../shared'; const url = config.getWebVitalsEndpoint(); @@ -35,7 +35,7 @@ function sendMetrics() { fetch(url, reqOptions).catch(console.error); } - if (isBrowser && isVercel && navigator.sendBeacon) { + if (isBrowser && isVercelIntegration && navigator.sendBeacon) { try { // See https://github.com/vercel/next.js/pull/26601 // Navigator has to be bound to ensure it does not error in some browsers diff --git a/src/withAxiom.ts b/src/withAxiom.ts index 0889c4a3..9d318927 100644 --- a/src/withAxiom.ts +++ b/src/withAxiom.ts @@ -1,8 +1,8 @@ import { NextConfig } from 'next'; import { Rewrite } from 'next/dist/lib/load-custom-routes'; -import { config, isEdgeRuntime } from './config'; +import { config, isEdgeRuntime, isVercelIntegration } from './config'; import { Logger, RequestReport } from './logger'; -import { type NextRequest, type NextResponse } from 'next/server'; +import { NextRequest, type NextResponse } from 'next/server'; import { EndpointType } from './shared'; export function withAxiomNextConfig(nextConfig: NextConfig): NextConfig { @@ -54,6 +54,7 @@ type NextHandler = ( arg?: T ) => Promise | Promise | NextResponse | Response; + export function withAxiomRouteHandler(handler: NextHandler): NextHandler { return async (req: Request | NextRequest, arg: any) => { let region = ''; @@ -61,9 +62,16 @@ export function withAxiomRouteHandler(handler: NextHandler): NextHandler { region = req.geo?.region ?? ''; } + let pathname = '' + if (req instanceof NextRequest) { + pathname = req.nextUrl.pathname + } else if (req instanceof Request) { + pathname = req.url.substring(req.headers.get('host')?.length || 0) + } + const report: RequestReport = { startTime: new Date().getTime(), - path: req.url, + path: pathname, method: req.method, host: req.headers.get('host'), userAgent: req.headers.get('user-agent'), @@ -72,23 +80,25 @@ export function withAxiomRouteHandler(handler: NextHandler): NextHandler { region, }; - const logger = new Logger({ req: report, source: isEdgeRuntime ? 'edge' : 'lambda' }); + const logger = new Logger({ req: report, source: isEdgeRuntime ? 'edge-log' : 'lambda-log' }); const axiomContext = req as AxiomRequest; const args = arg; axiomContext.log = logger; try { const result = await handler(axiomContext, args); + logger.attachResponseStatus(result.status) await logger.flush(); - if (isEdgeRuntime) { + if (isEdgeRuntime && isVercelIntegration) { logEdgeReport(report); } return result; } catch (error: any) { - logger.error('Error in Next route handler', { error }); + logger.error(error.message, { error }) + // logger.error('Error in Next route handler', { error }); logger.attachResponseStatus(500); await logger.flush(); - if (isEdgeRuntime) { + if (isEdgeRuntime && isVercelIntegration) { logEdgeReport(report); } throw error; From de537aef8df7e68b089b6c1bba602bc2556a4c85 Mon Sep 17 00:00:00 2001 From: Islam Shehata Date: Tue, 14 May 2024 18:30:42 +0300 Subject: [PATCH 02/13] introduce source field in log record root level --- src/logger.ts | 2 ++ src/platform/generic.ts | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/logger.ts b/src/logger.ts index 8a00b1f8..6f5cfca9 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -13,6 +13,7 @@ export interface LogEvent { _time: string; request?: RequestReport; git?: any, + source: string; platform?: PlatformInfo; vercel?: PlatformInfo; netlify?: NetlifyInfo; @@ -111,6 +112,7 @@ export class Logger { level: LogLevel[level].toString(), message, _time: new Date(Date.now()).toISOString(), + source: this.config.source!, fields: this.config.args || {}, "@axiom": { "next-axiom": Version, diff --git a/src/platform/generic.ts b/src/platform/generic.ts index 856d0551..cf7e70a9 100644 --- a/src/platform/generic.ts +++ b/src/platform/generic.ts @@ -39,6 +39,7 @@ export default class GenericConfig implements Provider { environment: this.environment, source: 'web-vital', }, + source: 'web-vital' })) } @@ -48,6 +49,7 @@ export default class GenericConfig implements Provider { key = "vercel" } + logEvent.source = source; logEvent[key] = { environment: this.environment, region: this.region, From 1e1b5d9e3bc6c0959d1009cff1a39f2de752ea41 Mon Sep 17 00:00:00 2001 From: Islam Shehata Date: Wed, 15 May 2024 13:55:50 +0300 Subject: [PATCH 03/13] remove /api from AXIOM_URL --- src/platform/base.ts | 2 -- src/platform/generic.ts | 17 ++--------------- 2 files changed, 2 insertions(+), 17 deletions(-) diff --git a/src/platform/base.ts b/src/platform/base.ts index bf1c967c..1dd9cf49 100644 --- a/src/platform/base.ts +++ b/src/platform/base.ts @@ -1,5 +1,4 @@ import { NextWebVitalsMetric } from 'next/app'; -import { RequestReport } from '../logger'; import { EndpointType } from '../shared'; // This is the base class for all platform providers. It contains all the different @@ -16,7 +15,6 @@ export default interface Provider { getIngestURL(t: EndpointType): string; wrapWebVitalsObject(metrics: NextWebVitalsMetric[]): any; injectPlatformMetadata(logEvent: any, source: string): void; - generateRequestMeta(req: any): RequestReport; getLogsEndpoint(): string getWebVitalsEndpoint(): string } diff --git a/src/platform/generic.ts b/src/platform/generic.ts index cf7e70a9..3f1d517c 100644 --- a/src/platform/generic.ts +++ b/src/platform/generic.ts @@ -1,5 +1,5 @@ import { GetServerSidePropsContext, NextApiRequest } from "next"; -import { LogEvent, RequestReport } from "../logger"; +import { LogEvent } from "../logger"; import { EndpointType } from "../shared"; import type Provider from "./base"; import { isBrowser, isVercel } from "../config"; @@ -20,7 +20,7 @@ export default class GenericConfig implements Provider { } getIngestURL(_: EndpointType): string { - return `${this.axiomUrl}/api/v1/datasets/${this.dataset}/ingest`; + return `${this.axiomUrl}/v1/datasets/${this.dataset}/ingest`; } getLogsEndpoint(): string { @@ -69,19 +69,6 @@ export default class GenericConfig implements Provider { } } - generateRequestMeta(req: NextApiRequest | GetServerSidePropsContext['req']): RequestReport { - return { - startTime: new Date().getTime(), - path: req.url!, - method: req.method!, - host: this.getHeaderOrDefault(req, 'host', ''), - userAgent: this.getHeaderOrDefault(req, 'user-agent', ''), - scheme: 'https', - ip: this.getHeaderOrDefault(req, 'x-forwarded-for', ''), - region: this.region, - }; - } - getHeaderOrDefault(req: NextApiRequest | GetServerSidePropsContext['req'], headerName: string, defaultValue: any) { return req.headers[headerName] ? req.headers[headerName] : defaultValue; } From 626519796a5a4a1ac68362103d33ae163d4694e3 Mon Sep 17 00:00:00 2001 From: Islam Shehata Date: Wed, 15 May 2024 13:56:46 +0300 Subject: [PATCH 04/13] fix request.path in withAxiom --- src/logger.ts | 1 + src/withAxiom.ts | 35 +++++++++++++++++++++++++++-------- 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/src/logger.ts b/src/logger.ts index 6f5cfca9..8f852c18 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -32,6 +32,7 @@ export enum LogLevel { export interface RequestReport { startTime: number; + endTime: number; statusCode?: number; ip?: string | null; region?: string | null; diff --git a/src/withAxiom.ts b/src/withAxiom.ts index 9d318927..6510a1f0 100644 --- a/src/withAxiom.ts +++ b/src/withAxiom.ts @@ -66,37 +66,56 @@ export function withAxiomRouteHandler(handler: NextHandler): NextHandler { if (req instanceof NextRequest) { pathname = req.nextUrl.pathname } else if (req instanceof Request) { - pathname = req.url.substring(req.headers.get('host')?.length || 0) + // pathname = req.url.substring(req.headers.get('host')?.length || 0) + pathname = new URL(req.url).pathname } const report: RequestReport = { startTime: new Date().getTime(), + endTime: new Date().getTime(), path: pathname, method: req.method, host: req.headers.get('host'), userAgent: req.headers.get('user-agent'), - scheme: 'https', + scheme: req.url.split('://')[0], ip: req.headers.get('x-forwarded-for'), region, }; - const logger = new Logger({ req: report, source: isEdgeRuntime ? 'edge-log' : 'lambda-log' }); + // main logger, mainly used to log reporting on the incoming HTTP request + const logger = new Logger({ req: report, source: isEdgeRuntime ? 'edge' : 'lambda' }); + // child logger to be used by the users within the handler + const log = logger.with({}) + log.config.source = isEdgeRuntime ? 'edge-log' : 'lambda-log' const axiomContext = req as AxiomRequest; const args = arg; - axiomContext.log = logger; + axiomContext.log = log; try { const result = await handler(axiomContext, args); - logger.attachResponseStatus(result.status) + report.endTime = new Date().getTime(); + + // report log record + report.statusCode = result.status; + logger.logHttpRequest(`[${req.method}] ${report.path} ${report.statusCode} ${report.endTime - report.startTime}ms`, report, {}); + // attach the response status to all children logs + log.attachResponseStatus(result.status) + + // flush the logger along with the child logger await logger.flush(); if (isEdgeRuntime && isVercelIntegration) { logEdgeReport(report); } return result; } catch (error: any) { - logger.error(error.message, { error }) - // logger.error('Error in Next route handler', { error }); - logger.attachResponseStatus(500); + report.endTime = new Date().getTime(); + // report log record + report.statusCode = 500; + logger.logHttpRequest(`[${req.method}] ${report.path} ${report.statusCode} ${report.endTime - report.startTime}ms`, report, {}); + + log.error(error.message, { error }) + log.attachResponseStatus(500); + await logger.flush(); if (isEdgeRuntime && isVercelIntegration) { logEdgeReport(report); From 5622b2d8a5b75b6da8e421344378ba3dbd7b8b82 Mon Sep 17 00:00:00 2001 From: Islam Shehata Date: Fri, 17 May 2024 14:18:18 +0300 Subject: [PATCH 05/13] introduce logger.middleware() method --- src/logger.ts | 33 +++++++++++++++++++++++++++------ src/withAxiom.ts | 8 +++++--- 2 files changed, 32 insertions(+), 9 deletions(-) diff --git a/src/logger.ts b/src/logger.ts index 8f852c18..1306254d 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -1,7 +1,10 @@ +import { NextRequest } from 'next/server'; import { config, isBrowser, isVercelIntegration, Version } from './config'; import { NetlifyInfo } from './platform/netlify'; import { isNoPrettyPrint, throttle } from './shared'; + + const url = config.getLogsEndpoint(); const LOG_LEVEL = process.env.NEXT_PUBLIC_AXIOM_LOG_LEVEL || 'debug'; @@ -17,8 +20,8 @@ export interface LogEvent { platform?: PlatformInfo; vercel?: PlatformInfo; netlify?: NetlifyInfo; - "@axiom": { - "next-axiom": string, + "@app": { + "next-axiom-version": string; } } @@ -41,6 +44,7 @@ export interface RequestReport { method: string; scheme: string; userAgent?: string | null; + durationMs?: number; } export interface PlatformInfo { @@ -115,8 +119,8 @@ export class Logger { _time: new Date(Date.now()).toISOString(), source: this.config.source!, fields: this.config.args || {}, - "@axiom": { - "next-axiom": Version, + "@app": { + "next-axiom-version": Version, } }; @@ -144,8 +148,8 @@ export class Logger { return logEvent; } - logHttpRequest(message: string, request: any, args: any) { - const logEvent = this._transformEvent(LogLevel.info, message, args); + logHttpRequest(level: LogLevel, message: string, request: any, args: any) { + const logEvent = this._transformEvent(level, message, args); logEvent.request = request; this.logEvents.push(logEvent); if (this.config.autoFlush) { @@ -153,6 +157,23 @@ export class Logger { } } + middleware(request: NextRequest) { + const req = { + ip: request.ip, + region: request.geo?.region, + method: request.method, + host: request.nextUrl.hostname, + path: request.nextUrl.pathname, + scheme: request.nextUrl.protocol.split(":")[0], + referer: request.headers.get('Referer'), + userAgent: request.headers.get('user-agent'), + } + + const message = `[${request.method}] [middleware: "middleware"] ${request.nextUrl.pathname}` + + return this.logHttpRequest(LogLevel.info, message, req, {}) + } + private _log = (level: LogLevel, message: string, args: { [key: string]: any } = {}) => { if (level < this.logLevel) { return; diff --git a/src/withAxiom.ts b/src/withAxiom.ts index 6510a1f0..3cf84e71 100644 --- a/src/withAxiom.ts +++ b/src/withAxiom.ts @@ -1,7 +1,7 @@ import { NextConfig } from 'next'; import { Rewrite } from 'next/dist/lib/load-custom-routes'; import { config, isEdgeRuntime, isVercelIntegration } from './config'; -import { Logger, RequestReport } from './logger'; +import { LogLevel, Logger, RequestReport } from './logger'; import { NextRequest, type NextResponse } from 'next/server'; import { EndpointType } from './shared'; @@ -97,7 +97,8 @@ export function withAxiomRouteHandler(handler: NextHandler): NextHandler { // report log record report.statusCode = result.status; - logger.logHttpRequest(`[${req.method}] ${report.path} ${report.statusCode} ${report.endTime - report.startTime}ms`, report, {}); + report.durationMs = report.endTime - report.startTime; + logger.logHttpRequest(LogLevel.info, `[${req.method}] ${report.path} ${report.statusCode} ${report.endTime - report.startTime}ms`, report, {}); // attach the response status to all children logs log.attachResponseStatus(result.status) @@ -111,7 +112,8 @@ export function withAxiomRouteHandler(handler: NextHandler): NextHandler { report.endTime = new Date().getTime(); // report log record report.statusCode = 500; - logger.logHttpRequest(`[${req.method}] ${report.path} ${report.statusCode} ${report.endTime - report.startTime}ms`, report, {}); + report.durationMs = report.endTime - report.startTime; + logger.logHttpRequest(LogLevel.error, `[${req.method}] ${report.path} ${report.statusCode} ${report.endTime - report.startTime}ms`, report, {}); log.error(error.message, { error }) log.attachResponseStatus(500); From cde11244fa6cc0f926f1d40f80e9b5b318d40003 Mon Sep 17 00:00:00 2001 From: Islam Shehata Date: Wed, 22 May 2024 15:10:56 +0300 Subject: [PATCH 06/13] update README.md --- README.md | 74 +++++++++++++++++------------------ examples/logger/error.tsx | 40 +++++++++++++------ examples/logger/middleware.ts | 19 +-------- 3 files changed, 66 insertions(+), 67 deletions(-) diff --git a/README.md b/README.md index 14b8fad2..e2c0f5b8 100644 --- a/README.md +++ b/README.md @@ -57,32 +57,13 @@ import { Logger } from 'next-axiom' import { NextResponse } from 'next/server' import type { NextFetchEvent, NextRequest } from 'next/server' -const logger = new Logger({ - source: 'traffic' -}); - -// This function can be marked `async` if using `await` inside export async function middleware(request: NextRequest, event: NextFetchEvent) { - const req = { - ip: request.ip, - region: request.geo?.region, - method: request.method, - host: request.nextUrl.host, - path: request.nextUrl.pathname, - scheme: request.nextUrl.protocol.split(":")[0], - referer: request.headers.get('Referer'), - userAgent: request.headers.get('user-agent'), - statusCode: 0, - } - - - const message = `[${request.method}] [middleware: "middleware"] ${request.nextUrl.pathname}` - - logger.logHttpRequest(message, req, {}) - event.waitUntil(logger.flush()) + const logger = new Logger({ source: 'middleware' }); // traffic, request + logger.middleware(request) + event.waitUntil(logger.flush()) return NextResponse.next() -} + // See "Matching Paths" below to learn more export const config = { @@ -191,35 +172,50 @@ You can also disable logging completely by setting the log level to `off`. To capture routing errors we can use the [Error Handling](https://nextjs.org/docs/app/building-your-application/routing/error-handling) mechanism of Next. -Create a new file named `error.ts` under your `/app` directory. Inside your component function use the logger to ingest the error to Axiom. e.g: +Create a new file named `error.tsx` under your `/app` directory. Inside your component function use the logger to ingest the error to Axiom. e.g: ```typescript "use client"; import NavTable from "@/components/NavTable"; +import { LogLevel } from "@/next-axiom/logger"; import { useLogger } from "next-axiom"; +import { usePathname } from "next/navigation"; -export default function Error({ +export default function ErrorPage({ error, }: { error: Error & { digest?: string }; }) { - const log = useLogger(); - log.error(error.message, { - error: error.name, - cause: error.cause, - stack: error.stack, - digest: error.digest, - }); - - return
- Ops! An Error has occurred:

`{ error.message }`

- -
+ const pathname = usePathname() + const log = useLogger({ source: "error.tsx" }); + let status = error.message == 'Invalid URL' ? 404 : 500; + + log.logHttpRequest( + LogLevel.error, + error.message, + { + host: window.location.href, + path: pathname, + statusCode: status, + }, + { + error: error.name, + cause: error.cause, + stack: error.stack, + digest: error.digest, + }, + ); + + return ( +
+ Ops! An Error has occurred:{" "} +

`{error.message}`

+
+
- -
; + ); } ``` diff --git a/examples/logger/error.tsx b/examples/logger/error.tsx index eb20ff39..6ab5876f 100644 --- a/examples/logger/error.tsx +++ b/examples/logger/error.tsx @@ -1,24 +1,42 @@ -'use client'; +"use client"; -import { useLogger } from 'next-axiom'; +import NavTable from "@/components/NavTable"; +import { LogLevel } from "@/next-axiom/logger"; +import { useLogger } from "next-axiom"; +import { usePathname } from "next/navigation"; -export default function Error({ +export default function ErrorPage({ error, }: { error: Error & { digest?: string }; }) { - const log = useLogger(); - log.error(error.message, { - error: error.name, - cause: error.cause, - stack: error.stack, - digest: error.digest, - }); + const pathname = usePathname() + const log = useLogger({ source: "error.tsx" }); + let status = error.message == 'Invalid URL' ? 404 : 500; + + log.logHttpRequest( + LogLevel.error, + error.message, + { + host: window.location.href, + path: pathname, + statusCode: status, + }, + { + error: error.name, + cause: error.cause, + stack: error.stack, + digest: error.digest, + }, + ); return (
- Ops! An Error has occurred:{' '} + Ops! An Error has occurred:{" "}

`{error.message}`

+
+ +
); } diff --git a/examples/logger/middleware.ts b/examples/logger/middleware.ts index af857be6..d955173b 100644 --- a/examples/logger/middleware.ts +++ b/examples/logger/middleware.ts @@ -2,24 +2,9 @@ import { Logger } from 'next-axiom' import { NextResponse } from 'next/server' import type { NextFetchEvent, NextRequest } from 'next/server' -const logger = new Logger({ source: 'traffic' }); - export async function middleware(request: NextRequest, event: NextFetchEvent) { - const req = { - ip: request.ip, - region: request.geo?.region, - method: request.method, - host: request.nextUrl.hostname, - path: request.nextUrl.pathname, - scheme: request.nextUrl.protocol.split(":")[0], - referer: request.headers.get('Referer'), - userAgent: request.headers.get('user-agent'), - } - - const message = `[${request.method}] [middleware: "middleware"] ${request.nextUrl.pathname}` - - logger.logHttpRequest(message, req, {}) - + const logger = new Logger({ source: 'middleware' }); // traffic, request + logger.middleware(request) event.waitUntil(logger.flush()) return NextResponse.next() From c092679fd711b952e46e6d38edb31e7b7e9a754a Mon Sep 17 00:00:00 2001 From: Islam Shehata Date: Thu, 23 May 2024 12:47:01 +0300 Subject: [PATCH 07/13] fix format --- src/logger.ts | 29 +++++++++++++---------------- src/withAxiom.ts | 31 ++++++++++++++++++++----------- 2 files changed, 33 insertions(+), 27 deletions(-) diff --git a/src/logger.ts b/src/logger.ts index d6d179b3..2733c290 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -3,8 +3,6 @@ import { config, isBrowser, isVercelIntegration, Version } from './config'; import { NetlifyInfo } from './platform/netlify'; import { isNoPrettyPrint, throttle } from './shared'; - - const url = config.getLogsEndpoint(); const LOG_LEVEL = process.env.NEXT_PUBLIC_AXIOM_LOG_LEVEL || 'debug'; @@ -15,14 +13,14 @@ export interface LogEvent { fields: any; _time: string; request?: RequestReport; - git?: any, + git?: any; source: string; platform?: PlatformInfo; vercel?: PlatformInfo; netlify?: NetlifyInfo; - "@app": { - "next-axiom-version": string; - } + '@app': { + 'next-axiom-version': string; + }; } export enum LogLevel { @@ -78,7 +76,6 @@ export class Logger { autoFlush: true, source: 'frontend-log', prettyPrint: prettyPrint, - }; constructor(public initConfig: LoggerConfig = {}) { @@ -122,9 +119,9 @@ export class Logger { _time: new Date(Date.now()).toISOString(), source: this.config.source!, fields: this.config.args || {}, - "@app": { - "next-axiom-version": Version, - } + '@app': { + 'next-axiom-version': Version, + }, }; // check if passed args is an object, if its not an object, add it to fields.args @@ -149,7 +146,7 @@ export class Logger { } return logEvent; - } + }; logHttpRequest(level: LogLevel, message: string, request: any, args: any) { const logEvent = this._transformEvent(level, message, args); @@ -167,21 +164,21 @@ export class Logger { method: request.method, host: request.nextUrl.hostname, path: request.nextUrl.pathname, - scheme: request.nextUrl.protocol.split(":")[0], + scheme: request.nextUrl.protocol.split(':')[0], referer: request.headers.get('Referer'), userAgent: request.headers.get('user-agent'), - } + }; - const message = `[${request.method}] [middleware: "middleware"] ${request.nextUrl.pathname}` + const message = `[${request.method}] [middleware: "middleware"] ${request.nextUrl.pathname}`; - return this.logHttpRequest(LogLevel.info, message, req, {}) + return this.logHttpRequest(LogLevel.info, message, req, {}); } private _log = (level: LogLevel, message: string, args: { [key: string]: any } = {}) => { if (level < this.logLevel) { return; } - const logEvent = this._transformEvent(level, message, args) + const logEvent = this._transformEvent(level, message, args); this.logEvents.push(logEvent); if (this.config.autoFlush) { diff --git a/src/withAxiom.ts b/src/withAxiom.ts index 3cf84e71..b0313283 100644 --- a/src/withAxiom.ts +++ b/src/withAxiom.ts @@ -54,7 +54,6 @@ type NextHandler = ( arg?: T ) => Promise | Promise | NextResponse | Response; - export function withAxiomRouteHandler(handler: NextHandler): NextHandler { return async (req: Request | NextRequest, arg: any) => { let region = ''; @@ -62,12 +61,12 @@ export function withAxiomRouteHandler(handler: NextHandler): NextHandler { region = req.geo?.region ?? ''; } - let pathname = '' + let pathname = ''; if (req instanceof NextRequest) { - pathname = req.nextUrl.pathname + pathname = req.nextUrl.pathname; } else if (req instanceof Request) { // pathname = req.url.substring(req.headers.get('host')?.length || 0) - pathname = new URL(req.url).pathname + pathname = new URL(req.url).pathname; } const report: RequestReport = { @@ -85,8 +84,8 @@ export function withAxiomRouteHandler(handler: NextHandler): NextHandler { // main logger, mainly used to log reporting on the incoming HTTP request const logger = new Logger({ req: report, source: isEdgeRuntime ? 'edge' : 'lambda' }); // child logger to be used by the users within the handler - const log = logger.with({}) - log.config.source = isEdgeRuntime ? 'edge-log' : 'lambda-log' + const log = logger.with({}); + log.config.source = isEdgeRuntime ? 'edge-log' : 'lambda-log'; const axiomContext = req as AxiomRequest; const args = arg; axiomContext.log = log; @@ -98,9 +97,14 @@ export function withAxiomRouteHandler(handler: NextHandler): NextHandler { // report log record report.statusCode = result.status; report.durationMs = report.endTime - report.startTime; - logger.logHttpRequest(LogLevel.info, `[${req.method}] ${report.path} ${report.statusCode} ${report.endTime - report.startTime}ms`, report, {}); + logger.logHttpRequest( + LogLevel.info, + `[${req.method}] ${report.path} ${report.statusCode} ${report.endTime - report.startTime}ms`, + report, + {} + ); // attach the response status to all children logs - log.attachResponseStatus(result.status) + log.attachResponseStatus(result.status); // flush the logger along with the child logger await logger.flush(); @@ -113,9 +117,14 @@ export function withAxiomRouteHandler(handler: NextHandler): NextHandler { // report log record report.statusCode = 500; report.durationMs = report.endTime - report.startTime; - logger.logHttpRequest(LogLevel.error, `[${req.method}] ${report.path} ${report.statusCode} ${report.endTime - report.startTime}ms`, report, {}); - - log.error(error.message, { error }) + logger.logHttpRequest( + LogLevel.error, + `[${req.method}] ${report.path} ${report.statusCode} ${report.endTime - report.startTime}ms`, + report, + {} + ); + + log.error(error.message, { error }); log.attachResponseStatus(500); await logger.flush(); From 29a544692350bde38387d18157e2cf7502d7fbd0 Mon Sep 17 00:00:00 2001 From: Islam Shehata Date: Thu, 23 May 2024 12:49:47 +0300 Subject: [PATCH 08/13] Update README.md Co-authored-by: Arne Bahlo --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index cee14072..f3fb0841 100644 --- a/README.md +++ b/README.md @@ -184,7 +184,9 @@ const logger = new Logger({ To capture routing errors we can use the [Error Handling](https://nextjs.org/docs/app/building-your-application/routing/error-handling) mechanism of Next. -Create a new file named `error.tsx` under your `/app` directory. Inside your component function use the logger to ingest the error to Axiom. e.g: +Create a new file named `error.tsx` under your `/app` directory. Inside your component function use the logger to ingest the error to Axiom. + +Example: ```typescript "use client"; From 9ca7301e1d6896be2ae9bd210d030fccaa77f1d8 Mon Sep 17 00:00:00 2001 From: Islam Shehata Date: Thu, 23 May 2024 12:50:04 +0300 Subject: [PATCH 09/13] Update README.md Co-authored-by: Arne Bahlo --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index f3fb0841..002aeb1b 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,6 @@ export async function middleware(request: NextRequest, event: NextFetchEvent) { return NextResponse.next() -// See "Matching Paths" below to learn more export const config = { } ``` From 6eeb7895eb2c147a1854f858ab0228f5d01c1a93 Mon Sep 17 00:00:00 2001 From: Islam Shehata Date: Thu, 23 May 2024 12:52:56 +0300 Subject: [PATCH 10/13] remove un-needed comments --- README.md | 7 ++++--- examples/logger/middleware.ts | 3 +-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 002aeb1b..3376b78f 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ Otherwise create a dataset and an API token in [Axiom settings](https://app.axio ## Capture traffic requests -Create a `middleware.ts` in the root dir of your app: +Create or edit the `middleware.ts` in the root directory of your app: ```typescript import { Logger } from 'next-axiom' @@ -58,11 +58,12 @@ import { NextResponse } from 'next/server' import type { NextFetchEvent, NextRequest } from 'next/server' export async function middleware(request: NextRequest, event: NextFetchEvent) { - const logger = new Logger({ source: 'middleware' }); // traffic, request + const logger = new Logger({ source: 'middleware' }); logger.middleware(request) event.waitUntil(logger.flush()) return NextResponse.next() +} export const config = { @@ -183,7 +184,7 @@ const logger = new Logger({ To capture routing errors we can use the [Error Handling](https://nextjs.org/docs/app/building-your-application/routing/error-handling) mechanism of Next. -Create a new file named `error.tsx` under your `/app` directory. Inside your component function use the logger to ingest the error to Axiom. +Create or edit the `error.tsx` file under your `/app` directory. Inside your component function use the logger to ingest the error to Axiom. Example: diff --git a/examples/logger/middleware.ts b/examples/logger/middleware.ts index d955173b..876a7885 100644 --- a/examples/logger/middleware.ts +++ b/examples/logger/middleware.ts @@ -3,13 +3,12 @@ import { NextResponse } from 'next/server' import type { NextFetchEvent, NextRequest } from 'next/server' export async function middleware(request: NextRequest, event: NextFetchEvent) { - const logger = new Logger({ source: 'middleware' }); // traffic, request + const logger = new Logger({ source: 'middleware' }); logger.middleware(request) event.waitUntil(logger.flush()) return NextResponse.next() } -// See "Matching Paths" below to learn more export const config = { } \ No newline at end of file From d5865f56c891365c32e3794fcfc00dfaac60e77f Mon Sep 17 00:00:00 2001 From: Islam Shehata Date: Thu, 23 May 2024 13:27:21 +0300 Subject: [PATCH 11/13] remove tailwind classes from error.tsx --- README.md | 6 +----- examples/logger/error.tsx | 6 +----- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 3376b78f..0d4c0aa3 100644 --- a/README.md +++ b/README.md @@ -222,12 +222,8 @@ export default function ErrorPage({ ); return ( -
+
Ops! An Error has occurred:{" "} -

`{error.message}`

-
- -
); } diff --git a/examples/logger/error.tsx b/examples/logger/error.tsx index 6ab5876f..c3c8a387 100644 --- a/examples/logger/error.tsx +++ b/examples/logger/error.tsx @@ -31,12 +31,8 @@ export default function ErrorPage({ ); return ( -
+
Ops! An Error has occurred:{" "} -

`{error.message}`

-
- -
); } From adc37611be587cefcdfcdd9b38662c000f2ba489 Mon Sep 17 00:00:00 2001 From: Islam Shehata Date: Thu, 23 May 2024 13:27:44 +0300 Subject: [PATCH 12/13] bump next-axiom to v1.3.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1f083aa3..bfb20739 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "next-axiom", "description": "Send WebVitals from your Next.js project to Axiom.", - "version": "1.2.0", + "version": "1.3.0", "author": "Axiom, Inc.", "license": "MIT", "contributors": [ From 35d5e11150f45edc366fb292e71b78e043f4b8d8 Mon Sep 17 00:00:00 2001 From: Islam Shehata Date: Thu, 23 May 2024 15:26:22 +0300 Subject: [PATCH 13/13] fix vercel integration test --- tests/vercelConfig.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/vercelConfig.test.ts b/tests/vercelConfig.test.ts index 61636143..ba4a94ad 100644 --- a/tests/vercelConfig.test.ts +++ b/tests/vercelConfig.test.ts @@ -1,5 +1,5 @@ import { test, expect, vi } from 'vitest'; -import { config } from '../src/config'; +import { config, isVercelIntegration } from '../src/config'; import { EndpointType } from '../src/shared'; import { Logger } from '../src/logger'; @@ -22,7 +22,7 @@ test('logging to console when running on lambda', async () => { const time = new Date(Date.now()).toISOString(); const logger = new Logger({ - source: 'lambda', + source: 'lambda-log', }); logger.info('hello, world!'); @@ -34,5 +34,5 @@ test('logging to console when running on lambda', async () => { expect(calledWithPayload.message).toEqual('hello, world!'); expect(calledWithPayload.level).toEqual('info'); expect(calledWithPayload._time).toEqual(time); - expect(calledWithPayload.vercel.source).toEqual('lambda'); + expect(calledWithPayload.vercel.source).toEqual('lambda-log'); });