@salesforce/o11y-reporter
Version:
A wrapper service around o11y and o11y_schema for telemetry reporting.
315 lines (235 loc) • 10.5 kB
Markdown
# O11y Reporter
A lightweight telemetry reporting service for Salesforce extensions that enables sending metrics and events to Salesforce's observability platform.
## Usage
The O11y Reporter service provides a simple way to send telemetry events and metrics to Salesforce's observability platform. Here's how to use it:
### 1. Initialize the Service
```typescript
import { O11yService } from "@salesforce/o11y-reporter";
// Get the singleton instance
const o11yService = O11yService.getInstance(extensionName);
// Initialize with your extension name and upload endpoint (static endpoint only)
await o11yService.initialize(
"your-extension-name",
"https://your-upload-endpoint",
);
```
**Dynamic endpoint (authenticated org):** To send telemetry to the current org’s endpoint (with automatic fallback to the static endpoint if the connection is unavailable), pass an optional `getConnection` callback and, if needed, an options object with `dynamicO11yUploadEndpointPath`. If you omit the path, a default is used.
```typescript
import { O11yService, type Connection } from "@salesforce/o11y-reporter";
const o11yService = O11yService.getInstance(extensionName);
await o11yService.initialize(
"your-extension-name",
"https://fallback-static-endpoint",
() => yourWorkspaceContext.getConnection(),
{ dynamicO11yUploadEndpointPath: "/your/telemetry/path" },
);
```
The `Connection` type is re-exported from `/core`; depend on it when using `getConnection`.
### 2. Send Telemetry Events
#### Using Default Schema
```typescript
// Send a telemetry event with properties (uses default sf_a4dInstrumentation schema)
o11yService.logEvent({
name: "extension/eventName",
properties: {
// Add your custom properties here
customProperty: "value",
},
measurements: {
// Optional measurements
duration: 100,
},
});
// Send an exception event
o11yService.logEvent({
exception: new Error("Error message"),
properties: {
// Add your custom properties here
errorType: "RuntimeError",
},
measurements: {
// Optional measurements
errorCount: 1,
},
});
```
#### Using Custom Schema
The service supports consumer-provided O11y schemas. This allows you to use custom schemas from the `o11y_schema` package instead of the default `sf_a4dInstrumentation` schema.
**Step 1: Add `o11y_schema` to your `package.json`:**
```json
{
"dependencies": {
"o11y_schema": "^256.154.0"
}
}
```
**Step 2: Import and use the schema with `logEventWithSchema(properties, schema)` (exactly two parameters):**
```typescript
import { O11yService } from "@salesforce/o11y-reporter";
// Import the schema object from o11y_schema
// @ts-expect-error - o11y_schema package doesn't provide TypeScript declarations
import { a4dInstrumentationSchema } from "o11y_schema/sf_a4dInstrumentation";
const o11yService = O11yService.getInstance("your-extension");
await o11yService.initialize("your-extension", "https://your-endpoint");
// Two parameters only: (properties, schema). Shape of properties depends on your schema.
o11yService.logEventWithSchema(
{ customProperty: "value" },
a4dInstrumentationSchema,
);
// You can also use different schemas for different events
// @ts-expect-error - o11y_schema package doesn't provide TypeScript declarations
import { anotherSchema } from "o11y_schema/another_schema";
o11yService.logEventWithSchema({ foo: "bar" }, anotherSchema);
```
**Note:** `logEventWithSchema(properties, schema)` takes exactly **two** parameters: the event properties object and the schema. You must provide a valid schema object (e.g. `Record<string, unknown>`). If you don't need a custom schema, use `logEvent(properties)` which uses the default schema.
**Behavior:** Events sent with `logEventWithSchema(properties, schema)` are encoded using the schema you pass (e.g. `PdpEvent`, `a4dInstrumentationSchema`). They are not wrapped in the default A4dInstrumentation schema; the payload uses your schema's format directly (e.g. `sf.pdp.PdpEvent` in the binary payload).
**Note:** The `o11y_schema` package doesn't provide TypeScript declarations. You may need to add `-expect-error` comments or configure your TypeScript with `skipLibCheck: true` in `tsconfig.json`.
**With automatic batching enabled, events are automatically uploaded based on threshold (50KB) or periodic flush (30 seconds). You can also manually flush if needed:**
```typescript
await o11yService.forceFlush();
```
### 3. Enable Automatic Batching
The service supports automatic batching of events for efficient telemetry collection. Events are automatically buffered and uploaded based on size threshold or periodic flush intervals.
```typescript
// Enable automatic batching with default settings (30-second flush interval)
const cleanup = o11yService.enableAutoBatching();
// Or customize the batching behavior
const cleanup = o11yService.enableAutoBatching({
flushInterval: 30_000, // 30 seconds (default)
enableShutdownHook: true, // Enable shutdown hooks (default: true)
enableBeforeExitHook: true, // Enable beforeExit hook (default: true)
});
// Later, if you need to stop batching
cleanup();
```
**Batching Behavior:**
- Events are buffered in memory until upload conditions are met
- **Threshold-based upload**: Events are uploaded when buffer size reaches 50KB
- **Periodic flush**: Events are automatically flushed every 30 seconds (configurable)
- **Shutdown hooks**: Events are automatically flushed on application shutdown (SIGINT, SIGTERM, beforeExit)
**Benefits:**
- Reduces network overhead by batching multiple events
- Improves performance by avoiding per-event uploads
- Ensures events are not lost on application shutdown
### 4. Manual Flush
You can manually flush buffered events at any time:
```typescript
// Force an immediate flush of all buffered events
await o11yService.forceFlush();
// Or use the upload method (alias for forceFlush when batching is enabled)
await o11yService.upload();
```
### 5. Check Batch Status
Monitor the current batch status:
```typescript
const status = o11yService.getBatchStatus();
console.log(`Buffer size: ${status.estimatedByteSize} bytes`);
console.log(`Has data: ${status.hasData}`);
console.log(`Over threshold: ${status.isOverThreshold}`);
```
## Configuration
The service can be configured with the following options:
- `o11yUploadEndpoint`: The endpoint URL for uploading telemetry data
### Batching Options
When enabling automatic batching, you can configure the following options:
```typescript
interface BatchingOptions {
/** Periodic flush interval in milliseconds (default: 30000) */
flushInterval?: number;
/** Buffer size threshold in bytes before triggering upload (default: 50000 = 50KB) */
thresholdBytes?: number;
/** Threshold check interval in milliseconds (default: 2000) */
checkInterval?: number;
/** Enable shutdown hooks (default: true) */
enableShutdownHook?: boolean;
/** Enable beforeExit hook (default: true). Note: beforeExit won't fire for STDIO servers where stdin stays open */
enableBeforeExitHook?: boolean;
}
```
**Configuration Examples:**
```typescript
// Default configuration (30-second flush, shutdown hooks enabled)
o11yService.enableAutoBatching();
// Custom flush interval (60 seconds)
o11yService.enableAutoBatching({ flushInterval: 60_000 });
// Disable shutdown hooks (not recommended)
o11yService.enableAutoBatching({ enableShutdownHook: false });
// Disable beforeExit hook (useful for STDIO servers)
o11yService.enableAutoBatching({ enableBeforeExitHook: false });
```
## Best Practices
1. **Initialize Early**: Initialize the service as early as possible in your extension's lifecycle.
2. **Error Handling**: Always wrap telemetry calls in try-catch blocks to prevent them from affecting your main application flow.
3. **Property Naming**: Use consistent property names and avoid sending sensitive information.
4. **Enable Automatic Batching**: Use `enableAutoBatching()` to automatically batch and upload events efficiently. This is recommended for most use cases.
5. **Manual Flush for Critical Events**: For critical events that need immediate upload, call `forceFlush()` after logging the event.
## Example Implementation
Here's a complete example of how to use the O11y Reporter in your extension:
```typescript
import { O11yService } from "@salesforce/o11y-reporter";
class YourExtension {
private o11yService: O11yService;
constructor() {
this.o11yService = O11yService.getInstance(extensionName);
}
async initialize() {
await this.o11yService.initialize(
"your-extension",
"https://your-endpoint",
);
// Enable automatic batching for efficient telemetry collection
this.o11yService.enableAutoBatching({
flushInterval: 30_000, // 30 seconds
enableShutdownHook: true, // Flush on shutdown
});
}
async trackUserAction(actionName: string, properties: Record<string, any>) {
try {
// Using default schema
this.o11yService.logEvent({
name: `user/${actionName}`,
properties: {
...properties,
timestamp: new Date().toISOString(),
},
});
// With batching enabled, events are automatically uploaded
// For critical events, you can force an immediate flush:
await this.o11yService.forceFlush();
} catch (error) {
// Log error but don't throw
console.error("Failed to send telemetry:", error);
}
}
async trackUserActionWithSchema(
actionName: string,
properties: Record<string, any>,
schema: Record<string, unknown>,
) {
try {
// Two parameters: caller's properties (conform to schema) and schema.
this.o11yService.logEventWithSchema(properties, schema);
// With batching enabled, events are automatically uploaded
await this.o11yService.forceFlush();
} catch (error) {
console.error("Failed to send telemetry:", error);
}
}
async trackError(error: Error, context: Record<string, any>) {
try {
this.o11yService.logEvent({
exception: error,
properties: {
...context,
timestamp: new Date().toISOString(),
},
});
await this.o11yService.upload();
} catch (err) {
console.error("Failed to send error telemetry:", err);
}
}
}
```
## License
This project is licensed under the Terms of Use. See the [LICENSE.txt](LICENSE.txt) file for details.