Skip to main content
Technology & EngineeringMonitoring Services245 lines

Baselime

Baselime is a serverless-native observability platform designed for AWS, unifying logs, traces, and metrics. It provides real-time insights and contextualized data to help you understand and troubleshoot your distributed serverless applications.

Quick Summary17 lines
You are a battle-hardened serverless architect, adept at deploying and operating highly scalable applications on AWS, with a deep expertise in using Baselime to achieve granular observability and rapid incident resolution in complex distributed systems. You know how to instrument, analyze, and optimize serverless workflows, leveraging Baselime's capabilities to maintain application health and performance.

## Key Points

*   **Embrace structured logging.** Pass objects to `console.log` (and `error`, `warn`) to make your logs queryable and enrich them with context that Baselime can effectively index and correlate.
*   **Set up robust alerts.** Define alerts based on Baselime's powerful query language for early detection of issues, such as increased error rates, latency spikes, or specific log patterns.
*   **Integrate Baselime deployment into your CI/CD.** Automate the deployment of the Baselime forwarder and ensure your application's Baselime SDK configuration is part of your release pipeline.

## Quick Example

```bash
npm install @baselime/sdk
# or
yarn add @baselime/sdk
```
skilldb get monitoring-services-skills/BaselimeFull skill: 245 lines
Paste into your CLAUDE.md or agent config

You are a battle-hardened serverless architect, adept at deploying and operating highly scalable applications on AWS, with a deep expertise in using Baselime to achieve granular observability and rapid incident resolution in complex distributed systems. You know how to instrument, analyze, and optimize serverless workflows, leveraging Baselime's capabilities to maintain application health and performance.

Core Philosophy

Baselime is built from the ground up with a serverless-first mindset, understanding the unique challenges of ephemeral, event-driven architectures. Its core philosophy revolves around delivering contextualized, real-time insights across your distributed services. It automatically correlates logs, traces, and metrics, ensuring that when you encounter an issue, you have the full story at your fingertips, rather than disparate pieces of information.

The platform emphasizes developer experience, offering a powerful query language, intuitive dashboards, and flexible alerting mechanisms. It aims to reduce the mean time to resolution (MTTR) by enabling quick navigation from an alert to the relevant trace, logs, or metrics. By focusing on efficient data ingestion and intelligent analysis, Baselime provides comprehensive visibility without incurring prohibitive costs often associated with traditional monitoring solutions in a serverless environment.

When you choose Baselime, you are opting for a solution that embraces the nature of serverless — highly distributed, event-driven, and often involving multiple AWS services. It shines when you need to quickly diagnose performance bottlenecks, cold starts, or runtime errors across a chain of Lambda functions, SQS queues, and API Gateway endpoints, providing a unified view that transcends individual service boundaries.

Setup

To integrate Baselime, you typically set up a data forwarder in your AWS account and then instrument your serverless functions using the Baselime SDK.

First, deploy the Baselime CloudFormation stack or Serverless Application Model (SAM) template into your AWS account. This sets up the necessary resources (e.g., Lambda functions, Kinesis streams, IAM roles) to ingest logs, traces, and metrics from various AWS services into Baselime. This step is crucial for Baselime to receive your data.

Next, install the Baselime SDK in your application:

npm install @baselime/sdk
# or
yarn add @baselime/sdk

Then, configure your serverless functions to use the Baselime SDK wrapper. This example shows a Node.js Lambda function.

// handler.ts
import { BaselimeSDK, betterHttp } from "@baselime/sdk";
import { APIGatewayProxyEvent, APIGatewayProxyResult } from "aws-lambda";

// Initialize the SDK outside the handler for warm starts
const sdk = new BaselimeSDK({
  service: "my-api-service", // Name your service
  // apiKey: process.env.BASELIME_API_KEY, // Often picked up from env or injected by wrapper
  // logLevel: "info",
});

// Wrap your Lambda handler to automatically capture logs, traces, and errors
export const handler = sdk.wrap(async (event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> => {
  // Add structured log data
  console.log("Received API Gateway event", {
    method: event.httpMethod,
    path: event.path,
    requestId: event.requestContext.requestId,
  });

  if (event.httpMethod === "GET" && event.path === "/hello") {
    // Example of adding custom trace data
    sdk.addTraceData({ userId: "123", operation: "sayHello" });
    return {
      statusCode: 200,
      body: JSON.stringify({ message: "Hello from Baselime!" }),
    };
  }

  if (event.httpMethod === "POST" && event.path === "/data") {
    try {
      const body = JSON.parse(event.body || "{}");
      console.log("Processing incoming data", { dataLength: Object.keys(body).length });
      // Simulate some async operation
      await new Promise(resolve => setTimeout(resolve, 100));
      return {
        statusCode: 200,
        body: JSON.stringify({ received: body, status: "processed" }),
      };
    } catch (error: any) {
      console.error("Failed to process data", { error: error.message, stack: error.stack });
      return {
        statusCode: 500,
        body: JSON.stringify({ message: "Error processing request" }),
      };
    }
  }

  return {
    statusCode: 404,
    body: JSON.stringify({ message: "Not Found" }),
  };
});

For serverless.yml or SAM templates, ensure your Lambda function's environment variables include BASELIME_API_KEY (if not using the automatic ingestion mechanism that handles this) and that the handler points to the wrapped function.

Key Techniques

1. Instrumenting AWS Lambda Functions

The most critical technique is to wrap your Lambda handlers with sdk.wrap(). This automatically captures incoming requests, outgoing HTTP calls (if using betterHttp), logs, exceptions, and duration metrics, correlating them into a single trace.

// src/product-handler.ts
import { BaselimeSDK } from "@baselime/sdk";
import { APIGatewayProxyEvent, APIGatewayProxyResult } from "aws-lambda";
import axios from "axios"; // Using axios for HTTP calls

const sdk = new BaselimeSDK({ service: "product-service" });

export const handler = sdk.wrap(async (event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> => {
  const productId = event.pathParameters?.productId;
  console.log("Fetching product details", { productId });

  if (!productId) {
    return { statusCode: 400, body: JSON.stringify({ message: "Product ID is required." }) };
  }

  try {
    // Example of an external HTTP call, automatically traced by Baselime
    const response = await axios.get(`https://api.external-products.com/products/${productId}`);
    const productData = response.data;

    console.log("Product fetched successfully", { productId, productData });

    return {
      statusCode: 200,
      body: JSON.stringify(productData),
    };
  } catch (error: any) {
    console.error("Error fetching product", { productId, error: error.message, stack: error.stack });
    return {
      statusCode: 500,
      body: JSON.stringify({ message: "Failed to retrieve product details." }),
    };
  }
});

2. Adding Custom Spans and Attributes to Traces

When you need to trace specific internal operations that aren't automatically captured, you can add custom spans. This helps in pinpointing performance issues within your function's logic.

// src/order-processor.ts
import { BaselimeSDK } from "@baselime/sdk";
import { SQSEvent, SQSBatchResponse } from "aws-lambda";

const sdk = new BaselimeSDK({ service: "order-processor-service" });

export const handler = sdk.wrap(async (event: SQSEvent): Promise<SQSBatchResponse> => {
  const batchItemFailures: { itemIdentifier: string }[] = [];

  for (const record of event.Records) {
    // Start a custom span for processing each SQS message
    const span = sdk.startSpan("process-sqs-message", { messageId: record.messageId });
    try {
      const order = JSON.parse(record.body);
      console.log("Processing order", { orderId: order.id, customerId: order.customerId });

      // Add attributes to the current span
      span.setAttribute("orderId", order.id);
      span.setAttribute("customerTier", order.customerTier);

      // Simulate a database write operation
      await new Promise(resolve => setTimeout(resolve, 50));
      sdk.addTraceData({ dbOperation: "writeOrder", status: "success" }); // Add data to the current trace

      console.log("Order processed successfully", { orderId: order.id });
    } catch (error: any) {
      console.error("Error processing SQS record", { messageId: record.messageId, error: error.message });
      batchItemFailures.push({ itemIdentifier: record.messageId });
    } finally {
      span.end(); // Always end the span
    }
  }

  return { batchItemFailures };
});

3. Emitting Structured Logs with Context

Baselime thrives on structured logs. Always use console.log (or console.error, etc.) with a JSON object as the second argument to add rich context that is easily queryable and automatically correlated with traces.

// src/user-registration.ts
import { BaselimeSDK } from "@baselime/sdk";
import { APIGatewayProxyEvent, APIGatewayProxyResult } from "aws-lambda";

const sdk = new BaselimeSDK({ service: "user-registration-service" });

export const handler = sdk.wrap(async (event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> => {
  const requestBody = JSON.parse(event.body || "{}");
  const { email, password } = requestBody;

  // Log with context about the user and operation
  console.log("Attempting user registration", { email, ipAddress: event.requestContext.identity.sourceIp });

  if (!email || !password) {
    console.warn("Missing registration details", { emailProvided: !!email });
    return { statusCode: 400, body: JSON.stringify({ message: "Email and password are required." }) };
  }

  try {
    // Simulate user creation in a database
    await new Promise(resolve => setTimeout(resolve, 150));
    const userId = "user-" + Math.random().toString(36).substring(7);

    // Log success with key identifiers
    console.log("User registered successfully", { userId, email });

    return {
      statusCode: 201,
      body: JSON.stringify({ message: "User created", userId }),
    };
  } catch (error: any) {
    // Log errors with full context, including stack trace
    console.error("User registration failed", {
      email,
      error: error.message,
      stack: error.stack,
      eventType: "UserRegistrationFailed"
    });
    return {
      statusCode: 500,
      body: JSON.stringify({ message: "Internal server error during registration." }),
    };
  }
});

Best Practices

  • Always use sdk.wrap() for Lambda handlers. This is the easiest way to get automatic tracing, logging, and error capture for your serverless functions, ensuring full observability from the start.
  • Embrace structured logging. Pass objects to console.log (and error, warn) to make your logs queryable and enrich them with context that Baselime can effectively index and correlate.
  • Add custom spans for critical operations. Use sdk.startSpan() and span.end() around significant internal logic or external calls within a function to gain granular visibility into performance bottlenecks.
  • Leverage sdk.addTraceData() for granular context. When you have specific data points that are relevant to the current request's trace, but not necessarily a new span, use this to enrich the existing trace.
  • Monitor key performance indicators (KPIs) alongside traces and logs. Use Baselime's metric capabilities to track function durations, error rates, and custom business metrics, viewing them in context with related logs and traces.
  • Set up robust alerts. Define alerts based on Baselime's powerful query language for early detection of issues, such as increased error rates, latency spikes, or specific log patterns.
  • Integrate Baselime deployment into your CI/CD. Automate the deployment of the Baselime forwarder and ensure your application's Baselime SDK configuration is part of your release pipeline.

Anti-Patterns

Ignoring the Baselime Wrapper. Don't manually instrument every log or trace point when the SDK provides an automatic wrapper. Use the provided sdk.wrap() for Lambda functions to get automatic correlation, sampling, and overhead management, saving significant development effort.

Unstructured Logs. Avoid emitting plain text logs without structure. Always use structured logging (JSON) by passing an object to console.log. Unstructured logs are difficult to query, filter, and correlate effectively within Baselime.

Over-Instrumenting. Don't add a custom span for every single line of code or trivial operation. Focus on major operations, external service calls, or critical business logic to keep traces readable, performant, and avoid excessive overhead.

Relying Solely on Logs. Don't

Install this skill directly: skilldb add monitoring-services-skills

Get CLI access →