Skip to content

Pick a single search head when search head cluster is detected #150

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
May 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 29 additions & 3 deletions out/notebooks/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
cancelSearchJob,
createSearchJob,
getClient,
getSearchHeadClusterMemberClient,
getSearchJob,
getSearchJobResults,
wait,
Expand All @@ -18,6 +19,7 @@ export class SplunkController {
protected supportedLanguages: string[];

protected _controller: vscode.NotebookController;
protected _service; // Splunk Javascript SDK Client
private _executionOrder = 0;
private _interrupted = false;
private _tokens = {};
Expand Down Expand Up @@ -55,6 +57,8 @@ export class SplunkController {
this.notebookType,
this.label
);
// Attempt to connect to individual search head if part of a search head cluster
this.refreshService();

this._controller.supportedLanguages = this.supportedLanguages;
this._controller.supportsExecutionOrder = true;
Expand All @@ -70,6 +74,29 @@ export class SplunkController {
this._execute([cell], notebookDocument, this._controller);
}

async refreshService() {
const config = vscode.workspace.getConfiguration();
const restUrl = config.get<string>('splunk.commands.splunkRestUrl');
const token = config.get<string>('splunk.commands.token');
// Create a new SDK client if one hasn't been created or token / url settings have been changed
if ((this._service === undefined)
|| (this._service === null)
|| (this._service._originalURL !== restUrl)
|| (this._service.sessionKey !== token)
) {
this._service = getClient();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this only be called if this._service is null?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah it would be undefined but let me fix this to be a bit more specific

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

// Check to see if the splunk deployment is part of a search head cluster, choose a single search head
// to target if so to ensure that adhoc jobs are immediately available (without replication)
try {
const newService = await getSearchHeadClusterMemberClient(this._service);
this._service = newService;
} catch (err) {
console.warn(`Error retrieving search head cluster information:`);
console.warn(err);
}
}
}

protected _execute(
cells: vscode.NotebookCell[],
_notebook: vscode.NotebookDocument,
Expand Down Expand Up @@ -145,9 +172,8 @@ export class SplunkController {

let query = cell.document.getText().trim().replace(/^\s+|\s+$/g, '');

const service = getClient()

let jobs = service.jobs();
await this.refreshService();
let jobs = this._service.jobs();

const tokenRegex = /\$([a-zA-Z0-9_.|]*?)\$/g;

Expand Down
9 changes: 3 additions & 6 deletions out/notebooks/spl2/controller.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
import * as vscode from 'vscode';

import {
dispatchSpl2Module,
getClient,
} from '../splunk';
import { dispatchSpl2Module } from '../splunk';
import { SplunkController } from '../controller';
import { splunkMessagesToOutputItems } from '../utils/messages';
import { getAppSubNamespace } from './serializer';
Expand All @@ -30,7 +27,6 @@ export class Spl2Controller extends SplunkController {
const execution = super._startExecution(cell);

const spl2Module = cell.document.getText().trim();
const service = getClient();
let fullNamespace: string = cell?.metadata?.splunk?.namespace || '';
// Get apps.<app>[.optional.sub.namespaces] from fullNamespace
const [app, subNamespace] = getAppSubNamespace(fullNamespace);
Expand All @@ -39,8 +35,9 @@ export class Spl2Controller extends SplunkController {

let job;
try {
await this.refreshService();
job = await dispatchSpl2Module(
service,
this._service,
spl2Module,
app,
subNamespace,
Expand Down
169 changes: 80 additions & 89 deletions out/notebooks/splunk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import * as vscode from 'vscode';
import { SplunkMessage } from './utils/messages';
import { getModuleStatements } from './utils/parsing';

export function getClient() {
export function getClient(): any {
const config = vscode.workspace.getConfiguration();
const restUrl = config.get<string>('splunk.commands.splunkRestUrl');
const token = config.get<string>('splunk.commands.token');
Expand All @@ -19,42 +19,16 @@ export function getClient() {
host: host,
port: port,
sessionKey: token,
version: '8',
authorization: 'Bearer',
});
service._originalURL = restUrl;

return service;
}

export function splunkLogin(service) {

return new Promise(function(resolve, reject) {

service.login(function(err, wasSuccessful) {
if (err !== null || !wasSuccessful) {
reject(err);
} else {
resolve(null);
}
});

});


}


export function createSearchJob(jobs, query, options) {
return new Promise(function(resolve, reject) {
jobs.create(query, options, function(err, data) {
if (err !== null) {
reject(err);
} else {
resolve(data);
}
});

});
export function createSearchJob(jobs, query, options): Promise<any> {
let request = jobs.create(query, options);
return request;
}

/**
Expand All @@ -69,6 +43,61 @@ function makeHeaders(service: any): object {
};
}

/**
* Check to see if the SDK client is part of a search head cluster, if so return a new
* client pointing to an individual search head member, such that any search ids created
* will be immediately available for results rather than waiting for artifact replication
* across the search head cluster.
* @param service Instance of the Javascript SDK Service
*
* @returns Promise<void>
*/
export function getSearchHeadClusterMemberClient(service: any): Promise<any> {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: should the @returns doctype be Promise<any> like the return signature?

const shcUrl = `${service.prefix}/services/shcluster/member/members?output_mode=json`;
console.log(`Attempting to determine SHC info if present using ${shcUrl}`);
return needle(
"GET",
shcUrl,
{
'headers': makeHeaders(service),
'followAllRedirects': true,
'timeout': 0,
'strictSSL': false,
'rejectUnauthorized' : false,
})
.then((response) => {
console.log(`Response from shcUrl status code: ${response.statusCode}`);
console.log(`Response from shcUrl body: \n'${JSON.stringify(response.body)}'`);
const data = response.body;
if (response.statusCode >= 400 ||
!Object.prototype.isPrototypeOf(data)
|| data.entry === undefined
|| !Array.isArray(data.entry)
|| data.entry.length === 0
|| data.entry[0].content === undefined
|| data.entry[0].content.mgmt_uri === undefined
) {
console.warn("Unsuccessful response from /services/shcluster/member/members endpoint encountered, reverting to original service client.")
return service;
}
// This is in the expected successful response format
vscode.window.showInformationMessage(`Discovered search head cluster members. Attempting to communicate directly with SH ${data.entry[0].content.mgmt_uri}`);
const url = new URL(data.entry[0].content.mgmt_uri);
const scheme = url.protocol.replace(':', '');
const port = url.port || (scheme === 'https' ? '443' : '80');
const host = url.hostname;
const newService = new splunk.Service({
scheme: scheme,
host: host,
port: port,
sessionKey: service.sessionKey,
authorization: 'Bearer',
});
newService._originalURL = service._originalURL;
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By keeping the original URL around we can decide whether the user has changed this value or not, if a new value was entered by the user in their settings then that will trigger creation of a new client and re-determining whether that is attached to a search head cluster.

return newService;
});
}

/**
* Update a module by calling the PUT /services/spl2/modules/<namespace>.<moduleName>
* @param service Instance of the Javascript SDK Service
Expand All @@ -77,7 +106,7 @@ function makeHeaders(service: any): object {
* @param module Full contents of the module to update with
* @returns Promise<void> (or throw Error containing data.messages[])
*/
export function updateSpl2Module(service: any, moduleName: string, namespace: string, module: string) {
export function updateSpl2Module(service: any, moduleName: string, namespace: string, module: string): Promise<void> {
// The Splunk SDK for Javascript doesn't currently support the spl2/modules endpoints
// nor does it support sending requests in JSON format (only receiving responses), so
// for now use the underlying needle library that the SDK uses for requests/responses
Expand Down Expand Up @@ -131,9 +160,9 @@ export function updateSpl2Module(service: any, moduleName: string, namespace: st
* @param namespace Namespace _within_ the apps.<app> to run, this will be used directly in the body of the request
* @param earliest Earliest time to be included in the body of the request
* @param latest Latest time to be included in the body of the request
* @returns A Promise containing the job id created (or throw an Error containing data.messages[])
* @returns A Promise containing the job information including sid created (or throw an Error containing data.messages[])
*/
export function dispatchSpl2Module(service: any, spl2Module: string, app: string, namespace: string, earliest: string, latest: string) {
export function dispatchSpl2Module(service: any, spl2Module: string, app: string, namespace: string, earliest: string, latest: string): Promise<any> {
// For now we're using /services/<app> which doesn't respect relative namespaces,
// so for now hardcode this to '' but if/when /servicesNS/<app>
namespace = '';
Expand Down Expand Up @@ -194,6 +223,7 @@ export function dispatchSpl2Module(service: any, spl2Module: string, app: string
.then((response) => {
console.log(`Response status code: ${response.statusCode}`);
console.log(`Response body: \n'${JSON.stringify(response.body)}'`);
console.log(`Response headers: \n'${JSON.stringify(response.headers)}'`);
const data = response.body;
if (response.statusCode >= 400 || !Array.prototype.isPrototypeOf(data) || data.length < 1) {
handleErrorPayloads(data, response.statusCode);
Expand Down Expand Up @@ -267,72 +297,33 @@ function handleErrorPayloads(data: any, statusCode: number) {
});
}

export function getSearchJobBySid(service, sid) {
return new Promise(function(resolve, reject) {
service.getJob(sid, function(err, data) {
if (err != null) {
reject(err);
} else {
resolve(data);
}
});
});
export function getSearchJobBySid(service, sid): Promise<any> {
let request = service.getJob(sid);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All of these refactors were necessary after upgrading the Javascript SDK to version 2+ see: https://github.com/splunk/splunk-sdk-javascript?tab=readme-ov-file#migrate-from-callbacksv1x-to-promiseasync-awaitv2x

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the callback version of the helper code here and in other places there was some error handling that I'm not seeing in the promise version, is this no longer necessary? I.e I would have expected to see some try/catch blocks.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so the resolve/reject handling that I removed is just part of the Promise formation and that's done in the SDK now. For these functions the expectation is that error handling is happening by the caller, which you can see happening here for the SPL2 case for example:

try {
await this.refreshService();
job = await dispatchSpl2Module(
this._service,
spl2Module,
app,
subNamespace,
earliest,
latest,
);
await super._finishExecution(job, cell, execution);
} catch (failedResponse) {
let outputItems: vscode.NotebookCellOutputItem[] = [];
if (!failedResponse.data || !failedResponse.data.messages) {
outputItems = [vscode.NotebookCellOutputItem.error(failedResponse)];
} else {
const messages = failedResponse.data.messages;
outputItems = splunkMessagesToOutputItems(messages);
}
execution.replaceOutput([new vscode.NotebookCellOutput(outputItems)]);
execution.end(false, Date.now());
}

return request;
}


export function getSearchJob(job) {
return new Promise(function(resolve, reject) {
job.fetch(function(err, job) {
if (err !== null) {
reject(err);
} else {
resolve(job);
}
});

});
export function getSearchJob(job): Promise<any> {
let request = job.fetch();
return request;
}

export function getJobSearchLog(job) {
return new Promise(function(resolve, reject) {
job.searchlog(function(err, log) {
if (err !== null) {
reject(err);
} else {
resolve(log);
}
});

});
export function getJobSearchLog(job): Promise<any> {
let request = job.searchlog();
return request;
}

export function getSearchJobResults(job) {
return new Promise(function(resolve, reject) {
job.get("results", {"output_mode": "json_cols"},function(err, results) {
if (err !== null) {
reject(err);
} else {
resolve(results);
}
});

});
export function getSearchJobResults(job): Promise<any> {
let request = job.get("results", {"output_mode": "json_cols"});
return request;
}

export function cancelSearchJob(job) {
return new Promise(function(resolve, reject) {
job.cancel(function(err, results) {
if (err !== null) {
reject(err);
} else {
resolve(results);
}

});
});
export function cancelSearchJob(job): Promise<any> {
let request = job.cancel();
return request;
}

export function wait(ms = 1000) {
export function wait(ms = 1000): Promise<void> {
return new Promise(resolve => {
setTimeout(resolve, ms);
});
Expand Down
16 changes: 8 additions & 8 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -460,7 +460,7 @@
"querystring-es3": "^0.2.1",
"react": "^16.14.0",
"react-dom": "^16.14.0",
"splunk-sdk": "^2.0.0",
"splunk-sdk": "^2.0.2",
"styled-components": "^5.1.1",
"tar-fs": "^2.1.1",
"ts-loader": "^9.4.2",
Expand Down