@agentscope/studio
Version:
AgentScope Studio is a powerful local monitoring and visualization tool designed to provide real-time insights into your system's performance and behavior.
639 lines (638 loc) • 26 kB
JavaScript
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.MigrateSpanTable1740000000000 = void 0;
const typeorm_1 = require("typeorm");
const objectUtils_1 = require("../../../shared/src/utils/objectUtils");
const timeUtils_1 = require("../../../shared/src/utils/timeUtils");
const Trace_1 = require("../models/Trace");
const processor_1 = require("../otel/processor");
const asString = (value, fallback = '') => typeof value === 'string' ? value : fallback;
const asNumber = (value, fallback = 0) => typeof value === 'number' ? value : fallback;
const asOptionalString = (value) => typeof value === 'string' ? value : undefined;
const asOptionalNumber = (value) => typeof value === 'number' ? value : undefined;
const toStringOrUndefined = (value) => value !== undefined && value !== null ? String(value) : undefined;
function parseJsonOrObject(value) {
if (typeof value === 'string') {
try {
return JSON.parse(value);
}
catch (_a) {
return {};
}
}
if (value && typeof value === 'object') {
return value;
}
return {};
}
function decodeStatus(status) {
var _a;
if (typeof status === 'string') {
const statusMap = {
OK: 1,
ERROR: 2,
UNSET: 0,
};
const upperStatus = status.toUpperCase();
return {
code: (_a = statusMap[upperStatus]) !== null && _a !== void 0 ? _a : 0,
message: '',
};
}
if (status && typeof status === 'object') {
const s = status;
if ('code' in s && typeof s.code === 'number') {
return {
code: s.code,
message: typeof s.message === 'string' ? s.message : '',
};
}
}
return { code: 0, message: '' };
}
function decodeEvents(eventsValue) {
if (!eventsValue)
return [];
let eventsArray = [];
if (typeof eventsValue === 'string') {
try {
eventsArray = JSON.parse(eventsValue);
}
catch (_a) {
eventsArray = [];
}
}
else if (Array.isArray(eventsValue)) {
eventsArray = eventsValue;
}
return eventsArray.map((event) => {
const e = event;
const timeUnixNano = asString(e.timestamp)
? (0, timeUtils_1.encodeUnixNano)(asString(e.timestamp))
: asString(e.timeUnixNano) || asString(e.time) || '0';
return {
name: asString(e.name),
time: timeUnixNano,
attributes: (e.attributes &&
typeof e.attributes === 'object' &&
e.attributes !== null
? e.attributes
: {}),
droppedAttributesCount: asNumber(e.droppedAttributesCount, 0),
};
});
}
function decodeResource(attributes) {
const serviceName = (0, objectUtils_1.getNestedValue)(attributes, 'service.name') ||
(0, objectUtils_1.getNestedValue)(attributes, 'project.service_name');
const resourceAttributes = {};
if (serviceName) {
resourceAttributes['service.name'] = serviceName;
}
const resourceKeys = [
'service.namespace',
'service.version',
'service.instance.id',
];
for (const key of resourceKeys) {
const value = (0, objectUtils_1.getNestedValue)(attributes, key);
if (value !== undefined) {
resourceAttributes[key] = value;
}
}
return {
attributes: resourceAttributes,
};
}
function decodeScope() {
return {
name: 'agentscope',
version: '1.0.7',
attributes: {},
};
}
function getConversationId(attributes, record) {
return (toStringOrUndefined((0, objectUtils_1.getNestedValue)(attributes, 'gen_ai.conversation.id')) ||
toStringOrUndefined((0, objectUtils_1.getNestedValue)(attributes, 'project.run_id')) ||
toStringOrUndefined(record.conversationId) ||
toStringOrUndefined(record.conversation_id) ||
'unknown');
}
function getSpanId(record, attributes) {
const spanId = toStringOrUndefined(record.id) ||
toStringOrUndefined(record.spanId) ||
toStringOrUndefined((0, objectUtils_1.getNestedValue)(attributes, 'span.id')) ||
toStringOrUndefined((0, objectUtils_1.getNestedValue)(attributes, 'spanId'));
if (!spanId) {
throw new Error(`Cannot determine spanId for record. Record has no 'id' field: ${JSON.stringify(record)}`);
}
return spanId;
}
// Helper methods to extract key fields (similar to SpanDao)
function extractServiceName(resource) {
const value = (0, objectUtils_1.getNestedValue)(resource.attributes, 'service.name');
return typeof value === 'string' ? value : undefined;
}
function extractOperationName(attributes) {
const value = (0, objectUtils_1.getNestedValue)(attributes, 'gen_ai.operation.name');
return typeof value === 'string' ? value : undefined;
}
function extractInstrumentationName(scope) {
// Try to get from attributes first (for backward compatibility)
const valueFromAttributes = (0, objectUtils_1.getNestedValue)(scope.attributes, 'server.name');
if (typeof valueFromAttributes === 'string') {
return valueFromAttributes;
}
// Fallback to scope.name
return scope.name;
}
function extractInstrumentationVersion(scope) {
// Try to get from attributes first (for backward compatibility)
const valueFromAttributes = (0, objectUtils_1.getNestedValue)(scope.attributes, 'server.version');
if (typeof valueFromAttributes === 'string') {
return valueFromAttributes;
}
// Fallback to scope.version
return scope.version;
}
function extractModel(attributes) {
const value = (0, objectUtils_1.getNestedValue)(attributes, 'gen_ai.request.model');
return typeof value === 'string' ? value : undefined;
}
function extractInputTokens(attributes) {
const value = (0, objectUtils_1.getNestedValue)(attributes, 'gen_ai.usage.input_tokens');
return typeof value === 'number' ? value : undefined;
}
function extractOutputTokens(attributes) {
const value = (0, objectUtils_1.getNestedValue)(attributes, 'gen_ai.usage.output_tokens');
return typeof value === 'number' ? value : undefined;
}
function calculateTotalTokens(inputTokens, outputTokens) {
// If both are numbers, return their sum
if (typeof inputTokens === 'number' && typeof outputTokens === 'number') {
return inputTokens + outputTokens;
}
// If only inputTokens is available, return it
if (typeof inputTokens === 'number') {
return inputTokens;
}
// If only outputTokens is available, return it
if (typeof outputTokens === 'number') {
return outputTokens;
}
// If neither is available, return undefined
return undefined;
}
function convertOldRecordToSpanTable(oldRecord) {
const r = oldRecord;
let attributes = parseJsonOrObject(r.attributes);
const convertedResult = processor_1.SpanProcessor.convertOldProtocolToNew(attributes, {
name: asString(r.name),
});
const spanName = convertedResult.span_name || asString(r.name);
attributes = convertedResult.attributes || attributes;
const startTimeUnixNano = asString(r.startTime)
? (0, timeUtils_1.encodeUnixNano)(asString(r.startTime))
: asString(r.startTimeUnixNano, '0');
const endTimeUnixNano = asString(r.endTime)
? (0, timeUtils_1.encodeUnixNano)(asString(r.endTime))
: asString(r.endTimeUnixNano, '0');
const latencyNs = asNumber(r.latencyMs, 0) > 0
? asNumber(r.latencyMs) * 1000000
: asNumber(r.latencyNs, 0) > 0
? asNumber(r.latencyNs)
: (0, timeUtils_1.getTimeDifferenceNano)(startTimeUnixNano, endTimeUnixNano);
const statusObj = decodeStatus(r.status);
const statusMessage = asOptionalString(r.statusMessage);
const status = statusMessage
? Object.assign(Object.assign({}, statusObj), { message: statusMessage }) : statusObj;
const events = decodeEvents(r.events);
const resource = decodeResource(attributes);
const scope = decodeScope();
const conversationId = getConversationId(attributes, r);
const spanId = getSpanId(r, attributes);
// Extract key fields for indexing
const serviceName = extractServiceName(resource);
const operationName = extractOperationName(attributes);
const instrumentationName = extractInstrumentationName(scope);
const instrumentationVersion = extractInstrumentationVersion(scope);
const model = extractModel(attributes);
const inputTokens = extractInputTokens(attributes);
const outputTokens = extractOutputTokens(attributes);
const totalTokens = calculateTotalTokens(inputTokens, outputTokens);
const statusCode = statusObj.code || 0;
const span = new Trace_1.SpanTable();
Object.assign(span, {
id: String(spanId),
traceId: toStringOrUndefined(r.traceId) || '',
spanId: String(spanId),
traceState: toStringOrUndefined(r.traceState),
parentSpanId: toStringOrUndefined(r.parentSpanId),
flags: asOptionalNumber(r.flags),
name: spanName,
kind: asNumber(r.kind, 0),
startTimeUnixNano: startTimeUnixNano,
endTimeUnixNano: endTimeUnixNano,
attributes: attributes,
droppedAttributesCount: 0,
events: events,
droppedEventsCount: 0,
links: [],
droppedLinksCount: 0,
status: status,
resource: resource,
scope: scope,
statusCode: statusCode,
serviceName: serviceName,
operationName: operationName,
instrumentationName: instrumentationName,
instrumentationVersion: instrumentationVersion,
model: model,
inputTokens: inputTokens,
outputTokens: outputTokens,
totalTokens: totalTokens,
conversationId: conversationId,
latencyNs: latencyNs,
});
return span;
}
/**
* Migration: Migrate old span_table structure to new structure
*
* Tasks:
* 1. Check if table exists and if migration has already been completed
* 2. Drop related views
* 3. Backup old table
* 4. Create new table structure
* 5. Migrate historical data
* 6. Delete backup table
*/
class MigrateSpanTable1740000000000 {
constructor() {
this.name = 'MigrateSpanTable1740000000000';
}
up(queryRunner) {
return __awaiter(this, void 0, void 0, function* () {
var _a, _b;
console.log('Starting migration: Migrating SpanTable structure...');
const tableName = 'span_table';
const viewName = 'model_invocation_view';
const oldTableName = 'span_table_old_backup';
// ========================================
// Step 1: Check if table exists
// ========================================
console.log('Step 1: Checking table structure...');
if (!(yield queryRunner.hasTable(tableName))) {
console.log('⏭️ span_table does not exist. Skipping migration.');
return;
}
const table = yield queryRunner.getTable(tableName);
if (!table) {
console.log('Unable to get table structure. Skipping migration.');
return;
}
// Check if migration has already been completed
// New structure has spanId and instrumentationVersion columns
const hasSpanIdColumn = table.findColumnByName('spanId') !== undefined;
const hasInstrumentationVersion = table.findColumnByName('instrumentationVersion') !== undefined;
if (hasSpanIdColumn && hasInstrumentationVersion) {
console.log('✅ Table already has new structure. Migration already completed.');
return;
}
// ========================================
// Step 2: Drop related views
// ========================================
console.log('Step 2: Dropping related views...');
try {
yield queryRunner.query(`DROP VIEW IF EXISTS ${viewName}`);
console.log(`Dropped view ${viewName}`);
}
catch (error) {
console.warn(`Error dropping view (may not exist):`, error);
}
try {
yield queryRunner.query(`DELETE FROM typeorm_metadata WHERE type = 'VIEW' AND name = ?`, [viewName]);
}
catch (_c) {
// Ignore error, table may not exist
}
// ========================================
// Step 3: Backup old table
// ========================================
console.log('Step 3: Backing up old table...');
// If backup table already exists, drop it first
if (yield queryRunner.hasTable(oldTableName)) {
yield queryRunner.dropTable(oldTableName, true);
}
yield queryRunner.query(`ALTER TABLE "${tableName}" RENAME TO "${oldTableName}"`);
console.log(`Renamed table ${tableName} -> ${oldTableName}`);
// ========================================
// Step 4: Create new table structure
// ========================================
console.log('Step 4: Creating new table structure...');
yield queryRunner.createTable(new typeorm_1.Table({
name: tableName,
columns: [
{
name: 'id',
type: 'varchar',
isPrimary: true,
isNullable: false,
},
{
name: 'traceId',
type: 'varchar',
isNullable: false,
},
{
name: 'spanId',
type: 'varchar',
isNullable: false,
},
{
name: 'traceState',
type: 'varchar',
isNullable: true,
},
{
name: 'parentSpanId',
type: 'varchar',
isNullable: true,
},
{
name: 'flags',
type: 'integer',
isNullable: true,
},
{
name: 'name',
type: 'varchar',
isNullable: false,
},
{
name: 'kind',
type: 'integer',
isNullable: false,
},
{
name: 'startTimeUnixNano',
type: 'varchar',
isNullable: false,
},
{
name: 'endTimeUnixNano',
type: 'varchar',
isNullable: false,
},
{
name: 'attributes',
type: 'json',
isNullable: false,
},
{
name: 'droppedAttributesCount',
type: 'integer',
isNullable: true,
},
{
name: 'events',
type: 'json',
isNullable: true,
},
{
name: 'droppedEventsCount',
type: 'integer',
isNullable: true,
},
{
name: 'links',
type: 'json',
isNullable: true,
},
{
name: 'droppedLinksCount',
type: 'integer',
isNullable: true,
},
{
name: 'status',
type: 'json',
isNullable: false,
},
{
name: 'resource',
type: 'json',
isNullable: false,
},
{
name: 'scope',
type: 'json',
isNullable: false,
},
{
name: 'statusCode',
type: 'integer',
isNullable: true,
},
{
name: 'serviceName',
type: 'varchar',
isNullable: true,
},
{
name: 'operationName',
type: 'varchar',
isNullable: true,
},
{
name: 'instrumentationName',
type: 'varchar',
isNullable: true,
},
{
name: 'instrumentationVersion',
type: 'varchar',
isNullable: true,
},
{
name: 'model',
type: 'varchar',
isNullable: true,
},
{
name: 'inputTokens',
type: 'integer',
isNullable: true,
},
{
name: 'outputTokens',
type: 'integer',
isNullable: true,
},
{
name: 'totalTokens',
type: 'integer',
isNullable: true,
},
{
name: 'conversationId',
type: 'varchar',
isNullable: true,
},
{
name: 'latencyNs',
type: 'float',
isNullable: false,
},
],
indices: [
{
name: 'IDX_span_traceId',
columnNames: ['traceId'],
},
{
name: 'IDX_span_spanId',
columnNames: ['spanId'],
},
{
name: 'IDX_span_parentSpanId',
columnNames: ['parentSpanId'],
},
{
name: 'IDX_span_startTimeUnixNano',
columnNames: ['startTimeUnixNano'],
},
{
name: 'IDX_span_statusCode',
columnNames: ['statusCode'],
},
{
name: 'IDX_span_latencyNs',
columnNames: ['latencyNs'],
},
{
name: 'IDX_span_serviceName',
columnNames: ['serviceName'],
},
{
name: 'IDX_span_operationName',
columnNames: ['operationName'],
},
{
name: 'IDX_span_instrumentationName',
columnNames: ['instrumentationName'],
},
{
name: 'IDX_span_model',
columnNames: ['model'],
},
{
name: 'IDX_span_inputTokens',
columnNames: ['inputTokens'],
},
{
name: 'IDX_span_outputTokens',
columnNames: ['outputTokens'],
},
{
name: 'IDX_span_totalTokens',
columnNames: ['totalTokens'],
},
{
name: 'IDX_span_conversationId',
columnNames: ['conversationId'],
},
],
}), true);
console.log('New table structure created successfully');
// ========================================
// Step 5: Migrate historical data
// ========================================
console.log('Step 5: Migrating historical data...');
const oldRecords = yield queryRunner.query(`SELECT * FROM ${oldTableName}`);
if (oldRecords.length === 0) {
console.log('No data to migrate. Dropping backup table');
yield queryRunner.dropTable(oldTableName, true);
console.log('✅ Migration completed!');
return;
}
console.log(`Found ${oldRecords.length} records to migrate`);
let migratedCount = 0;
let errorCount = 0;
const batchSize = 100;
for (let i = 0; i < oldRecords.length; i += batchSize) {
const batch = oldRecords.slice(i, i + batchSize);
const spanTableArray = [];
for (const oldRecord of batch) {
try {
const spanTable = convertOldRecordToSpanTable(oldRecord);
spanTableArray.push(spanTable);
}
catch (error) {
console.error(`Failed to convert record (id: ${(oldRecord === null || oldRecord === void 0 ? void 0 : oldRecord.id) || 'unknown'}):`, error);
errorCount++;
}
}
if (spanTableArray.length > 0) {
try {
// Use queryRunner.manager to save data
yield queryRunner.manager.save(Trace_1.SpanTable, spanTableArray);
migratedCount += spanTableArray.length;
}
catch (error) {
console.error(`Batch save failed:`, error);
errorCount += spanTableArray.length;
}
}
if ((i + batchSize) % 1000 === 0 ||
i + batchSize >= oldRecords.length) {
console.log(`Progress: Migrated ${Math.min(i + batchSize, oldRecords.length)}/${oldRecords.length} records`);
}
}
console.log(`Data migration completed. Success: ${migratedCount}, Failed: ${errorCount}`);
// ========================================
// Step 6: Validate and drop backup table
// ========================================
console.log('Step 6: Validating data and dropping backup table...');
const newTableCount = yield queryRunner.query(`SELECT COUNT(*) as count FROM ${tableName}`);
const count = ((_a = newTableCount[0]) === null || _a === void 0 ? void 0 : _a.count) || ((_b = newTableCount[0]) === null || _b === void 0 ? void 0 : _b.COUNT) || 0;
if (count !== migratedCount) {
console.warn(`Warning: New table record count (${count}) does not match migrated count (${migratedCount})`);
}
yield queryRunner.dropTable(oldTableName, true);
console.log('Backup table dropped');
console.log('✅ Migration completed!');
});
}
down(queryRunner) {
return __awaiter(this, void 0, void 0, function* () {
console.log('Starting migration rollback...');
const tableName = 'span_table';
const oldTableName = 'span_table_old_backup';
// If backup table exists, restore it
if (yield queryRunner.hasTable(oldTableName)) {
// Drop new table
if (yield queryRunner.hasTable(tableName)) {
yield queryRunner.dropTable(tableName, true);
}
// Restore old table
yield queryRunner.query(`ALTER TABLE "${oldTableName}" RENAME TO "${tableName}"`);
console.log('Restored old table structure');
}
else {
console.warn('Backup table does not exist. Cannot rollback');
}
console.log('✅ Rollback completed');
});
}
}
exports.MigrateSpanTable1740000000000 = MigrateSpanTable1740000000000;