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

Fix agent config indicator when applied through fleet integration #131820

Merged
merged 29 commits into from
May 23, 2022
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
c92ba74
Fix agent config indicator when applied through fleet integration
gbamparop May 9, 2022
bab0ced
Add synthrace scenario
gbamparop May 11, 2022
7329fe0
Update docs in apmAgentConfigurationIndex with applied_by_agent
gbamparop May 11, 2022
d50d06c
Add API tests
gbamparop May 11, 2022
abae232
Move tests to agent_configuration subfolder
gbamparop May 11, 2022
29efe60
Index agent_config metrics to metrics-apm.internal-default
gbamparop May 11, 2022
44abd38
Revert Update docs in apmAgentConfigurationIndex with applied_by_agent
gbamparop May 12, 2022
e62bbc2
Apply logic to both listConfigurations and findExactConfiguration
gbamparop May 12, 2022
74bd08c
Revert omitTimestampAndId
gbamparop May 12, 2022
8b76c61
Merge branch 'main' into update-agent-config-indicator
kibanamachine May 12, 2022
57a3627
Mark etag as required in AgentConfiguration type
gbamparop May 16, 2022
560c644
Merge branch 'update-agent-config-indicator' of https://github.com/gb…
gbamparop May 16, 2022
0aa86ff
Pick fields for AgentConfigFields from ApmFields
gbamparop May 16, 2022
1cea1ea
Make requests for agent configs and agent_config metrics in parallel
gbamparop May 16, 2022
4f5b9de
Use reduce instead of mapValues / keyBy to construct the map
gbamparop May 17, 2022
e547c6f
Set size for the aggregation
gbamparop May 17, 2022
4757af5
Conver convertConfigSettingsToString to be immutable
gbamparop May 17, 2022
29205d6
Change AgentConfigMetrics to extend Metricset
gbamparop May 17, 2022
b1f8d4e
Merge branch 'main' into update-agent-config-indicator
kibanamachine May 17, 2022
5737fc0
Change timerange to the last minute
gbamparop May 17, 2022
e19ec2a
Clean synthrace data
gbamparop May 17, 2022
06171e3
Merge branch 'main' into update-agent-config-indicator
kibanamachine May 18, 2022
d9b00bf
Merge branch 'main' of https://github.com/elastic/kibana into update-…
gbamparop May 19, 2022
758d67a
Skip agent config metrics tests
gbamparop May 19, 2022
e918b75
clean synthrace data
gbamparop May 20, 2022
fc5fa66
Create a registry for agent config tests
gbamparop May 20, 2022
9445e65
Change timerange back to 15m
gbamparop May 20, 2022
98f4ee4
Add observer entity to synthrace
gbamparop May 20, 2022
f2cebe3
Merge branch 'main' into update-agent-config-indicator
kibanamachine May 23, 2022
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
1 change: 1 addition & 0 deletions packages/elastic-apm-synthtrace/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
export { timerange } from './lib/timerange';
export { apm } from './lib/apm';
export { stackMonitoring } from './lib/stack_monitoring';
export { agentConfig } from './lib/agent_config';
export { cleanWriteTargets } from './lib/utils/clean_write_targets';
export { createLogger, LogLevel } from './lib/utils/create_logger';

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import { Entity } from '../entity';
import { AgentConfigFields } from './agent_config_fields';
import { AgentConfigMetrics } from './agent_config_metrics';

export class AgentConfig extends Entity<AgentConfigFields> {
Copy link
Member

Choose a reason for hiding this comment

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

This is an interesting one, it doesn't really match with how we use entities, but I can see how it's the easiest option. Thinking out loud: these are internal APM Server metrics, so not from self-instrumentation. Maybe something like Observer?

observer()
	.agentConfig()
	.serviceName('foo')
	.environment('bar')

and then automatically set etag when serializing?

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 feedback, could you please explain a bit further / elaborate on how we intend to use entities in synthrace?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Also it seems that etag is calculated in kibana from AgentConfigurationIntake which included the settings.

Copy link
Member

Choose a reason for hiding this comment

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

@gbamparop entities are things that collect and send data. not data by itself. does that make sense? and can you clarify what you mean with:

Also it seems that etag is calculated in kibana from AgentConfigurationIntake which included the settings.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@gbamparop entities are things that collect and send data. not data by itself. does that make sense? and can you clarify what you mean with:

@dgieselaar etag is a hash based on all the config settings, that's why I was thinking to pass the etag when creating an agent config with synthrace. Do you think it'll be better to pass service name / environment and just create a hash based on this?

metrics() {
return new AgentConfigMetrics({
...this.fields,
'processor.event': 'metric',
'processor.name': 'metric',
'metricset.name': 'agent_config',
agent_config_applied: 1,
});
}
}

export function agentConfig(etag: string) {
return new AgentConfig({
'labels.etag': etag,
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import { Fields } from '../entity';

export interface Observer {
hostname: string;
id: string;
ephemeral_id: string;
type: string;
version: string;
}

export type AgentConfigFields = Fields &
Copy link
Member

Choose a reason for hiding this comment

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

can you pick these from ApmFields for consistency where possible?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done in 0aa86ff

Partial<{
'processor.event': string;
'processor.name': string;
'labels.etag': string;
'metricset.name': string;
observer: Observer;
agent_config_applied: number;
'ecs.version': string;
'event.agent_id_status': string;
'event.ingested': string;
}>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import { Serializable } from '../serializable';
import { AgentConfigFields } from './agent_config_fields';

export class AgentConfigMetrics extends Serializable<AgentConfigFields> {
Copy link
Member

Choose a reason for hiding this comment

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

Does it make sense to extend this from Metricset?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated in 29205d6

timestamp(timestamp: number): this {
super.timestamp(timestamp);
this.fields['event.ingested'] = new Date(timestamp).toISOString();
Copy link
Member

Choose a reason for hiding this comment

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

This field is set by an ingest pipeline. I wonder if there's a more appropriate place to set this.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I can just remove this (although it's defined it doesn't seem to be set for ApmFields either). What do you think?

return this;
}
}
9 changes: 9 additions & 0 deletions packages/elastic-apm-synthtrace/src/lib/agent_config/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

export { agentConfig } from './agent_config';
4 changes: 3 additions & 1 deletion packages/elastic-apm-synthtrace/src/lib/stream_processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,9 @@ export class StreamProcessor<TFields extends Fields = ApmFields> {
const eventType = d.processor.event as keyof ApmElasticsearchOutputWriteTargets;
let dataStream = writeTargets[eventType];
if (eventType === 'metric') {
if (!d.service?.name) {
if (d.metricset?.name === 'agent_config') {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Is there another place preferred to set the data stream for these metrics? cc @Mpdreamz

dataStream = 'metrics-apm.internal-default';
} else if (!d.service?.name) {
dataStream = 'metrics-apm.app-default';
} else {
if (!d.transaction && !d.span) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import { agentConfig, timerange } from '../..';
import { Scenario } from '../scenario';
import { getLogger } from '../utils/get_common_services';
import { RunOptions } from '../utils/parse_run_cli_flags';
import { AgentConfigFields } from '../../lib/agent_config/agent_config_fields';

const scenario: Scenario<AgentConfigFields> = async (runOptions: RunOptions) => {
const logger = getLogger(runOptions);

return {
generate: ({ from, to }) => {
const agentConfigMetrics = agentConfig('test-etag').metrics();

const range = timerange(from, to);
return range
.interval('30s')
.rate(1)
.generator((timestamp) => {
const events = logger.perf('generating_agent_config_events', () => {
return agentConfigMetrics.timestamp(timestamp);
});
return events;
});
},
};
};

export default scenario;
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export type AgentConfigurationIntake = t.TypeOf<
>;

export type AgentConfiguration = {
id: string;
'@timestamp': number;
applied_by_agent?: boolean;
etag?: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,5 +106,14 @@ export async function createInternalESClient({
params,
});
},
bulk: (operationName: string, params: estypes.BulkRequest) => {
return callEs(operationName, {
requestType: 'bulk',
cb: (signal) => {
return asInternalUser.bulk(params, { signal, meta: true });
},
params,
});
},
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export function createOrUpdateConfiguration({
}) {
const { internalClient, indices } = setup;

const params: APMIndexDocumentParams<AgentConfiguration> = {
const params: APMIndexDocumentParams<Omit<AgentConfiguration, 'id'>> = {
refresh: true,
index: indices.apmAgentConfigurationIndex,
body: {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* 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 { termQuery, rangeQuery } from '@kbn/observability-plugin/server';
import { keyBy, mapValues } from 'lodash';
import datemath from '@kbn/datemath';
import { METRICSET_NAME } from '../../../../common/elasticsearch_fieldnames';
import { Setup } from '../../../lib/helpers/setup_request';

export async function getConfigsAppliedToAgentsThroughFleet({
setup,
}: {
setup: Setup;
}) {
const { internalClient, indices } = setup;

const params = {
index: indices.metric,
size: 200,
Copy link
Member

Choose a reason for hiding this comment

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

why 200? should it be configurable?

Copy link
Member

Choose a reason for hiding this comment

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

actually looks like size should be 0?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That was meant for the aggregation. What do you think the size should be? Config list returns 200.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated in e547c6f

body: {
query: {
bool: {
filter: [
...termQuery(METRICSET_NAME, 'agent_config'),
Copy link
Member

Choose a reason for hiding this comment

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

I'm curious if this is the best way. Given it's a single etag value that we're interested in, would it make sense to pass the etag explicitly, and set terminate_after:1 here, rather than trying to "join" this data on the Kibana server?

Copy link
Member

Choose a reason for hiding this comment

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

Ah we also use it for a list of agent configurations. That means we cannot set terminate_after: 1. In that case, I suggest we parallelise the requests in both scenarios, I think that should work.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Makes sense, will do them in parallel

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done in 1cea1ea

...rangeQuery(
datemath.parse('now-15m')!.valueOf(),
datemath.parse('now')!.valueOf()
),
],
},
},
aggs: {
config_by_etag: {
terms: {
field: 'labels.etag',
Copy link
Member

Choose a reason for hiding this comment

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

and should size: 200 be set here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated in e547c6f

},
},
},
},
};

const response = await internalClient.search(
'get_config_applied_to_agent_through_fleet',
params
);

return mapValues(
keyBy(response.aggregations?.config_by_etag.buckets, 'key'),
'key'
) as Record<string, string>;
Copy link
Member

Choose a reason for hiding this comment

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

Maybe a simple forEach + mutation of a Record<string, true> object is more readable here? It took me some time to process what this is actually doing

Copy link
Contributor Author

Choose a reason for hiding this comment

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

.reduce could be more readable from what we currently have, or you prefer forEach?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated in 4f5b9de

}
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,5 @@ export async function listConfigurations({ setup }: { setup: Setup }) {

return resp.hits.hits
.map(convertConfigSettingsToString)
.map((hit) => hit._source);
.map((hit) => ({ ...hit._source, id: hit._id }));
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* 2.0.
*/

import { isEmpty } from 'lodash';
import { Setup } from '../../../lib/helpers/setup_request';
import { AgentConfiguration } from '../../../../common/agent_configuration/configuration_types';

Expand Down Expand Up @@ -34,3 +35,39 @@ export async function markAppliedByAgent({
params
);
}

export async function markAppliedByAgentThroughFleet({
configsAppliedToAgentsThroughFleet,
configurations,
setup,
}: {
configsAppliedToAgentsThroughFleet: Record<string, string>;
configurations: AgentConfiguration[];
setup: Setup;
}) {
const { internalClient, indices } = setup;

// Update only the configs that still have applied_by_agent=true
// but have been applied by an agent
const configsToUpdate = configurations.filter(
(config) =>
!config.applied_by_agent &&
config.etag !== undefined &&
configsAppliedToAgentsThroughFleet.hasOwnProperty(config.etag)
);

if (isEmpty(configsToUpdate)) {
return;
}

const body = configsToUpdate.flatMap((doc) => [
{ update: { _id: doc.id } },
{ doc: { applied_by_agent: true } },
]);

return internalClient.bulk('mark_config_applied_by_agent_through_fleet', {
index: indices.apmAgentConfigurationIndex,
body,
refresh: 'wait_for',
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,17 @@ import { getEnvironments } from './get_environments';
import { deleteConfiguration } from './delete_configuration';
import { createApmServerRoute } from '../../apm_routes/create_apm_server_route';
import { getAgentNameByService } from './get_agent_name_by_service';
import { markAppliedByAgent } from './mark_applied_by_agent';
import {
markAppliedByAgent,
markAppliedByAgentThroughFleet,
} from './mark_applied_by_agent';
import {
serviceRt,
agentConfigurationIntakeRt,
} from '../../../../common/agent_configuration/runtime_types/agent_configuration_intake_rt';
import { getSearchAggregatedTransactions } from '../../../lib/helpers/transactions';
import { syncAgentConfigsToApmPackagePolicies } from '../../fleet/sync_agent_configs_to_apm_package_policies';
import { getConfigsAppliedToAgentsThroughFleet } from './get_config_applied_to_agent_through_fleet';

// get list of configurations
const agentConfigurationRoute = createApmServerRoute({
Expand All @@ -38,8 +42,36 @@ const agentConfigurationRoute = createApmServerRoute({
>;
}> => {
const setup = await setupRequest(resources);
const configurations = await listConfigurations({ setup });
return { configurations };

const [configsAppliedToAgentsThroughFleet, configurations] =
await Promise.all([
getConfigsAppliedToAgentsThroughFleet({ setup }),
listConfigurations({ setup }),
]);

const updatedConfigs = configurations.map(
(
agentConfig
): import('./../../../../common/agent_configuration/configuration_types').AgentConfiguration => {
return {
...agentConfig,
applied_by_agent:
agentConfig.applied_by_agent ||
(agentConfig.etag !== undefined &&
configsAppliedToAgentsThroughFleet.hasOwnProperty(
agentConfig.etag
)),
};
}
);

markAppliedByAgentThroughFleet({
configsAppliedToAgentsThroughFleet,
configurations,
setup,
});

return { configurations: updatedConfigs };
},
});

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* 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 { timerange, agentConfig } from '@elastic/apm-synthtrace';
import type { ApmSynthtraceEsClient } from '@elastic/apm-synthtrace';

export async function addAgentConfigMetrics({
synthtraceEsClient,
start,
end,
etag,
}: {
synthtraceEsClient: ApmSynthtraceEsClient;
start: number;
end: number;
etag?: string;
}) {
const agentConfigMetrics = agentConfig(etag ?? 'test-etag').metrics();

const agentConfigEvents = [
timerange(start, end)
.interval('1m')
.rate(1)
.generator((timestamp) => agentConfigMetrics.timestamp(timestamp)),
];

await synthtraceEsClient.index(agentConfigEvents);
}
Loading