services.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775
  1. /*jshint globalstrict:true */
  2. /*global angular:true */
  3. 'use strict';
  4. angular.module('kibana.services', [])
  5. .service('eventBus', function($rootScope) {
  6. // An array of registed types
  7. var _types = []
  8. this.broadcast = function(from,to,type,data) {
  9. if(_.isUndefined(data))
  10. var data = from
  11. var packet = {
  12. time: new Date(),
  13. type: type,
  14. from: from,
  15. to: to,
  16. data: data
  17. }
  18. if(_.contains(_types,'$kibana_debug'))
  19. $rootScope.$broadcast('$kibana_debug',packet);
  20. $rootScope.$broadcast(type,{
  21. from: from,
  22. to: to,
  23. data: data
  24. });
  25. }
  26. // This sets up an $on listener that checks to see if the event (packet) is
  27. // addressed to the scope in question and runs the registered function if it
  28. // is.
  29. this.register = function(scope,type,fn) {
  30. _types = _.union(_types,[type])
  31. scope.$on(type,function(event,packet){
  32. var _id = scope.$id;
  33. var _to = packet.to;
  34. var _from = packet.from;
  35. var _type = packet.type
  36. var _time = packet.time
  37. var _group = (!(_.isUndefined(scope.panel))) ? scope.panel.group : ["NONE"]
  38. if(!(_.isArray(_to)))
  39. _to = [_to];
  40. if(!(_.isArray(_group)))
  41. _group = [_group];
  42. // Transmit event only if the sender is not the receiver AND one of the following:
  43. // 1) Receiver has group in _to 2) Receiver's $id is in _to
  44. // 3) Event is addressed to ALL 4) Receiver is in ALL group
  45. if((_.intersection(_to,_group).length > 0 ||
  46. _.indexOf(_to,_id) > -1 ||
  47. _.indexOf(_group,'ALL') > -1 ||
  48. _.indexOf(_to,'ALL') > -1) &&
  49. _from !== _id
  50. ) {
  51. fn(event,packet.data,{time:_time,to:_to,from:_from,type:_type});
  52. }
  53. });
  54. }
  55. })
  56. /*
  57. Service: fields
  58. Provides a global list of all seen fields for use in editor panels
  59. */
  60. .factory('fields', function($rootScope) {
  61. var fields = {
  62. list : []
  63. }
  64. $rootScope.$on('fields', function(event,f) {
  65. fields.list = _.union(f.data.all,fields.list)
  66. })
  67. return fields;
  68. })
  69. .service('kbnIndex',function($http) {
  70. // returns a promise containing an array of all indices matching the index
  71. // pattern that exist in a given range
  72. this.indices = function(from,to,pattern,interval) {
  73. var possible = [];
  74. _.each(expand_range(fake_utc(from),fake_utc(to),interval),function(d){
  75. possible.push(d.format(pattern));
  76. });
  77. return all_indices().then(function(p) {
  78. var indices = _.intersection(possible,p);
  79. indices.reverse();
  80. return indices
  81. })
  82. };
  83. // returns a promise containing an array of all indices in an elasticsearch
  84. // cluster
  85. function all_indices() {
  86. var something = $http({
  87. url: config.elasticsearch + "/_aliases",
  88. method: "GET"
  89. }).error(function(data, status, headers, config) {
  90. // Handle error condition somehow?
  91. });
  92. return something.then(function(p) {
  93. var indices = [];
  94. _.each(p.data, function(v,k) {
  95. indices.push(k)
  96. });
  97. return indices;
  98. });
  99. }
  100. // this is stupid, but there is otherwise no good way to ensure that when
  101. // I extract the date from an object that I get the UTC date. Stupid js.
  102. // I die a little inside every time I call this function.
  103. // Update: I just read this again. I died a little more inside.
  104. // Update2: More death.
  105. function fake_utc(date) {
  106. date = moment(date).clone().toDate()
  107. return moment(new Date(date.getTime() + date.getTimezoneOffset() * 60000));
  108. }
  109. // Create an array of date objects by a given interval
  110. function expand_range(start, end, interval) {
  111. if(_.contains(['hour','day','week','month','year'],interval)) {
  112. var range;
  113. start = moment(start).clone();
  114. range = [];
  115. while (start.isBefore(end)) {
  116. range.push(start.clone());
  117. switch (interval) {
  118. case 'hour':
  119. start.add('hours',1)
  120. break
  121. case 'day':
  122. start.add('days',1)
  123. break
  124. case 'week':
  125. start.add('weeks',1)
  126. break
  127. case 'month':
  128. start.add('months',1)
  129. break
  130. case 'year':
  131. start.add('years',1)
  132. break
  133. }
  134. }
  135. range.push(moment(end).clone());
  136. return range;
  137. } else {
  138. return false;
  139. }
  140. }
  141. })
  142. .service('timer', function($timeout) {
  143. // This service really just tracks a list of $timeout promises to give us a
  144. // method for cancelling them all when we need to
  145. var timers = [];
  146. this.register = function(promise) {
  147. timers.push(promise);
  148. return promise;
  149. }
  150. this.cancel = function(promise) {
  151. timers = _.without(timers,promise)
  152. $timeout.cancel(promise)
  153. }
  154. this.cancel_all = function() {
  155. _.each(timers, function(t){
  156. $timeout.cancel(t);
  157. });
  158. timers = new Array();
  159. }
  160. })
  161. .service('query', function(dashboard) {
  162. // Create an object to hold our service state on the dashboard
  163. dashboard.current.services.query = dashboard.current.services.query || {};
  164. _.defaults(dashboard.current.services.query,{
  165. idQueue : [],
  166. list : {},
  167. ids : [],
  168. });
  169. // For convenience
  170. var _q = dashboard.current.services.query;
  171. this.colors = [
  172. "#7EB26D","#EAB839","#6ED0E0","#EF843C","#E24D42","#1F78C1","#BA43A9","#705DA0", //1
  173. "#508642","#CCA300","#447EBC","#C15C17","#890F02","#0A437C","#6D1F62","#584477", //2
  174. "#B7DBAB","#F4D598","#70DBED","#F9BA8F","#F29191","#82B5D8","#E5A8E2","#AEA2E0", //3
  175. "#629E51","#E5AC0E","#64B0C8","#E0752D","#BF1B00","#0A50A1","#962D82","#614D93", //4
  176. "#9AC48A","#F2C96D","#65C5DB","#F9934E","#EA6460","#5195CE","#D683CE","#806EB7", //5
  177. "#3F6833","#967302","#2F575E","#99440A","#58140C","#052B51","#511749","#3F2B5B", //6
  178. "#E0F9D7","#FCEACA","#CFFAFF","#F9E2D2","#FCE2DE","#BADFF4","#F9D9F9","#DEDAF7" //7
  179. ];
  180. // Save a reference to this
  181. var self = this;
  182. this.init = function() {
  183. _q = dashboard.current.services.query;
  184. self.list = dashboard.current.services.query.list;
  185. self.ids = dashboard.current.services.query.ids;
  186. if (self.ids.length == 0) {
  187. self.set({});
  188. }
  189. }
  190. // This is used both for adding queries and modifying them. If an id is passed, the query at that id is updated
  191. this.set = function(query,id) {
  192. if(!_.isUndefined(id)) {
  193. if(!_.isUndefined(self.list[id])) {
  194. _.extend(self.list[id],query);
  195. return id;
  196. } else {
  197. return false;
  198. }
  199. } else {
  200. var _id = nextId();
  201. var _query = {
  202. query: '*',
  203. alias: '',
  204. color: colorAt(_id),
  205. id: _id
  206. }
  207. _.defaults(query,_query)
  208. self.list[_id] = query;
  209. self.ids.push(_id)
  210. return _id;
  211. }
  212. }
  213. this.remove = function(id) {
  214. if(!_.isUndefined(self.list[id])) {
  215. delete self.list[id];
  216. // This must happen on the full path also since _.without returns a copy
  217. self.ids = dashboard.current.services.query.ids = _.without(self.ids,id)
  218. _q.idQueue.unshift(id)
  219. _q.idQueue.sort(function(a,b){return a-b});
  220. return true;
  221. } else {
  222. return false;
  223. }
  224. }
  225. this.findQuery = function(queryString) {
  226. return _.findWhere(self.list,{query:queryString})
  227. }
  228. var nextId = function() {
  229. if(_q.idQueue.length > 0) {
  230. return _q.idQueue.shift()
  231. } else {
  232. return self.ids.length;
  233. }
  234. }
  235. var colorAt = function(id) {
  236. return self.colors[id % self.colors.length]
  237. }
  238. self.init();
  239. })
  240. .service('filterSrv', function(dashboard, ejsResource) {
  241. // Create an object to hold our service state on the dashboard
  242. dashboard.current.services.filter = dashboard.current.services.filter || {};
  243. _.defaults(dashboard.current.services.filter,{
  244. idQueue : [],
  245. list : {},
  246. ids : [],
  247. });
  248. // For convenience
  249. var ejs = ejsResource(config.elasticsearch);
  250. var _f = dashboard.current.services.filter;
  251. // Save a reference to this
  252. var self = this;
  253. // Call this whenever we need to reload the important stuff
  254. this.init = function() {
  255. // Accessors
  256. self.list = dashboard.current.services.filter.list;
  257. self.ids = dashboard.current.services.filter.ids;
  258. _f = dashboard.current.services.filter;
  259. _.each(self.getByType('time',true),function(time) {
  260. self.list[time.id].from = new Date(time.from)
  261. self.list[time.id].to = new Date(time.to)
  262. })
  263. }
  264. // This is used both for adding filters and modifying them.
  265. // If an id is passed, the filter at that id is updated
  266. this.set = function(filter,id) {
  267. _.defaults(filter,{mandate:'must'})
  268. filter.active = true;
  269. if(!_.isUndefined(id)) {
  270. if(!_.isUndefined(self.list[id])) {
  271. _.extend(self.list[id],filter);
  272. return id;
  273. } else {
  274. return false;
  275. }
  276. } else {
  277. if(_.isUndefined(filter.type)) {
  278. return false;
  279. } else {
  280. var _id = nextId();
  281. var _filter = {
  282. alias: '',
  283. id: _id
  284. }
  285. _.defaults(filter,_filter)
  286. self.list[_id] = filter;
  287. self.ids.push(_id)
  288. return _id;
  289. }
  290. }
  291. }
  292. this.getBoolFilter = function(ids) {
  293. // A default match all filter, just in case there are no other filters
  294. var bool = ejs.BoolFilter().must(ejs.MatchAllFilter());
  295. _.each(ids,function(id) {
  296. if(self.list[id].active) {
  297. switch(self.list[id].mandate)
  298. {
  299. case 'mustNot':
  300. bool = bool.mustNot(self.getEjsObj(id));
  301. break;
  302. case 'should':
  303. bool = bool.should(self.getEjsObj(id));
  304. break;
  305. default:
  306. bool = bool.must(self.getEjsObj(id));
  307. }
  308. }
  309. })
  310. return bool;
  311. }
  312. this.getEjsObj = function(id) {
  313. return self.toEjsObj(self.list[id])
  314. }
  315. this.toEjsObj = function (filter) {
  316. if(!filter.active) {
  317. return false
  318. }
  319. switch(filter.type)
  320. {
  321. case 'time':
  322. return ejs.RangeFilter(filter.field)
  323. .from(filter.from)
  324. .to(filter.to)
  325. break;
  326. case 'range':
  327. return ejs.RangeFilter(filter.field)
  328. .from(filter.from)
  329. .to(filter.to)
  330. break;
  331. case 'querystring':
  332. return ejs.QueryFilter(ejs.QueryStringQuery(filter.query))
  333. break;
  334. case 'terms':
  335. return ejs.TermsFilter(filter.field,filter.value)
  336. break;
  337. case 'exists':
  338. return ejs.ExistsFilter(filter.field)
  339. break;
  340. case 'missing':
  341. return ejs.MissingFilter(filter.field)
  342. break;
  343. default:
  344. return false;
  345. }
  346. }
  347. this.getByType = function(type,inactive) {
  348. return _.pick(self.list,self.idsByType(type,inactive))
  349. }
  350. this.removeByType = function(type) {
  351. var ids = self.idsByType(type)
  352. _.each(ids,function(id) {
  353. self.remove(id)
  354. })
  355. return ids;
  356. }
  357. this.idsByType = function(type,inactive) {
  358. var _require = inactive ? {type:type} : {type:type,active:true}
  359. return _.pluck(_.where(self.list,_require),'id')
  360. }
  361. // This special function looks for all time filters, and returns a time range according to the mode
  362. this.timeRange = function(mode) {
  363. var _t = _.where(self.list,{type:'time',active:true})
  364. if(_t.length == 0) {
  365. return false;
  366. }
  367. switch(mode) {
  368. case "min":
  369. return {
  370. from: new Date(_.max(_.pluck(_t,'from'))),
  371. to: new Date(_.min(_.pluck(_t,'to')))
  372. }
  373. break;
  374. case "max":
  375. return {
  376. from: new Date(_.min(_.pluck(_t,'from'))),
  377. to: new Date(_.max(_.pluck(_t,'to')))
  378. }
  379. break;
  380. default:
  381. return false;
  382. }
  383. }
  384. this.remove = function(id) {
  385. if(!_.isUndefined(self.list[id])) {
  386. delete self.list[id];
  387. // This must happen on the full path also since _.without returns a copy
  388. self.ids = dashboard.current.services.filter.ids = _.without(self.ids,id)
  389. _f.idQueue.unshift(id)
  390. _f.idQueue.sort(function(a,b){return a-b});
  391. return true;
  392. } else {
  393. return false;
  394. }
  395. }
  396. var nextId = function() {
  397. if(_f.idQueue.length > 0) {
  398. return _f.idQueue.shift()
  399. } else {
  400. return self.ids.length;
  401. }
  402. }
  403. // Now init
  404. self.init();
  405. })
  406. .service('dashboard', function($routeParams, $http, $rootScope, $injector, ejsResource, timer, kbnIndex) {
  407. // A hash of defaults to use when loading a dashboard
  408. var _dash = {
  409. title: "",
  410. editable: true,
  411. rows: [],
  412. services: {},
  413. index: {
  414. interval: 'none',
  415. pattern: '_all',
  416. default: '_all'
  417. },
  418. };
  419. // An elasticJS client to use
  420. var ejs = ejsResource(config.elasticsearch);
  421. var gist_pattern = /(^\d{5,}$)|(^[a-z0-9]{10,}$)|(gist.github.com(\/*.*)\/[a-z0-9]{5,}\/*$)/;
  422. // Store a reference to this
  423. var self = this;
  424. var filterSrv,query;
  425. this.current = {};
  426. this.last = {};
  427. $rootScope.$on('$routeChangeSuccess',function(){
  428. // Clear the current dashboard to prevent reloading
  429. self.current = {};
  430. self.indices = [];
  431. route();
  432. })
  433. var route = function() {
  434. // Is there a dashboard type and id in the URL?
  435. if(!(_.isUndefined($routeParams.type)) && !(_.isUndefined($routeParams.id))) {
  436. var _type = $routeParams.type;
  437. var _id = $routeParams.id;
  438. if(_type === 'elasticsearch')
  439. self.elasticsearch_load('dashboard',_id)
  440. if(_type === 'temp')
  441. self.elasticsearch_load('temp',_id)
  442. if(_type === 'file')
  443. self.file_load(_id)
  444. // No dashboard in the URL
  445. } else {
  446. // Check if browser supports localstorage, and if there's a dashboard
  447. if (Modernizr.localstorage &&
  448. !(_.isUndefined(localStorage['dashboard'])) &&
  449. localStorage['dashboard'] !== ''
  450. ) {
  451. var dashboard = JSON.parse(localStorage['dashboard']);
  452. _.defaults(dashboard,_dash);
  453. self.dash_load(dashboard)
  454. // No? Ok, grab default.json, its all we have now
  455. } else {
  456. self.file_load('default.json')
  457. }
  458. }
  459. }
  460. // Since the dashboard is responsible for index computation, we can compute and assign the indices
  461. // here before telling the panels to refresh
  462. this.refresh = function() {
  463. if(self.current.index.interval !== 'none') {
  464. if(filterSrv.idsByType('time').length > 0) {
  465. var _range = filterSrv.timeRange('min');
  466. kbnIndex.indices(_range.from,_range.to,
  467. self.current.index.pattern,self.current.index.interval
  468. ).then(function (p) {
  469. if(p.length > 0) {
  470. self.indices = p;
  471. } else {
  472. self.indices = [self.current.index.default]
  473. }
  474. $rootScope.$broadcast('refresh')
  475. });
  476. } else {
  477. // This is not optimal, we should be getting the entire index list here, or at least every
  478. // index that possibly matches the pattern
  479. self.indices = [self.current.index.default]
  480. $rootScope.$broadcast('refresh')
  481. }
  482. } else {
  483. self.indices = [self.current.index.default]
  484. $rootScope.$broadcast('refresh')
  485. }
  486. }
  487. this.dash_load = function(dashboard) {
  488. // Cancel all timers
  489. timer.cancel_all();
  490. // If not using time based indices, use the default index
  491. if(dashboard.index.interval === 'none') {
  492. self.indices = [dashboard.index.default]
  493. }
  494. self.current = _.clone(dashboard);
  495. // Ok, now that we've setup the current dashboard, we can inject our services
  496. query = $injector.get('query');
  497. filterSrv = $injector.get('filterSrv')
  498. // Make sure these re-init
  499. query.init();
  500. filterSrv.init();
  501. if(dashboard.index.interval !== 'none' && filterSrv.idsByType('time').length == 0) {
  502. //if(dashboard.index.interval !== 'none') {
  503. self.refresh();
  504. }
  505. return true;
  506. }
  507. this.gist_id = function(string) {
  508. if(self.is_gist(string))
  509. return string.match(gist_pattern)[0].replace(/.*\//, '');
  510. }
  511. this.is_gist = function(string) {
  512. if(!_.isUndefined(string) && string != '' && !_.isNull(string.match(gist_pattern)))
  513. return string.match(gist_pattern).length > 0 ? true : false;
  514. else
  515. return false
  516. }
  517. this.to_file = function() {
  518. var blob = new Blob([angular.toJson(self.current,true)], {type: "application/json;charset=utf-8"});
  519. // from filesaver.js
  520. saveAs(blob, self.current.title+"-"+new Date().getTime());
  521. return true;
  522. }
  523. this.set_default = function(dashboard) {
  524. if (Modernizr.localstorage) {
  525. localStorage['dashboard'] = angular.toJson(dashboard || self.current);
  526. return true;
  527. } else {
  528. return false;
  529. }
  530. }
  531. this.purge_default = function() {
  532. if (Modernizr.localstorage) {
  533. localStorage['dashboard'] = '';
  534. return true;
  535. } else {
  536. return false;
  537. }
  538. }
  539. // TOFIX: Pretty sure this breaks when you're on a saved dashboard already
  540. this.share_link = function(title,type,id) {
  541. return {
  542. location : location.href.replace(location.hash,""),
  543. type : type,
  544. id : id,
  545. link : location.href.replace(location.hash,"")+"#dashboard/"+type+"/"+id,
  546. title : title
  547. };
  548. }
  549. this.file_load = function(file) {
  550. return $http({
  551. url: "dashboards/"+file,
  552. method: "GET",
  553. }).then(function(result) {
  554. var _dashboard = result.data
  555. _.defaults(_dashboard,_dash);
  556. self.dash_load(_dashboard);
  557. return true;
  558. },function(result) {
  559. return false;
  560. });
  561. }
  562. this.elasticsearch_load = function(type,id) {
  563. var request = ejs.Request().indices(config.kibana_index).types(type);
  564. var results = request.query(
  565. ejs.IdsQuery(id)
  566. ).doSearch();
  567. return results.then(function(results) {
  568. if(_.isUndefined(results)) {
  569. return false;
  570. } else {
  571. self.dash_load(angular.fromJson(results.hits.hits[0]['_source']['dashboard']))
  572. return true;
  573. }
  574. });
  575. }
  576. this.elasticsearch_save = function(type,title,ttl) {
  577. // Clone object so we can modify it without influencing the existing obejct
  578. var save = _.clone(self.current)
  579. // Change title on object clone
  580. if (type === 'dashboard') {
  581. var id = save.title = _.isUndefined(title) ? self.current.title : title;
  582. }
  583. // Create request with id as title. Rethink this.
  584. var request = ejs.Document(config.kibana_index,type,id).source({
  585. user: 'guest',
  586. group: 'guest',
  587. title: save.title,
  588. dashboard: angular.toJson(save)
  589. })
  590. if (type === 'temp')
  591. request = request.ttl(ttl)
  592. // TOFIX: Implement error handling here
  593. return request.doIndex(
  594. // Success
  595. function(result) {
  596. return result;
  597. },
  598. // Failure
  599. function(result) {
  600. return false;
  601. }
  602. );
  603. }
  604. this.elasticsearch_delete = function(id) {
  605. return ejs.Document(config.kibana_index,'dashboard',id).doDelete(
  606. // Success
  607. function(result) {
  608. return result;
  609. },
  610. // Failure
  611. function(result) {
  612. return false;
  613. }
  614. );
  615. }
  616. this.elasticsearch_list = function(query,count) {
  617. var request = ejs.Request().indices(config.kibana_index).types('dashboard');
  618. return request.query(
  619. ejs.QueryStringQuery(query || '*')
  620. ).size(count).doSearch(
  621. // Success
  622. function(result) {
  623. return result;
  624. },
  625. // Failure
  626. function(result) {
  627. return false;
  628. }
  629. );
  630. }
  631. // TOFIX: Gist functionality
  632. this.save_gist = function(title,dashboard) {
  633. var save = _.clone(dashboard || self.current)
  634. save.title = title || self.current.title;
  635. return $http({
  636. url: "https://api.github.com/gists",
  637. method: "POST",
  638. data: {
  639. "description": save.title,
  640. "public": false,
  641. "files": {
  642. "kibana-dashboard.json": {
  643. "content": angular.toJson(save,true)
  644. }
  645. }
  646. }
  647. }).then(function(data, status, headers, config) {
  648. return data.data.html_url;
  649. }, function(data, status, headers, config) {
  650. return false;
  651. });
  652. }
  653. this.gist_list = function(id) {
  654. return $http.jsonp("https://api.github.com/gists/"+id+"?callback=JSON_CALLBACK"
  655. ).then(function(response) {
  656. var files = []
  657. _.each(response.data.data.files,function(v,k) {
  658. try {
  659. var file = JSON.parse(v.content)
  660. files.push(file)
  661. } catch(e) {
  662. // Nothing?
  663. }
  664. });
  665. return files;
  666. }, function(data, status, headers, config) {
  667. return false;
  668. });
  669. }
  670. })
  671. .service('keylistener', function($rootScope) {
  672. var keys = [];
  673. $(document).keydown(function (e) {
  674. keys[e.which] = true;
  675. });
  676. $(document).keyup(function (e) {
  677. delete keys[e.which];
  678. });
  679. this.keyActive = function(key) {
  680. return keys[key] == true;
  681. }
  682. });