Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 60 additions & 8 deletions src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ export class BearerToken {
}

export class API {
private bearerToken: BearerToken | null = null;

/**
* Creates a new API client instance.
*
Expand All @@ -47,32 +49,62 @@ export class API {
return `agentops-ts-sdk/${process.env.npm_package_version || 'unknown'}`;
}

/**
* Set the bearer token for authenticated requests
*/
setBearerToken(token: BearerToken): void {
this.bearerToken = token;
}

/**
* Fetch data from the API using the specified path and method.
*
* @param path - The API endpoint path
* @param method - The HTTP method to use (GET or POST)
* @param body - The request body for POST requests
* @param headers - Additional headers to include in the request
* @returns The parsed JSON response
*/
private async fetch<T>(path: string, method: 'GET' | 'POST', body?: any): Promise<T> {
private async fetch<T>(
path: string,
method: 'GET' | 'POST',
body?: any,
headers?: Record<string, string>
): Promise<T> {
const url = `${this.endpoint}${path}`;
debug(`${method} ${url}`);

const defaultHeaders: Record<string, string> = {
'User-Agent': this.userAgent,
'Content-Type': 'application/json',
};

// Add authorization header if bearer token is available
if (this.bearerToken) {
defaultHeaders['Authorization'] = this.bearerToken.getAuthHeader();
}

// Merge with additional headers
const finalHeaders = { ...defaultHeaders, ...headers };

const response = await fetch(url, {
method: method,
headers: {
'User-Agent': this.userAgent,
'Content-Type': 'application/json',
},
headers: finalHeaders,
body: body ? JSON.stringify(body) : undefined
});

if (!response.ok) {
throw new Error(`Request failed: ${response.status} ${response.statusText}`);
let errorMessage = `Request failed: ${response.status} ${response.statusText}`;
try {
const errorData = await response.json();
if (errorData.error) {
errorMessage = errorData.error;
}
} catch {
// Ignore JSON parsing errors
}
throw new Error(errorMessage);
}

debug(`${response.status}`);
return await response.json() as T;
}

Expand All @@ -84,4 +116,24 @@ export class API {
async authenticate(): Promise<TokenResponse> {
return this.fetch<TokenResponse>('/v3/auth/token', 'POST', { api_key: this.apiKey });
}

/**
* Upload log content to the API.
*
* @param logContent - The log content to upload
* @param traceId - The trace ID to associate with the logs
* @returns A promise that resolves when the upload is complete
*/
async uploadLogFile(logContent: string, traceId: string): Promise<{ id: string }> {
if (!this.bearerToken) {
throw new Error('Authentication required. Bearer token not set.');
}

return this.fetch<{ id: string }>(
'/v4/logs/upload/',
'POST',
logContent,
{ 'Trace-Id': traceId }
);
}
}
25 changes: 24 additions & 1 deletion src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Config, LogLevel } from './types';
import { API, TokenResponse, BearerToken } from './api';
import { TracingCore } from './tracing';
import { getGlobalResource } from './attributes';
import { loggingService } from './logging/service';

const debug = require('debug')('agentops:client');

Expand Down Expand Up @@ -83,10 +84,17 @@ export class Client {
}
this.api = new API(this.config.apiKey, this.config.apiEndpoint!);

// Get auth token and set it on the API instance
const authToken = await this.getAuthToken();
this.api.setBearerToken(authToken);

// Initialize logging service
loggingService.initialize(this.api);

const resource = await getGlobalResource(this.config.serviceName!);
this.core = new TracingCore(
this.config,
await this.getAuthToken(),
authToken,
this.registry.getActiveInstrumentors(this.config.serviceName!),
resource
);
Expand Down Expand Up @@ -127,6 +135,9 @@ export class Client {
return;
}

// Disable logging service
loggingService.disable();

if(this.core) {
await this.core.shutdown();
}
Expand Down Expand Up @@ -180,5 +191,17 @@ export class Client {
return this.authToken;
}

/**
* Upload captured console logs to the AgentOps API.
*
* @param traceId - The trace ID to associate with the logs
* @returns Promise resolving to upload result with ID, or null if no logs to upload
* @throws {Error} When the SDK is not initialized or upload fails
*/
async uploadLogFile(traceId: string): Promise<{ id: string } | null> {
this.ensureInitialized();
return loggingService.uploadLogs(traceId);
}

}

26 changes: 0 additions & 26 deletions src/log.ts

This file was deleted.

39 changes: 39 additions & 0 deletions src/logging/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Log Upload Functionality

Simple log capture and upload functionality for AgentOps TypeScript SDK, matching the Python SDK implementation.

## How it works

1. When the SDK is initialized, console methods (log, info, warn, error, debug) are automatically patched
2. All console output is captured to an in-memory buffer with timestamps
3. Logs can be uploaded to the API using `uploadLogFile(traceId)`
4. Buffer is cleared after successful upload

## Usage

```typescript
import { agentops } from 'agentops';

// Initialize SDK - starts capturing console output
await agentops.init({ apiKey: 'your-api-key' });

// Your application code - all console output is captured
console.log('Application started');
console.error('An error occurred');

// Upload logs when needed
const result = await agentops.uploadLogFile('trace-123');
if (result) {
console.log(`Logs uploaded: ${result.id}`);
}

// Shutdown SDK
await agentops.shutdown();
```

## Implementation Details

- **Buffer**: Simple array-based buffer that stores timestamped log entries
- **Format**: `YYYY-MM-DDTHH:mm:ss.sssZ - LEVEL - message`
- **API Endpoint**: POST to `/v4/logs/upload/` with trace ID in headers
- **Cleanup**: Original console methods restored on SDK shutdown
39 changes: 39 additions & 0 deletions src/logging/buffer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/**
* Simple memory buffer for capturing console logs
*/
export class LogBuffer {
private buffer: string[] = [];

/**
* Append a log entry to the buffer
*/
append(entry: string): void {
const timestamp = new Date().toISOString();
const formattedEntry = `${timestamp} - ${entry}`;
this.buffer.push(formattedEntry);
}

/**
* Get all buffer content as a single string
*/
getContent(): string {
return this.buffer.join('\n');
}

/**
* Clear the buffer
*/
clear(): void {
this.buffer = [];
}

/**
* Check if buffer is empty
*/
isEmpty(): boolean {
return this.buffer.length === 0;
}
}

// Global log buffer instance
export const globalLogBuffer = new LogBuffer();
3 changes: 3 additions & 0 deletions src/logging/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { LogBuffer, globalLogBuffer } from './buffer';
export { LoggingInstrumentor, loggingInstrumentor } from './instrumentor';
export { LoggingService, loggingService } from './service';
82 changes: 82 additions & 0 deletions src/logging/instrumentor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { globalLogBuffer } from './buffer';

export class LoggingInstrumentor {
private originalMethods: Map<string, Function> = new Map();
private isPatched: boolean = false;

/**
* Patch console methods to capture output to the log buffer
*/
patch(): void {
if (this.isPatched) {
return;
}

// List of console methods to patch
const methodsToPatch = ['log', 'info', 'warn', 'error', 'debug'];

methodsToPatch.forEach(method => {
const originalMethod = (console as any)[method];
this.originalMethods.set(method, originalMethod);

// Create a patched version that logs to buffer and calls original
(console as any)[method] = (...args: any[]) => {
// Format the message
const message = args
.map(arg => {
if (typeof arg === 'object') {
try {
return JSON.stringify(arg);
} catch {
return String(arg);
}
}
return String(arg);
})
.join(' ');

// Add level prefix and append to buffer
const levelPrefix = method.toUpperCase();
globalLogBuffer.append(`${levelPrefix} - ${message}`);

// Call the original method
originalMethod.apply(console, args);
};
});

this.isPatched = true;
}

/**
* Restore original console methods
*/
unpatch(): void {
if (!this.isPatched) {
return;
}

this.originalMethods.forEach((originalMethod, method) => {
(console as any)[method] = originalMethod;
});

this.originalMethods.clear();
this.isPatched = false;
}

/**
* Setup cleanup handlers to restore console on exit
*/
setupCleanup(): void {
const cleanup = () => {
this.unpatch();
globalLogBuffer.clear();
};

process.on('exit', cleanup);
process.on('SIGINT', cleanup);
process.on('SIGTERM', cleanup);
}
}

// Global logging instrumentor instance
export const loggingInstrumentor = new LoggingInstrumentor();
Loading