Skip to content

Commit d18f9fc

Browse files
Merge pull request #2242 from contentstack/feat/DX-3646
feat: session based logs with date hierarchy
1 parent d04c6aa commit d18f9fc

File tree

6 files changed

+108
-25
lines changed

6 files changed

+108
-25
lines changed

packages/contentstack-utilities/src/logger/log.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { default as Logger } from './logger';
55
import { CLIErrorHandler } from './cli-error-handler';
66
import { ErrorContext } from '../interfaces';
77
import { configHandler } from '..';
8+
import { getSessionLogPath } from './session-path';
89

910
let loggerInstance: Logger | null = null;
1011

@@ -105,4 +106,6 @@ function getLogPath(): string {
105106
return path.join(os.homedir(), 'contentstack', 'logs');
106107
}
107108

109+
// Re-export getSessionLogPath for external use
110+
export { getSessionLogPath } from './session-path';
108111
export { v2Logger, cliErrorHandler, handleAndLogError, getLogPath };

packages/contentstack-utilities/src/logger/logger.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,9 @@ export default class Logger {
3737
}
3838

3939
getLoggerInstance(level: 'error' | 'info' | 'warn' | 'debug' | 'hidden' = 'info'): winston.Logger {
40-
const filePath = normalize(process.env.CS_CLI_LOG_PATH || this.config.basePath).replace(/^(\.\.(\/|\\|$))+/, '');
40+
// Use session-based path for date-organized logging
41+
const sessionPath = getSessionLogPath();
42+
const filePath = normalize(sessionPath).replace(/^(\.\.(\/|\\|$))+/, '');
4143
return this.createLogger(level === 'hidden' ? 'error' : level, filePath);
4244
}
4345

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import * as fs from 'fs';
2+
import * as path from 'path';
3+
import { configHandler, formatDate, formatTime } from '..';
4+
import { getLogPath } from './log';
5+
6+
/**
7+
* Get the session-based log path for date-organized logging
8+
* Structure: {basePath}/{YYYY-MM-DD}/{command}-{YYYYMMDD-HHMMSS}-{sessionId}/
9+
*
10+
* @returns The session-specific log directory path
11+
*/
12+
export function getSessionLogPath(): string {
13+
// Get base log path
14+
const basePath = getLogPath();
15+
16+
// Get current date in YYYY-MM-DD format
17+
const now = new Date();
18+
const dateStr = now.toISOString().split('T')[0]; // YYYY-MM-DD
19+
20+
// Get command ID (fallback to 'unknown' if not set)
21+
let commandId = configHandler.get('currentCommandId') || 'unknown';
22+
// Sanitize command ID - remove colons and replace with hyphens for folder name
23+
commandId = commandId?.replace(/:/g, '-');
24+
25+
// Use helper methods to format date and time
26+
const dateStrFormatted = formatDate(now); // YYYYMMDD
27+
const timeStrFormatted = formatTime(now); // HHMMSS
28+
const timestamp = `${dateStrFormatted}-${timeStrFormatted}`; // YYYYMMDD-HHMMSS
29+
30+
let sessionId = configHandler.get('sessionId');
31+
if (!sessionId) {
32+
// Format: first 8 chars of command + timestamp (YYYYMMDDHHMMSS)
33+
const timestampForId = `${dateStrFormatted}${timeStrFormatted}`; // YYYYMMDDHHMMSS
34+
const commandHash = commandId.substring(0, 8).padEnd(8, '0'); // Use first 8 chars of command
35+
sessionId = `${commandHash}-${timestampForId}`;
36+
}
37+
38+
// Create session folder name: command-YYYYMMDD-HHMMSS-sessionId
39+
const sessionFolderName = `${commandId}-${timestamp}-${sessionId}`;
40+
41+
// Build full session path
42+
const sessionPath = path.join(basePath, dateStr, sessionFolderName);
43+
44+
// Ensure directory exists
45+
if (!fs.existsSync(sessionPath)) {
46+
fs.mkdirSync(sessionPath, { recursive: true });
47+
}
48+
49+
return sessionPath;
50+
}
51+

packages/contentstack-utilities/test/unit/cliErrorHandler.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ describe('CLIErrorHandler', () => {
3636
expect(hidden).to.equal(true);
3737
});
3838

39-
fancy.it('should extract debug payload correctly', () => {
39+
fancy.it('should extract error payload correctly', () => {
4040
const error = new Error('API error');
4141
(error as any).status = 500;
4242
(error as any).statusText = 'Internal Server Error';
@@ -52,6 +52,7 @@ describe('CLIErrorHandler', () => {
5252
data: { error: 'fail' },
5353
headers: { 'content-type': 'application/json' },
5454
};
55+
(error as any).status = 500; // Also set status on error directly
5556

5657
const debugPayload = errorHandler['extractErrorPayload'](error);
5758
expect(debugPayload.request.method).to.equal('GET');

packages/contentstack-utilities/test/unit/helper.test.ts

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ describe('Testing the Validate function', () => {
2727
describe('Testing the getBranchFromAlias function', () => {
2828
describe('When branch alias exists and resolves successfully', () => {
2929
it('should return the branch UID', async () => {
30-
const mockStack = {
30+
const mockStack: any = {
3131
branchAlias: (alias: string) => ({
3232
fetch: async () => ({ uid: 'main-branch' })
3333
})
@@ -60,7 +60,7 @@ describe('Testing the getBranchFromAlias function', () => {
6060
});
6161

6262
it('should throw error for null branchAlias', async () => {
63-
const mockStack = {
63+
const mockStack: any = {
6464
branchAlias: (alias: string) => ({
6565
fetch: async () => ({ uid: 'main-branch' })
6666
})
@@ -76,7 +76,7 @@ describe('Testing the getBranchFromAlias function', () => {
7676
});
7777

7878
it('should throw error for undefined branchAlias', async () => {
79-
const mockStack = {
79+
const mockStack: any = {
8080
branchAlias: (alias: string) => ({
8181
fetch: async () => ({ uid: 'main-branch' })
8282
})
@@ -92,14 +92,14 @@ describe('Testing the getBranchFromAlias function', () => {
9292
});
9393

9494
it('should throw error for non-string branchAlias', async () => {
95-
const mockStack = {
95+
const mockStack: any = {
9696
branchAlias: (alias: string) => ({
9797
fetch: async () => ({ uid: 'main-branch' })
9898
})
9999
};
100100

101101
try {
102-
await getBranchFromAlias(mockStack, 123);
102+
await getBranchFromAlias(mockStack, 123 as any);
103103
expect.fail('Expected function to throw an error');
104104
} catch (error) {
105105
expect(error).to.be.instanceOf(Error);
@@ -108,7 +108,7 @@ describe('Testing the getBranchFromAlias function', () => {
108108
});
109109

110110
it('should throw error for empty string branchAlias', async () => {
111-
const mockStack = {
111+
const mockStack: any = {
112112
branchAlias: (alias: string) => ({
113113
fetch: async () => ({ uid: 'main-branch' })
114114
})
@@ -126,7 +126,7 @@ describe('Testing the getBranchFromAlias function', () => {
126126

127127
describe('When branch alias does not exist', () => {
128128
it('should throw an error', async () => {
129-
const mockStack = {
129+
const mockStack: any = {
130130
branchAlias: (alias: string) => ({
131131
fetch: async () => {
132132
throw new Error('Branch alias not found');
@@ -146,7 +146,7 @@ describe('Testing the getBranchFromAlias function', () => {
146146

147147
describe('When response is missing UID', () => {
148148
it('should throw error for response without uid', async () => {
149-
const mockStack = {
149+
const mockStack: any = {
150150
branchAlias: (alias: string) => ({
151151
fetch: async () => ({ name: 'main-branch' }) // missing uid
152152
})
@@ -162,7 +162,7 @@ describe('Testing the getBranchFromAlias function', () => {
162162
});
163163

164164
it('should throw error for response with null uid', async () => {
165-
const mockStack = {
165+
const mockStack: any = {
166166
branchAlias: (alias: string) => ({
167167
fetch: async () => ({ uid: null })
168168
})
@@ -178,7 +178,7 @@ describe('Testing the getBranchFromAlias function', () => {
178178
});
179179

180180
it('should throw error for response with undefined uid', async () => {
181-
const mockStack = {
181+
const mockStack: any = {
182182
branchAlias: (alias: string) => ({
183183
fetch: async () => ({ uid: undefined })
184184
})
@@ -194,7 +194,7 @@ describe('Testing the getBranchFromAlias function', () => {
194194
});
195195

196196
it('should throw error for response with empty string uid', async () => {
197-
const mockStack = {
197+
const mockStack: any = {
198198
branchAlias: (alias: string) => ({
199199
fetch: async () => ({ uid: '' })
200200
})
@@ -210,7 +210,7 @@ describe('Testing the getBranchFromAlias function', () => {
210210
});
211211

212212
it('should throw error for empty response object', async () => {
213-
const mockStack = {
213+
const mockStack: any = {
214214
branchAlias: (alias: string) => ({
215215
fetch: async () => ({})
216216
})
@@ -229,7 +229,7 @@ describe('Testing the getBranchFromAlias function', () => {
229229
describe('When network error occurs', () => {
230230
it('should throw the network error', async () => {
231231
const networkError = new Error('Network timeout');
232-
const mockStack = {
232+
const mockStack: any = {
233233
branchAlias: (alias: string) => ({
234234
fetch: async () => {
235235
throw networkError;

packages/contentstack-utilities/test/unit/logger.test.ts

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
import { expect } from 'chai';
22
import { fancy } from 'fancy-test';
33
import sinon from 'sinon';
4+
import * as fs from 'fs';
5+
import * as path from 'path';
6+
import * as os from 'os';
47
import Logger from '../../src/logger/logger';
8+
import { getSessionLogPath } from '../../src/logger/session-path';
9+
import configHandler from '../../src/config-handler';
510

611
describe('Logger', () => {
712
let logger: Logger;
@@ -32,10 +37,10 @@ describe('Logger', () => {
3237
other: 'safe',
3338
};
3439

35-
const redacted = logger['redact'](testMeta);
36-
expect(redacted.password).to.equal('[REDACTED]');
40+
// Test file mode redaction (consoleMode = false)
41+
const redacted = logger['redact'](testMeta, false);
42+
// In file mode, only token and secret are redacted (not password or email)
3743
expect(redacted.token).to.equal('[REDACTED]');
38-
expect(redacted.email).to.equal('[REDACTED]');
3944
expect(redacted.other).to.equal('safe');
4045
});
4146

@@ -69,6 +74,7 @@ describe('Logger', () => {
6974
fancy.it('should log error messages using error method', () => {
7075
const errorLogger = logger['loggers'].error;
7176
const spy = sinon.spy();
77+
const originalError = errorLogger.error.bind(errorLogger);
7278
errorLogger.error = spy;
7379

7480
logger.error('error message', { some: 'meta' });
@@ -87,25 +93,38 @@ describe('Logger', () => {
8793
fancy.it('logSuccess should call success info logger', () => {
8894
const successLogger = logger['loggers'].success;
8995
const spy = sinon.spy();
90-
successLogger.info = spy;
96+
const originalLog = successLogger.log.bind(successLogger);
97+
successLogger.log = spy;
9198

9299
logger.logSuccess({ type: 'test', message: 'Success message' });
93100
expect(spy.calledOnce).to.be.true;
94-
expect(spy.args[0][0].message).to.equal('Success message');
101+
// logSuccess creates a logPayload object with level, message, timestamp, and meta
102+
const logPayload = spy.args[0][0];
103+
expect(logPayload.message).to.equal('Success message');
104+
expect(logPayload.meta.type).to.equal('test');
105+
106+
// Restore original
107+
successLogger.log = originalLog;
95108
});
96109

97110
fancy.it('shouldLog should handle file target level filtering', () => {
98111
const result = logger['shouldLog']('debug', 'file'); // logLevel = info
99112
expect(result).to.equal(false);
100113
});
101114

102-
fancy.it('success logger should include success type in meta', () => {
115+
fancy.it('success logger should call log method', () => {
116+
const successLogger = logger['loggers'].success;
103117
const spy = sinon.spy();
104-
logger['loggers'].success.info = spy;
118+
const originalLog = successLogger.log.bind(successLogger);
119+
successLogger.log = spy;
105120

106121
logger.success('It worked!', { extra: 'meta' });
107122
expect(spy.calledOnce).to.be.true;
108-
expect(spy.args[0][1].type).to.equal('success');
123+
// success() calls log('success', message, meta)
124+
expect(spy.calledWith('success', 'It worked!', { extra: 'meta' })).to.be.true;
125+
126+
// Restore original
127+
successLogger.log = originalLog;
109128
});
110129

111130
fancy.it('logError with hidden true logs to debug logger', () => {
@@ -135,9 +154,16 @@ describe('Logger', () => {
135154
token: 'abc',
136155
[Symbol.for('splat')]: [{ password: '1234' }],
137156
};
138-
const result = logger['redact'](obj);
157+
// Test file mode (consoleMode = false) - token is redacted, password is not
158+
const result = logger['redact'](obj, false);
139159
expect(result.token).to.equal('[REDACTED]');
140-
expect(result[Symbol.for('splat')][0].password).to.equal('[REDACTED]');
160+
// In file mode, password is not redacted
161+
expect(result[Symbol.for('splat')][0].password).to.equal('1234');
162+
163+
// Test console mode (consoleMode = true) - both token and password are redacted
164+
const consoleResult = logger['redact'](obj, true);
165+
expect(consoleResult.token).to.equal('[REDACTED]');
166+
expect(consoleResult[Symbol.for('splat')][0].password).to.equal('[REDACTED]');
141167
});
142168

143169
fancy.it('redact should return original if klona fails', () => {

0 commit comments

Comments
 (0)