Skip to content
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

[Asset Management] Osquery telemetry updates #100754

Merged
merged 11 commits into from
Jun 17, 2021
3 changes: 2 additions & 1 deletion x-pack/plugins/osquery/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import { PackagePolicy, PackagePolicyInput, PackagePolicyInputStream } from '../

export const savedQuerySavedObjectType = 'osquery-saved-query';
export const packSavedObjectType = 'osquery-pack';
export type SavedObjectType = 'osquery-saved-query' | 'osquery-pack';
export const usageMetricSavedObjectType = 'osquery-usage-metric';
export type SavedObjectType = 'osquery-saved-query' | 'osquery-pack' | 'osquery-usage-metric';

/**
* This makes any optional property the same as Required<T> would but also has the
Expand Down
1 change: 1 addition & 0 deletions x-pack/plugins/osquery/kibana.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"kibanaVersion": "kibana",
"optionalPlugins": [
"home",
"usageCollection",
"lens"
],
"requiredBundles": [
Expand Down
6 changes: 6 additions & 0 deletions x-pack/plugins/osquery/server/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { OsqueryPluginSetup, OsqueryPluginStart, SetupPlugins, StartPlugins } fr
import { defineRoutes } from './routes';
import { osquerySearchStrategyProvider } from './search_strategy/osquery';
import { initSavedObjects } from './saved_objects';
import { initUsageCollectors } from './usage';
import { OsqueryAppContext, OsqueryAppContextService } from './lib/osquery_app_context_services';
import { ConfigType } from './config';

Expand Down Expand Up @@ -48,6 +49,11 @@ export class OsqueryPlugin implements Plugin<OsqueryPluginSetup, OsqueryPluginSt
};

initSavedObjects(core.savedObjects, osqueryContext);
initUsageCollectors({
core,
osqueryContext,
usageCollection: plugins.usageCollection,
});
defineRoutes(router, osqueryContext);

core.getStartServices().then(([, depsStart]) => {
Expand Down
57 changes: 34 additions & 23 deletions x-pack/plugins/osquery/server/routes/action/create_action_route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import {
CreateActionRequestBodySchema,
} from '../../../common/schemas/routes/action/create_action_request_body_schema';

import { incrementCount } from '../usage';

export const createActionRoute = (router: IRouter, osqueryContext: OsqueryAppContext) => {
router.post(
{
Expand All @@ -39,34 +41,43 @@ export const createActionRoute = (router: IRouter, osqueryContext: OsqueryAppCon
osqueryContext,
agentSelection
);

incrementCount(soClient, 'live_query');
if (!selectedAgents.length) {
incrementCount(soClient, 'live_query', 'errors');
return response.badRequest({ body: new Error('No agents found for selection') });
}

const action = {
action_id: uuid.v4(),
'@timestamp': moment().toISOString(),
expiration: moment().add(1, 'days').toISOString(),
type: 'INPUT_ACTION',
input_type: 'osquery',
agents: selectedAgents,
data: {
id: uuid.v4(),
query: request.body.query,
},
};
const actionResponse = await esClient.index<{}, {}>({
index: '.fleet-actions',
body: action,
});
try {
const action = {
action_id: uuid.v4(),
'@timestamp': moment().toISOString(),
expiration: moment().add(1, 'days').toISOString(),
type: 'INPUT_ACTION',
input_type: 'osquery',
agents: selectedAgents,
data: {
id: uuid.v4(),
query: request.body.query,
},
};
const actionResponse = await esClient.index<{}, {}>({
index: '.fleet-actions',
body: action,
});

return response.ok({
body: {
response: actionResponse,
actions: [action],
},
});
return response.ok({
body: {
response: actionResponse,
actions: [action],
},
});
} catch (error) {
incrementCount(soClient, 'live_query', 'errors');
return response.customError({
statusCode: 500,
body: new Error(`Error occurred whlie processing ${error}`),
});
}
}
);
};
8 changes: 8 additions & 0 deletions x-pack/plugins/osquery/server/routes/usage/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

export * from './recorder';
135 changes: 135 additions & 0 deletions x-pack/plugins/osquery/server/routes/usage/recorder.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { savedObjectsClientMock } from '../../../../../../src/core/server/mocks';

import { usageMetricSavedObjectType } from '../../../common/types';

import {
CounterValue,
createMetricObjects,
getRouteMetric,
incrementCount,
RouteString,
routeStrings,
} from './recorder';

const savedObjectsClient = savedObjectsClientMock.create();

// eslint-disable-next-line @typescript-eslint/no-explicit-any
function checkGetCalls(calls: any[]) {
expect(calls.length).toEqual(routeStrings.length);
for (let i = 0; i < routeStrings.length; ++i) {
expect(calls[i]).toEqual([usageMetricSavedObjectType, routeStrings[i]]);
}
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
function checkCreateCalls(calls: any[], expectedCallRoutes: string[] = routeStrings) {
expect(calls.length).toEqual(expectedCallRoutes.length);
for (let i = 0; i < expectedCallRoutes.length; ++i) {
expect(calls[i][0]).toEqual(usageMetricSavedObjectType);
expect(calls[i][2].id).toEqual(expectedCallRoutes[i]);
}
}

describe('Usage metric recorder', () => {
describe('Metric initalizer', () => {
const get = savedObjectsClient.get as jest.Mock;
const create = savedObjectsClient.create as jest.Mock;
afterEach(() => {
get.mockClear();
create.mockClear();
});
it('should seed route metrics objects', async () => {
get.mockRejectedValueOnce('stub value');
create.mockReturnValueOnce('stub value');
const result = await createMetricObjects(savedObjectsClient);
checkGetCalls(get.mock.calls);
checkCreateCalls(create.mock.calls);
expect(result).toBe(true);
});

it('should handle previously seeded objects properly', async () => {
get.mockReturnValueOnce('stub value');
create.mockRejectedValueOnce('stub value');
const result = await createMetricObjects(savedObjectsClient);
checkGetCalls(get.mock.calls);
checkCreateCalls(create.mock.calls, []);
expect(result).toBe(true);
});

it('should report failure to create the metrics object', async () => {
get.mockRejectedValueOnce('stub value');
create.mockRejectedValueOnce('stub value');
const result = await createMetricObjects(savedObjectsClient);
checkGetCalls(get.mock.calls);
checkCreateCalls(create.mock.calls);
expect(result).toBe(false);
});
});

describe('Incrementation', () => {
let counterMap: { [key: string]: CounterValue };
const get = savedObjectsClient.get as jest.Mock;
const update = savedObjectsClient.update as jest.Mock;
update.mockImplementation(
async (objectType: string, route: RouteString, newVal: CounterValue) => {
counterMap[`${objectType}-${route}`] = newVal;
}
);
get.mockImplementation(async (objectType: string, route: RouteString) => ({
attributes: counterMap[`${objectType}-${route}`],
}));
beforeEach(() => {
counterMap = routeStrings.reduce((acc, route) => {
acc[`${usageMetricSavedObjectType}-${route}`] = {
count: 0,
errors: 0,
};
return acc;
}, {} as { [key: string]: CounterValue });
get.mockClear();
update.mockClear();
});
it('should increment the route counter', async () => {
expect(await getRouteMetric(savedObjectsClient, 'live_query')).toEqual({
count: 0,
errors: 0,
});
await incrementCount(savedObjectsClient, 'live_query');
expect(await getRouteMetric(savedObjectsClient, 'live_query')).toEqual({
count: 1,
errors: 0,
});
});

it('should allow incrementing the error counter', async () => {
expect(await getRouteMetric(savedObjectsClient, 'live_query')).toEqual({
count: 0,
errors: 0,
});
await incrementCount(savedObjectsClient, 'live_query', 'errors');
expect(await getRouteMetric(savedObjectsClient, 'live_query')).toEqual({
count: 0,
errors: 1,
});
});

it('should allow adjustment of the increment', async () => {
expect(await getRouteMetric(savedObjectsClient, 'live_query')).toEqual({
count: 0,
errors: 0,
});
await incrementCount(savedObjectsClient, 'live_query', 'count', 2);
expect(await getRouteMetric(savedObjectsClient, 'live_query')).toEqual({
count: 2,
errors: 0,
});
});
});
});
65 changes: 65 additions & 0 deletions x-pack/plugins/osquery/server/routes/usage/recorder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { SavedObjectsClientContract } from 'kibana/server';
import { usageMetricSavedObjectType } from '../../../common/types';
import { LiveQuerySessionUsage } from '../../usage/types';

export interface RouteUsageMetric {
queries: number;
errors: number;
}

export type RouteString = 'live_query';

export const routeStrings: RouteString[] = ['live_query'];

export async function createMetricObjects(soClient: SavedObjectsClientContract) {
const res = await Promise.allSettled(
routeStrings.map(async (route) => {
try {
await soClient.get(usageMetricSavedObjectType, route);
} catch (e) {
await soClient.create(
usageMetricSavedObjectType,
{
errors: 0,
count: 0,
},
{
id: route,
}
);
}
})
);
return !res.some((e) => e.status === 'rejected');
}

export async function getCount(soClient: SavedObjectsClientContract, route: RouteString) {
return await soClient.get<LiveQuerySessionUsage>(usageMetricSavedObjectType, route);
}

export interface CounterValue {
count: number;
errors: number;
}

export async function incrementCount(
soClient: SavedObjectsClientContract,
route: RouteString,
key: keyof CounterValue = 'count',
increment = 1
) {
const metric = await soClient.get<CounterValue>(usageMetricSavedObjectType, route);
metric.attributes[key] += increment;
await soClient.update(usageMetricSavedObjectType, route, metric.attributes);
Copy link
Contributor

Choose a reason for hiding this comment

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

Does this ever flush the state, or does it just increment forever?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

this is a cumulative count. i'm not entirely certain about the life times of saved objects, what events do they get flushed in outside of clearing them manually? i might be able to do something like clearing the count on fetch, but it felt like a wash to me.

are there any situations where delta metrics would be better for this kind of thing?

Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks for bearing with me. I have been OOO for a couple of days :-)

what events do they get flushed in outside of clearing them manually?

They don't - unless a space is deleted AFAIK.

are there any situations where delta metrics would be better for this kind of thing?

Delta is maybe not a good word to describe it, but from what I have seen the usage collector runs every 24 hr and developers typically try to get a snapshot of the last 24 hours. That makes me think that clearing the count on read is what is needed. But I guess it only matters to people / systems consuming the telemetry. It's your call :- )

Copy link
Contributor Author

Choose a reason for hiding this comment

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

thanks for the context! i feel better about changing it knowing there's prior art, and knowing about the collection interval.

}

export async function getRouteMetric(soClient: SavedObjectsClientContract, route: RouteString) {
return (await getCount(soClient, route)).attributes;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { SavedObjectsType } from '../../../../../../src/core/server';

import { usageMetricSavedObjectType } from '../../../common/types';

export const usageMetricSavedObjectMappings: SavedObjectsType['mappings'] = {
properties: {
count: {
type: 'long',
},
errors: {
type: 'long',
},
},
};

export const usageMetricType: SavedObjectsType = {
name: usageMetricSavedObjectType,
hidden: false,
namespaceType: 'single',
mappings: usageMetricSavedObjectMappings,
};
3 changes: 3 additions & 0 deletions x-pack/plugins/osquery/server/saved_objects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { CoreSetup } from '../../../../src/core/server';

import { OsqueryAppContext } from './lib/osquery_app_context_services';
import { savedQueryType, packType } from './lib/saved_query/saved_object_mappings';
import { usageMetricType } from './routes/usage/saved_object_mappings';

const types = [savedQueryType, packType];

Expand All @@ -20,6 +21,8 @@ export const initSavedObjects = (
) => {
const config = osqueryContext.config();

savedObjects.registerType(usageMetricType);

if (config.savedQueries) {
savedObjects.registerType(savedQueryType);
}
Expand Down
2 changes: 2 additions & 0 deletions x-pack/plugins/osquery/server/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
PluginStart as DataPluginStart,
} from '../../../../src/plugins/data/server';
import { FleetStartContract } from '../../fleet/server';
import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/server';
import { PluginSetupContract } from '../../features/server';

// eslint-disable-next-line @typescript-eslint/no-empty-interface
Expand All @@ -19,6 +20,7 @@ export interface OsqueryPluginSetup {}
export interface OsqueryPluginStart {}

export interface SetupPlugins {
usageCollection?: UsageCollectionSetup;
actions: ActionsPlugin['setup'];
data: DataPluginSetup;
features: PluginSetupContract;
Expand Down
Loading