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.
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 linesYou 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(anderror,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()andspan.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
Related Skills
BetterStack
"BetterStack (formerly Better Uptime + Logtail): uptime monitoring, log management, status pages, incident management, alerting"
Checkly
"Checkly: synthetic monitoring, API checks, browser checks, Playwright-based E2E monitoring, monitoring-as-code CLI"
Cronitor
Cronitor is a robust monitoring service designed to ensure your background jobs (cron jobs, scheduled tasks, async workers) and APIs run reliably. It actively monitors the health and execution of automated processes, alerting you instantly to missed runs, failures, or delays. Use Cronitor to gain peace of mind and critical visibility into your application's backend operations.
Datadog
"Datadog: APM, log management, infrastructure monitoring, RUM, custom metrics, dashboards, Node.js tracing"
Grafana Cloud
Grafana Cloud is a fully managed observability platform that unifies metrics (Prometheus/Graphite), logs (Loki), and traces (Tempo) within a single Grafana interface. Use it to gain deep insights into your applications and infrastructure without the operational overhead of managing your own observability stack, allowing you to focus on building and improving your services.
Highlight.io
"Highlight.io: open-source monitoring, session replay, error tracking, logging, tracing, Next.js SDK, self-hosted option"