elastic / elastic/example-mcp-dashbuilder

[Agentic Interface] Add Structured Logging to `example-mcp-dashbuilder`

Open
#31 0 comments 0 reactions 1 assignee Claimed by @walterra View on GitHub
Dominant language
TypeScript
Stars
17
Forks
6
PR merge metrics
No merged PRs in 30d

Description

The MCP server currently has no logging infrastructure. The only output is some `console.log`/`console.error` calls in `setup.ts` (interactive wizard) and one in `lens-reverse-translator.ts`. Tool executions, ES|QL queries, Kibana API calls, and errors are completely invisible at runtime. This makes debugging, performance analysis, and future o11y integration difficult.

## Goals

1. Add a structured JSON logger (Pino) with configurable level and transports.
2. Log tool calls (name, duration, success/error) to stderr (not stdout — stdio transport uses stdout for MCP protocol).
3. Log Elasticsearch and Kibana HTTP interactions at `debug` level.
4. Log errors with stack traces in a consistent format.
5. Optional file transport for persistent logs.
6. **Do not** add OpenTelemetry yet — that's a follow-up. But design the logger so OTel transport can be added later without changes to call sites.

## Design

### Logger module: `server/src/utils/logger.ts`

Create a Pino logger factory:

```typescript
import pino from 'pino';

export const logger = pino({
name: 'mcp-dashbuilder',
level: process.env.LOG_LEVEL ?? 'info',
transport: {
targets: [
// Stderr only — stdout is reserved for MCP stdio transport
{
target: 'pino-pretty',
options: { colorize: true, destination: 2 }, // fd 2 = stderr
level: process.env.LOG_LEVEL ?? 'info',
},
// Optional: file transport (enabled when LOG_DIR is set)
...(process.env.LOG_DIR
? [{
target: 'pino/file',
options: {
destination: `${process.env.LOG_DIR}/mcp-dashbuilder-${new Date().toISOString().split('T')[0]}.log`,
},
level: process.env.LOG_LEVEL ?? 'info',
}]
: []),
],
},
});
```

Key constraint: **all log output must go to stderr** (fd 2) since stdout is the MCP stdio transport channel. Pino-pretty defaults to stdout, so `destination: 2` is required.

### Dependencies

Add to `server/package.json`:
- `pino` (structured logger)
- `pino-pretty` (dev dependency — pretty console output)

### Where to log

| Location | Level | What to log |
|---|---|---|
| `register-tool.ts` wrapper | `info` | Tool name, duration ms, success/error |
| `register-tool.ts` wrapper | `error` | Tool name, error message, stack trace |
| `es-client.ts` | `debug` | ES|QL query text (truncated), response time |
| `kibana-client.ts` | `debug` | Kibana API method + path, response status, duration |
| `dashboard-store.ts` | `debug` | Dashboard create/update/delete operations |
| `index.ts` (server startup) | `info` | Server started, transport type, tool count |
| `lens-reverse-translator.ts:53` | `warn` | Replace existing `console.error` |

### Tool wrapper enhancement

Update `registerTool` and `registerAppOnlyTool` in `register-tool.ts` to log every call:

```typescript
const wrappedHandler = async (args: Record) => {
const start = performance.now();
const parsed = schema.parse(args);
try {
const result = await handler(parsed);
logger.info({ tool: name, durationMs: Math.round(performance.now() - start) }, 'tool.success');
return result;
} catch (err) {
logger.error({ tool: name, durationMs: Math.round(performance.now() - start), err }, 'tool.error');
throw err;
}
};
```

### Error serialization

Add a `serializeError` helper (following ECS-compatible pattern) for consistent error fields:

```typescript
export function serializeError(error: unknown): Record {
if (error instanceof Error) {
return {
'error.message': error.message,
'error.type': error.name,
...(error.stack ? { 'error.stack_trace': error.stack } : {}),
};
}
return { 'error.message': String(error), 'error.type': typeof error };
}
```

### Environment variables

| Variable | Default | Description |
|---|---|---|
| `LOG_LEVEL` | `info` | Pino log level (`trace`, `debug`, `info`, `warn`, `error`, `fatal`) |
| `LOG_DIR` | _(unset)_ | If set, writes JSON log files to this directory |

## Scope

### In scope
- `server/src/utils/logger.ts` — new file
- `server/src/utils/register-tool.ts` — add timing + logging to wrappers
- `server/src/utils/es-client.ts` — debug-level query logging
- `server/src/utils/kibana-client.ts` — debug-level API call logging
- `server/src/index.ts` — startup log
- `server/src/utils/lens-reverse-translator.ts` — replace `console.error`
- `server/package.json` — add pino deps

### Out of scope
- OpenTelemetry integration (follow-up issue)
- `setup.ts` — interactive CLI wizard, `console.log` is appropriate there
- Preview app logging

## Acceptance criteria

- [ ] All tool calls logged to stderr with name + duration + outcome
- [ ] `LOG_LEVEL=debug` surfaces ES|QL queries and Kibana API calls
- [ ] No log output on stdout (would corrupt MCP stdio transport)
- [ ] Existing `console.error` in `lens-reverse-translator.ts` replaced with `logger.warn`
- [ ] `LOG_DIR` env var enables file logging when set
- [ ] Server startup emits one info-level log line with version and tool count

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.