Skip to content

Commit f35382f

Browse files
committed
protocol: add tracing
1 parent b2517c1 commit f35382f

5 files changed

Lines changed: 104 additions & 45 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "patch",
3+
"comment": "protocol: add tracing",
4+
"packageName": "@apibara/protocol",
5+
"email": "francesco@ceccon.me",
6+
"dependentChangeType": "patch"
7+
}

packages/protocol/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
"vitest": "^1.6.0"
5656
},
5757
"dependencies": {
58+
"@opentelemetry/api": "^1.9.0",
5859
"consola": "^3.4.2",
5960
"long": "^5.2.3",
6061
"nice-grpc": "^2.1.8",

packages/protocol/src/rpc/data-stream.ts

Lines changed: 88 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,12 @@ import type { StreamDataRequest, StreamDataResponse } from "../stream";
44
import { type ChainTracker, createChainTracker } from "./chain-tracker";
55
import type { RpcStreamConfig } from "./config";
66
import { blockInfoToCursor } from "./helpers";
7+
import { createTracer } from "./otel";
78

89
const DEFAULT_HEARTBEAT_INTERVAL_MS = 30_000;
910

11+
const tracer = createTracer();
12+
1013
type State<TFilter, TBlock> = {
1114
// The network-specific config.
1215
config: RpcStreamConfig<TFilter, TBlock>;
@@ -118,53 +121,93 @@ async function* dataStreamLoop<TFilter, TBlock>(
118121
state: State<TFilter, TBlock>,
119122
): AsyncGenerator<StreamDataResponse<TBlock>> {
120123
while (shouldContinue(state)) {
121-
const { cursor, chainTracker } = state;
122-
123-
// Always check for heartbeats first to ensure we don't miss any.
124-
if (shouldSendHeartbeat(state)) {
125-
state.lastHeartbeat = Date.now();
126-
yield { _tag: "heartbeat" };
127-
}
128-
129-
if (shouldRefreshFinalized(state)) {
130-
const finalizedInfo = await state.config.fetchCursor({
131-
blockTag: "finalized",
132-
});
133-
134-
if (finalizedInfo === null) {
135-
throw new Error("Failed to fetch finalized cursor");
136-
}
137-
138-
const finalized = blockInfoToCursor(finalizedInfo);
139-
const finalizedChanged =
140-
state.chainTracker.updateFinalized(finalizedInfo);
141-
142-
// Only send finalized if it's needed.
143-
if (finalizedChanged && state.cursor.orderKey > finalized.orderKey) {
144-
yield { _tag: "finalize", finalize: { cursor: finalized } };
145-
}
146-
147-
state.lastFinalizedRefresh = Date.now();
148-
}
149-
150-
const finalized = chainTracker.finalized();
124+
const messages = await tracer.startActiveSpan(
125+
"data-stream.loop",
126+
async (span) => {
127+
try {
128+
const { cursor, chainTracker } = state;
129+
130+
const attributes: Record<string, string | number | boolean> = {
131+
cursor: cursor.orderKey.toString(),
132+
head: chainTracker.head().orderKey.toString(),
133+
finalized: chainTracker.finalized().orderKey.toString(),
134+
actionSendHeartbeat: false,
135+
actionRefreshFinalized: false,
136+
actionBackfillFinalized: false,
137+
actionWaitForHeadChange: false,
138+
actionProduceLiveBlocks: false,
139+
};
151140

152-
// console.log(
153-
// `[DS] RpcLoop: c=${cursor.orderKey} f=${finalized.orderKey} h=${chainTracker.head().orderKey}`,
154-
// );
141+
const messages: StreamDataResponse<TBlock>[] = [];
142+
143+
if (shouldSendHeartbeat(state)) {
144+
state.lastHeartbeat = Date.now();
145+
attributes.actionSendHeartbeat = true;
146+
messages.push({ _tag: "heartbeat" });
147+
}
148+
149+
if (shouldRefreshFinalized(state)) {
150+
attributes.actionRefreshFinalized = true;
151+
const finalizedInfo = await state.config.fetchCursor({
152+
blockTag: "finalized",
153+
});
154+
155+
if (finalizedInfo === null) {
156+
throw new Error("Failed to fetch finalized cursor");
157+
}
158+
159+
const finalized = blockInfoToCursor(finalizedInfo);
160+
const finalizedChanged =
161+
state.chainTracker.updateFinalized(finalizedInfo);
162+
163+
if (
164+
finalizedChanged &&
165+
state.cursor.orderKey > finalized.orderKey
166+
) {
167+
messages.push({
168+
_tag: "finalize",
169+
finalize: { cursor: finalized },
170+
});
171+
}
172+
173+
state.lastFinalizedRefresh = Date.now();
174+
}
175+
176+
const finalized = chainTracker.finalized();
177+
178+
if (cursor.orderKey < finalized.orderKey) {
179+
attributes.actionBackfillFinalized = true;
180+
for await (const msg of backfillFinalizedBlocks(state)) {
181+
messages.push(msg);
182+
}
183+
} else {
184+
if (isAtHead(state)) {
185+
attributes.actionWaitForHeadChange = true;
186+
for await (const msg of waitForHeadChange(state)) {
187+
messages.push(msg);
188+
}
189+
} else {
190+
attributes.actionProduceLiveBlocks = true;
191+
for await (const msg of produceLiveBlocks(state)) {
192+
messages.push(msg);
193+
}
194+
}
195+
}
196+
197+
span.setAttributes(attributes);
198+
199+
return messages;
200+
} catch (error) {
201+
span.recordException(error as Error);
202+
throw error;
203+
} finally {
204+
span.end();
205+
}
206+
},
207+
);
155208

156-
if (cursor.orderKey < finalized.orderKey) {
157-
yield* backfillFinalizedBlocks(state);
158-
} else {
159-
// If we're at the head, wait for a change.
160-
//
161-
// We don't want to produce a block immediately, but re-run the loop so
162-
// that it's like any other iteration.
163-
if (isAtHead(state)) {
164-
yield* waitForHeadChange(state);
165-
} else {
166-
yield* produceLiveBlocks(state);
167-
}
209+
for (const message of messages) {
210+
yield message;
168211
}
169212
}
170213
}

packages/protocol/src/rpc/otel.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { trace } from "@opentelemetry/api";
2+
3+
export function createTracer() {
4+
return trace.getTracer("@apibara/protocol");
5+
}

pnpm-lock.yaml

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)