Skip to content

Commit e00e26e

Browse files
Fleet Usage telemetry extension (elastic#145353)
## Summary Closes elastic/ingest-dev#1261 Added a snippet to the telemetry that I added for each requirement. Please review and let me know if any changes are needed. Also asked a few questions below. @jlind23 @kpollich 6. is blocked by [elasticsearch change](elastic/elasticsearch#91701) to give kibana_system the missing privilege to read logs-elastic_agent* indices. Took inspiration for task versioning from https://github.com/elastic/kibana/pull/144494/files#diff-0c7c49bf5c55c45c19e9c42d5428e99e52c3a39dd6703633f427724d36108186 - [x] 1. Elastic Agent versions Versions of all the Elastic Agent running: `agent.version` field on `.fleet-agents` documents ``` "agent_versions": [ "8.6.0" ], ``` - [x] 2. Fleet server configuration Think we can query for `.fleet-policies` where some `input` has `type: 'fleet-server'` for this, as well as use the `Fleet Server Hosts` settings that we define via saved objects in Fleet ``` "fleet_server_config": { "policies": [ { "input_config": { "server": { "limits.max_agents": 10000 }, "server.runtime": "gc_percent:20" } } ] } ``` - [x] 3. Number of policies Count of `.fleet-policies` index To confirm, did we mean agent policies here? ``` "agent_policies": { "count": 7, ``` - [x] 4. Output type contained in those policies Collecting this from ts logic, querying from `.fleet-policies` index. The alternative would be to write a painless script (because the `outputs` are an object with dynamic keys, we can't do an aggregation directly). ``` "agent_policies": { "output_types": [ "elasticsearch" ] } ``` Did we mean to just collect the types here, or any other info? e.g. output urls - [x] 5. Average number of checkin failures We only have the most recent checkin status and timestamp on `.fleet-agents`. Do we mean here to publish the total last checkin failure count? E.g. 3 if 3 agents are in failure checkin status currently. Or do we mean to publish specific info for all agents (`last_checkin_status`, `last_checkin` time, `last_checkin_message`)? Are the only statuses `error` and `degraded` that we want to send? ``` "agent_last_checkin_status": { "error": 0, "degraded": 0 }, ``` - [ ] 6. Top 3 most common errors in the Elastic Agent logs Do we mean here elastic-agent logs only, or fleet-server logs as well (maybe separately)? I found an alternative way to query the message field using sampler and categorize text aggregation: ``` GET logs-elastic_agent*/_search { "size": 0, "query": { "bool": { "must": [ { "term": { "log.level": "error" } }, { "range": { "@timestamp": { "gte": "now-1h" } } } ] } }, "aggregations": { "message_sample": { "sampler": { "shard_size": 200 }, "aggs": { "categories": { "categorize_text": { "field": "message", "size": 10 } } } } } } ``` Example response: ``` "aggregations": { "message_sample": { "doc_count": 112, "categories": { "buckets": [ { "doc_count": 73, "key": "failed to unenroll offline agents", "regex": ".*?failed.+?to.+?unenroll.+?offline.+?agents.*?", "max_matching_length": 36 }, { "doc_count": 7, "key": """stderr panic close of closed channel n ngoroutine running Stop ngithub.com/elastic/beats/v7/libbeat/cmd/instance Beat launch.func5 \n\t/go/src/github.com/elastic/beats/libbeat/cmd/instance/beat.go n ``` - [x] 7. Number of checkin failure over the past period of time I think this is almost the same as #5. The difference would be to report new failures happened only in the last hour, or report all agents in failure state. (which would be an increasing number if the agent stays in failed state). Do we want these 2 separate telemetry fields? EDIT: removed the last1hr query, instead added a new field to report agents enrolled per policy (top 10). See comments below. ``` "agent_checkin_status": { "error": 3, "degraded": 0 }, "agents_per_policy": [2, 1000], ``` - [x] 8. Number of Elastic Agent and number of fleet server This is already there in the existing telemetry: ``` "agents": { "total_enrolled": 0, "healthy": 0, "unhealthy": 0, "offline": 0, "total_all_statuses": 1, "updating": 0 }, "fleet_server": { "total_enrolled": 0, "healthy": 0, "unhealthy": 0, "offline": 0, "updating": 0, "total_all_statuses": 0, "num_host_urls": 1 }, ``` ### Checklist - [ ] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios Co-authored-by: Kibana Machine <[email protected]>
1 parent f1cdc08 commit e00e26e

File tree

10 files changed

+783
-204
lines changed

10 files changed

+783
-204
lines changed

x-pack/plugins/fleet/server/collectors/agent_collectors.ts

Lines changed: 83 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@
77

88
import type { SavedObjectsClient, ElasticsearchClient } from '@kbn/core/server';
99

10-
import type { FleetConfigType } from '../../common/types';
10+
import { AGENTS_INDEX } from '../../common';
1111
import * as AgentService from '../services/agents';
12+
import { appContextService } from '../services';
1213

1314
export interface AgentUsage {
1415
total_enrolled: number;
@@ -20,7 +21,6 @@ export interface AgentUsage {
2021
}
2122

2223
export const getAgentUsage = async (
23-
config: FleetConfigType,
2424
soClient?: SavedObjectsClient,
2525
esClient?: ElasticsearchClient
2626
): Promise<AgentUsage> => {
@@ -47,3 +47,84 @@ export const getAgentUsage = async (
4747
updating,
4848
};
4949
};
50+
51+
export interface AgentData {
52+
agent_versions: string[];
53+
agent_checkin_status: {
54+
error: number;
55+
degraded: number;
56+
};
57+
agents_per_policy: number[];
58+
}
59+
60+
const DEFAULT_AGENT_DATA = {
61+
agent_versions: [],
62+
agent_checkin_status: { error: 0, degraded: 0 },
63+
agents_per_policy: [],
64+
};
65+
66+
export const getAgentData = async (
67+
esClient: ElasticsearchClient,
68+
abortController: AbortController
69+
): Promise<AgentData> => {
70+
try {
71+
const transformLastCheckinStatusBuckets = (resp: any) =>
72+
((resp?.aggregations?.last_checkin_status as any).buckets ?? []).reduce(
73+
(acc: any, bucket: any) => {
74+
if (acc[bucket.key] !== undefined) acc[bucket.key] = bucket.doc_count;
75+
return acc;
76+
},
77+
{ error: 0, degraded: 0 }
78+
);
79+
const response = await esClient.search(
80+
{
81+
index: AGENTS_INDEX,
82+
query: {
83+
bool: {
84+
filter: [
85+
{
86+
term: {
87+
active: 'true',
88+
},
89+
},
90+
],
91+
},
92+
},
93+
size: 0,
94+
aggs: {
95+
versions: {
96+
terms: { field: 'agent.version' },
97+
},
98+
last_checkin_status: {
99+
terms: { field: 'last_checkin_status' },
100+
},
101+
policies: {
102+
terms: { field: 'policy_id' },
103+
},
104+
},
105+
},
106+
{ signal: abortController.signal }
107+
);
108+
const versions = ((response?.aggregations?.versions as any).buckets ?? []).map(
109+
(bucket: any) => bucket.key
110+
);
111+
const statuses = transformLastCheckinStatusBuckets(response);
112+
113+
const agentsPerPolicy = ((response?.aggregations?.policies as any).buckets ?? []).map(
114+
(bucket: any) => bucket.doc_count
115+
);
116+
117+
return {
118+
agent_versions: versions,
119+
agent_checkin_status: statuses,
120+
agents_per_policy: agentsPerPolicy,
121+
};
122+
} catch (error) {
123+
if (error.statusCode === 404) {
124+
appContextService.getLogger().debug('Index .fleet-agents does not exist yet.');
125+
} else {
126+
throw error;
127+
}
128+
return DEFAULT_AGENT_DATA;
129+
}
130+
};
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import type { ElasticsearchClient } from '@kbn/core/server';
9+
10+
import { AGENT_POLICY_INDEX } from '../../common';
11+
import { ES_SEARCH_LIMIT } from '../../common/constants';
12+
import { appContextService } from '../services';
13+
14+
export interface AgentPoliciesUsage {
15+
count: number;
16+
output_types: string[];
17+
}
18+
19+
const DEFAULT_AGENT_POLICIES_USAGE = {
20+
count: 0,
21+
output_types: [],
22+
};
23+
24+
export const getAgentPoliciesUsage = async (
25+
esClient: ElasticsearchClient,
26+
abortController: AbortController
27+
): Promise<AgentPoliciesUsage> => {
28+
try {
29+
const res = await esClient.search(
30+
{
31+
index: AGENT_POLICY_INDEX,
32+
size: ES_SEARCH_LIMIT,
33+
track_total_hits: true,
34+
rest_total_hits_as_int: true,
35+
},
36+
{ signal: abortController.signal }
37+
);
38+
39+
const agentPolicies = res.hits.hits;
40+
41+
const outputTypes = new Set<string>();
42+
agentPolicies.forEach((item) => {
43+
const source = (item._source as any) ?? {};
44+
Object.keys(source.data.outputs).forEach((output) => {
45+
outputTypes.add(source.data.outputs[output].type);
46+
});
47+
});
48+
49+
return {
50+
count: res.hits.total as number,
51+
output_types: Array.from(outputTypes),
52+
};
53+
} catch (error) {
54+
if (error.statusCode === 404) {
55+
appContextService.getLogger().debug('Index .fleet-policies does not exist yet.');
56+
} else {
57+
throw error;
58+
}
59+
return DEFAULT_AGENT_POLICIES_USAGE;
60+
}
61+
};

x-pack/plugins/fleet/server/collectors/fleet_server_collector.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77

88
import type { SavedObjectsClient, ElasticsearchClient } from '@kbn/core/server';
99

10+
import { PACKAGE_POLICY_SAVED_OBJECT_TYPE, SO_SEARCH_LIMIT } from '../constants';
11+
1012
import { packagePolicyService } from '../services';
1113
import { getAgentStatusForAgentPolicy } from '../services/agents';
1214
import { listFleetServerHosts } from '../services/fleet_server_host';
@@ -84,3 +86,47 @@ export const getFleetServerUsage = async (
8486
num_host_urls: numHostsUrls,
8587
};
8688
};
89+
90+
export const getFleetServerConfig = async (soClient: SavedObjectsClient): Promise<any> => {
91+
const res = await packagePolicyService.list(soClient, {
92+
page: 1,
93+
perPage: SO_SEARCH_LIMIT,
94+
kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name:fleet_server`,
95+
});
96+
const getInputConfig = (item: any) => {
97+
const config = (item.inputs[0] ?? {}).compiled_input;
98+
if (config?.server) {
99+
// whitelist only server limits, timeouts and runtime, sometimes fields are coming in "server.limits" format instead of nested object
100+
const newConfig = Object.keys(config)
101+
.filter((key) => key.startsWith('server'))
102+
.reduce((acc: any, curr: string) => {
103+
if (curr === 'server') {
104+
acc.server = {};
105+
Object.keys(config.server)
106+
.filter(
107+
(key) =>
108+
key.startsWith('limits') ||
109+
key.startsWith('timeouts') ||
110+
key.startsWith('runtime')
111+
)
112+
.forEach((serverKey: string) => {
113+
acc.server[serverKey] = config.server[serverKey];
114+
return acc;
115+
});
116+
} else {
117+
acc[curr] = config[curr];
118+
}
119+
return acc;
120+
}, {});
121+
122+
return newConfig;
123+
} else {
124+
return {};
125+
}
126+
};
127+
const policies = res.items.map((item) => ({
128+
input_config: getInputConfig(item),
129+
}));
130+
131+
return { policies };
132+
};

x-pack/plugins/fleet/server/collectors/register.ts

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,14 @@ import type { CoreSetup } from '@kbn/core/server';
1111
import type { FleetConfigType } from '..';
1212

1313
import { getIsAgentsEnabled } from './config_collectors';
14-
import { getAgentUsage } from './agent_collectors';
14+
import { getAgentUsage, getAgentData } from './agent_collectors';
1515
import type { AgentUsage } from './agent_collectors';
1616
import { getInternalClients } from './helpers';
1717
import { getPackageUsage } from './package_collectors';
1818
import type { PackageUsage } from './package_collectors';
19-
import { getFleetServerUsage } from './fleet_server_collector';
19+
import { getFleetServerUsage, getFleetServerConfig } from './fleet_server_collector';
2020
import type { FleetServerUsage } from './fleet_server_collector';
21+
import { getAgentPoliciesUsage } from './agent_policies';
2122

2223
export interface Usage {
2324
agents_enabled: boolean;
@@ -26,11 +27,33 @@ export interface Usage {
2627
fleet_server: FleetServerUsage;
2728
}
2829

29-
export const fetchUsage = async (core: CoreSetup, config: FleetConfigType) => {
30+
export const fetchFleetUsage = async (
31+
core: CoreSetup,
32+
config: FleetConfigType,
33+
abortController: AbortController
34+
) => {
35+
const [soClient, esClient] = await getInternalClients(core);
36+
if (!soClient || !esClient) {
37+
return;
38+
}
39+
const usage = {
40+
agents_enabled: getIsAgentsEnabled(config),
41+
agents: await getAgentUsage(soClient, esClient),
42+
fleet_server: await getFleetServerUsage(soClient, esClient),
43+
packages: await getPackageUsage(soClient),
44+
...(await getAgentData(esClient, abortController)),
45+
fleet_server_config: await getFleetServerConfig(soClient),
46+
agent_policies: await getAgentPoliciesUsage(esClient, abortController),
47+
};
48+
return usage;
49+
};
50+
51+
// used by kibana daily collector
52+
const fetchUsage = async (core: CoreSetup, config: FleetConfigType) => {
3053
const [soClient, esClient] = await getInternalClients(core);
3154
const usage = {
3255
agents_enabled: getIsAgentsEnabled(config),
33-
agents: await getAgentUsage(config, soClient, esClient),
56+
agents: await getAgentUsage(soClient, esClient),
3457
fleet_server: await getFleetServerUsage(soClient, esClient),
3558
packages: await getPackageUsage(soClient),
3659
};
@@ -41,7 +64,7 @@ export const fetchAgentsUsage = async (core: CoreSetup, config: FleetConfigType)
4164
const [soClient, esClient] = await getInternalClients(core);
4265
const usage = {
4366
agents_enabled: getIsAgentsEnabled(config),
44-
agents: await getAgentUsage(config, soClient, esClient),
67+
agents: await getAgentUsage(soClient, esClient),
4568
fleet_server: await getFleetServerUsage(soClient, esClient),
4669
};
4770
return usage;

0 commit comments

Comments
 (0)