Skip to content

Commit b11ceab

Browse files
authored
Add configurable logging for spice.js (#294)
* Update EditorConfig for trailing commas, and add matching Prettier config * Add logging option to enable or disable console output - Introduce `logging` config option to SpiceClient - Add Logger utility to control logging output - Update code to use Logger instead of direct console calls - Update tests to verify logging behavior * Fix branch slug formatting for Vercel preview URLs
1 parent 1e48acd commit b11ceab

9 files changed

Lines changed: 174 additions & 34 deletions

File tree

.editorconfig

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
[*]
2-
quote_type = single
2+
quote_type = single
3+
trailing_comma = all

.github/actions/setup-vercel-endpoint/action.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,8 @@ runs:
4444
echo "Detected tag/release: ${BRANCH_NAME}, using production endpoint"
4545
else
4646
# For branches, construct the git branch preview URL
47-
# Remove periods, replace / with - and convert to lowercase for Vercel format
48-
BRANCH_SLUG=$(echo "$BRANCH_NAME" | sed 's/\.//g' | sed 's/\//-/g' | tr '[:upper:]' '[:lower:]')
47+
# Remove periods, replace first / with -, remove remaining /, convert to lowercase for Vercel format
48+
BRANCH_SLUG=$(echo "$BRANCH_NAME" | sed 's/\.//g' | sed 's/\//-/' | sed 's/\///g' | tr '[:upper:]' '[:lower:]')
4949
VERCEL_ENDPOINT="https://spice-js-git-${BRANCH_SLUG}-spice.vercel.app"
5050
echo "Detected branch: ${BRANCH_NAME}, using preview endpoint"
5151
fi

.prettierrc.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"singleQuote": true,
3+
"trailingComma": "all",
4+
"printWidth": 80,
5+
"tabWidth": 2,
6+
"semi": true,
7+
"arrowParens": "always"
8+
}

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ Querying data is done through a `SpiceClient` object that initializes the connec
3333
- `apiKey` (string, optional): API key to authenticate with the endpoint.
3434
- `flightUrl` (string, optional): URL of the Flight endpoint to use (default: `localhost:50051`)
3535
- `httpUrl` (string, optional): URL of the HTTP endpoint to use (default: `http://localhost:8090`)
36+
- `logging` (boolean, optional): Enable or disable logging output (default: `true`). Set to `false` to silence all library console output.
3637

3738
Read more about the Spice.ai Apache Arrow Flight API at [docs.spice.ai](https://docs.spice.ai/api/sql-query-api/apache-arrow-flight-api).
3839

src/client-common.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
normalizeSchema,
2323
serializeArrowField,
2424
} from './arrow-utils';
25+
import { Logger } from './logger';
2526

2627
// Retry will be imported by the platform-specific entry point
2728
export interface RetryModule {
@@ -426,6 +427,7 @@ export class SpiceClient {
426427
private _retry: RetryModule;
427428
private _isSpiceCloud: boolean = false;
428429
private _flightOnly: boolean = false;
430+
private _logger: Logger;
429431

430432
public constructor(
431433
params: string | SpiceClientConfig = {},
@@ -444,6 +446,7 @@ export class SpiceClient {
444446
this._flightUrl = 'flight.spiceai.io:443';
445447
this._userAgent = platform.getUserAgent();
446448
this._flightOnly = false;
449+
this._logger = new Logger(true); // Default: logging enabled
447450
} else {
448451
const {
449452
apiKey,
@@ -453,8 +456,12 @@ export class SpiceClient {
453456
userAgent,
454457
customHeaders,
455458
flightOnly,
459+
logging,
456460
} = params;
457461

462+
// Initialize logger (default: enabled)
463+
this._logger = new Logger(logging !== false);
464+
458465
this._apiKey = apiKey;
459466
this._httpUrl = httpUrl || 'http://127.0.0.1:8090';
460467
this._flightUrl = flightUrl || '127.0.0.1:50051';
@@ -493,6 +500,7 @@ export class SpiceClient {
493500
this._flightUrl,
494501
this._userAgent,
495502
this._flightTlsEnabled,
503+
this._logger,
496504
);
497505
}
498506

@@ -549,7 +557,7 @@ export class SpiceClient {
549557
);
550558
}
551559

552-
console.debug(configLines.join('\n'));
560+
this._logger.debug(configLines.join('\n'));
553561
}
554562

555563
private async doQueryRequest(
@@ -719,7 +727,9 @@ export class SpiceClient {
719727
}
720728
}
721729
} catch (parseError) {
722-
console.warn(`[spice.js] Failed to parse JSON line: ${parseError}`);
730+
this._logger.warn(
731+
`[spice.js] Failed to parse JSON line: ${parseError}`,
732+
);
723733
}
724734
}
725735

src/grpc/client.node.ts

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import * as protobuf from 'protobufjs';
1010
import { EventEmitter } from 'stream';
1111
import { FlightClient, FlightInfo, DescriptorType, Ticket } from '../flight';
1212
import { platform } from '../platform/node';
13+
import { Logger } from '../logger';
1314

1415
const PROTO_PATH = './proto/Flight.proto';
1516
const PROTO_DOWNLOAD_URL =
@@ -33,9 +34,9 @@ let grpcAvailable = false;
3334
/**
3435
* Downloads the Flight.proto file from the remote URL and keeps it in memory
3536
*/
36-
async function downloadProtoFile(): Promise<string> {
37+
async function downloadProtoFile(logger?: Logger): Promise<string> {
3738
try {
38-
console.log('[spice.js] Downloading Flight.proto from remote source...');
39+
logger?.info('[spice.js] Downloading Flight.proto from remote source...');
3940
const response = await platform.fetch(PROTO_DOWNLOAD_URL, {
4041
method: 'GET',
4142
headers: {},
@@ -48,11 +49,11 @@ async function downloadProtoFile(): Promise<string> {
4849
}
4950

5051
const content = await response.text();
51-
console.log('[spice.js] Flight.proto downloaded successfully');
52+
logger?.info('[spice.js] Flight.proto downloaded successfully');
5253

5354
return content;
5455
} catch (error: any) {
55-
console.warn(`[spice.js] Failed to download proto file: ${error.message}`);
56+
logger?.warn(`[spice.js] Failed to download proto file: ${error.message}`);
5657
throw error;
5758
}
5859
}
@@ -61,15 +62,15 @@ async function downloadProtoFile(): Promise<string> {
6162
* Loads proto content from local file or downloads it
6263
* Returns the proto content as a string
6364
*/
64-
async function loadProtoContent(): Promise<string | null> {
65+
async function loadProtoContent(logger?: Logger): Promise<string | null> {
6566
// Try local file first
6667
if (fs.existsSync(fullProtoPath)) {
6768
return fs.readFileSync(fullProtoPath, 'utf-8');
6869
}
6970

7071
// Try to download
7172
try {
72-
return await downloadProtoFile();
73+
return await downloadProtoFile(logger);
7374
} catch (error) {
7475
return null;
7576
}
@@ -78,7 +79,7 @@ async function loadProtoContent(): Promise<string | null> {
7879
/**
7980
* Loads proto definition from content in memory using protobufjs
8081
*/
81-
function loadProtoFromContent(content: string): any {
82+
function loadProtoFromContent(content: string, logger?: Logger): any {
8283
try {
8384
// Parse the proto content directly in memory using protobufjs
8485
const root = protobuf.parse(content, { keepCase: false }).root;
@@ -98,7 +99,10 @@ function loadProtoFromContent(content: string): any {
9899
const arrow = grpc.loadPackageDefinition(packageDefinition).arrow as any;
99100
return arrow.flight.protocol;
100101
} catch (error: any) {
101-
console.log('[spice.js] Failed to load proto from content:', error.message);
102+
logger?.info(
103+
'[spice.js] Failed to load proto from content:',
104+
error.message,
105+
);
102106
throw error;
103107
}
104108
}
@@ -131,17 +135,20 @@ export class GrpcFlightClient {
131135
private flightTlsEnabled: boolean;
132136
private initPromise: Promise<void>;
133137
private useGrpc: boolean = grpcAvailable;
138+
private logger: Logger;
134139

135140
constructor(
136141
apiKey: string | undefined,
137142
flightUrl: string,
138143
userAgent: string,
139144
flightTlsEnabled: boolean,
145+
logger?: Logger,
140146
) {
141147
this.apiKey = apiKey;
142148
this.flightUrl = flightUrl;
143149
this.userAgent = userAgent;
144150
this.flightTlsEnabled = flightTlsEnabled;
151+
this.logger = logger || new Logger(true);
145152
this.initPromise = this.initialize();
146153
}
147154

@@ -157,7 +164,7 @@ export class GrpcFlightClient {
157164
try {
158165
// Check if we already have proto content in memory
159166
if (!protoContent) {
160-
protoContent = await loadProtoContent();
167+
protoContent = await loadProtoContent(this.logger);
161168
}
162169

163170
if (!protoContent) {
@@ -166,7 +173,7 @@ export class GrpcFlightClient {
166173
}
167174

168175
// Load the proto from content
169-
const proto = loadProtoFromContent(protoContent);
176+
const proto = loadProtoFromContent(protoContent, this.logger);
170177

171178
if (!proto?.FlightService) {
172179
throw new Error('Invalid proto file structure');
@@ -176,7 +183,7 @@ export class GrpcFlightClient {
176183
grpcAvailable = true;
177184
this.useGrpc = true;
178185
} catch (error: any) {
179-
console.warn(
186+
this.logger.warn(
180187
`[spice.js] gRPC initialization failed: ${error.message}. Using HTTP endpoint.`,
181188
);
182189
this.useGrpc = false;

src/interfaces.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@ export interface SpiceClientConfig {
1111
* @default false
1212
*/
1313
flightOnly?: boolean;
14+
/**
15+
* Enable or disable logging output from the library.
16+
* When false, all console output is suppressed.
17+
* @default true
18+
*/
19+
logging?: boolean;
1420
}
1521

1622
export interface SchemaField {

src/logger.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/**
2+
* Simple logger with enable/disable support for spice.js
3+
*
4+
* When disabled, all logging methods are no-ops.
5+
* When enabled, logs are forwarded to the appropriate console methods.
6+
*/
7+
export class Logger {
8+
private readonly enabled: boolean;
9+
10+
constructor(enabled: boolean = true) {
11+
this.enabled = enabled;
12+
}
13+
14+
/**
15+
* Log debug messages (uses console.debug)
16+
*/
17+
debug(...args: unknown[]): void {
18+
if (this.enabled) {
19+
console.debug(...args);
20+
}
21+
}
22+
23+
/**
24+
* Log informational messages (uses console.log)
25+
*/
26+
info(...args: unknown[]): void {
27+
if (this.enabled) {
28+
console.log(...args);
29+
}
30+
}
31+
32+
/**
33+
* Log warning messages (uses console.warn)
34+
*/
35+
warn(...args: unknown[]): void {
36+
if (this.enabled) {
37+
console.warn(...args);
38+
}
39+
}
40+
41+
/**
42+
* Log error messages (uses console.error)
43+
*/
44+
error(...args: unknown[]): void {
45+
if (this.enabled) {
46+
console.error(...args);
47+
}
48+
}
49+
50+
/**
51+
* Log messages (alias for info, uses console.log)
52+
*/
53+
log(...args: unknown[]): void {
54+
if (this.enabled) {
55+
console.log(...args);
56+
}
57+
}
58+
59+
/**
60+
* Check if logging is enabled
61+
*/
62+
isEnabled(): boolean {
63+
return this.enabled;
64+
}
65+
}
66+
67+
/**
68+
* Create a new logger instance
69+
* @param enabled - Whether logging is enabled (default: true)
70+
*/
71+
export function createLogger(enabled: boolean = true): Logger {
72+
return new Logger(enabled);
73+
}

0 commit comments

Comments
 (0)