Skip to content

Commit 75d8fb3

Browse files
miguelg719kamathseanmcguire12
authored
Log improvement (format) (#599)
* first format for logging (not final?), verbose levels * build errors fix for e2e tests * prettier * fix dev dep * peer deps * fix example * revert inference logger * verbose 1 * changeset --------- Co-authored-by: Anirudh Kamath <[email protected]> Co-authored-by: Sean McGuire <[email protected]>
1 parent 3f79506 commit 75d8fb3

12 files changed

+801
-42
lines changed

Diff for: .changeset/lemon-walls-wish.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@browserbasehq/stagehand": minor
3+
---
4+
5+
cleaner logging with pino

Diff for: docs/logging.md

+184
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
# Stagehand Logging System
2+
3+
The Stagehand logging system uses [Pino](https://getpino.io/) to provide structured, efficient, and configurable logging.
4+
5+
## Log Levels
6+
7+
Stagehand uses three primary log levels:
8+
9+
| Level | Name | Description |
10+
| ----- | ----- | --------------------------------------- |
11+
| 0 | error | Critical errors and important warnings |
12+
| 1 | info | Standard information messages (default) |
13+
| 2 | debug | Detailed information for debugging |
14+
15+
The verbosity of logging is controlled by the `verbose` option when creating a Stagehand instance:
16+
17+
```typescript
18+
const stagehand = new Stagehand({
19+
verbose: 2, // Show all logs up to debug level
20+
// other options...
21+
});
22+
```
23+
24+
## Using the Logger
25+
26+
The logging system is automatically initialized with your Stagehand instance. You can access it directly via:
27+
28+
```typescript
29+
// Log an error
30+
stagehand.log({
31+
message: "An error occurred",
32+
level: 0,
33+
category: "error",
34+
});
35+
36+
// Log info (level 1 is default)
37+
stagehand.log({
38+
message: "Operation completed",
39+
category: "operation",
40+
});
41+
42+
// Log debug information
43+
stagehand.log({
44+
message: "Debug details",
45+
level: 2,
46+
category: "debug",
47+
auxiliary: {
48+
details: {
49+
value: JSON.stringify({ key: "value" }),
50+
type: "object",
51+
},
52+
},
53+
});
54+
```
55+
56+
## Inference Logging
57+
58+
For detailed logging of inference operations (act, extract, observe), Stagehand provides specialized logging:
59+
60+
```typescript
61+
// Enable inference logging to file
62+
const stagehand = new Stagehand({
63+
logInferenceToFile: true,
64+
// other options...
65+
});
66+
```
67+
68+
When enabled, inference logs are written to the `inference_summary` directory in your project.
69+
70+
## Pretty Printing
71+
72+
By default, logs in development are formatted with colors and readable timestamps using Pino's pretty formatting. For production environments or when sending logs to external systems, you can disable pretty printing.
73+
74+
## Customizing Logging
75+
76+
### Using Your Own Logger
77+
78+
You can provide your own custom logger when creating a Stagehand instance:
79+
80+
```typescript
81+
const stagehand = new Stagehand({
82+
logger: (logLine) => {
83+
// Your custom logging logic here
84+
console.log(`[${logLine.category}] ${logLine.message}`);
85+
},
86+
// other options...
87+
});
88+
```
89+
90+
When you provide a custom logger, Stagehand will automatically disable its internal Pino logger to prevent duplicate logging. Your logger will receive all log events directly.
91+
92+
### Configuring Pino
93+
94+
If you want to use Pino but with custom configuration:
95+
96+
```typescript
97+
import { StagehandLogger } from "@browserbasehq/stagehand/lib/logger";
98+
99+
// Create a custom configured logger
100+
const customLogger = new StagehandLogger({
101+
pretty: true,
102+
level: "debug",
103+
// Other Pino options...
104+
});
105+
106+
// Pass it to Stagehand
107+
const stagehand = new Stagehand({
108+
logger: (logLine) => customLogger.log(logLine),
109+
// other options...
110+
});
111+
```
112+
113+
## Advanced Usage
114+
115+
### Creating a New StagehandLogger Instance
116+
117+
You can create a standalone logger for use in your application:
118+
119+
```typescript
120+
import { StagehandLogger } from "@browserbasehq/stagehand/lib/logger";
121+
122+
const logger = new StagehandLogger({
123+
pretty: true,
124+
level: "debug",
125+
});
126+
127+
logger.info("Information message");
128+
logger.debug("Debug message", { details: "some data" });
129+
logger.error("Error message", { error: "details" });
130+
```
131+
132+
### Configuring Log Output
133+
134+
You can direct logs to a file or other destination:
135+
136+
```typescript
137+
import fs from "fs";
138+
import { StagehandLogger } from "@browserbasehq/stagehand/lib/logger";
139+
140+
const fileStream = fs.createWriteStream("./logs/application.log", {
141+
flags: "a",
142+
});
143+
144+
const logger = new StagehandLogger({
145+
destination: fileStream,
146+
});
147+
```
148+
149+
### Disabling Pino Explicitly
150+
151+
If you want to handle all logging yourself without using Pino:
152+
153+
```typescript
154+
import { StagehandLogger } from "@browserbasehq/stagehand/lib/logger";
155+
156+
const logger = new StagehandLogger(
157+
{
158+
usePino: false,
159+
},
160+
(logLine) => {
161+
// Your custom logging logic
162+
console.log(`[${logLine.level}] ${logLine.message}`);
163+
},
164+
);
165+
```
166+
167+
## Troubleshooting
168+
169+
If you're not seeing logs:
170+
171+
1. Check your `verbose` setting - it may be too low for the log levels you're trying to see
172+
2. Verify that your log messages have the correct level set
173+
3. If using a custom logger, ensure it's correctly handling the log messages
174+
175+
If you're seeing duplicate logs:
176+
177+
1. Make sure you're not creating multiple instances of StagehandLogger that log to the same output
178+
2. If providing a custom logger to Stagehand, it will automatically disable the internal Pino logger
179+
180+
If logs are not being written to files:
181+
182+
1. Ensure you have write permissions to the target directory
183+
2. Check that the `logInferenceToFile` option is enabled
184+
3. Verify that the destination path exists or can be created

Diff for: lib/index.ts

+45-15
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import { ApiResponse, ErrorResponse } from "@/types/api";
4141
import { AgentExecuteOptions, AgentResult } from "../types/agent";
4242
import { StagehandAgentHandler } from "./handlers/agentHandler";
4343
import { StagehandOperatorHandler } from "./handlers/operatorHandler";
44+
import { StagehandLogger } from "./logger";
4445

4546
import {
4647
StagehandError,
@@ -60,6 +61,21 @@ const BROWSERBASE_REGION_DOMAIN = {
6061
"ap-southeast-1": "wss://connect.apse1.browserbase.com",
6162
};
6263

64+
// Initialize the global logger
65+
let globalLogger: StagehandLogger;
66+
67+
const defaultLogger = async (logLine: LogLine) => {
68+
if (!globalLogger) {
69+
// Create a global logger with Pino enabled for the default logger
70+
// but disable it in test environments
71+
globalLogger = new StagehandLogger({
72+
pretty: true,
73+
// Let StagehandLogger auto-detect test environment and disable Pino if needed
74+
});
75+
}
76+
globalLogger.log(logLine);
77+
};
78+
6379
async function getBrowser(
6480
apiKey: string | undefined,
6581
projectId: string | undefined,
@@ -137,7 +153,7 @@ async function getBrowser(
137153
logger({
138154
category: "init",
139155
message: "failed to resume session",
140-
level: 1,
156+
level: 0,
141157
auxiliary: {
142158
error: {
143159
value: error.message,
@@ -196,7 +212,6 @@ async function getBrowser(
196212
message: browserbaseSessionID
197213
? "browserbase session resumed"
198214
: "browserbase session started",
199-
level: 0,
200215
auxiliary: {
201216
sessionUrl: {
202217
value: sessionUrl,
@@ -220,7 +235,6 @@ async function getBrowser(
220235
logger({
221236
category: "init",
222237
message: "launching local browser",
223-
level: 0,
224238
auxiliary: {
225239
headless: {
226240
value: headless.toString(),
@@ -233,7 +247,6 @@ async function getBrowser(
233247
logger({
234248
category: "init",
235249
message: "local browser launch options",
236-
level: 0,
237250
auxiliary: {
238251
localLaunchOptions: {
239252
value: JSON.stringify(localBrowserLaunchOptions),
@@ -361,10 +374,6 @@ async function applyStealthScripts(context: BrowserContext) {
361374
});
362375
}
363376

364-
const defaultLogger = async (logLine: LogLine) => {
365-
console.log(logLineToString(logLine));
366-
};
367-
368377
export class Stagehand {
369378
private stagehandPage!: StagehandPage;
370379
private stagehandContext!: StagehandContext;
@@ -395,6 +404,8 @@ export class Stagehand {
395404
private cleanupCalled = false;
396405
public readonly actTimeoutMs: number;
397406
public readonly logInferenceToFile?: boolean;
407+
private stagehandLogger: StagehandLogger;
408+
398409
protected setActivePage(page: StagehandPage): void {
399410
this.stagehandPage = page;
400411
}
@@ -492,15 +503,31 @@ export class Stagehand {
492503
},
493504
) {
494505
this.externalLogger = logger || defaultLogger;
506+
507+
// Initialize the Stagehand logger
508+
this.stagehandLogger = new StagehandLogger(
509+
{
510+
pretty: true,
511+
usePino: !logger, // Only use Pino if no custom logger is provided
512+
},
513+
this.externalLogger,
514+
);
515+
495516
this.enableCaching =
496517
enableCaching ??
497518
(process.env.ENABLE_CACHING && process.env.ENABLE_CACHING === "true");
519+
498520
this.llmProvider =
499521
llmProvider || new LLMProvider(this.logger, this.enableCaching);
522+
500523
this.intEnv = env;
501524
this.apiKey = apiKey ?? process.env.BROWSERBASE_API_KEY;
502525
this.projectId = projectId ?? process.env.BROWSERBASE_PROJECT_ID;
526+
503527
this.verbose = verbose ?? 0;
528+
// Update logger verbosity level
529+
this.stagehandLogger.setVerbosity(this.verbose);
530+
504531
this.debugDom = debugDom ?? false;
505532
if (llmClient) {
506533
this.llmClient = llmClient;
@@ -556,11 +583,15 @@ export class Stagehand {
556583
if (this.cleanupCalled) return;
557584
this.cleanupCalled = true;
558585

559-
console.log(`[${signal}] received. Ending Browserbase session...`);
586+
this.stagehandLogger.info(
587+
`[${signal}] received. Ending Browserbase session...`,
588+
);
560589
try {
561590
await this.close();
562591
} catch (err) {
563-
console.error("Error ending Browserbase session:", err);
592+
this.stagehandLogger.error("Error ending Browserbase session:", {
593+
error: String(err),
594+
});
564595
} finally {
565596
// Exit explicitly once cleanup is done
566597
process.exit(0);
@@ -604,7 +635,7 @@ export class Stagehand {
604635
}
605636

606637
if (initOptions) {
607-
console.warn(
638+
this.stagehandLogger.warn(
608639
"Passing parameters to init() is deprecated and will be removed in the next major version. Use constructor options instead.",
609640
);
610641
}
@@ -646,7 +677,7 @@ export class Stagehand {
646677
this.browserbaseSessionID,
647678
this.localBrowserLaunchOptions,
648679
).catch((e) => {
649-
console.error("Error in init:", e);
680+
this.stagehandLogger.error("Error in init:", { error: String(e) });
650681
const br: BrowserResult = {
651682
context: undefined,
652683
debugUrl: undefined,
@@ -721,9 +752,8 @@ export class Stagehand {
721752
log(logObj: LogLine): void {
722753
logObj.level = logObj.level ?? 1;
723754

724-
if (this.externalLogger) {
725-
this.externalLogger(logObj);
726-
}
755+
// Use our Pino-based logger
756+
this.stagehandLogger.log(logObj);
727757

728758
this.pending_logs_to_send_to_browserbase.push({
729759
...logObj,

Diff for: lib/llm/AnthropicClient.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ export class AnthropicClient extends LLMClient {
5757
logger({
5858
category: "anthropic",
5959
message: "creating chat completion",
60-
level: 1,
60+
level: 2,
6161
auxiliary: {
6262
options: {
6363
value: JSON.stringify(optionsWithoutImage),
@@ -241,7 +241,7 @@ export class AnthropicClient extends LLMClient {
241241
logger({
242242
category: "anthropic",
243243
message: "response",
244-
level: 1,
244+
level: 2,
245245
auxiliary: {
246246
response: {
247247
value: JSON.stringify(response),
@@ -293,7 +293,7 @@ export class AnthropicClient extends LLMClient {
293293
logger({
294294
category: "anthropic",
295295
message: "transformed response",
296-
level: 1,
296+
level: 2,
297297
auxiliary: {
298298
transformedResponse: {
299299
value: JSON.stringify(transformedResponse),
@@ -332,7 +332,7 @@ export class AnthropicClient extends LLMClient {
332332
logger({
333333
category: "anthropic",
334334
message: "error creating chat completion",
335-
level: 1,
335+
level: 0,
336336
auxiliary: {
337337
requestId: {
338338
value: options.requestId,

0 commit comments

Comments
 (0)