diff --git a/src/shim/telemetryClient.ts b/src/shim/telemetryClient.ts index 289124cd..3c49e83a 100644 --- a/src/shim/telemetryClient.ts +++ b/src/shim/telemetryClient.ts @@ -172,9 +172,13 @@ export class TelemetryClient { const attributes: Attributes = { ...telemetry.properties, }; - attributes[SEMATTRS_HTTP_METHOD] = "HTTP"; + + // Extract HTTP method from request name if it follows "METHOD path" pattern + const httpMethod = this._extractHttpMethod(telemetry.name); + attributes[SEMATTRS_HTTP_METHOD] = httpMethod; attributes[SEMATTRS_HTTP_URL] = telemetry.url; attributes[SEMATTRS_HTTP_STATUS_CODE] = telemetry.resultCode; + const options: SpanOptions = { kind: SpanKind.SERVER, attributes: attributes, @@ -188,6 +192,30 @@ export class TelemetryClient { span.end(endTime); } + /** + * Extract HTTP method from request name if it follows "METHOD path" pattern + * @param name The request name (e.g., "GET /", "POST /api/users") + * @returns The HTTP method (e.g., "GET", "POST") or "HTTP" as fallback + */ + private _extractHttpMethod(name?: string): string { + if (!name) { + return "HTTP"; + } + + // Common HTTP methods + const httpMethods = ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS", "TRACE", "CONNECT"]; + + // Check if name starts with an HTTP method followed by a space + for (const method of httpMethods) { + if (name.startsWith(`${method } `)) { + return method; + } + } + + // Fallback to "HTTP" if no method pattern found + return "HTTP"; + } + /** * Log a dependency. Note that the default client will attempt to collect dependencies automatically so only use this for dependencies * that aren't automatically captured or if you've disabled automatic dependency collection. diff --git a/test/unitTests/shim/telemetryClient.tests.ts b/test/unitTests/shim/telemetryClient.tests.ts index c12486be..466ec3c7 100644 --- a/test/unitTests/shim/telemetryClient.tests.ts +++ b/test/unitTests/shim/telemetryClient.tests.ts @@ -261,6 +261,74 @@ describe("shim/TelemetryClient", () => { assert.equal(spans[0].attributes["http.url"], "http://test.com"); }); + it("trackRequest with HTTP method in name", async () => { + const telemetry: RequestTelemetry = { + name: "GET /", + duration: 6, + resultCode: "304", + url: "http://localhost:4001/", + success: false, + }; + client.trackRequest(telemetry); + await tracerProvider.forceFlush(); + const spans = testProcessor.spansProcessed; + assert.equal(spans.length, 1); + assert.equal(spans[0].name, "GET /"); + assert.equal(spans[0].kind, 1, "Span Kind"); // Incoming + // HTTP method should be extracted from name, not hardcoded as "HTTP" + assert.equal(spans[0].attributes["http.method"], "GET"); + assert.equal(spans[0].attributes["http.status_code"], "304"); + assert.equal(spans[0].attributes["http.url"], "http://localhost:4001/"); + }); + + it("trackRequest with different HTTP methods", async () => { + const testCases = [ + { name: "POST /api/users", expectedMethod: "POST" }, + { name: "PUT /api/users/123", expectedMethod: "PUT" }, + { name: "DELETE /api/users/123", expectedMethod: "DELETE" }, + { name: "PATCH /api/users/123", expectedMethod: "PATCH" }, + { name: "HEAD /health", expectedMethod: "HEAD" }, + { name: "OPTIONS /api", expectedMethod: "OPTIONS" }, + ]; + + for (let i = 0; i < testCases.length; i++) { + const testCase = testCases[i]; + const telemetry: RequestTelemetry = { + name: testCase.name, + duration: 100, + resultCode: "200", + url: "http://test.com", + success: true, + }; + client.trackRequest(telemetry); + } + + await tracerProvider.forceFlush(); + const spans = testProcessor.spansProcessed; + assert.equal(spans.length, testCases.length); + + for (let i = 0; i < testCases.length; i++) { + assert.equal(spans[i].attributes["http.method"], testCases[i].expectedMethod); + } + }); + + it("trackRequest with non-HTTP method name fallback", async () => { + const telemetry: RequestTelemetry = { + name: "Custom Operation Name", + duration: 50, + resultCode: "200", + url: "http://test.com", + success: true, + }; + client.trackRequest(telemetry); + await tracerProvider.forceFlush(); + const spans = testProcessor.spansProcessed; + assert.equal(spans.length, 1); + assert.equal(spans[0].name, "Custom Operation Name"); + // Should fallback to "HTTP" when no method pattern found + assert.equal(spans[0].attributes["http.method"], "HTTP"); + }); + it("trackMetric", async () => { const telemetry = { name: "TestName",