diff --git a/scout-brain/lib/models/_index.js b/scout-brain/lib/models/_index.js index f50a6cfd0f5..e13151bd8f7 100644 --- a/scout-brain/lib/models/_index.js +++ b/scout-brain/lib/models/_index.js @@ -1,5 +1,18 @@ -// @todo: When schema for Index finalized in server, -// make them real props here. module.exports = require('ampersand-model').extend({ - extraProperties: 'allow' + idAttribute: '_id', + props: { + key: 'object', + name: 'string', + ns: 'string', + v: 'number', + size: 'number' + }, + derived: { + _id: { + deps: ['name', 'ns'], + fn: function() { + return this.ns + '.' + this.name; + } + } + } }); diff --git a/scout-brain/lib/models/collection.js b/scout-brain/lib/models/collection.js index c6867ecd9ee..a8606201fbe 100644 --- a/scout-brain/lib/models/collection.js +++ b/scout-brain/lib/models/collection.js @@ -1,19 +1,7 @@ -var AmpersandState = require('ampersand-state'); var AmpersandModel = require('ampersand-model'); -var AmpersandCollection = require('ampersand-collection'); -var debug = require('debug')('scout-brain:models:collection'); - +var _ = require('underscore'); var types = require('../types'); - -// @todo: When schema for Index finalized in server, -// make them real props here. -var CollectionIndex = AmpersandState.extend({ - extraProperties: 'allow' -}); - -var CollectionIndexes = AmpersandCollection.extend({ - model: CollectionIndex -}); +var IndexCollection = require('./_index-collection'); var Collection = AmpersandModel.extend({ idAttribute: '_id', @@ -23,7 +11,6 @@ var Collection = AmpersandModel.extend({ required: true }, database: 'string', - index_sizes: 'number', document_count: 'number', document_size: 'number', storage_size: 'number', @@ -32,31 +19,118 @@ var Collection = AmpersandModel.extend({ padding_factor: 'number', extent_count: 'number', extent_last_size: 'number', + /** + * http://docs.mongodb.org/manual/reference/command/collStats/#collStats.userFlags + */ flags_user: 'number', - flags_system: 'number' + flags_system: 'number', + /** + * Is this a capped collection? + */ + capped: { + type: 'boolean', + default: false + }, + /** + * Is this collection using power of 2 allocation? + * + * http://docs.mongodb.org/manual/core/storage/#power-of-2-allocation + */ + power_of_two: { + type: 'boolean', + default: true + }, + /** + * The total size in memory of all records in a collection. This value does + * not include the record header, which is 16 bytes per record, but does + * include the record’s padding. Additionally size does not include the + * size of any indexes associated with the collection, which the + * totalIndexSize field reports.. + * + * http://docs.mongodb.org/manual/reference/command/collStats/#collStats.size + */ + size: 'number', + /** + * New in version 3.0.0. + * + * A document that reports data from the storage engine for each index + * in the collection. + * + * The fields in this document are the names of the indexes, while the + * values themselves are documents that contain statistics for the index + * provided by the storage engine. These statistics are for + * internal diagnostic use. + * + * http://docs.mongodb.org/manual/reference/command/collStats/#collStats.indexDetails + */ + index_details: 'object', + /** + * New in version 3.0.0. + * + * wiredTiger only appears when using the wiredTiger storage engine. This + * document contains data reported directly by the WiredTiger engine and + * other data for internal diagnostic use. + * + * http://docs.mongodb.org/manual/reference/command/collStats/#collStats.wiredTiger + */ + wired_tiger: 'object' }, - // extraProperties: 'reject', collections: { - indexes: CollectionIndexes + indexes: IndexCollection }, derived: { + document_size_average: { + deps: ['document_size', 'document_count'], + fn: function() { + if (!this.document_size || !this.document_count) return; + return this.document_size / this.document_count; + } + }, + index_size_average: { + deps: ['index_size', 'index_count'], + fn: function() { + if (!this.index_size || !this.index_count) return; + return this.index_size / this.index_count; + } + }, name: { deps: ['_id'], fn: function() { - debug('%s -> %j', this._id, types.ns(this._id)); + if (!this._id) return undefined; return types.ns(this._id).collection; } }, specialish: { - name: { - deps: ['_id'], - fn: function() { - debug('%s -> %j', this._id, types.ns(this._id)); - return types.ns(this._id).specialish; - } + deps: ['_id'], + fn: function() { + if (!this._id) return undefined; + return types.ns(this._id).specialish; } } - } + }, + parse: function(d) { + // @todo: update scout-server to just do this. + if (d.index_sizes) { + _.each(d.indexes, function(data, name) { + d.indexes[name].size = d.index_sizes[name]; + }); + } + return d; + }, + serialize: function() { + var res = this.getAttributes({ + props: true, + derived: true + }, true); + + _.each(this._children, function(value, key) { + res[key] = this[key].serialize(); + }, this); + _.each(this._collections, function(value, key) { + res[key] = this[key].serialize(); + }, this); + return res; + }, }); module.exports = Collection; diff --git a/scout-server/lib/routes/collection.js b/scout-server/lib/routes/collection.js index bdbf5953414..4225646da6c 100644 --- a/scout-server/lib/routes/collection.js +++ b/scout-server/lib/routes/collection.js @@ -24,18 +24,21 @@ function getCollectionFeatures(req, fn) { req.db.command({ collStats: req.ns.collection }, { - verbose: 1 - }, function(err, data) { - if (err) return fn(err); - - req.collection_features = { - capped: data.capped, - max: data.max, - size: data.size, - power_of_two: data.userFlags === 1 - }; - fn(null, req.collection_features); - }); + verbose: 1 + }, function(err, data) { + if (err) return fn(err); + + req.collection_features = { + capped: data.capped, + max_document_count: data.max, + max_document_size: data.maxSize, + size: data.size, + power_of_two: data.userFlags === 1, + index_details: data.indexDetails || {}, + wired_tiger: data.wiredTiger || {} + }; + fn(null, req.collection_features); + }); } function getCollectionStats(req, fn) { @@ -47,33 +50,29 @@ function getCollectionStats(req, fn) { req.db.command({ collStats: req.ns.collection }, { - verbose: 1 - }, function(err, data) { - if (err) return fn(err); - - req.collection_stats = { - index_sizes: data.indexSizes, - document_count: data.count, - document_size: data.size, - storage_size: data.storageSize, - index_count: data.nindexes, - index_size: data.totalIndexSize, - padding_factor: data.paddingFactor, - extent_count: data.numExtents, - extent_last_size: data.lastExtentSize, - flags_user: data.userFlags, - flags_system: data.systemFlags - }; - fn(null, req.collection_stats); - }); + verbose: 1 + }, function(err, data) { + if (err) return fn(err); + + req.collection_stats = { + index_sizes: data.indexSizes, + document_count: data.count, + document_size: data.size, + storage_size: data.storageSize, + index_count: data.nindexes, + index_size: data.totalIndexSize, + padding_factor: data.paddingFactor, + extent_count: data.numExtents, + extent_last_size: data.lastExtentSize, + flags_user: data.userFlags, + flags_system: data.systemFlags + }; + fn(null, req.collection_stats); + }); } function getCollectionIndexes(req, fn) { - req.db.collection('system.indexes') - .find({ - ns: req.ns.toString() - }) - .toArray(fn); + req.db.collection(req.ns.collection).listIndexes().toArray(fn); } // @todo: Move to scount-sync.collection.fetch(). @@ -128,14 +127,14 @@ module.exports = { query: req.json('query'), size: req.int('size', 5) }) - .pipe(_idToDocument(req.db, req.ns.collection, { - fields: req.json('fields') - })) - .pipe(EJSON.createStringifyStream()) - .pipe(setHeaders(req, res, { - 'content-type': 'application/json' - })) - .pipe(res); + .pipe(_idToDocument(req.db, req.ns.collection, { + fields: req.json('fields') + })) + .pipe(EJSON.createStringifyStream()) + .pipe(setHeaders(req, res, { + 'content-type': 'application/json' + })) + .pipe(res); }, bulk: function(req, res, next) { diff --git a/scout-ui/src/collection-stats/index.jade b/scout-ui/src/collection-stats/index.jade new file mode 100644 index 00000000000..256e450c7ce --- /dev/null +++ b/scout-ui/src/collection-stats/index.jade @@ -0,0 +1,17 @@ +div.row.hidden + .col-md-6 + dl.dl-horizontal + dt # documents + dd(data-hook='document_count') + dt total document size + dd(data-hook='document_size') + dt average document size + dd(data-hook='document_size_average') + .col-md-6 + dl.dl-horizontal + dt # indexes + dd(data-hook='index_count') + dt total index size + dd(data-hook='index_size') + dt average index size + dd(data-hook='index_size_average') diff --git a/scout-ui/src/collection-stats/index.js b/scout-ui/src/collection-stats/index.js new file mode 100644 index 00000000000..54cab48d1c2 --- /dev/null +++ b/scout-ui/src/collection-stats/index.js @@ -0,0 +1,69 @@ +var AmpersandView = require('ampersand-view'); +var numeral = require('numeral'); + +var CollectionStatsView = AmpersandView.extend({ + bindings: { + 'model._id': { + hook: 'name' + }, + document_count: { + hook: 'document_count' + }, + document_size: { + hook: 'document_size' + }, + document_size_average: { + hook: 'document_size_average' + }, + index_count: { + hook: 'index_count' + }, + index_size: { + hook: 'index_size' + }, + index_size_average: { + hook: 'index_size_average' + }, + }, + derived: { + document_count: { + deps: ['model.document_count'], + fn: function() { + return numeral(this.model.document_count).format('0.0a'); + } + }, + document_size: { + deps: ['model.document_size'], + fn: function() { + return numeral(this.model.document_size).format('0.0b'); + } + }, + document_size_average: { + deps: ['model.document_size_average'], + fn: function() { + return numeral(this.model.document_size_average).format('0.0b'); + } + }, + index_count: { + deps: ['model.index_count'], + fn: function() { + return numeral(this.model.index_count).format('0.0a'); + } + }, + index_size: { + deps: ['model.index_size'], + fn: function() { + return numeral(this.model.index_size).format('0.0b'); + } + }, + index_size_average: { + deps: ['model.index_size_average'], + fn: function() { + return numeral(this.model.index_size_average).format('0.0b'); + } + } + }, + template: require('./index.jade') +}); + +module.exports = CollectionStatsView; diff --git a/scout-ui/src/home/collection.jade b/scout-ui/src/home/collection.jade index 0daa241e994..75e8b40bf5a 100644 --- a/scout-ui/src/home/collection.jade +++ b/scout-ui/src/home/collection.jade @@ -2,5 +2,6 @@ .panel .panel-heading h3(data-hook='name') + div(data-hook='stats-container') .panel-body div(data-hook='fields-container') diff --git a/scout-ui/src/home/index.js b/scout-ui/src/home/index.js index a53dbd43f63..0dd2151d05b 100644 --- a/scout-ui/src/home/index.js +++ b/scout-ui/src/home/index.js @@ -5,7 +5,7 @@ var debug = require('debug')('scout-ui:home'); var app = require('ampersand-app'); var format = require('util').format; var SidebarView = require('../sidebar'); - +var CollectionStatsView = require('../collection-stats'); var FieldListView = require('../field-list'); require('bootstrap/js/dropdown'); @@ -27,6 +27,7 @@ var CollectionView = AmpersandView.extend({ this.schema.ns = this.model._id; this.listenTo(this.schema, 'error', this.onError); this.schema.fetch(); + this.model.fetch(); }, template: require('./collection.jade'), onError: function(schema, err) { @@ -34,6 +35,16 @@ var CollectionView = AmpersandView.extend({ console.error('Error getting schema: ', err); }, subviews: { + stats: { + hook: 'stats-container', + prepareView: function(el) { + return new CollectionStatsView({ + el: el, + parent: this, + model: this.model + }); + } + }, fields: { waitFor: 'schema.fields', hook: 'fields-container', diff --git a/scout-ui/src/models/index.js b/scout-ui/src/models/index.js index 305bd99cf6e..24c5f7e761c 100644 --- a/scout-ui/src/models/index.js +++ b/scout-ui/src/models/index.js @@ -44,6 +44,13 @@ var SampledSchema = Schema.extend({ wrapError(this, options); var model = this; + var collection; + if (this.parent && this.parent.model && this.parent.model.documents) { + collection = this.parent.model.documents; + collection.reset(); + } + + window.schema = this; window.data = []; var parser = this.stream() @@ -52,6 +59,9 @@ var SampledSchema = Schema.extend({ }) .on('data', function(doc) { window.data.push(doc); + if (collection) { + collection.add(doc); + } }) .on('end', function() { process.nextTick(function() { @@ -127,8 +137,11 @@ var Collection = core.Collection.extend({ } } } + }, + scout: function() { + return client.collection.bind(client, this.getId()); } -}); +}, WithScout); module.exports = { types: brain.types, @@ -138,6 +151,7 @@ module.exports = { collections: core.CollectionCollection.extend(WithSelectable, { model: Collection, parse: function(res) { + // Hide specialish namespaces (eg `local.*`, `*oplog*`) from sidebar. return res.filter(function(d) { return !types.ns(d._id).specialish; });