diff --git a/packages/api/src/controllers/archive.js b/packages/api/src/controllers/archive.js index 92c31c0fd..a9147169a 100644 --- a/packages/api/src/controllers/archive.js +++ b/packages/api/src/controllers/archive.js @@ -5,6 +5,7 @@ const { archivedir, clearArchiveLinksCache, resolveArchiveFolder } = require('.. const socket = require('../utility/socket'); const { saveFreeTableData } = require('../utility/freeTableStorage'); const loadFilesRecursive = require('../utility/loadFilesRecursive'); +const getJslFileName = require('../utility/getJslFileName'); module.exports = { folders_meta: true, @@ -150,6 +151,15 @@ module.exports = { return true; }, + saveJslData_meta: true, + async saveJslData({ folder, file, jslid }) { + const source = getJslFileName(jslid); + const target = path.join(resolveArchiveFolder(folder), `${file}.jsonl`); + await fs.copyFile(source, target); + socket.emitChanged(`archive-files-changed-${folder}`); + return true; + }, + async getNewArchiveFolder({ database }) { const isLink = database.endsWith(database); const name = isLink ? database.slice(0, -5) : database; diff --git a/packages/api/src/controllers/jsldata.js b/packages/api/src/controllers/jsldata.js index 0ab2f2ecf..4778e2f59 100644 --- a/packages/api/src/controllers/jsldata.js +++ b/packages/api/src/controllers/jsldata.js @@ -7,6 +7,7 @@ const DatastoreProxy = require('../utility/DatastoreProxy'); const { saveFreeTableData } = require('../utility/freeTableStorage'); const getJslFileName = require('../utility/getJslFileName'); const JsonLinesDatastore = require('../utility/JsonLinesDatastore'); +const requirePluginFunction = require('../utility/requirePluginFunction'); const socket = require('../utility/socket'); function readFirstLine(file) { @@ -99,10 +100,13 @@ module.exports = { // return readerInfo; // }, - async ensureDatastore(jslid) { + async ensureDatastore(jslid, formatterFunction) { let datastore = this.datastores[jslid]; - if (!datastore) { - datastore = new JsonLinesDatastore(getJslFileName(jslid)); + if (!datastore || datastore.formatterFunction != formatterFunction) { + if (datastore) { + datastore._closeReader(); + } + datastore = new JsonLinesDatastore(getJslFileName(jslid), formatterFunction); // datastore = new DatastoreProxy(getJslFileName(jslid)); this.datastores[jslid] = datastore; } @@ -131,8 +135,8 @@ module.exports = { }, getRows_meta: true, - async getRows({ jslid, offset, limit, filters }) { - const datastore = await this.ensureDatastore(jslid); + async getRows({ jslid, offset, limit, filters, formatterFunction }) { + const datastore = await this.ensureDatastore(jslid, formatterFunction); return datastore.getRows(offset, limit, _.isEmpty(filters) ? null : filters); }, @@ -150,8 +154,8 @@ module.exports = { }, loadFieldValues_meta: true, - async loadFieldValues({ jslid, field, search }) { - const datastore = await this.ensureDatastore(jslid); + async loadFieldValues({ jslid, field, search, formatterFunction }) { + const datastore = await this.ensureDatastore(jslid, formatterFunction); const res = new Set(); await datastore.enumRows(row => { if (!filterName(search, row[field])) return true; @@ -188,4 +192,85 @@ module.exports = { await fs.promises.writeFile(getJslFileName(jslid), text); return true; }, + + extractTimelineChart_meta: true, + async extractTimelineChart({ jslid, timestampFunction, aggregateFunction, measures }) { + const timestamp = requirePluginFunction(timestampFunction); + const aggregate = requirePluginFunction(aggregateFunction); + const datastore = new JsonLinesDatastore(getJslFileName(jslid)); + let mints = null; + let maxts = null; + // pass 1 - counts stats, time range + await datastore.enumRows(row => { + const ts = timestamp(row); + if (!mints || ts < mints) mints = ts; + if (!maxts || ts > maxts) maxts = ts; + return true; + }); + const minTime = new Date(mints).getTime(); + const maxTime = new Date(maxts).getTime(); + const duration = maxTime - minTime; + const STEPS = 100; + let stepCount = duration > 100 * 1000 ? STEPS : Math.round((maxTime - minTime) / 1000); + if (stepCount < 2) { + stepCount = 2; + } + const stepDuration = duration / stepCount; + const labels = _.range(stepCount).map(i => new Date(minTime + stepDuration / 2 + stepDuration * i)); + + // const datasets = measures.map(m => ({ + // label: m.label, + // data: Array(stepCount).fill(0), + // })); + + const mproc = measures.map(m => ({ + ...m, + })); + + const data = Array(stepCount) + .fill(0) + .map(() => ({})); + + // pass 2 - count measures + await datastore.enumRows(row => { + const ts = timestamp(row); + let part = Math.round((new Date(ts).getTime() - minTime) / stepDuration); + if (part < 0) part = 0; + if (part >= stepCount) part - stepCount - 1; + if (data[part]) { + data[part] = aggregate(data[part], row, stepDuration); + } + return true; + }); + + datastore._closeReader(); + + // const measureByField = _.fromPairs(measures.map((m, i) => [m.field, i])); + + // for (let mindex = 0; mindex < measures.length; mindex++) { + // for (let stepIndex = 0; stepIndex < stepCount; stepIndex++) { + // const measure = measures[mindex]; + // if (measure.perSecond) { + // datasets[mindex].data[stepIndex] /= stepDuration / 1000; + // } + // if (measure.perField) { + // datasets[mindex].data[stepIndex] /= datasets[measureByField[measure.perField]].data[stepIndex]; + // } + // } + // } + + // for (let i = 0; i < measures.length; i++) { + // if (measures[i].hidden) { + // datasets[i] = null; + // } + // } + + return { + labels, + datasets: mproc.map(m => ({ + label: m.label, + data: data.map(d => d[m.field] || 0), + })), + }; + }, }; diff --git a/packages/api/src/controllers/sessions.js b/packages/api/src/controllers/sessions.js index 71e04bf84..a010dba4e 100644 --- a/packages/api/src/controllers/sessions.js +++ b/packages/api/src/controllers/sessions.js @@ -150,6 +150,31 @@ module.exports = { return true; }, + startProfiler_meta: true, + async startProfiler({ sesid }) { + const jslid = uuidv1(); + const session = this.opened.find(x => x.sesid == sesid); + if (!session) { + throw new Error('Invalid session'); + } + + console.log(`Starting profiler, sesid=${sesid}`); + session.loadingReader_jslid = jslid; + session.subprocess.send({ msgtype: 'startProfiler', jslid }); + + return { state: 'ok', jslid }; + }, + + stopProfiler_meta: true, + async stopProfiler({ sesid }) { + const session = this.opened.find(x => x.sesid == sesid); + if (!session) { + throw new Error('Invalid session'); + } + session.subprocess.send({ msgtype: 'stopProfiler' }); + return { state: 'ok' }; + }, + // cancel_meta: true, // async cancel({ sesid }) { // const session = this.opened.find((x) => x.sesid == sesid); diff --git a/packages/api/src/proc/sessionProcess.js b/packages/api/src/proc/sessionProcess.js index 4b7d140fe..4772e4404 100644 --- a/packages/api/src/proc/sessionProcess.js +++ b/packages/api/src/proc/sessionProcess.js @@ -16,6 +16,7 @@ let storedConnection; let afterConnectCallbacks = []; // let currentHandlers = []; let lastPing = null; +let currentProfiler = null; class TableWriter { constructor() { @@ -210,6 +211,31 @@ function waitConnected() { }); } +async function handleStartProfiler({ jslid }) { + await waitConnected(); + const driver = requireEngineDriver(storedConnection); + + if (!allowExecuteCustomScript(driver)) { + process.send({ msgtype: 'done' }); + return; + } + + const writer = new TableWriter(); + writer.initializeFromReader(jslid); + + currentProfiler = await driver.startProfiler(systemConnection, { + row: data => writer.rowFromReader(data), + }); + currentProfiler.writer = writer; +} + +async function handleStopProfiler({ jslid }) { + const driver = requireEngineDriver(storedConnection); + currentProfiler.writer.close(); + driver.stopProfiler(systemConnection, currentProfiler); + currentProfiler = null; +} + async function handleExecuteQuery({ sql }) { await waitConnected(); const driver = requireEngineDriver(storedConnection); @@ -280,6 +306,8 @@ const messageHandlers = { connect: handleConnect, executeQuery: handleExecuteQuery, executeReader: handleExecuteReader, + startProfiler: handleStartProfiler, + stopProfiler: handleStopProfiler, ping: handlePing, // cancel: handleCancel, }; diff --git a/packages/api/src/utility/JsonLinesDatastore.js b/packages/api/src/utility/JsonLinesDatastore.js index 68fec9455..a5e12da47 100644 --- a/packages/api/src/utility/JsonLinesDatastore.js +++ b/packages/api/src/utility/JsonLinesDatastore.js @@ -3,6 +3,7 @@ const AsyncLock = require('async-lock'); const lock = new AsyncLock(); const stableStringify = require('json-stable-stringify'); const { evaluateCondition } = require('dbgate-sqltree'); +const requirePluginFunction = require('./requirePluginFunction'); function fetchNextLineFromReader(reader) { return new Promise((resolve, reject) => { @@ -22,14 +23,16 @@ function fetchNextLineFromReader(reader) { } class JsonLinesDatastore { - constructor(file) { + constructor(file, formatterFunction) { this.file = file; + this.formatterFunction = formatterFunction; this.reader = null; this.readedDataRowCount = 0; this.readedSchemaRow = false; // this.firstRowToBeReturned = null; this.notifyChangedCallback = null; this.currentFilter = null; + this.rowFormatter = requirePluginFunction(formatterFunction); } _closeReader() { @@ -62,6 +65,11 @@ class JsonLinesDatastore { ); } + parseLine(line) { + const res = JSON.parse(line); + return this.rowFormatter ? this.rowFormatter(res) : res; + } + async _readLine(parse) { // if (this.firstRowToBeReturned) { // const res = this.firstRowToBeReturned; @@ -84,14 +92,14 @@ class JsonLinesDatastore { } } if (this.currentFilter) { - const parsedLine = JSON.parse(line); + const parsedLine = this.parseLine(line); if (evaluateCondition(this.currentFilter, parsedLine)) { this.readedDataRowCount += 1; return parse ? parsedLine : true; } } else { this.readedDataRowCount += 1; - return parse ? JSON.parse(line) : true; + return parse ? this.parseLine(line) : true; } } diff --git a/packages/api/src/utility/requirePluginFunction.js b/packages/api/src/utility/requirePluginFunction.js new file mode 100644 index 000000000..11f9e33eb --- /dev/null +++ b/packages/api/src/utility/requirePluginFunction.js @@ -0,0 +1,16 @@ +const _ = require('lodash'); +const requirePlugin = require('../shell/requirePlugin'); + +function requirePluginFunction(functionName) { + if (!functionName) return null; + if (functionName.includes('@')) { + const [shortName, packageName] = functionName.split('@'); + const plugin = requirePlugin(packageName); + if (plugin.functions) { + return plugin.functions[shortName]; + } + } + return null; +} + +module.exports = requirePluginFunction; diff --git a/packages/sqltree/src/evaluateExpression.ts b/packages/sqltree/src/evaluateExpression.ts index 2f99943c6..e2d78f5e8 100644 --- a/packages/sqltree/src/evaluateExpression.ts +++ b/packages/sqltree/src/evaluateExpression.ts @@ -6,7 +6,7 @@ import { dumpSqlSourceRef } from './dumpSqlSource'; export function evaluateExpression(expr: Expression, values) { switch (expr.exprType) { case 'column': - return values[expr.columnName]; + return _.get(values, expr.columnName); case 'placeholder': return values.__placeholder; diff --git a/packages/types/engines.d.ts b/packages/types/engines.d.ts index e2da64c8b..fac146b74 100644 --- a/packages/types/engines.d.ts +++ b/packages/types/engines.d.ts @@ -77,6 +77,11 @@ export interface EngineDriver { supportsDatabaseUrl?: boolean; supportsDatabaseDump?: boolean; supportsServerSummary?: boolean; + supportsDatabaseProfiler?: boolean; + profilerFormatterFunction?: string; + profilerTimestampFunction?: string; + profilerChartAggregateFunction?: string; + profilerChartMeasures?: { label: string; field: string }[]; isElectronOnly?: boolean; supportedCreateDatabase?: boolean; showConnectionField?: (field: string, values: any) => boolean; @@ -130,6 +135,8 @@ export interface EngineDriver { callMethod(pool, method, args); serverSummary(pool): Promise; summaryCommand(pool, command, row): Promise; + startProfiler(pool, options): Promise; + stopProfiler(pool, profiler): Promise; analyserClass?: any; dumperClass?: any; diff --git a/packages/web/src/appobj/ArchiveFileAppObject.svelte b/packages/web/src/appobj/ArchiveFileAppObject.svelte index 02c92fbdf..4cecca45d 100644 --- a/packages/web/src/appobj/ArchiveFileAppObject.svelte +++ b/packages/web/src/appobj/ArchiveFileAppObject.svelte @@ -41,7 +41,10 @@ } export const extractKey = data => data.fileName; - export const createMatcher = ({ fileName }) => filter => filterName(filter, fileName); + export const createMatcher = + ({ fileName }) => + filter => + filterName(filter, fileName); const ARCHIVE_ICONS = { 'table.yaml': 'img table', 'view.sql': 'img view', @@ -67,7 +70,7 @@ import ImportExportModal from '../modals/ImportExportModal.svelte'; import { showModal } from '../modals/modalTools'; - import { archiveFilesAsDataSheets, currentArchive, extensions, getCurrentDatabase } from '../stores'; + import { archiveFilesAsDataSheets, currentArchive, extensions, getCurrentDatabase, getExtensions } from '../stores'; import createQuickExportMenu from '../utility/createQuickExportMenu'; import { exportQuickExportFile } from '../utility/exportFileTools'; @@ -198,6 +201,29 @@ ), data.fileType.endsWith('.sql') && { text: 'Open SQL', onClick: handleOpenSqlFile }, data.fileType.endsWith('.yaml') && { text: 'Open YAML', onClick: handleOpenYamlFile }, + data.fileType == 'jsonl' && { + text: 'Open in profiler', + submenu: getExtensions() + .drivers.filter(eng => eng.profilerFormatterFunction) + .map(eng => ({ + text: eng.title, + onClick: () => { + openNewTab({ + title: 'Profiler', + icon: 'img profiler', + tabComponent: 'ProfilerTab', + props: { + jslidLoad: `archive://${data.folderName}/${data.fileName}`, + engine: eng.engine, + // profilerFormatterFunction: eng.profilerFormatterFunction, + // profilerTimestampFunction: eng.profilerTimestampFunction, + // profilerChartAggregateFunction: eng.profilerChartAggregateFunction, + // profilerChartMeasures: eng.profilerChartMeasures, + }, + }); + }, + })), + }, ]; } diff --git a/packages/web/src/appobj/DatabaseAppObject.svelte b/packages/web/src/appobj/DatabaseAppObject.svelte index f660ad802..d1baed29a 100644 --- a/packages/web/src/appobj/DatabaseAppObject.svelte +++ b/packages/web/src/appobj/DatabaseAppObject.svelte @@ -254,6 +254,18 @@ }); }; + const handleDatabaseProfiler = () => { + openNewTab({ + title: 'Profiler', + icon: 'img profiler', + tabComponent: 'ProfilerTab', + props: { + conid: connection._id, + database: name, + }, + }); + }; + async function handleConfirmSql(sql) { saveScriptToDatabase({ conid: connection._id, database: name }, sql, false); } @@ -284,7 +296,8 @@ !connection.singleDatabase && { onClick: handleDropDatabase, text: 'Drop database' }, { divider: true }, driver?.databaseEngineTypes?.includes('sql') && { onClick: handleShowDiagram, text: 'Show diagram' }, - isSqlOrDoc && { onClick: handleSqlGenerator, text: 'SQL Generator' }, + driver?.databaseEngineTypes?.includes('sql') && { onClick: handleSqlGenerator, text: 'SQL Generator' }, + driver?.supportsDatabaseProfiler && { onClick: handleDatabaseProfiler, text: 'Database profiler' }, isSqlOrDoc && { onClick: handleOpenJsonModel, text: 'Open model as JSON' }, isSqlOrDoc && { onClick: handleExportModel, text: 'Export DB model - experimental' }, isSqlOrDoc && diff --git a/packages/web/src/datagrid/CellValue.svelte b/packages/web/src/datagrid/CellValue.svelte index e0e22b85a..e2e5eb1f2 100644 --- a/packages/web/src/datagrid/CellValue.svelte +++ b/packages/web/src/datagrid/CellValue.svelte @@ -75,11 +75,17 @@ {:else if value.$oid} ObjectId("{value.$oid}") {:else if _.isPlainObject(value)} - (JSON) + {@const svalue = JSON.stringify(value, undefined, 2)} + {#if svalue.length < 100}{JSON.stringify(value)}{:else}(JSON){/if} {:else if _.isArray(value)} JSON.stringify(x)).join('\n')}>[{value.length} items] {:else if _.isPlainObject(jsonParsedValue)} - (JSON) + {@const svalue = JSON.stringify(jsonParsedValue, undefined, 2)} + {#if svalue.length < 100}{JSON.stringify(jsonParsedValue)}{:else}(JSON){/if} {:else if _.isArray(jsonParsedValue)} JSON.stringify(x)).join('\n')} >[{jsonParsedValue.length} items] setFilter(keys.map(x => getFilterValueExpression(x)).join(',')), }); } diff --git a/packages/web/src/datagrid/DataGridCore.svelte b/packages/web/src/datagrid/DataGridCore.svelte index 29adf299f..ef9013d7a 100644 --- a/packages/web/src/datagrid/DataGridCore.svelte +++ b/packages/web/src/datagrid/DataGridCore.svelte @@ -355,6 +355,7 @@ export let pureName = undefined; export let schemaName = undefined; export let allowDefineVirtualReferences = false; + export let formatterFunction; export let isLoadedAll; export let loadedTime; @@ -1743,6 +1744,7 @@ {conid} {database} {jslid} + {formatterFunction} driver={display?.driver} filterType={useEvalFilters ? 'eval' : col.filterType || getFilterType(col.dataType)} filter={display.getFilter(col.uniqueName)} diff --git a/packages/web/src/datagrid/JslDataGridCore.svelte b/packages/web/src/datagrid/JslDataGridCore.svelte index cb2afde3e..f3e95722b 100644 --- a/packages/web/src/datagrid/JslDataGridCore.svelte +++ b/packages/web/src/datagrid/JslDataGridCore.svelte @@ -12,12 +12,13 @@ }); async function loadDataPage(props, offset, limit) { - const { jslid, display } = props; + const { jslid, display, formatterFunction } = props; const response = await apiCall('jsldata/get-rows', { jslid, offset, limit, + formatterFunction, filters: display ? display.compileFilters() : null, }); @@ -34,6 +35,9 @@ const response = await apiCall('jsldata/get-stats', { jslid }); return response.rowCount; } + + export let formatterPlugin; + export let formatterFunction; + + + + + {#if jslid} + + + {#key jslid} + + {/key} + + + {#if isLoadingChart} + + {:else} + + {/if} + + + {:else} + + {/if} + + + + + + + diff --git a/packages/web/src/tabs/index.js b/packages/web/src/tabs/index.js index db9ab5037..54c202cd2 100644 --- a/packages/web/src/tabs/index.js +++ b/packages/web/src/tabs/index.js @@ -27,6 +27,7 @@ import * as ConnectionTab from './ConnectionTab.svelte'; import * as MapTab from './MapTab.svelte'; import * as PerspectiveTab from './PerspectiveTab.svelte'; import * as ServerSummaryTab from './ServerSummaryTab.svelte'; +import * as ProfilerTab from './ProfilerTab.svelte'; export default { TableDataTab, @@ -58,4 +59,5 @@ export default { MapTab, PerspectiveTab, ServerSummaryTab, + ProfilerTab, }; diff --git a/plugins/dbgate-plugin-mongo/src/backend/driver.js b/plugins/dbgate-plugin-mongo/src/backend/driver.js index 0621fab16..b28ccde5e 100644 --- a/plugins/dbgate-plugin-mongo/src/backend/driver.js +++ b/plugins/dbgate-plugin-mongo/src/backend/driver.js @@ -38,7 +38,7 @@ async function getScriptableDb(pool) { const db = pool.__getDatabase(); const collections = await db.listCollections().toArray(); for (const collection of collections) { - db[collection.name] = db.collection(collection.name); + _.set(db, collection.name, db.collection(collection.name)); } return db; } @@ -165,6 +165,49 @@ const driver = { options.done(); }, + async startProfiler(pool, options) { + const db = await getScriptableDb(pool); + const old = await db.command({ profile: -1 }); + await db.command({ profile: 2 }); + const cursor = await db.collection('system.profile').find({ + ns: /^((?!(admin\.\$cmd|\.system|\.tmp\.)).)*$/, + ts: { $gt: new Date() }, + 'command.profile': { $exists: false }, + 'command.collStats': { $exists: false }, + 'command.collstats': { $exists: false }, + 'command.createIndexes': { $exists: false }, + 'command.listIndexes': { $exists: false }, + // "command.cursor": {"$exists": false}, + 'command.create': { $exists: false }, + 'command.dbstats': { $exists: false }, + 'command.scale': { $exists: false }, + 'command.explain': { $exists: false }, + 'command.killCursors': { $exists: false }, + 'command.count': { $ne: 'system.profile' }, + op: /^((?!(getmore|killcursors)).)/i, + }); + + cursor.addCursorFlag('tailable', true); + cursor.addCursorFlag('awaitData', true); + + cursor + .forEach((row) => { + // console.log('ROW', row); + options.row(row); + }) + .catch((err) => { + console.error('Cursor stopped with error:', err.message); + }); + return { + cursor, + old, + }; + }, + async stopProfiler(pool, { cursor, old }) { + cursor.close(); + const db = await getScriptableDb(pool); + await db.command({ profile: old.was, slowms: old.slowms }); + }, async readQuery(pool, sql, structure) { try { const json = JSON.parse(sql); diff --git a/plugins/dbgate-plugin-mongo/src/backend/index.js b/plugins/dbgate-plugin-mongo/src/backend/index.js index df0b602e4..2743dd1b3 100644 --- a/plugins/dbgate-plugin-mongo/src/backend/index.js +++ b/plugins/dbgate-plugin-mongo/src/backend/index.js @@ -1,6 +1,16 @@ const driver = require('./driver'); +const { + formatProfilerEntry, + extractProfileTimestamp, + aggregateProfileChartEntry, +} = require('../frontend/profilerFunctions'); module.exports = { packageName: 'dbgate-plugin-mongo', drivers: [driver], + functions: { + formatProfilerEntry, + extractProfileTimestamp, + aggregateProfileChartEntry, + }, }; diff --git a/plugins/dbgate-plugin-mongo/src/frontend/driver.js b/plugins/dbgate-plugin-mongo/src/frontend/driver.js index ca50a0735..931055634 100644 --- a/plugins/dbgate-plugin-mongo/src/frontend/driver.js +++ b/plugins/dbgate-plugin-mongo/src/frontend/driver.js @@ -33,6 +33,15 @@ const driver = { defaultPort: 27017, supportsDatabaseUrl: true, supportsServerSummary: true, + supportsDatabaseProfiler: true, + profilerFormatterFunction: 'formatProfilerEntry@dbgate-plugin-mongo', + profilerTimestampFunction: 'extractProfileTimestamp@dbgate-plugin-mongo', + profilerChartAggregateFunction: 'aggregateProfileChartEntry@dbgate-plugin-mongo', + profilerChartMeasures: [ + { label: 'Req count/s', field: 'countPerSec' }, + { label: 'Avg duration', field: 'avgDuration' }, + { label: 'Max duration', field: 'maxDuration' }, + ], databaseUrlPlaceholder: 'e.g. mongodb://username:password@mongodb.mydomain.net/dbname', getQuerySplitterOptions: () => mongoSplitterOptions, diff --git a/plugins/dbgate-plugin-mongo/src/frontend/index.js b/plugins/dbgate-plugin-mongo/src/frontend/index.js index 3bf256c7c..da0a77580 100644 --- a/plugins/dbgate-plugin-mongo/src/frontend/index.js +++ b/plugins/dbgate-plugin-mongo/src/frontend/index.js @@ -1,6 +1,12 @@ import driver from './driver'; +import { formatProfilerEntry, extractProfileTimestamp, aggregateProfileChartEntry } from './profilerFunctions'; export default { packageName: 'dbgate-plugin-mongo', drivers: [driver], + functions: { + formatProfilerEntry, + extractProfileTimestamp, + aggregateProfileChartEntry, + }, }; diff --git a/plugins/dbgate-plugin-mongo/src/frontend/profilerFunctions.js b/plugins/dbgate-plugin-mongo/src/frontend/profilerFunctions.js new file mode 100644 index 000000000..8eff17b72 --- /dev/null +++ b/plugins/dbgate-plugin-mongo/src/frontend/profilerFunctions.js @@ -0,0 +1,103 @@ +const _ = require('lodash'); + +function formatProfilerEntry(obj) { + const ts = obj.ts; + const stats = { millis: obj.millis }; + let op = obj.op; + let doc; + let query; + let ext; + if (op == 'query') { + const cmd = obj.command || obj.query; + doc = cmd.find; + query = cmd.filter; + ext = _.pick(cmd, ['sort', 'limit', 'skip']); + } else if (op == 'update') { + doc = obj.ns.split('.').slice(-1)[0]; + query = obj.command && obj.command.q; + ext = _.pick(obj, ['nModified', 'nMatched']); + } else if (op == 'insert') { + doc = obj.ns.split('.').slice(-1)[0]; + ext = _.pick(obj, ['ninserted']); + } else if (op == 'remove') { + doc = obj.ns.split('.').slice(-1)[0]; + query = obj.command && obj.command.q; + } else if (op == 'command' && obj.command) { + const cmd = obj.command; + if (cmd.count) { + op = 'count'; + query = cmd.query; + } else if (cmd.aggregate) { + op = 'aggregate'; + query = cmd.pipeline; + } else if (cmd.distinct) { + op = 'distinct'; + query = cmd.query; + ext = _.pick(cmd, ['key']); + } else if (cmd.drop) { + op = 'drop'; + } else if (cmd.findandmodify) { + op = 'findandmodify'; + query = cmd.query; + ext = _.pick(cmd, ['sort', 'update', 'remove', 'fields', 'upsert', 'new']); + } else if (cmd.group) { + op = 'group'; + doc = cmd.group.ns; + ext = _.pick(cmd, ['key', 'initial', 'cond', '$keyf', '$reduce', 'finalize']); + } else if (cmd.map) { + op = 'map'; + doc = cmd.mapreduce; + query = _.omit(cmd, ['mapreduce', 'map', 'reduce']); + ext = { map: cmd.map, reduce: cmd.reduce }; + } else { + // unknown command + op = 'unknown'; + query = obj; + } + } else { + // unknown operation + query = obj; + } + + return { + ts, + op, + doc, + query, + ext, + stats, + }; +} + +function extractProfileTimestamp(obj) { + return obj.ts; +} + +function aggregateProfileChartEntry(aggr, obj, stepDuration) { + // const fmt = formatProfilerEntry(obj); + + const countAll = (aggr.countAll || 0) + 1; + const sumMillis = (aggr.sumMillis || 0) + obj.millis; + const maxDuration = obj.millis > (aggr.maxDuration || 0) ? obj.millis : aggr.maxDuration || 0; + + return { + countAll, + sumMillis, + countPerSec: (countAll / stepDuration) * 1000, + avgDuration: sumMillis / countAll, + maxDuration, + }; + + // return { + // ts: fmt.ts, + // millis: fmt.stats.millis, + // countAll: 1, + // countPerSec: 1, + // }; +} + +module.exports = { + formatProfilerEntry, + extractProfileTimestamp, + aggregateProfileChartEntry, +};