Skip to content

Commit 78a73e8

Browse files
committed
Merge branch 'main' into metrics-units
2 parents a0734c5 + f733699 commit 78a73e8

File tree

8 files changed

+189
-26
lines changed

8 files changed

+189
-26
lines changed

src/handlers/ConfigurationHandler.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ const log = LoggerFactory.getLogger('ConfigurationHandler');
77

88
export function configurationHandler(components: ServerComponents): (params: DidChangeConfigurationParams) => void {
99
return (_params: DidChangeConfigurationParams): void => {
10+
TelemetryService.instance.get('ConfigurationHandler').count('count', 1);
1011
// Pull configuration from LSP workspace and notify all components via subscriptions (fire-and-forget)
1112
components.settingsManager.syncConfiguration().catch((error) => {
12-
TelemetryService.instance.get('ConfigurationHandler').count('count', 1);
1313
log.error(error, `Failed to sync configuration`);
1414
});
1515
};

src/handlers/ExecutionHandler.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ export function executionHandler(
3737
});
3838
}
3939
case ANALYZE_DIAGNOSTIC: {
40+
TelemetryService.instance.get('CodeAction').count(`accepted.diagnoseWithAI`, 1);
4041
return executeWithError(components, async () => {
4142
const message = await components.cfnAI.analyzeDiagnostic(
4243
params.arguments?.[0] as string,
@@ -66,6 +67,15 @@ export function executionHandler(
6667
components.diagnosticCoordinator
6768
.handleClearCfnDiagnostic(uri, diagnosticId)
6869
.catch((err) => log.error(err, `Error clearing diagnostic`));
70+
TelemetryService.instance.get('CodeAction').count(`accepted.clearDiagnostic`, 1);
71+
}
72+
break;
73+
}
74+
case TRACK_CODE_ACTION_ACCEPTED: {
75+
const args = params.arguments ?? [];
76+
if (args.length > 0) {
77+
const actionType = args[0] as string;
78+
TelemetryService.instance.get('CodeAction').count(`accepted.${actionType}`, 1);
6979
}
7080
break;
7181
}
@@ -91,3 +101,4 @@ export const OPTIMIZE_TEMPLATE = '/command/llm/template/optimize';
91101
export const ANALYZE_DIAGNOSTIC = '/command/llm/diagnostic/analyze';
92102
export const RECOMMEND_RELATED_RESOURCES = '/command/llm/template/recommend-related';
93103
export const CLEAR_DIAGNOSTIC = '/command/template/clear-diagnostic';
104+
export const TRACK_CODE_ACTION_ACCEPTED = '/command/codeAction/track';

src/server/CfnInfraCore.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { LspComponents } from '../protocol/LspComponents';
99
import { DiagnosticCoordinator } from '../services/DiagnosticCoordinator';
1010
import { SettingsManager } from '../settings/SettingsManager';
1111
import { ClientMessage } from '../telemetry/ClientMessage';
12+
import { LoggerFactory } from '../telemetry/LoggerFactory';
1213
import { TelemetryService } from '../telemetry/TelemetryService';
1314
import { Closeable, closeSafely } from '../utils/Closeable';
1415
import { Configurable, Configurables } from '../utils/Configurable';
@@ -71,6 +72,8 @@ export class CfnInfraCore implements Configurables, Closeable {
7172
}
7273

7374
async close() {
74-
return await closeSafely(this.dataStoreFactory, TelemetryService.instance);
75+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
76+
// @ts-ignore
77+
return await closeSafely(this.dataStoreFactory, TelemetryService.instance, LoggerFactory._instance);
7578
}
7679
}

src/services/CodeActionService.ts

Lines changed: 54 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,23 +15,26 @@ import { SyntaxTreeManager } from '../context/syntaxtree/SyntaxTreeManager';
1515
import { NodeSearch } from '../context/syntaxtree/utils/NodeSearch';
1616
import { NodeType } from '../context/syntaxtree/utils/NodeType';
1717
import { DocumentManager } from '../document/DocumentManager';
18-
import { ANALYZE_DIAGNOSTIC } from '../handlers/ExecutionHandler';
18+
import { ANALYZE_DIAGNOSTIC, TRACK_CODE_ACTION_ACCEPTED } from '../handlers/ExecutionHandler';
1919
import { CfnInfraCore } from '../server/CfnInfraCore';
2020
import { CFN_VALIDATION_SOURCE } from '../stacks/actions/ValidationWorkflow';
2121
import { LoggerFactory } from '../telemetry/LoggerFactory';
22-
import { Track } from '../telemetry/TelemetryDecorator';
22+
import { ScopedTelemetry } from '../telemetry/ScopedTelemetry';
23+
import { Telemetry, Track } from '../telemetry/TelemetryDecorator';
2324
import { pointToPosition } from '../utils/TypeConverters';
2425
import { ExtractToParameterProvider } from './extractToParameter/ExtractToParameterProvider';
2526

2627
export interface CodeActionFix {
2728
title: string;
2829
kind: string;
30+
actionType: string;
2931
diagnostic: Diagnostic;
3032
textEdits: TextEdit[];
3133
command?: Command;
3234
}
3335

3436
export class CodeActionService {
37+
@Telemetry() private readonly telemetry!: ScopedTelemetry;
3538
private static readonly REMOVE_ERROR_TITLE = 'Hide validation error';
3639
private readonly log = LoggerFactory.getLogger(CodeActionService);
3740

@@ -43,8 +46,10 @@ export class CodeActionService {
4346
private readonly syntaxTreeManager: SyntaxTreeManager,
4447
private readonly documentManager: DocumentManager,
4548
private readonly contextManager: ContextManager,
46-
private readonly extractToParameterProvider?: ExtractToParameterProvider,
47-
) {}
49+
private readonly extractToParameterProvider: ExtractToParameterProvider,
50+
) {
51+
this.initializeCounters();
52+
}
4853

4954
/**
5055
* Process diagnostics and generate code actions with fixes
@@ -76,6 +81,13 @@ export class CodeActionService {
7681
return codeActions;
7782
}
7883

84+
private initializeCounters(): void {
85+
this.telemetry.count('quickfix.cfnLintFixOffered', 0);
86+
this.telemetry.count('quickfix.clearDiagnosticOffered', 0);
87+
this.telemetry.count('refactor.extractToParameterOffered', 0);
88+
this.telemetry.count('refactor.extractAllToParameterOffered', 0);
89+
}
90+
7991
/**
8092
* Generate fixes for a specific diagnostic
8193
*/
@@ -112,10 +124,12 @@ export class CodeActionService {
112124
* Generate fixes for CFN Validation diagnostics
113125
*/
114126
private generateCfnValidationFixes(diagnostic: Diagnostic, uri: string): CodeActionFix[] {
127+
this.telemetry.count('quickfix.clearDiagnosticOffered', 1);
115128
return [
116129
{
117130
title: CodeActionService.REMOVE_ERROR_TITLE,
118131
kind: 'quickfix',
132+
actionType: 'clearDiagnostic',
119133
diagnostic,
120134
textEdits: [],
121135
command: {
@@ -165,6 +179,8 @@ export class CodeActionService {
165179
}
166180
}
167181

182+
this.telemetry.count('quickfix.cfnLintFixOffered', fixes.length);
183+
168184
return fixes;
169185
}
170186

@@ -182,6 +198,7 @@ export class CodeActionService {
182198
fixes.push({
183199
title: `Remove invalid property '${propertyName}'`,
184200
kind: 'quickfix',
201+
actionType: 'removeProperty',
185202
diagnostic,
186203
textEdits: [
187204
{
@@ -284,6 +301,7 @@ export class CodeActionService {
284301
fixes.push({
285302
title: `Add required property '${propertyName}'`,
286303
kind: 'quickfix',
304+
actionType: 'addRequiredProperty',
287305
diagnostic,
288306
textEdits: [
289307
{
@@ -324,6 +342,12 @@ export class CodeActionService {
324342

325343
if (fix.command) {
326344
codeAction.command = fix.command;
345+
} else {
346+
codeAction.command = {
347+
title: 'Track code action',
348+
command: TRACK_CODE_ACTION_ACCEPTED,
349+
arguments: [fix.actionType],
350+
};
327351
}
328352

329353
return codeAction;
@@ -520,20 +544,27 @@ export class CodeActionService {
520544
const canExtract = this.extractToParameterProvider.canExtract(context);
521545

522546
if (canExtract) {
523-
const extractAction = this.generateExtractToParameterAction(params, context);
547+
const extractAction = this.telemetry.measure('refactor.extractToParameter', () =>
548+
this.generateExtractToParameterAction(params, context),
549+
);
550+
524551
if (extractAction) {
525552
refactorActions.push(extractAction);
553+
this.telemetry.count('refactor.extractToParameterOffered', 1);
526554
}
527555

528-
const hasMultiple = this.extractToParameterProvider.hasMultipleOccurrences(
529-
context,
530-
params.textDocument.uri,
556+
const hasMultiple = this.telemetry.measure('refactor.hasMultipleOccurrences', () =>
557+
this.extractToParameterProvider.hasMultipleOccurrences(context, params.textDocument.uri),
531558
);
532559

533560
if (hasMultiple) {
534-
const extractAllAction = this.generateExtractAllOccurrencesToParameterAction(params, context);
561+
const extractAllAction = this.telemetry.measure('refactor.extractAllToParameter', () =>
562+
this.generateExtractAllOccurrencesToParameterAction(params, context),
563+
);
564+
535565
if (extractAllAction) {
536566
refactorActions.push(extractAllAction);
567+
this.telemetry.count('refactor.extractAllToParameterOffered', 1);
537568
}
538569
}
539570
}
@@ -579,7 +610,13 @@ export class CodeActionService {
579610
command: {
580611
title: 'Position cursor in parameter description',
581612
command: 'aws.cloudformation.extractToParameter.positionCursor',
582-
arguments: [params.textDocument.uri, extractionResult.parameterName, context.documentType],
613+
arguments: [
614+
params.textDocument.uri,
615+
extractionResult.parameterName,
616+
context.documentType,
617+
TRACK_CODE_ACTION_ACCEPTED,
618+
'extractToParameter',
619+
],
583620
},
584621
};
585622
} catch (error) {
@@ -625,7 +662,13 @@ export class CodeActionService {
625662
command: {
626663
title: 'Position cursor in parameter description',
627664
command: 'aws.cloudformation.extractToParameter.positionCursor',
628-
arguments: [params.textDocument.uri, extractionResult.parameterName, context.documentType],
665+
arguments: [
666+
params.textDocument.uri,
667+
extractionResult.parameterName,
668+
context.documentType,
669+
TRACK_CODE_ACTION_ACCEPTED,
670+
'extractAllToParameter',
671+
],
629672
},
630673
};
631674
} catch (error) {

src/telemetry/LoggerFactory.ts

Lines changed: 88 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
/* eslint-disable @typescript-eslint/no-unsafe-function-type */
2+
import { readdir, stat, unlink, readFile, writeFile } from 'fs/promises';
3+
import { join } from 'path';
4+
import { DateTime } from 'luxon';
25
import pino, { LevelWithSilent, Logger } from 'pino';
36
import { AwsMetadata } from '../server/InitParams';
4-
import { ExtensionName } from '../utils/ExtensionConfig';
7+
import { pathToArtifact } from '../utils/ArtifactsDir';
8+
import { Closeable } from '../utils/Closeable';
9+
import { ExtensionId, ExtensionName } from '../utils/ExtensionConfig';
510
import { TelemetrySettings } from './TelemetryConfig';
611

712
export const LogLevel: Record<LevelWithSilent, number> = {
@@ -14,12 +19,16 @@ export const LogLevel: Record<LevelWithSilent, number> = {
1419
trace: 6,
1520
} as const;
1621

17-
export class LoggerFactory {
22+
export class LoggerFactory implements Closeable {
23+
private static readonly LogsDirectory = pathToArtifact('logs');
24+
private static readonly MaxFileSize = 50 * 1024 * 1024; // 50MB
25+
1826
private static readonly _instance: LoggerFactory = new LoggerFactory();
1927

2028
private readonly baseLogger: Logger;
2129
private readonly logLevel: LevelWithSilent;
2230
private readonly loggers = new Map<string, Logger>();
31+
private readonly interval: NodeJS.Timeout;
2332

2433
private constructor(level?: LevelWithSilent) {
2534
this.logLevel = level ?? TelemetrySettings.logLevel;
@@ -28,15 +37,81 @@ export class LoggerFactory {
2837
name: ExtensionName,
2938
level: this.logLevel,
3039
transport: {
31-
target: 'pino-pretty',
32-
options: {
33-
colorize: false,
34-
translateTime: 'SYS:hh:MM:ss TT',
35-
ignore: 'pid,hostname,name,clazz',
36-
messageFormat: '[{clazz}] {msg}',
37-
},
40+
targets: [
41+
{
42+
target: 'pino-pretty',
43+
options: {
44+
colorize: false,
45+
translateTime: 'SYS:hh:MM:ss TT',
46+
ignore: 'pid,hostname,name,clazz',
47+
messageFormat: '[{clazz}] {msg}',
48+
},
49+
},
50+
{
51+
target: 'pino/file',
52+
options: {
53+
destination: join(
54+
LoggerFactory.LogsDirectory,
55+
`${ExtensionId}-${DateTime.utc().toFormat('yyyy-MM-dd')}.log`,
56+
),
57+
mkdir: true,
58+
},
59+
},
60+
],
3861
},
3962
});
63+
64+
void this.cleanOldLogs();
65+
this.interval = setInterval(
66+
() => {
67+
void this.trimLogs();
68+
},
69+
10 * 60 * 1000,
70+
);
71+
}
72+
73+
private async cleanOldLogs() {
74+
try {
75+
const files = await readdir(LoggerFactory.LogsDirectory);
76+
const oneWeekAgo = DateTime.utc().minus({ weeks: 1 });
77+
78+
for (const file of files) {
79+
if (!file.endsWith('.log')) continue;
80+
81+
const filePath = join(LoggerFactory.LogsDirectory, file);
82+
const stats = await stat(filePath);
83+
84+
if (DateTime.fromJSDate(stats.mtime) < oneWeekAgo) {
85+
await unlink(filePath);
86+
}
87+
}
88+
89+
await this.trimLogs();
90+
} catch (err) {
91+
this.baseLogger.error(err, 'Error cleaning up old logs');
92+
}
93+
}
94+
95+
private async trimLogs() {
96+
try {
97+
const files = await readdir(LoggerFactory.LogsDirectory);
98+
99+
for (const file of files) {
100+
if (!file.endsWith('.log')) continue;
101+
102+
const filePath = join(LoggerFactory.LogsDirectory, file);
103+
const stats = await stat(filePath);
104+
105+
if (stats.size > LoggerFactory.MaxFileSize) {
106+
const content = await readFile(filePath, 'utf8');
107+
const lines = content.split('\n');
108+
const trimmed = lines.slice(-Math.floor(lines.length / 2)).join('\n');
109+
await writeFile(filePath, trimmed);
110+
}
111+
}
112+
} catch (err) {
113+
this.baseLogger.error(err, 'Error trimming old logs');
114+
}
40115
}
41116

42117
private reconfigure(newLevel: LevelWithSilent) {
@@ -57,6 +132,10 @@ export class LoggerFactory {
57132
return logger;
58133
}
59134

135+
close() {
136+
clearInterval(this.interval);
137+
}
138+
60139
static getLogger(clazz: string | Function): Logger {
61140
return LoggerFactory._instance.getLogger(clazz);
62141
}

tst/integration/ExtractToParameter.e2e.test.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,10 +110,12 @@ describe('Extract to Parameter - End-to-End CodeAction Workflow Tests', () => {
110110
expect(extractAction?.command).toBeDefined();
111111
expect(extractAction?.command?.command).toBe('aws.cloudformation.extractToParameter.positionCursor');
112112
expect(extractAction?.command?.arguments).toBeDefined();
113-
expect(extractAction?.command?.arguments?.length).toBe(3);
113+
expect(extractAction?.command?.arguments?.length).toBe(5);
114114
expect(extractAction?.command?.arguments?.[0]).toBe(uri);
115115
expect(typeof extractAction?.command?.arguments?.[1]).toBe('string'); // parameter name
116116
expect(extractAction?.command?.arguments?.[2]).toBe('JSON'); // document type
117+
expect(extractAction?.command?.arguments?.[3]).toBe('/command/codeAction/track'); // tracking command
118+
expect(extractAction?.command?.arguments?.[4]).toBe('extractToParameter'); // action type
117119

118120
// Step 10: Sequential extraction test - Apply first extraction and perform second
119121
const firstUpdatedContent = applyWorkspaceEdit(template, changes);
@@ -292,10 +294,12 @@ Resources:
292294
expect(extractAction.command).toBeDefined();
293295
expect(extractAction.command?.command).toBe('aws.cloudformation.extractToParameter.positionCursor');
294296
expect(extractAction.command?.arguments).toBeDefined();
295-
expect(extractAction.command?.arguments?.length).toBe(3);
297+
expect(extractAction.command?.arguments?.length).toBe(5);
296298
expect(extractAction.command?.arguments?.[0]).toBe(uri);
297299
expect(typeof extractAction.command?.arguments?.[1]).toBe('string'); // parameter name
298300
expect(extractAction.command?.arguments?.[2]).toBe('YAML'); // document type
301+
expect(extractAction.command?.arguments?.[3]).toBe('/command/codeAction/track'); // tracking command
302+
expect(extractAction.command?.arguments?.[4]).toBe('extractToParameter'); // action type
299303

300304
// Sequential extraction test: Apply first extraction and perform second
301305
const firstUpdatedContent = applyWorkspaceEdit(template, changes);

0 commit comments

Comments
 (0)