Skip to content

Commit fcbaa97

Browse files
authored
Merge pull request #5 from logtide-dev/feature/richer-traces
Feature/richer traces
2 parents 395d4e0 + 2fc16fa commit fcbaa97

39 files changed

+2093
-108
lines changed

CHANGELOG.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,24 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [0.6.0] - 2026-02-28
9+
10+
### Added
11+
- **OTLP Span Events**: Breadcrumbs are now automatically converted to OTLP Span Events, providing a detailed timeline of events within the trace viewer.
12+
- **Child Spans API**: New `startChildSpan()` and `finishChildSpan()` APIs in `@logtide/core` to create hierarchical spans for operations like DB queries or external API calls.
13+
- **Rich Span Attributes**: Added standardized attributes to request spans across all frameworks:
14+
- `http.user_agent`, `net.peer.ip`, `http.query_string` (at start)
15+
- `http.status_code`, `duration_ms`, `http.route` (at finish)
16+
- **Express Error Handler**: Exported `logtideErrorHandler` to capture unhandled errors and associate them with the current request scope.
17+
18+
### Changed
19+
- **Enriched Breadcrumbs**: Request/Response breadcrumbs now include more metadata (`method`, `url`, `status`, `duration_ms`) by default.
20+
- **Improved Nuxt Tracing**: Nitro plugin now accurately captures response status codes and durations.
21+
- **Improved Angular Tracing**: `LogtideHttpInterceptor` now captures status codes for both successful and failed outgoing requests.
22+
23+
### Fixed
24+
- Fixed a bug in Nuxt Nitro plugin where spans were always marked as 'ok' regardless of the actual response status.
25+
826
## [0.5.6] - 2026-02-08
927

1028
### Changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"private": true,
3-
"version": "0.5.6",
3+
"version": "0.6.0",
44
"scripts": {
55
"build": "pnpm -r --filter @logtide/* build",
66
"test": "pnpm -r --filter @logtide/* test",

packages/angular/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@logtide/angular",
3-
"version": "0.5.6",
3+
"version": "0.6.0",
44
"description": "LogTide SDK integration for Angular — ErrorHandler, HTTP Interceptor, trace propagation",
55
"type": "module",
66
"main": "./dist/index.cjs",

packages/angular/src/http-interceptor.ts

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@ import {
55
HttpHandler,
66
HttpEvent,
77
HttpErrorResponse,
8+
HttpResponse,
89
} from '@angular/common/http';
910
import { Observable, tap } from 'rxjs';
10-
import { hub, createTraceparent, generateSpanId } from '@logtide/core';
11+
import { hub, createTraceparent } from '@logtide/core';
1112

1213
/**
1314
* Angular HTTP Interceptor that:
@@ -26,6 +27,7 @@ export class LogtideHttpInterceptor implements HttpInterceptor {
2627

2728
// Start a span for this outgoing request
2829
let spanId: string | undefined;
30+
const startTime = Date.now();
2931

3032
if (client) {
3133
const span = client.startSpan({
@@ -35,6 +37,7 @@ export class LogtideHttpInterceptor implements HttpInterceptor {
3537
attributes: {
3638
'http.method': req.method,
3739
'http.url': req.urlWithParams,
40+
'http.target': req.url,
3841
},
3942
});
4043

@@ -52,22 +55,50 @@ export class LogtideHttpInterceptor implements HttpInterceptor {
5255
type: 'http',
5356
category: 'http.request',
5457
message: `${req.method} ${req.urlWithParams}`,
55-
timestamp: Date.now(),
58+
timestamp: startTime,
5659
data: { method: req.method, url: req.urlWithParams },
5760
});
5861
}
5962

6063
return next.handle(clonedReq).pipe(
6164
tap({
62-
next: () => {
63-
// On success, finish span
64-
if (client && spanId) {
65-
client.finishSpan(spanId, 'ok');
65+
next: (event: HttpEvent<unknown>) => {
66+
if (event instanceof HttpResponse) {
67+
// On success, finish span with status code
68+
if (client && spanId) {
69+
const durationMs = Date.now() - startTime;
70+
client.finishSpan(spanId, event.status >= 500 ? 'error' : 'ok', {
71+
extraAttributes: {
72+
'http.status_code': event.status,
73+
'duration_ms': durationMs,
74+
},
75+
});
76+
77+
hub.addBreadcrumb({
78+
type: 'http',
79+
category: 'http.response',
80+
message: `${req.method} ${req.urlWithParams}${event.status}`,
81+
level: event.status >= 400 ? 'warn' : 'info',
82+
timestamp: Date.now(),
83+
data: {
84+
method: req.method,
85+
url: req.urlWithParams,
86+
status: event.status,
87+
duration_ms: durationMs,
88+
},
89+
});
90+
}
6691
}
6792
},
6893
error: (error: HttpErrorResponse) => {
94+
const durationMs = Date.now() - startTime;
6995
if (client && spanId) {
70-
client.finishSpan(spanId, 'error');
96+
client.finishSpan(spanId, 'error', {
97+
extraAttributes: {
98+
'http.status_code': error.status,
99+
'duration_ms': durationMs,
100+
},
101+
});
71102
}
72103

73104
hub.addBreadcrumb({
@@ -81,13 +112,15 @@ export class LogtideHttpInterceptor implements HttpInterceptor {
81112
url: req.urlWithParams,
82113
status: error.status,
83114
statusText: error.statusText,
115+
duration_ms: durationMs,
84116
},
85117
});
86118

87119
hub.captureError(error, {
88120
'http.method': req.method,
89121
'http.url': req.urlWithParams,
90-
'http.status': error.status,
122+
'http.status_code': error.status,
123+
'duration_ms': durationMs,
91124
});
92125
},
93126
}),

packages/core/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@logtide/core",
3-
"version": "0.5.6",
3+
"version": "0.6.0",
44
"description": "Core client, hub, scope, transports, and utilities for the LogTide SDK",
55
"type": "module",
66
"main": "./dist/index.cjs",

packages/core/src/child-span.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import type { Span, SpanAttributes, SpanEvent } from '@logtide/types';
2+
import { hub } from './hub';
3+
import type { Scope } from './scope';
4+
5+
/**
6+
* Start a child span under the given scope.
7+
* If no client is registered, returns a no-op span.
8+
*/
9+
export function startChildSpan(name: string, scope: Scope, attributes?: SpanAttributes): Span {
10+
const client = hub.getClient();
11+
if (!client) {
12+
return {
13+
traceId: scope.traceId,
14+
spanId: '0000000000000000',
15+
name,
16+
status: 'unset',
17+
startTime: Date.now(),
18+
attributes: attributes ?? {},
19+
};
20+
}
21+
return client.startChildSpan(name, scope, attributes);
22+
}
23+
24+
/**
25+
* Finish a child span by ID via the hub client.
26+
*/
27+
export function finishChildSpan(
28+
spanId: string,
29+
status: 'ok' | 'error' = 'ok',
30+
options?: { extraAttributes?: SpanAttributes; events?: SpanEvent[] },
31+
): void {
32+
hub.getClient()?.finishChildSpan(spanId, status, options);
33+
}

packages/core/src/client.ts

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import type {
66
Integration,
77
LogLevel,
88
Span,
9+
SpanAttributes,
10+
SpanEvent,
911
Transport,
1012
} from '@logtide/types';
1113
import { resolveDSN } from './dsn';
@@ -41,7 +43,10 @@ class DefaultTransport implements Transport {
4143
});
4244

4345
this.spanTransport = new BatchTransport({
44-
inner: new OtlpHttpTransport(dsn, options.service || 'unknown'),
46+
inner: new OtlpHttpTransport(dsn, options.service || 'unknown', {
47+
environment: options.environment,
48+
release: options.release,
49+
}),
4550
batchSize: options.batchSize,
4651
flushInterval: options.flushInterval,
4752
maxBufferSize: options.maxBufferSize,
@@ -189,13 +194,40 @@ export class LogtideClient implements IClient {
189194
return this.spanManager.startSpan(options);
190195
}
191196

192-
finishSpan(spanId: string, status: 'ok' | 'error' = 'ok'): void {
193-
const span = this.spanManager.finishSpan(spanId, status);
197+
finishSpan(
198+
spanId: string,
199+
status: 'ok' | 'error' = 'ok',
200+
options?: { extraAttributes?: SpanAttributes; events?: SpanEvent[] },
201+
): void {
202+
const span = this.spanManager.finishSpan(spanId, status, options);
194203
if (span && this.transport.sendSpans) {
195204
this.transport.sendSpans([span]);
196205
}
197206
}
198207

208+
/**
209+
* Start a child span under the given scope.
210+
*/
211+
startChildSpan(name: string, scope: Scope, attributes?: SpanAttributes): Span {
212+
return this.startSpan({
213+
name,
214+
traceId: scope.traceId,
215+
parentSpanId: scope.spanId,
216+
attributes,
217+
});
218+
}
219+
220+
/**
221+
* Finish a child span by ID.
222+
*/
223+
finishChildSpan(
224+
spanId: string,
225+
status: 'ok' | 'error' = 'ok',
226+
options?: { extraAttributes?: SpanAttributes; events?: SpanEvent[] },
227+
): void {
228+
this.finishSpan(spanId, status, options);
229+
}
230+
199231
// ─── Integrations ─────────────────────────────────────
200232

201233
addIntegration(integration: Integration): void {

packages/core/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export type {
66
Span,
77
SpanStatus,
88
SpanAttributes,
9+
SpanEvent,
910
Breadcrumb,
1011
BreadcrumbType,
1112
Transport,
@@ -21,6 +22,7 @@ export { hub } from './hub';
2122
export { Scope } from './scope';
2223
export { SpanManager, type StartSpanOptions } from './span-manager';
2324
export { BreadcrumbBuffer } from './breadcrumb-buffer';
25+
export { startChildSpan, finishChildSpan } from './child-span';
2426

2527
// DSN
2628
export { parseDSN, resolveDSN } from './dsn';

packages/core/src/span-manager.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { Span, SpanAttributes, SpanStatus } from '@logtide/types';
1+
import type { Span, SpanAttributes, SpanEvent, SpanStatus } from '@logtide/types';
22
import { generateSpanId, generateTraceId } from './utils/trace-id';
33

44
export interface StartSpanOptions {
@@ -26,12 +26,26 @@ export class SpanManager {
2626
return span;
2727
}
2828

29-
finishSpan(spanId: string, status: SpanStatus = 'ok'): Span | undefined {
29+
finishSpan(
30+
spanId: string,
31+
status: SpanStatus = 'ok',
32+
options?: { extraAttributes?: SpanAttributes; events?: SpanEvent[] },
33+
): Span | undefined {
3034
const span = this.activeSpans.get(spanId);
3135
if (!span) return undefined;
3236

3337
span.endTime = Date.now();
3438
span.status = status;
39+
40+
if (options) {
41+
if (options.extraAttributes) {
42+
Object.assign(span.attributes, options.extraAttributes);
43+
}
44+
if (options.events && options.events.length > 0) {
45+
span.events = (span.events ?? []).concat(options.events);
46+
}
47+
}
48+
3549
this.activeSpans.delete(spanId);
3650
return span;
3751
}

0 commit comments

Comments
 (0)