Skip to content

Commit ca6c034

Browse files
authored
Merge pull request #15 from HarperFast/feature/enhanced-logging
Enhanced logging for Grafana observability (Issue #11)
2 parents 09bf2cb + ce3fa36 commit ca6c034

13 files changed

+1292
-39
lines changed

GLOBALS_LOGGING_RESEARCH.md

Lines changed: 528 additions & 0 deletions
Large diffs are not rendered by default.

LOGGING_ANALYSIS_BY_FILE.md

Lines changed: 468 additions & 0 deletions
Large diffs are not rendered by default.

src/config-loader.js

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,36 +21,47 @@ const __dirname = dirname(__filename);
2121
export function loadConfig(configPath = null) {
2222
try {
2323
let config;
24+
let source;
2425

2526
// Handle different input types
2627
if (configPath === null || configPath === undefined) {
2728
// Default to config.yaml in project root
2829
const path = join(__dirname, '..', 'config.yaml');
30+
logger.debug(`[ConfigLoader.loadConfig] Loading config from default path: ${path}`);
2931
const fileContent = readFileSync(path, 'utf8');
3032
config = parse(fileContent);
33+
source = path;
3134
} else if (typeof configPath === 'string') {
3235
// Path to config file
36+
logger.debug(`[ConfigLoader.loadConfig] Loading config from: ${configPath}`);
3337
const fileContent = readFileSync(configPath, 'utf8');
3438
config = parse(fileContent);
39+
source = configPath;
3540
} else if (typeof configPath === 'object') {
3641
// Config object passed directly (for testing)
42+
logger.debug('[ConfigLoader.loadConfig] Using config object passed directly');
3743
// Check if it's an options object with 'config' property
3844
if (configPath.config) {
3945
config = configPath.config;
4046
} else {
4147
config = configPath;
4248
}
49+
source = 'object';
4350
} else {
4451
throw new Error('configPath must be a string, object, or null');
4552
}
4653

4754
if (!config) {
55+
logger.error('[ConfigLoader.loadConfig] Failed to parse configuration');
4856
throw new Error('Failed to parse configuration');
4957
}
5058

59+
logger.info(`[ConfigLoader.loadConfig] Successfully loaded config from: ${source}`);
60+
5161
// Normalize to multi-table format if needed
5262
return normalizeConfig(config);
5363
} catch (error) {
64+
logger.error(`[ConfigLoader.loadConfig] Configuration loading failed: ${error.message}`);
5465
throw new Error(`Failed to load configuration: ${error.message}`);
5566
}
5667
}
@@ -64,17 +75,22 @@ export function loadConfig(configPath = null) {
6475
*/
6576
function normalizeConfig(config) {
6677
if (!config.bigquery) {
78+
logger.error('[ConfigLoader.normalizeConfig] bigquery section missing in configuration');
6779
throw new Error('bigquery section missing in configuration');
6880
}
6981

7082
// Check if already in multi-table format
7183
if (config.bigquery.tables && Array.isArray(config.bigquery.tables)) {
84+
logger.info(
85+
`[ConfigLoader.normalizeConfig] Config already in multi-table format with ${config.bigquery.tables.length} tables`
86+
);
7287
// Validate multi-table configuration
7388
validateMultiTableConfig(config);
7489
return config;
7590
}
7691

7792
// Legacy single-table format - wrap in tables array
93+
logger.info('[ConfigLoader.normalizeConfig] Converting legacy single-table config to multi-table format');
7894
const legacyBigQueryConfig = config.bigquery;
7995

8096
// Extract table-specific config
@@ -92,6 +108,10 @@ function normalizeConfig(config) {
92108
},
93109
};
94110

111+
logger.debug(
112+
`[ConfigLoader.normalizeConfig] Created table config: ${tableConfig.dataset}.${tableConfig.table} -> ${tableConfig.targetTable}`
113+
);
114+
95115
// Create normalized multi-table config
96116
const normalizedConfig = {
97117
operations: config.operations, // Preserve operations config if present
@@ -108,6 +128,7 @@ function normalizeConfig(config) {
108128
},
109129
};
110130

131+
logger.info('[ConfigLoader.normalizeConfig] Successfully normalized config to multi-table format');
111132
return normalizedConfig;
112133
}
113134

@@ -118,11 +139,15 @@ function normalizeConfig(config) {
118139
* @private
119140
*/
120141
function validateMultiTableConfig(config) {
142+
logger.debug('[ConfigLoader.validateMultiTableConfig] Validating multi-table configuration');
143+
121144
if (!config.bigquery.tables || !Array.isArray(config.bigquery.tables)) {
145+
logger.error('[ConfigLoader.validateMultiTableConfig] bigquery.tables must be an array');
122146
throw new Error('bigquery.tables must be an array');
123147
}
124148

125149
if (config.bigquery.tables.length === 0) {
150+
logger.error('[ConfigLoader.validateMultiTableConfig] bigquery.tables array cannot be empty');
126151
throw new Error('bigquery.tables array cannot be empty');
127152
}
128153

@@ -132,29 +157,38 @@ function validateMultiTableConfig(config) {
132157
for (const table of config.bigquery.tables) {
133158
// Check required fields
134159
if (!table.id) {
160+
logger.error('[ConfigLoader.validateMultiTableConfig] Missing required field: table.id');
135161
throw new Error('Missing required field: table.id');
136162
}
137163
if (!table.dataset) {
164+
logger.error(`[ConfigLoader.validateMultiTableConfig] Missing 'dataset' for table: ${table.id}`);
138165
throw new Error(`Missing required field 'dataset' for table: ${table.id}`);
139166
}
140167
if (!table.table) {
168+
logger.error(`[ConfigLoader.validateMultiTableConfig] Missing 'table' for table: ${table.id}`);
141169
throw new Error(`Missing required field 'table' for table: ${table.id}`);
142170
}
143171
if (!table.timestampColumn) {
172+
logger.error(`[ConfigLoader.validateMultiTableConfig] Missing 'timestampColumn' for table: ${table.id}`);
144173
throw new Error(`Missing required field 'timestampColumn' for table: ${table.id}`);
145174
}
146175
if (!table.targetTable) {
176+
logger.error(`[ConfigLoader.validateMultiTableConfig] Missing 'targetTable' for table: ${table.id}`);
147177
throw new Error(`Missing required field 'targetTable' for table: ${table.id}`);
148178
}
149179

150180
// Check for duplicate IDs
151181
if (tableIds.has(table.id)) {
182+
logger.error(`[ConfigLoader.validateMultiTableConfig] Duplicate table ID: ${table.id}`);
152183
throw new Error(`Duplicate table ID: ${table.id}`);
153184
}
154185
tableIds.add(table.id);
155186

156187
// Check for duplicate target Harper tables
157188
if (targetTables.has(table.targetTable)) {
189+
logger.error(
190+
`[ConfigLoader.validateMultiTableConfig] Duplicate targetTable '${table.targetTable}' for: ${table.id}`
191+
);
158192
throw new Error(
159193
`Duplicate targetTable '${table.targetTable}' for table: ${table.id}. ` +
160194
`Each BigQuery table must sync to a DIFFERENT Harper table. ` +
@@ -164,7 +198,15 @@ function validateMultiTableConfig(config) {
164198
);
165199
}
166200
targetTables.add(table.targetTable);
201+
202+
logger.debug(
203+
`[ConfigLoader.validateMultiTableConfig] Validated table: ${table.id} (${table.dataset}.${table.table} -> ${table.targetTable})`
204+
);
167205
}
206+
207+
logger.info(
208+
`[ConfigLoader.validateMultiTableConfig] Successfully validated ${config.bigquery.tables.length} table configurations`
209+
);
168210
}
169211

170212
/**

src/globals.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,17 @@ class Globals {
77
Globals.instance = this;
88
}
99
set(key, value) {
10+
logger.debug(`[Globals.set] Setting '${key}' = ${JSON.stringify(value)}`);
1011
this.data[key] = value;
1112
}
1213
get(key) {
13-
return this.data[key];
14+
const value = this.data[key];
15+
if (value === undefined) {
16+
logger.debug(`[Globals.get] Key '${key}' not found`);
17+
} else {
18+
logger.debug(`[Globals.get] Retrieved '${key}' = ${JSON.stringify(value)}`);
19+
}
20+
return value;
1421
}
1522
}
1623

src/query-builder.js

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,20 +13,25 @@
1313
*/
1414
export function formatColumnList(columns) {
1515
if (!Array.isArray(columns)) {
16+
logger.error('[formatColumnList] Invalid input: columns must be an array');
1617
throw new Error('columns must be an array');
1718
}
1819

1920
if (columns.length === 0) {
21+
logger.error('[formatColumnList] Invalid input: columns array cannot be empty');
2022
throw new Error('columns array cannot be empty');
2123
}
2224

2325
// Special case: ['*'] means SELECT *
2426
if (columns.length === 1 && columns[0] === '*') {
27+
logger.debug('[formatColumnList] Using wildcard SELECT *');
2528
return '*';
2629
}
2730

2831
// Format as comma-separated list with proper spacing
29-
return columns.join(', ');
32+
const formatted = columns.join(', ');
33+
logger.debug(`[formatColumnList] Formatted ${columns.length} columns: ${formatted}`);
34+
return formatted;
3035
}
3136

3237
/**
@@ -41,16 +46,24 @@ export function formatColumnList(columns) {
4146
*/
4247
export function buildPullPartitionQuery({ dataset, table, timestampColumn, columns }) {
4348
if (!dataset || !table || !timestampColumn) {
49+
logger.error(
50+
'[buildPullPartitionQuery] Missing required parameters: dataset, table, and timestampColumn are required'
51+
);
4452
throw new Error('dataset, table, and timestampColumn are required');
4553
}
4654

4755
if (!columns || !Array.isArray(columns)) {
56+
logger.error('[buildPullPartitionQuery] Invalid columns parameter: must be a non-empty array');
4857
throw new Error('columns must be a non-empty array');
4958
}
5059

60+
logger.info(
61+
`[buildPullPartitionQuery] Building pull query for ${dataset}.${table} with ${columns.length === 1 && columns[0] === '*' ? 'all columns' : `${columns.length} columns`}`
62+
);
63+
5164
const columnList = formatColumnList(columns);
5265

53-
return `
66+
const query = `
5467
SELECT ${columnList}
5568
FROM \`${dataset}.${table}\`
5669
WHERE
@@ -64,6 +77,9 @@ export function buildPullPartitionQuery({ dataset, table, timestampColumn, colum
6477
ORDER BY ${timestampColumn} ASC
6578
LIMIT CAST(@batchSize AS INT64)
6679
`;
80+
81+
logger.debug('[buildPullPartitionQuery] Query construction complete');
82+
return query;
6783
}
6884

6985
/**
@@ -126,13 +142,18 @@ export class QueryBuilder {
126142
*/
127143
constructor({ dataset, table, timestampColumn, columns = ['*'] }) {
128144
if (!dataset || !table || !timestampColumn) {
145+
logger.error('[QueryBuilder] Missing required parameters: dataset, table, and timestampColumn are required');
129146
throw new Error('dataset, table, and timestampColumn are required');
130147
}
131148

132149
this.dataset = dataset;
133150
this.table = table;
134151
this.timestampColumn = timestampColumn;
135152
this.columns = columns;
153+
154+
logger.info(
155+
`[QueryBuilder] Initialized for ${dataset}.${table} with timestamp column '${timestampColumn}' and ${columns.length === 1 && columns[0] === '*' ? 'all columns' : `${columns.length} columns`}`
156+
);
136157
}
137158

138159
/**

0 commit comments

Comments
 (0)