The following tutorial guides you through the design and implementation of the HiddenSearchSwapper Module, which is a JavaScript-based module that will swap out the underlying search depending on the user-provided time range provided. The HiddenSearchSwapper is referred to as 'hidden' because it has no visible elements, in contrast with modules like SimpleResultsTable or EventsViewer.
The HiddenSearchSwapper module is required because of the nature of the searches that power many Web Intelligence views and the large volumes of data that are involved in web analytics. The best user experience could only be achieved if one type of search was used for real-time ranges, another type was used for short-duration time ranges, and a summary-index powered search was used for long-duration time ranges.
The primary requirements for the HiddenSearchSwapper module were defined as follows:
In general, Splunk Modules are implementations of the AJAX design pattern. AJAX allows for a fast, dynamic user experience and as such is ideal for interaction with Splunk search, job, event, and result objects.
Since the purpose for creating the module was to change a view's underlying search based on a user-specified time range, creating a custom module was clearly the correct design pattern to implement.
Additionally, since the Splunk App Framework exposes JavaScript objects that enable module JavaScript to interact with a view's context, search, job, and time range objects, the module was designed with a bias towards pure JavaScript in order to minimize any server-side requirements.
The high-level architecture of the module was defined as follows:
Since Splunk Modules are configured on a per-view basis via Splunk View XML, it was important to consider how best to expose the module configuration. The following example configuration, provided in HiddenSearchSwapper.conf, illustrates the desired approach:
[module] className = Splunk.Module.HiddenSearchSwapper superClass = Splunk.Module description = Given the time range selected by time range picker, adjusts the context's search [param:rangeMap] required = True label = Use this to specify a list of searches and time ranges associated with them ### EXAMPLE CONFIG # # <module name="HiddenSearchSwapper"> # <param name="rangeMap"> # <param name="default"> # <param name="search">index=main fiz=baz</param> # </param> # <param name="rt"> # <param name="search">index=main foo=bar</param> # </param> # <param name="1d"> # <param name="search">index=summary marker=search1</param> # </param> # <param name="1w"> # <param name="savedsearch">my cool saved search that actually exists</param> # </param> # <param name="1m"> # <param name="search">index=summary marker=search3</param> # </param> # </param> # </module>
The best practice when implementing new modules is to sub-class the base module Splunk.Module, as defined in AbstractModule.js. Using other modules as the base class for custom modules can cause unexpected behavior and is often best avoided.
It is also expected that new modules will be sub-classed using jQuery.klass(), the preferred method of sub-classing and inheritance for all Splunk Modules.
Splunk.Module.HiddenSearchSwapper = $.klass(Splunk.Module, {
...
});
The HiddenSearchSwapper module exposes the following methods:
All module classes must have an initialize() method. Since this module super-classes another module, the best practice is to use $super to ensure that Splunk.Module.initialize() is executed before Splunk.Module.HiddenSearchSwapper.initialize().
Setting this.childEnforcement to Splunk.Module.ALWAYS_REQUIRE makes use of the inherited Splunk.Module.validateHierarchy() method. Simply put, configuring a view to use a HiddenSearchSwapper with no children represents a configuration error, and this is the proper way to provide messaging to that effect.
In general, it is highly recommended to instantiate both a logger and messenger member. Even if your module does not use the logger or messenger in its current iteration, it makes sense to make them available by default.
NB: The comments below do not appear in the actual module JavaScript file, but are provided for illustrative purposes.
initialize: function($super, container) {
// ensures Splunk.Module.initialize()
$super(container);
// ensures that instances of this module have children
this.childEnforcement = Splunk.Module.ALWAYS_REQUIRE;
// sets up a logger member that enables logging to web_service.log
this.logger = Splunk.Logger.getLogger("hidden_search_swapper.js");
// sets up a messenger member that enables user messaging via the MessageBar module
this.messenger = Splunk.Messenger.System.getInstance();
// this.ParseXHR will reference a synchronous XHR call to /util/time/parser
this.ParseXHR = null;
// this.SSXHR will reference a synchronous XHR call to the module controller
this.SSXHR = null;
// this._allTime will store the search associated with an 'all time' time range
this._allTime = {};
// this.default will store a 'fail-safe' search
this._default = {};
// this._minRange will store the smallest range assigned to any search
this._minRange = null;
// this._ranges will store searches associates with various time ranges
this._ranges = {};
// this._rt will store searches associates with real-time searches
this._rt = {};
// we call this._setRanges() to load the view's module configuration
this._setRanges();
},
getModifiedContext() overrides the inherited Splunk.Module.getModifiedContext() method, which by default simply returns the current context.
getModifiedContext() is called by Splunk.Module.pushContextToChildren() each time a new or modified context is received from the module's parent. By overriding this method, we can interrogate the context and, if necessary, replace the baseSearch before the context is passed on to the module's children.
/*
* receive a new context and act upon it
* if the current time range is real time, use this._rt
* if the current time range is all time, use this._allTime
* otherwise, we will need to call the module controller to times
* @return context {Object} the context to be passed to children
*/
getModifiedContext: function() {
// use getContext(), get(), and getTimeRange()
var timeRange = this.getContext().get("search").getTimeRange();
var context; // empty context
// use isRealTime()
if (timeRange.isRealTime()) {
// simple case - swap out the baseSearch with this._rt
context = this._swapSearch(this._rt);
// use isAllTime()
} else if (timeRange.isAllTime()) {
// simple case - swap out the baseSearch with this._allTime
context = this._swapSearch(this._allTime);
} else {
// complex case - parse the current time range for duration, then
// send the duration to the search router to get the correct context
var duration = this._parseRange(timeRange);
context = this._searchRouter(duration);
}
// getModifiedContext() has a context to return a valid Splunk Context or null
return context;
},
_getSavedSearch() wraps an XML-HTTP request to the HiddenSearchSwapper's controller, which emulates the functionality of the HiddenSavedSearch controller by returning a JSON object given a valid saved search name. The primary differences between the HiddenSearchSwapper controller and the HiddenSavedSearch controller are as follows:
/*
* given a saved search name, returns an object representing the saved search
* @param savedSearchName {String} the name of the saved search
* @param useHistory {Bool} attempt to resurrect if true
* @return savedSearch {Object} a saved search object
*/
_getSavedSearch: function(savedSearchName, useHistory) {
var search;
// use getResultParams() to retrieve existing result params
var params = this.getResultParams();
// insert or override result params for 'savedSearchName', 'client_app', and 'useHistory'
params['savedSearchName'] = savedSearchName;
params['client_app'] = Splunk.util.getCurrentApp();
if (useHistory) {
params['useHistory'] = useHistory;
}
// Use jQuery.ajax() to make an XHR to the module's controller
this.SSXHR = $.ajax({
// syncronous request, as this should block
async: false,
type:'GET',
// use the utilities make_url(), getConfigValue(), and
// propToQueryString() for portability and reuse
url: Splunk.util.make_url('module',
Splunk.util.getConfigValue('SYSTEM_NAMESPACE'),
this.moduleType,
'render?' +
Splunk.util.propToQueryString(params)),
// it is recommended to set the 'X-Splunk-Module' header
beforeSend: function(xhr) {
xhr.setRequestHeader('X-Splunk-Module', this.moduleType);
},
// jQuery.ajax().complete() allows us to bind an action to the call's completion
// since this call is synchronous, complete() is preferable to success()
complete: function(data) {
this.logger.debug('response OK from server');
// JSON.parse() the response
search = JSON.parse(data.responseText);
}.bind(this),
// on error, send a message to the system messenger and return null
error: function() {
this.messenger.send('error',
'splunk.search',
_('Unable to get saved search from controller'));
this.logger.debug('response ERROR from server');
return null;
}.bind(this)
});
// return the saved search JSON
return search;
},
In order to parse both absolute and relative time ranges, it is necessary to make an XHR call to /util/time/parser, which is a built-in Splunk resource that returns a JSON structure of given unix timestamps translated into both ISO-8601 format and a localized string.
We will use the ISO times along with the Splunk utility getEpochTimeFromISO() to return an absolute duration between the first and last times in the user-specified time range.
/*
* internal function to parse the time range into an absolute duration
* @param timeRange {Object} Splunk time range object
* @return duration {number} the range between earliest and latest
*/
_parseRange: function(timeRange) {
// use getEarliestTimeTerms() and getLatestTimeTerms() to get user-specified time terms
var earliest = timeRange.getEarliestTimeTerms();
var latest = timeRange.getLatestTimeTerms();
// use make_url() to create the URI
var url = Splunk.util.make_url('/util/time/parser?ts='
+encodeURIComponent(earliest)+
'&ts='+encodeURIComponent(latest));
// need to create Iso reference out here to form a closure
// so that it will be in scope within the XHR request
var Iso = {};
// Use jQuery.ajax() to make an XHR to the /util/time/parser
this.ParseXHR = $.ajax({
// synchronous, we need this call to be blocking
async: false,
type:'GET',
// use the previously compiled URI
url: url,
// best to use complete() since the call is synchronous
complete: function(data) {
this.logger.debug('response OK from server');
// parse the response into Iso
Iso['earliest'] = JSON.parse(data.responseText)[earliest].iso;
Iso['latest'] = JSON.parse(data.responseText)[latest].iso;
}.bind(this),
error: function() {
this.messenger.send('error',
'splunk.search',
_('Unable to parse times'));
this.logger.debug('response ERROR from server');
return null;
}.bind(this)
});
// use parseInt and getEpochTimeFromISO() to return an absolute duration
return parseInt(Splunk.util.getEpochTimeFromISO(
Iso['latest']))
-parseInt(Splunk.util.getEpochTimeFromISO(
Iso['earliest']));
},
The _searchRouter() method offloads routing duties from getModifiedContext().
/*
* routes to _swapSearch based on duration
* @param duration {Number} the duration of the search
* @return context {Object} the new context
*/
_searchRouter: function(duration) {
// check to see if we can swap in the default search, which happens if the
// user-selected time range is smaller than the smallest configured time range
// for the module
if (duration <= this._minRange) {
return this._swapSearch(this._default);
} else {
// since this._ranges is sorted, this is a quick way to get the fail-safe range
for (first in this._ranges) break;
var match = first;
// iterate through the rest of this._ranges to determine if the given
// duration is less than the next range within this._ranges
for (range in this._ranges) {
if ((+range) >= duration) {
break;
}
// if we get here via the above break, we have a match
// if we never match, match is the highest range in the sorted this._ranges
match = range;
}
// call this._swapSearch providing the search stored in
// this._ranges for the determined matching duration
return this._swapSearch(this._ranges[match]);
}
},
Time to make the donuts! When called by _searchRouter() with a given search string, _swapSearch() does the actual 'search swapping' within the current context.
Of particular note are the calls to Splunk.Job.setAsAutoCancellable() and Splunk.Search.abandonJob(). These calls ensure that if the context's previous search was dispatched (i.e. there was an active job associated with the old search), the dispatched job is marked as auto-cancellable and abandoned by the view.
After a job is marked as auto-cancellable and abandoned, it will be reaped by the jobs subsystem safely and automatically.
/*
* swaps out the current search with either a search string or saved search
* @param _search {Object} contains either a 'search' or 'savedsearch' key
* @return context {Object} the new context
*/
_swapSearch: function(_search) {
// get the current search using getContext() and get()
var context = this.getContext();
var search = context.get('search');
// set job as auto-cancellable and abandon it
search.job.setAsAutoCancellable(true);
search.abandonJob();
// check for a 'savedsearch' key first, as saved searches have precedence
if ('savedsearch' in _search) {
// if this is a saved search, we need to call the module controller
// via this._getSavedSearch() to get the search string
var swapSearch = this._getSavedSearch(_search['savedsearch'], false);
// set the baseSearch and then set the search within the context
search.setBaseSearch(swapSearch['fullSearch']);
context.set('search', search);
} else {
// if this is a plain search string, set the baseSearch and then
// set the search within the context
var fullSearch = _search['search'];
search.setBaseSearch(fullSearch);
context.set('search', search);
}
// return the modified context
return context;
},
The view 'Traffic Patterns', as defined in /webintelligence/default/data/ui/views/ops_traffic_patterns.xml, uses the HiddenSearchSwapper module to swap out the base search depending on the time range selected by the user in the parent TimeRangePicker module.
Under the 'rangeMap' param, the following ranges are defined:
<module name="TimeRangePicker"
layoutPanel="mainSearchControls" autoRun="True">
<param name="selected">Last 5 minutes</param>
<param name="searchWhenChanged">True</param>
<module name="HiddenSearchSwapper" layoutPanel="panel_row1_col1" group="Hits By Host" autoRun="True">
<param name="rangeMap">
<param name="default">
<param name="savedsearch">RealtimeOps - Traffic By Host</param>
</param>
<param name="rt">
<param name="savedsearch">RealtimeOps - Traffic By Host</param>
</param>
<param name="10m">
<param name="search">`timerange_hack` source="Web Traffic by host*"
| timechart span=5m sum(hits) BY myhost
| rename myhost as host
</param>
</param>
<param name="2h">
<param name="search">`timerange_hack` source="Web Traffic by host*"
| timechart span=1h sum(hits) BY myhost
| rename myhost as host
</param>
</param>
<param name="24h">
<param name="search">`timerange_hack` source="Web Traffic by host*"
| timechart span=1d sum(hits) BY myhost
| rename myhost as host
</param>
</param>
<param name="168h">
<param name="savedsearch">ReportOps - Traffic By Host</param>
</param>
</param>