@devskin/agent 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +156 -0
  2. package/dist/agent.d.ts +28 -0
  3. package/dist/agent.d.ts.map +1 -0
  4. package/dist/agent.js +221 -0
  5. package/dist/agent.js.map +1 -0
  6. package/dist/api-client.d.ts +14 -0
  7. package/dist/api-client.d.ts.map +1 -0
  8. package/dist/api-client.js +102 -0
  9. package/dist/api-client.js.map +1 -0
  10. package/dist/index.d.ts +11 -0
  11. package/dist/index.d.ts.map +1 -0
  12. package/dist/index.js +37 -0
  13. package/dist/index.js.map +1 -0
  14. package/dist/instrumentation/express.d.ts +5 -0
  15. package/dist/instrumentation/express.d.ts.map +1 -0
  16. package/dist/instrumentation/express.js +100 -0
  17. package/dist/instrumentation/express.js.map +1 -0
  18. package/dist/instrumentation/http.d.ts +3 -0
  19. package/dist/instrumentation/http.d.ts.map +1 -0
  20. package/dist/instrumentation/http.js +144 -0
  21. package/dist/instrumentation/http.js.map +1 -0
  22. package/dist/span.d.ts +21 -0
  23. package/dist/span.d.ts.map +1 -0
  24. package/dist/span.js +105 -0
  25. package/dist/span.js.map +1 -0
  26. package/dist/types.d.ts +75 -0
  27. package/dist/types.d.ts.map +1 -0
  28. package/dist/types.js +17 -0
  29. package/dist/types.js.map +1 -0
  30. package/dist/utils/context.d.ts +19 -0
  31. package/dist/utils/context.d.ts.map +1 -0
  32. package/dist/utils/context.js +42 -0
  33. package/dist/utils/context.js.map +1 -0
  34. package/dist/utils/id-generator.d.ts +4 -0
  35. package/dist/utils/id-generator.d.ts.map +1 -0
  36. package/dist/utils/id-generator.js +16 -0
  37. package/dist/utils/id-generator.js.map +1 -0
  38. package/package.json +46 -0
  39. package/src/agent.ts +276 -0
  40. package/src/api-client.ts +125 -0
  41. package/src/index.ts +32 -0
  42. package/src/instrumentation/express.ts +143 -0
  43. package/src/instrumentation/http.ts +180 -0
  44. package/src/span.ts +178 -0
  45. package/src/types.ts +128 -0
  46. package/src/utils/context.ts +87 -0
  47. package/src/utils/id-generator.ts +22 -0
  48. package/tsconfig.json +28 -0
@@ -0,0 +1,180 @@
1
+ import * as http from 'http';
2
+ import * as https from 'https';
3
+ import { Agent } from '../agent';
4
+ import { SpanBuilder, TransactionBuilder } from '../span';
5
+ import { SpanKind } from '../types';
6
+ import { Context } from '../utils/context';
7
+
8
+ /**
9
+ * Instrument HTTP module
10
+ */
11
+ export function instrumentHttp(agent: Agent): void {
12
+ const config = agent.getConfig();
13
+
14
+ // Instrument outgoing HTTP requests
15
+ instrumentHttpRequest(http, agent);
16
+ instrumentHttpRequest(https, agent);
17
+
18
+ // Instrument incoming HTTP requests (server)
19
+ instrumentHttpServer(http, agent);
20
+ instrumentHttpServer(https, agent);
21
+ }
22
+
23
+ /**
24
+ * Instrument outgoing HTTP requests
25
+ */
26
+ function instrumentHttpRequest(module: typeof http | typeof https, agent: Agent): void {
27
+ const originalRequest = module.request;
28
+
29
+ (module as any).request = function (
30
+ this: any,
31
+ ...args: any[]
32
+ ) {
33
+ let options = args[0];
34
+ let callback = args[1];
35
+
36
+ // Handle overloaded signatures
37
+ if (typeof options === 'string') {
38
+ options = new URL(options);
39
+ }
40
+
41
+ // Create a span for the outgoing request
42
+ const config = agent.getConfig();
43
+ const url = typeof args[0] === 'string' ? args[0] : options.href || `${options.protocol}//${options.host || options.hostname}${options.path}`;
44
+
45
+ const span = new SpanBuilder(
46
+ `HTTP ${options.method || 'GET'}`,
47
+ SpanKind.CLIENT,
48
+ config.serviceName,
49
+ config.serviceVersion,
50
+ config.environment,
51
+ agent
52
+ );
53
+
54
+ span.setAttributes({
55
+ 'http.method': options.method || 'GET',
56
+ 'http.url': url,
57
+ 'http.target': options.path || '/',
58
+ 'net.peer.name': options.hostname || options.host,
59
+ 'net.peer.port': options.port,
60
+ });
61
+
62
+ // Add trace context to outgoing request headers
63
+ const traceId = Context.getTraceId();
64
+ const spanId = span.getSpan().span_id;
65
+
66
+ if (!options.headers) {
67
+ options.headers = {};
68
+ }
69
+ options.headers['x-trace-id'] = traceId;
70
+ options.headers['x-span-id'] = spanId;
71
+
72
+ // Wrap callback
73
+ const wrappedCallback = (res: http.IncomingMessage) => {
74
+ span.setAttributes({
75
+ 'http.status_code': res.statusCode,
76
+ 'http.response.size': res.headers['content-length'],
77
+ });
78
+
79
+ if (res.statusCode && res.statusCode >= 400) {
80
+ span.setStatus('error' as any, `HTTP ${res.statusCode}`);
81
+ }
82
+
83
+ res.on('end', () => {
84
+ span.end();
85
+ });
86
+
87
+ if (callback) {
88
+ callback(res);
89
+ }
90
+ };
91
+
92
+ const req = originalRequest.call(this, options, wrappedCallback);
93
+
94
+ req.on('error', (error: Error) => {
95
+ span.recordError(error);
96
+ span.end();
97
+ });
98
+
99
+ return req;
100
+ };
101
+ }
102
+
103
+ /**
104
+ * Instrument incoming HTTP requests (server)
105
+ */
106
+ function instrumentHttpServer(module: typeof http | typeof https, agent: Agent): void {
107
+ const originalCreateServer = module.createServer;
108
+
109
+ (module as any).createServer = function (
110
+ this: any,
111
+ ...args: any[]
112
+ ): http.Server | https.Server {
113
+ const server = originalCreateServer.call(this, ...args) as http.Server | https.Server;
114
+ const config = agent.getConfig();
115
+
116
+ // Wrap the request listener
117
+ server.on('request', (req: http.IncomingMessage, res: http.ServerResponse) => {
118
+ // Check if we should sample this request
119
+ if (!agent.shouldSample()) {
120
+ return;
121
+ }
122
+
123
+ // Extract trace context from headers
124
+ const incomingTraceId = req.headers['x-trace-id'] as string;
125
+ const incomingSpanId = req.headers['x-span-id'] as string;
126
+
127
+ const transaction = new TransactionBuilder(
128
+ `${req.method} ${req.url}`,
129
+ 'http.request',
130
+ config.serviceName,
131
+ config.serviceVersion,
132
+ config.environment,
133
+ true,
134
+ agent
135
+ );
136
+
137
+ // If there's an incoming trace ID, use it
138
+ if (incomingTraceId) {
139
+ transaction.getTransaction().trace_id = incomingTraceId;
140
+ if (incomingSpanId) {
141
+ transaction.getTransaction().parent_span_id = incomingSpanId;
142
+ }
143
+ }
144
+
145
+ transaction.setAttributes({
146
+ 'http.method': req.method,
147
+ 'http.url': req.url,
148
+ 'http.target': req.url,
149
+ 'http.host': req.headers.host,
150
+ 'http.scheme': (req as any).protocol || 'http',
151
+ 'http.user_agent': req.headers['user-agent'],
152
+ 'net.peer.ip': req.socket.remoteAddress,
153
+ 'net.peer.port': req.socket.remotePort,
154
+ });
155
+
156
+ // Run the rest of the request handling in context
157
+ Context.run({ transaction: transaction.getTransaction() }, () => {
158
+ const originalEnd = res.end;
159
+
160
+ (res as any).end = function (this: http.ServerResponse, ...endArgs: any[]) {
161
+ transaction.setAttributes({
162
+ 'http.status_code': res.statusCode,
163
+ 'http.response.size': res.getHeader('content-length'),
164
+ });
165
+
166
+ if (res.statusCode >= 400) {
167
+ transaction.setStatus('error' as any, `HTTP ${res.statusCode}`);
168
+ }
169
+
170
+ transaction.setResult(res.statusCode < 400 ? 'success' : 'error');
171
+ transaction.end();
172
+
173
+ return originalEnd.call(this, ...endArgs);
174
+ };
175
+ });
176
+ });
177
+
178
+ return server;
179
+ };
180
+ }
package/src/span.ts ADDED
@@ -0,0 +1,178 @@
1
+ import { Span, SpanKind, SpanStatus, SpanEvent, Transaction } from './types';
2
+ import { generateSpanId, generateTraceId } from './utils/id-generator';
3
+ import { Context } from './utils/context';
4
+
5
+ /**
6
+ * Span builder for creating and managing spans
7
+ */
8
+ export class SpanBuilder {
9
+ private span: Span;
10
+ private agent: any; // Reference to agent for reporting
11
+
12
+ constructor(
13
+ name: string,
14
+ kind: SpanKind,
15
+ serviceName: string,
16
+ serviceVersion?: string,
17
+ environment?: string,
18
+ agent?: any
19
+ ) {
20
+ const parentSpan = Context.getCurrentSpan();
21
+ const traceId = Context.getTraceId() || generateTraceId();
22
+
23
+ this.span = {
24
+ span_id: generateSpanId(),
25
+ trace_id: traceId,
26
+ parent_span_id: parentSpan?.span_id,
27
+ name,
28
+ kind,
29
+ start_time: new Date(),
30
+ status: SpanStatus.OK,
31
+ attributes: {},
32
+ events: [],
33
+ service_name: serviceName,
34
+ service_version: serviceVersion,
35
+ environment,
36
+ };
37
+
38
+ this.agent = agent;
39
+
40
+ // Set this span as current in context
41
+ Context.setSpan(this.span);
42
+ }
43
+
44
+ /**
45
+ * Set an attribute on the span
46
+ */
47
+ setAttribute(key: string, value: any): this {
48
+ this.span.attributes[key] = value;
49
+ return this;
50
+ }
51
+
52
+ /**
53
+ * Set multiple attributes
54
+ */
55
+ setAttributes(attributes: Record<string, any>): this {
56
+ Object.assign(this.span.attributes, attributes);
57
+ return this;
58
+ }
59
+
60
+ /**
61
+ * Add an event to the span
62
+ */
63
+ addEvent(name: string, attributes?: Record<string, any>): this {
64
+ this.span.events.push({
65
+ timestamp: new Date(),
66
+ name,
67
+ attributes,
68
+ });
69
+ return this;
70
+ }
71
+
72
+ /**
73
+ * Set the span status
74
+ */
75
+ setStatus(status: SpanStatus, message?: string): this {
76
+ this.span.status = status;
77
+ if (message) {
78
+ this.span.status_message = message;
79
+ }
80
+ return this;
81
+ }
82
+
83
+ /**
84
+ * Mark the span as having an error
85
+ */
86
+ recordError(error: Error): this {
87
+ this.setStatus(SpanStatus.ERROR, error.message);
88
+ this.setAttributes({
89
+ 'error.type': error.name,
90
+ 'error.message': error.message,
91
+ 'error.stack': error.stack,
92
+ });
93
+ this.addEvent('exception', {
94
+ 'exception.type': error.name,
95
+ 'exception.message': error.message,
96
+ });
97
+ return this;
98
+ }
99
+
100
+ /**
101
+ * End the span
102
+ */
103
+ end(): void {
104
+ this.span.end_time = new Date();
105
+ this.span.duration_ms = this.span.end_time.getTime() - this.span.start_time.getTime();
106
+
107
+ // Report span to agent
108
+ if (this.agent && typeof this.agent.reportSpan === 'function') {
109
+ this.agent.reportSpan(this.span);
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Get the span data
115
+ */
116
+ getSpan(): Span {
117
+ return this.span;
118
+ }
119
+ }
120
+
121
+ /**
122
+ * Transaction builder for creating root spans (transactions)
123
+ */
124
+ export class TransactionBuilder extends SpanBuilder {
125
+ private transaction: Transaction;
126
+
127
+ constructor(
128
+ name: string,
129
+ type: string,
130
+ serviceName: string,
131
+ serviceVersion?: string,
132
+ environment?: string,
133
+ sampled = true,
134
+ agent?: any
135
+ ) {
136
+ super(name, SpanKind.SERVER, serviceName, serviceVersion, environment, agent);
137
+
138
+ const span = this.getSpan();
139
+ this.transaction = {
140
+ ...span,
141
+ transaction_type: type,
142
+ transaction_name: name,
143
+ sampled,
144
+ };
145
+
146
+ // Set transaction in context
147
+ Context.setTransaction(this.transaction);
148
+ }
149
+
150
+ /**
151
+ * Set the transaction result
152
+ */
153
+ setResult(result: string): this {
154
+ this.transaction.result = result;
155
+ return this;
156
+ }
157
+
158
+ /**
159
+ * End the transaction
160
+ */
161
+ end(): void {
162
+ this.transaction.end_time = new Date();
163
+ this.transaction.duration_ms =
164
+ this.transaction.end_time.getTime() - this.transaction.start_time.getTime();
165
+
166
+ // Report transaction to agent
167
+ if ((this as any).agent && typeof (this as any).agent.reportTransaction === 'function') {
168
+ (this as any).agent.reportTransaction(this.transaction);
169
+ }
170
+ }
171
+
172
+ /**
173
+ * Get the transaction data
174
+ */
175
+ getTransaction(): Transaction {
176
+ return this.transaction;
177
+ }
178
+ }
package/src/types.ts ADDED
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Configuration for DevSkin APM Agent
3
+ */
4
+ export interface AgentConfig {
5
+ /** DevSkin backend URL */
6
+ serverUrl: string;
7
+
8
+ /** API key for authentication */
9
+ apiKey: string;
10
+
11
+ /** Service name */
12
+ serviceName: string;
13
+
14
+ /** Service version */
15
+ serviceVersion?: string;
16
+
17
+ /** Environment (production, staging, development) */
18
+ environment?: string;
19
+
20
+ /** Enable/disable the agent */
21
+ enabled?: boolean;
22
+
23
+ /** Sample rate (0.0 to 1.0) */
24
+ sampleRate?: number;
25
+
26
+ /** Enable HTTP instrumentation */
27
+ instrumentHttp?: boolean;
28
+
29
+ /** Enable Express instrumentation */
30
+ instrumentExpress?: boolean;
31
+
32
+ /** Batch size for sending data */
33
+ batchSize?: number;
34
+
35
+ /** Flush interval in milliseconds */
36
+ flushInterval?: number;
37
+
38
+ /** Enable debug logging */
39
+ debug?: boolean;
40
+ }
41
+
42
+ /**
43
+ * Span kind
44
+ */
45
+ export enum SpanKind {
46
+ SERVER = 'server',
47
+ CLIENT = 'client',
48
+ INTERNAL = 'internal',
49
+ PRODUCER = 'producer',
50
+ CONSUMER = 'consumer',
51
+ }
52
+
53
+ /**
54
+ * Span status
55
+ */
56
+ export enum SpanStatus {
57
+ OK = 'ok',
58
+ ERROR = 'error',
59
+ }
60
+
61
+ /**
62
+ * Span data structure
63
+ */
64
+ export interface Span {
65
+ span_id: string;
66
+ trace_id: string;
67
+ parent_span_id?: string;
68
+ name: string;
69
+ kind: SpanKind;
70
+ start_time: Date;
71
+ end_time?: Date;
72
+ duration_ms?: number;
73
+ status: SpanStatus;
74
+ status_message?: string;
75
+ attributes: Record<string, any>;
76
+ events: SpanEvent[];
77
+ service_name: string;
78
+ service_version?: string;
79
+ environment?: string;
80
+ }
81
+
82
+ /**
83
+ * Span event
84
+ */
85
+ export interface SpanEvent {
86
+ timestamp: Date;
87
+ name: string;
88
+ attributes?: Record<string, any>;
89
+ }
90
+
91
+ /**
92
+ * Transaction (root span)
93
+ */
94
+ export interface Transaction extends Span {
95
+ transaction_type: string;
96
+ transaction_name: string;
97
+ result?: string;
98
+ sampled: boolean;
99
+ }
100
+
101
+ /**
102
+ * Log entry
103
+ */
104
+ export interface LogEntry {
105
+ timestamp: Date;
106
+ level: string;
107
+ message: string;
108
+ trace_id?: string;
109
+ span_id?: string;
110
+ attributes?: Record<string, any>;
111
+ service_name: string;
112
+ environment?: string;
113
+ }
114
+
115
+ /**
116
+ * Error data
117
+ */
118
+ export interface ErrorData {
119
+ timestamp: Date;
120
+ message: string;
121
+ type: string;
122
+ stack_trace?: string;
123
+ trace_id?: string;
124
+ span_id?: string;
125
+ attributes?: Record<string, any>;
126
+ service_name: string;
127
+ environment?: string;
128
+ }
@@ -0,0 +1,87 @@
1
+ import { AsyncLocalStorage } from 'async_hooks';
2
+ import { Span, Transaction } from '../types';
3
+
4
+ /**
5
+ * Context data stored in AsyncLocalStorage
6
+ */
7
+ interface ContextData {
8
+ transaction?: Transaction;
9
+ currentSpan?: Span;
10
+ traceId?: string;
11
+ spanId?: string;
12
+ }
13
+
14
+ /**
15
+ * AsyncLocalStorage instance for context propagation
16
+ */
17
+ const asyncLocalStorage = new AsyncLocalStorage<ContextData>();
18
+
19
+ /**
20
+ * Context manager for maintaining trace context across async operations
21
+ */
22
+ export class Context {
23
+ /**
24
+ * Run a function with a new context
25
+ */
26
+ static run<T>(context: ContextData, fn: () => T): T {
27
+ return asyncLocalStorage.run(context, fn);
28
+ }
29
+
30
+ /**
31
+ * Get the current context
32
+ */
33
+ static get(): ContextData | undefined {
34
+ return asyncLocalStorage.getStore();
35
+ }
36
+
37
+ /**
38
+ * Get the current transaction
39
+ */
40
+ static getCurrentTransaction(): Transaction | undefined {
41
+ return asyncLocalStorage.getStore()?.transaction;
42
+ }
43
+
44
+ /**
45
+ * Get the current span
46
+ */
47
+ static getCurrentSpan(): Span | undefined {
48
+ return asyncLocalStorage.getStore()?.currentSpan;
49
+ }
50
+
51
+ /**
52
+ * Get the current trace ID
53
+ */
54
+ static getTraceId(): string | undefined {
55
+ return asyncLocalStorage.getStore()?.traceId;
56
+ }
57
+
58
+ /**
59
+ * Get the current span ID
60
+ */
61
+ static getSpanId(): string | undefined {
62
+ return asyncLocalStorage.getStore()?.spanId;
63
+ }
64
+
65
+ /**
66
+ * Set the current transaction
67
+ */
68
+ static setTransaction(transaction: Transaction): void {
69
+ const store = asyncLocalStorage.getStore();
70
+ if (store) {
71
+ store.transaction = transaction;
72
+ store.traceId = transaction.trace_id;
73
+ store.spanId = transaction.span_id;
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Set the current span
79
+ */
80
+ static setSpan(span: Span): void {
81
+ const store = asyncLocalStorage.getStore();
82
+ if (store) {
83
+ store.currentSpan = span;
84
+ store.spanId = span.span_id;
85
+ }
86
+ }
87
+ }
@@ -0,0 +1,22 @@
1
+ import { randomBytes } from 'crypto';
2
+
3
+ /**
4
+ * Generate a random trace ID (16 bytes / 32 hex chars)
5
+ */
6
+ export function generateTraceId(): string {
7
+ return randomBytes(16).toString('hex');
8
+ }
9
+
10
+ /**
11
+ * Generate a random span ID (8 bytes / 16 hex chars)
12
+ */
13
+ export function generateSpanId(): string {
14
+ return randomBytes(8).toString('hex');
15
+ }
16
+
17
+ /**
18
+ * Check if a value should be sampled based on sample rate
19
+ */
20
+ export function shouldSample(sampleRate: number): boolean {
21
+ return Math.random() < sampleRate;
22
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "commonjs",
5
+ "lib": ["ES2020"],
6
+ "declaration": true,
7
+ "declarationMap": true,
8
+ "sourceMap": true,
9
+ "outDir": "./dist",
10
+ "rootDir": "./src",
11
+ "baseUrl": "./src",
12
+ "paths": {
13
+ "@/*": ["./*"]
14
+ },
15
+ "removeComments": true,
16
+ "esModuleInterop": true,
17
+ "forceConsistentCasingInFileNames": true,
18
+ "skipLibCheck": true,
19
+ "resolveJsonModule": true,
20
+ "moduleResolution": "node",
21
+ "types": ["node"],
22
+ "strict": false,
23
+ "noImplicitAny": false,
24
+ "strictNullChecks": false
25
+ },
26
+ "include": ["src/**/*"],
27
+ "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"]
28
+ }