By Kris Black | Published on 2/6/2024

Why Add Logging to Your Astro Site?

Modern web applications need robust logging for monitoring, debugging, and security. While Vercel provides excellent analytics and error tracking, adding Axiom gives you deeper insights into your application's behavior, especially for API endpoints and custom events.

In this guide, we'll walk through implementing Axiom logging in an Astro site, covering both client-side and server-side logging. We'll focus on practical implementation with real code examples.

Prerequisites

Before we begin, you'll need:

  • An existing Astro project
  • An Axiom account (free tier available)
  • Basic understanding of TypeScript

Initial Setup

First, let's set up our environment variables. Create or update your .env file:

# Axiom Configuration
AXIOM_TOKEN=your-token-here
AXIOM_DATASET=your-dataset-name
AXIOM_ORG_ID=your-org-id

Make sure to add these variables to your deployment platform (e.g., Vercel) as well.

Creating the Logger Class

Let's create a TypeScript logger class that handles our logging needs. Create a new file at src/utils/logger.ts:

import fetch from 'node-fetch';

// Define log levels
export type LogLevel = 'debug' | 'info' | 'warn' | 'error';

// Define log data structure
interface LogData {
  message: string;
  level: LogLevel;
  context?: Record<string, any>;
  timestamp?: string;
  [key: string]: any;
}

class Logger {
  private static instance: Logger;
  private readonly dataset: string;
  private readonly token: string;
  private readonly isDevelopment: boolean;

  private constructor() {
    this.dataset = import.meta.env.AXIOM_DATASET || 'your-dataset';
    this.token = import.meta.env.AXIOM_TOKEN || '';
    this.isDevelopment = import.meta.env.DEV === true;
  }

  public static getInstance(): Logger {
    if (!Logger.instance) {
      Logger.instance = new Logger();
    }
    return Logger.instance;
  }

  private async ingest(data: LogData) {
    try {
      // Always add timestamp if not provided
      const logEntry = {
        ...data,
        timestamp: data.timestamp || new Date().toISOString(),
        environment: this.isDevelopment ? 'development' : 'production'
      };

      // In development, also log to console
      if (this.isDevelopment) {
        console[data.level](logEntry);
      }

      // Send to Axiom
      const response = await fetch('https://api.axiom.co/v1/datasets/' + this.dataset + '/ingest', {
        method: 'POST',
        headers: {
          'Authorization': 'Bearer ' + this.token,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify([logEntry])
      });

      if (!response.ok) {
        throw new Error('Failed to send log to Axiom: ' + response.statusText);
      }
    } catch (error) {
      console.error('Logging failed:', error);
      console.error('Original log:', data);
    }
  }

  public async log(level: LogLevel, message: string, context?: Record<string, any>) {
    await this.ingest({
      level,
      message,
      context,
      service: 'api'
    });
  }

  public async debug(message: string, context?: Record<string, any>) {
    await this.log('debug', message, context);
  }

  public async info(message: string, context?: Record<string, any>) {
    await this.log('info', message, context);
  }

  public async warn(message: string, context?: Record<string, any>) {
    await this.log('warn', message, context);
  }

  public async error(message: string, context?: Record<string, any>) {
    await this.log('error', message, context);
  }
}

Adding Client-Side Logging

For client-side logging, including Web Vitals tracking, add this script to your src/layouts/partials/HeaderScripts.astro:

<!-- Axiom Analytics -->
<script is:inline define:vars={{ dataset: import.meta.env.AXIOM_DATASET, token: import.meta.env.AXIOM_TOKEN }}>
  // Initialize Axiom logging
  window.axiom = {
    dataset,
    token,
    webVitals: true
  };

  // Web Vitals reporting
  function sendToAxiom({ name, delta, id }) {
    const body = {
      timestamp: new Date().toISOString(),
      name,
      value: delta,
      id,
      page: window.location.pathname,
    };

    fetch('https://api.axiom.co/v1/datasets/' + dataset + '/ingest', {
      method: 'POST',
      headers: {
        'Authorization': 'Bearer ' + token,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify([body])
    }).catch(console.error);
  }

  // Report Web Vitals
  if (window.webVitals) {
    window.webVitals.getCLS(sendToAxiom);
    window.webVitals.getFID(sendToAxiom);
    window.webVitals.getLCP(sendToAxiom);
    window.webVitals.getFCP(sendToAxiom);
    window.webVitals.getTTFB(sendToAxiom);
  }
</script>

<!-- Load Web Vitals library -->
<script defer src="https://unpkg.com/web-vitals"></script>

Implementing API Logging

For API endpoints, use the logger in your API routes. Here's an example:

import type { APIRoute } from 'astro';
import { logger } from '../utils/logger';

export const post: APIRoute = async ({ request, clientAddress }) => {
  try {
    // Log incoming request
    await logger.info('API Request', {
      path: new URL(request.url).pathname,
      method: request.method,
      clientAddress
    });

    // Your API logic here
    const data = await request.json();
    
    // Log successful response
    await logger.info('API Response', {
      success: true,
      data
    });

    return new Response(JSON.stringify({ success: true }), {
      status: 200
    });
  } catch (error) {
    // Log error
    await logger.error('API Error', {
      error: error instanceof Error ? error.message : 'Unknown error',
      stack: error instanceof Error ? error.stack : undefined
    });

    return new Response(JSON.stringify({ success: false }), {
      status: 500
    });
  }
};

Viewing Your Logs

To view your logs:

  1. Log into your Axiom dashboard
  2. Select your dataset
  3. Use the Stream view for real-time logs
  4. Use the Query interface for detailed analysis

Common queries you might want to try:

  • level:"error" - View all errors
  • environment:"production" - Production logs only
  • name:"CLS" - Core Web Vitals measurements

Best Practices

  • Environment Separation: Always tag logs with the environment (development/production)
  • Contextual Information: Include relevant context with each log
  • Security: Never log sensitive information
  • Performance: Use batch logging when possible
  • Error Handling: Always include error stacks and context

Wrapping Up

With Axiom logging implemented, you now have comprehensive insights into your Astro application's behavior. You can track performance metrics, monitor API endpoints, and quickly identify issues in both development and production environments.

Remember to regularly review your logs and set up alerts for critical events to maintain a healthy application.

#Astro #Axiom #WebDevelopment #Logging #Monitoring

#WebVitals #APIMonitoring #DevTools #TypeScript #WebPerformance