From d40ba54669aedc7ad07e9bcc56b50f817e9c7710 Mon Sep 17 00:00:00 2001 From: Hector Hernandez <39923391+hectorhdzg@users.noreply.github.com> Date: Wed, 30 Sep 2026 16:54:16 -0700 Subject: [PATCH 1/2] test: add upstream compatibility contracts Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../upstream/azureResourceDetectors.test.ts | 335 ++++++++++++++++++ .../unit/upstream/resourceDetectors.test.ts | 218 ++++++++++++ .../resourceIdentityContracts.test.ts | 162 +++++++++ .../upstream/telemetrySdkContracts.test.ts | 190 ++++++++++ 4 files changed, 905 insertions(+) create mode 100644 test/internal/unit/upstream/azureResourceDetectors.test.ts create mode 100644 test/internal/unit/upstream/resourceDetectors.test.ts create mode 100644 test/internal/unit/upstream/resourceIdentityContracts.test.ts create mode 100644 test/internal/unit/upstream/telemetrySdkContracts.test.ts diff --git a/test/internal/unit/upstream/azureResourceDetectors.test.ts b/test/internal/unit/upstream/azureResourceDetectors.test.ts new file mode 100644 index 00000000..56161dc5 --- /dev/null +++ b/test/internal/unit/upstream/azureResourceDetectors.test.ts @@ -0,0 +1,335 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { EventEmitter } from "node:events"; +import fs from "node:fs"; +import http from "node:http"; +import type { ClientRequest, IncomingMessage } from "node:http"; +import { + azureAksDetector, + azureAppServiceDetector, + azureContainerAppsDetector, + azureFunctionsDetector, + azureVmDetector, +} from "@opentelemetry/resource-detector-azure"; +import { detectResources } from "@opentelemetry/resources"; +import type { ResourceDetector } from "@opentelemetry/resources"; +import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +const originalEnv = { ...process.env }; +const azureEnvironmentVariables = [ + "CLUSTER_RESOURCE_ID", + "CONTAINER_APP_ENV_DNS_SUFFIX", + "CONTAINER_APP_HOSTNAME", + "CONTAINER_APP_NAME", + "CONTAINER_APP_PORT", + "CONTAINER_APP_REPLICA_NAME", + "CONTAINER_APP_REVISION", + "FUNCTIONS_EXTENSION_VERSION", + "REGION_NAME", + "WEBSITE_HOME_STAMPNAME", + "WEBSITE_HOSTNAME", + "WEBSITE_INSTANCE_ID", + "WEBSITE_MEMORY_LIMIT_MB", + "WEBSITE_OWNER_NAME", + "WEBSITE_RESOURCE_GROUP", + "WEBSITE_SITE_NAME", + "WEBSITE_SKU", + "WEBSITE_SLOT_NAME", +] as const; + +async function detect(detector: ResourceDetector) { + const resource = detectResources({ detectors: [detector] }); + await resource.waitForAsyncAttributes?.(); + return resource.attributes; +} + +function mockVmRequest( + result: + | { body: string; statusCode: number } + | { error: Error }, + onOptions?: (options: unknown) => void, +) { + return vi.spyOn(http, "request").mockImplementation( + ((options: unknown, callback: (response: IncomingMessage) => void) => { + onOptions?.(options); + const request = new EventEmitter() as ClientRequest; + request.destroy = vi.fn(); + request.end = () => { + queueMicrotask(() => { + if ("error" in result) { + request.emit("error", result.error); + return; + } + + const response = new EventEmitter() as IncomingMessage; + response.statusCode = result.statusCode; + response.setEncoding = vi.fn(); + callback(response); + response.emit("data", result.body); + response.emit("end"); + }); + return request; + }; + return request; + }) as unknown as typeof http.request, + ); +} + +describe("@opentelemetry/resource-detector-azure contracts", () => { + beforeEach(() => { + for (const name of azureEnvironmentVariables) { + delete process.env[name]; + } + delete process.env.OTEL_NODE_RESOURCE_DETECTORS; + }); + + afterEach(() => { + vi.restoreAllMocks(); + vi.unstubAllEnvs(); + }); + + afterAll(() => { + process.env = originalEnv; + }); + + it.each([ + ["App Service", azureAppServiceDetector], + ["Container Apps", azureContainerAppsDetector], + ["Functions", azureFunctionsDetector], + ])("returns no %s attributes outside its Azure environment", async (_name, detector) => { + await expect(detect(detector)).resolves.toEqual({}); + }); + + it("returns no AKS attributes without environment or mounted metadata", async () => { + vi.spyOn(fs, "existsSync").mockReturnValue(false); + + await expect(detect(azureAksDetector)).resolves.toEqual({}); + }); + + it("detects a minimal Azure App Service environment without inventing optional attributes", async () => { + vi.stubEnv("WEBSITE_SITE_NAME", "minimal-api"); + + await expect(detect(azureAppServiceDetector)).resolves.toEqual({ + "cloud.platform": "azure.app_service", + "cloud.provider": "azure", + "service.name": "minimal-api", + }); + }); + + it("detects Azure App Service attributes and its ARM resource ID", async () => { + vi.stubEnv("WEBSITE_SITE_NAME", "orders-api"); + vi.stubEnv("WEBSITE_OWNER_NAME", "subscription-id+hosting-plan"); + vi.stubEnv("WEBSITE_RESOURCE_GROUP", "production-rg"); + vi.stubEnv("REGION_NAME", "westus3"); + vi.stubEnv("WEBSITE_SLOT_NAME", "staging"); + vi.stubEnv("WEBSITE_HOSTNAME", "orders-api.azurewebsites.net"); + vi.stubEnv("WEBSITE_INSTANCE_ID", "instance-1"); + vi.stubEnv("WEBSITE_HOME_STAMPNAME", "stamp-1"); + + await expect(detect(azureAppServiceDetector)).resolves.toEqual({ + "azure.app.service.stamp": "stamp-1", + "cloud.platform": "azure.app_service", + "cloud.provider": "azure", + "cloud.region": "westus3", + "cloud.resource_id": + "/subscriptions/subscription-id/resourceGroups/production-rg/providers/Microsoft.Web/sites/orders-api", + "deployment.environment.name": "staging", + "host.id": "orders-api.azurewebsites.net", + "service.instance.id": "instance-1", + "service.name": "orders-api", + }); + }); + + it("keeps Azure Container Apps mutually exclusive from App Service", async () => { + vi.stubEnv("WEBSITE_SITE_NAME", "container-backed-site"); + vi.stubEnv("CONTAINER_APP_NAME", "checkout"); + vi.stubEnv("CONTAINER_APP_REVISION", "checkout--0000042"); + vi.stubEnv("CONTAINER_APP_HOSTNAME", "checkout.internal.example"); + vi.stubEnv("CONTAINER_APP_ENV_DNS_SUFFIX", "internal.example"); + vi.stubEnv("CONTAINER_APP_PORT", "8080"); + vi.stubEnv("CONTAINER_APP_REPLICA_NAME", "checkout--0000042-abcd"); + + await expect(detect(azureAppServiceDetector)).resolves.toEqual({}); + }); + + it("keeps Azure Functions mutually exclusive from App Service", async () => { + vi.stubEnv("WEBSITE_SITE_NAME", "thumbnail-function"); + vi.stubEnv("FUNCTIONS_EXTENSION_VERSION", "~4"); + vi.stubEnv("WEBSITE_INSTANCE_ID", "function-instance"); + vi.stubEnv("WEBSITE_MEMORY_LIMIT_MB", "1536"); + vi.stubEnv("REGION_NAME", "eastus2"); + vi.stubEnv("WEBSITE_OWNER_NAME", "subscription-id"); + vi.stubEnv("WEBSITE_RESOURCE_GROUP", "functions-rg"); + + await expect(detect(azureAppServiceDetector)).resolves.toEqual({}); + await expect(detect(azureFunctionsDetector)).resolves.toEqual({ + "cloud.platform": "azure.functions", + "cloud.provider": "azure", + "cloud.region": "eastus2", + "cloud.resource_id": + "/subscriptions/subscription-id/resourceGroups/functions-rg/providers/Microsoft.Web/sites/thumbnail-function", + "faas.instance": "function-instance", + "faas.max_memory": "1536", + "process.pid": process.pid, + "service.name": "thumbnail-function", + }); + }); + + it("detects Flex Consumption as Azure Functions without FUNCTIONS_EXTENSION_VERSION", async () => { + vi.stubEnv("WEBSITE_SITE_NAME", "flex-function"); + vi.stubEnv("WEBSITE_SKU", "FlexConsumption"); + + await expect(detect(azureFunctionsDetector)).resolves.toEqual({ + "cloud.platform": "azure.functions", + "cloud.provider": "azure", + "cloud.region": undefined, + "process.pid": process.pid, + "service.name": "flex-function", + }); + }); + + it("requires a site name even when the Azure Functions runtime marker exists", async () => { + vi.stubEnv("FUNCTIONS_EXTENSION_VERSION", "~4"); + + await expect(detect(azureFunctionsDetector)).resolves.toEqual({}); + }); + + function stubCompleteContainerAppsEnvironment() { + vi.stubEnv("CONTAINER_APP_NAME", "checkout"); + vi.stubEnv("CONTAINER_APP_REVISION", "checkout--0000042"); + vi.stubEnv("CONTAINER_APP_HOSTNAME", "checkout.internal.example"); + vi.stubEnv("CONTAINER_APP_ENV_DNS_SUFFIX", "internal.example"); + vi.stubEnv("CONTAINER_APP_PORT", "8080"); + vi.stubEnv("CONTAINER_APP_REPLICA_NAME", "checkout--0000042-abcd"); + } + + it("detects Azure Container Apps from the complete platform signature", async () => { + stubCompleteContainerAppsEnvironment(); + + await expect(detect(azureContainerAppsDetector)).resolves.toEqual({ + "azure.container_app.instance.id": "checkout--0000042-abcd", + "azure.container_app.name": "checkout", + "azure.container_app.version": "checkout--0000042", + "cloud.platform": "azure.container_apps", + "cloud.provider": "azure", + "host.name": "checkout.internal.example", + }); + }); + + it.each([ + "CONTAINER_APP_NAME", + "CONTAINER_APP_REVISION", + "CONTAINER_APP_HOSTNAME", + "CONTAINER_APP_ENV_DNS_SUFFIX", + "CONTAINER_APP_PORT", + "CONTAINER_APP_REPLICA_NAME", + ])("requires %s before identifying Azure Container Apps", async (missingVariable) => { + stubCompleteContainerAppsEnvironment(); + delete process.env[missingVariable]; + + await expect(detect(azureContainerAppsDetector)).resolves.toEqual({}); + }); + + it("extracts the AKS cluster name from the native metadata resource ID", async () => { + const resourceId = + "/subscriptions/subscription-id/resourceGroups/aks-rg/providers/Microsoft.ContainerService/managedClusters/production-aks"; + vi.stubEnv("CLUSTER_RESOURCE_ID", resourceId); + + await expect(detect(azureAksDetector)).resolves.toEqual({ + "cloud.platform": "azure.aks", + "cloud.provider": "azure", + "cloud.resource_id": resourceId, + "k8s.cluster.name": "production-aks", + }); + }); + + it("falls back to the final resource ID segment for nonstandard AKS IDs", async () => { + vi.stubEnv("CLUSTER_RESOURCE_ID", "/custom/cluster/resource/nonstandard-aks"); + + await expect(detect(azureAksDetector)).resolves.toMatchObject({ + "cloud.resource_id": "/custom/cluster/resource/nonstandard-aks", + "k8s.cluster.name": "nonstandard-aks", + }); + }); + + it("reads AKS metadata from the mounted ConfigMap when the environment variable is absent", async () => { + const resourceId = + "/subscriptions/subscription-id/resourceGroups/aks-rg/providers/Microsoft.ContainerService/managedClusters/file-backed-aks"; + vi.spyOn(fs, "existsSync").mockReturnValue(true); + vi.spyOn(fs, "readFileSync").mockReturnValue( + `# generated metadata\nclusterResourceId=${resourceId}\n`, + ); + + await expect(detect(azureAksDetector)).resolves.toEqual({ + "cloud.platform": "azure.aks", + "cloud.provider": "azure", + "cloud.resource_id": resourceId, + "k8s.cluster.name": "file-backed-aks", + }); + }); + + it("ignores unreadable AKS metadata files", async () => { + vi.spyOn(fs, "existsSync").mockReturnValue(true); + vi.spyOn(fs, "readFileSync").mockImplementation(() => { + throw new Error("permission denied"); + }); + + await expect(detect(azureAksDetector)).resolves.toEqual({}); + }); + + const vmMetadata = { + location: "centralus", + name: "worker-01", + resourceId: + "/subscriptions/subscription-id/resourceGroups/vm-rg/providers/Microsoft.Compute/virtualMachines/worker-01", + sku: "2022-datacenter", + version: "10.0.20348", + vmId: "f4f6a51f-327d-4a55-97d1-94ed92f64018", + vmScaleSetName: "workers", + vmSize: "Standard_D4s_v5", + }; + + it("requests and resolves Azure VM metadata through asynchronous resource attributes", async () => { + let requestOptions: unknown; + mockVmRequest( + { body: JSON.stringify(vmMetadata), statusCode: 200 }, + (options) => (requestOptions = options), + ); + + await expect(detect(azureVmDetector)).resolves.toEqual({ + "azure.vm.scaleset.name": "workers", + "azure.vm.sku": "2022-datacenter", + "cloud.platform": "azure.vm", + "cloud.provider": "azure", + "cloud.region": "centralus", + "cloud.resource_id": vmMetadata.resourceId, + "host.id": vmMetadata.vmId, + "host.name": vmMetadata.name, + "host.type": vmMetadata.vmSize, + "os.version": vmMetadata.version, + }); + expect(requestOptions).toMatchObject({ + headers: { Metadata: "True" }, + host: "169.254.169.254", + method: "GET", + path: "/metadata/instance/compute?api-version=2021-12-13&format=json", + timeout: 5000, + }); + }); + + it.each([ + ["a non-success response", { body: "not found", statusCode: 404 }], + ["invalid JSON", { body: "{not-json", statusCode: 200 }], + ])("returns no Azure VM attributes for %s", async (_case, result) => { + mockVmRequest(result); + + await expect(detect(azureVmDetector)).resolves.toEqual({}); + }); + + it("returns no Azure VM attributes when the metadata request fails", async () => { + mockVmRequest({ error: new Error("network unavailable") }); + + await expect(detect(azureVmDetector)).resolves.toEqual({}); + }); +}); diff --git a/test/internal/unit/upstream/resourceDetectors.test.ts b/test/internal/unit/upstream/resourceDetectors.test.ts new file mode 100644 index 00000000..30b5d617 --- /dev/null +++ b/test/internal/unit/upstream/resourceDetectors.test.ts @@ -0,0 +1,218 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { + defaultResource, + detectResources, + envDetector, + hostDetector, + osDetector, + processDetector, + resourceFromAttributes, + serviceInstanceIdDetector, +} from "@opentelemetry/resources"; +import type { ResourceDetector } from "@opentelemetry/resources"; +import os from "node:os"; +import { afterEach, describe, expect, it, vi } from "vitest"; + +async function detect(detectors: ResourceDetector[]) { + const resource = detectResources({ detectors }); + await resource.waitForAsyncAttributes?.(); + return resource; +} + +describe("@opentelemetry/resources contracts", () => { + afterEach(() => { + vi.unstubAllEnvs(); + }); + + it("provides the default SDK and service identity attributes", () => { + expect(defaultResource().attributes).toMatchObject({ + "service.name": expect.stringMatching(/^unknown_service:/), + "telemetry.sdk.language": "nodejs", + "telemetry.sdk.name": "opentelemetry", + "telemetry.sdk.version": expect.stringMatching(/^\d+\.\d+\.\d+/), + }); + }); + + it("decodes resource environment variables and gives OTEL_SERVICE_NAME precedence", async () => { + vi.stubEnv( + "OTEL_RESOURCE_ATTRIBUTES", + "service.name=attribute-name,custom.key=hello%20world,custom.empty=", + ); + vi.stubEnv("OTEL_SERVICE_NAME", "explicit-service-name"); + + const resource = await detect([envDetector]); + + expect(resource.attributes).toEqual({ + "service.name": "explicit-service-name", + "custom.key": "hello world", + "custom.empty": "", + }); + }); + + it("returns an empty resource when resource environment variables are absent", async () => { + vi.stubEnv("OTEL_RESOURCE_ATTRIBUTES", ""); + vi.stubEnv("OTEL_SERVICE_NAME", ""); + + await expect(detect([envDetector])).resolves.toMatchObject({ attributes: {} }); + }); + + it("uses the last value when an environment attribute is repeated", async () => { + vi.stubEnv("OTEL_RESOURCE_ATTRIBUTES", "duplicate=first,duplicate=second"); + + const resource = await detect([envDetector]); + + expect(resource.attributes).toEqual({ duplicate: "second" }); + }); + + it.each([ + ["an unescaped equals sign", "valid=value,invalid=one=two"], + ["an empty key", "=value"], + ["invalid percent encoding", "valid=value,invalid=%E0%A4%A"], + ["an oversized key", `${"k".repeat(256)}=value`], + ["an oversized value", `key=${"v".repeat(256)}`], + ])("discards all OTEL_RESOURCE_ATTRIBUTES for %s", async (_case, value) => { + vi.stubEnv("OTEL_RESOURCE_ATTRIBUTES", value); + vi.stubEnv("OTEL_SERVICE_NAME", "service-survives-invalid-attributes"); + + const resource = await detect([envDetector]); + + expect(resource.attributes).toEqual({ + "service.name": "service-survives-invalid-attributes", + }); + }); + + it("detects host attributes from the current operating system", async () => { + const resource = await detect([hostDetector]); + + expect(resource.attributes).toMatchObject({ + "host.arch": expect.any(String), + "host.name": os.hostname(), + "host.id": expect.any(String), + }); + }); + + it("detects OS attributes from the current operating system", async () => { + const resource = await detect([osDetector]); + + expect(resource.attributes).toEqual({ + "os.type": expect.stringMatching(/^(aix|darwin|freebsd|linux|openbsd|sunos|windows)$/), + "os.version": os.release(), + }); + }); + + it("detects the current Node.js process contract", async () => { + const resource = await detect([processDetector]); + + expect(resource.attributes).toMatchObject({ + "process.pid": process.pid, + "process.executable.name": process.title, + "process.executable.path": process.execPath, + "process.command_args": expect.arrayContaining([process.execPath]), + "process.runtime.description": "Node.js", + "process.runtime.name": "nodejs", + "process.runtime.version": process.versions.node, + }); + }); + + it("generates a distinct RFC 4122 service instance ID for each detection", async () => { + const first = await detect([serviceInstanceIdDetector]); + const second = await detect([serviceInstanceIdDetector]); + const uuidPattern = + /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; + + expect(first.attributes["service.instance.id"]).toMatch(uuidPattern); + expect(second.attributes["service.instance.id"]).toMatch(uuidPattern); + expect(second.attributes["service.instance.id"]).not.toBe( + first.attributes["service.instance.id"], + ); + }); + + it("lets incoming resource attributes override existing attributes", () => { + const original = resourceFromAttributes({ original: true, shared: "original" }); + const updating = resourceFromAttributes({ updating: true, shared: "updating" }); + + expect(original.merge(updating).attributes).toEqual({ + original: true, + shared: "updating", + updating: true, + }); + }); + + it("preserves matching schema URLs and drops conflicting schema URLs", () => { + const original = resourceFromAttributes({ original: true }, { schemaUrl: "schema-v1" }); + const matching = resourceFromAttributes({ matching: true }, { schemaUrl: "schema-v1" }); + const conflicting = resourceFromAttributes( + { conflicting: true }, + { schemaUrl: "schema-v2" }, + ); + + expect(original.merge(matching).schemaUrl).toBe("schema-v1"); + expect(original.merge(conflicting).schemaUrl).toBeUndefined(); + }); + + it("omits undefined and rejected asynchronous attributes", async () => { + const resource = resourceFromAttributes({ + array: ["one", "two"], + boolean: false, + rejected: Promise.reject(new Error("optional attribute failed")), + undefined, + zero: 0, + }); + + await resource.waitForAsyncAttributes?.(); + + expect(resource.attributes).toEqual({ + array: ["one", "two"], + boolean: false, + zero: 0, + }); + }); + + it("isolates a detector that throws and continues with later detectors", async () => { + const throwingDetector: ResourceDetector = { + detect: () => { + throw new Error("detector failed"); + }, + }; + const succeedingDetector: ResourceDetector = { + detect: () => ({ attributes: { detected: true } }), + }; + + const resource = await detect([throwingDetector, succeedingDetector]); + + expect(resource.attributes).toEqual({ detected: true }); + }); + + it("merges synchronous and asynchronous detector results in detector order", async () => { + const firstDetector: ResourceDetector = { + detect: () => ({ + attributes: { + first: "first-value", + shared: "first-value", + }, + }), + }; + const secondDetector: ResourceDetector = { + detect: () => ({ + attributes: { + second: Promise.resolve("second-value"), + shared: "second-value", + rejected: Promise.reject(new Error("optional detector failed")), + }, + }), + }; + + const resource = detectResources({ detectors: [firstDetector, secondDetector] }); + + expect(resource.asyncAttributesPending).toBe(true); + await resource.waitForAsyncAttributes?.(); + expect(resource.asyncAttributesPending).toBe(false); + expect(resource.attributes).toEqual({ + first: "first-value", + second: "second-value", + shared: "second-value", + }); + }); +}); diff --git a/test/internal/unit/upstream/resourceIdentityContracts.test.ts b/test/internal/unit/upstream/resourceIdentityContracts.test.ts new file mode 100644 index 00000000..bc0843a8 --- /dev/null +++ b/test/internal/unit/upstream/resourceIdentityContracts.test.ts @@ -0,0 +1,162 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { execFile } from "node:child_process"; +import { promisify } from "node:util"; +import { azureAppServiceDetector } from "@opentelemetry/resource-detector-azure"; +import { + detectResources, + envDetector, + osDetector, + resourceFromAttributes, + serviceInstanceIdDetector, +} from "@opentelemetry/resources"; +import type { Resource } from "@opentelemetry/resources"; +import { NodeSDK } from "@opentelemetry/sdk-node"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { getCloudRoleInstance } from "../../../../src/azureMonitor/metrics/utils.js"; +import { parseResourceDetectorsFromEnvVar } from "../../../../src/utils/common.js"; + +const execFileAsync = promisify(execFile); +const originalResourceDetectors = process.env.OTEL_NODE_RESOURCE_DETECTORS; + +async function settle(resource: Resource): Promise { + await resource.waitForAsyncAttributes?.(); + return resource; +} + +describe("service.instance.id compatibility contracts", () => { + afterEach(() => { + vi.unstubAllEnvs(); + if (originalResourceDetectors === undefined) { + delete process.env.OTEL_NODE_RESOURCE_DETECTORS; + } else { + process.env.OTEL_NODE_RESOURCE_DETECTORS = originalResourceDetectors; + } + }); + + it("keeps generated service instance IDs opt-in in the distro defaults", async () => { + delete process.env.OTEL_NODE_RESOURCE_DETECTORS; + vi.stubEnv("OTEL_RESOURCE_ATTRIBUTES", ""); + vi.stubEnv("OTEL_SERVICE_NAME", ""); + + const detectors = parseResourceDetectorsFromEnvVar(); + const resource = await settle(detectResources({ detectors })); + + expect(detectors).toEqual([envDetector, osDetector]); + expect(resource.attributes).not.toHaveProperty("service.instance.id"); + }); + + it("allows environment configuration to override a generated service instance ID", async () => { + vi.stubEnv("OTEL_RESOURCE_ATTRIBUTES", "service.instance.id=configured-instance"); + + const resource = await settle( + detectResources({ + detectors: [serviceInstanceIdDetector, envDetector], + }), + ); + + expect(resource.attributes["service.instance.id"]).toBe("configured-instance"); + }); + + it("allows an Azure detector to override a generated service instance ID", async () => { + vi.stubEnv("WEBSITE_SITE_NAME", "orders-api"); + vi.stubEnv("WEBSITE_INSTANCE_ID", "azure-instance"); + + const resource = await settle( + detectResources({ + detectors: [serviceInstanceIdDetector, azureAppServiceDetector], + }), + ); + + expect(resource.attributes["service.instance.id"]).toBe("azure-instance"); + }); + + it("preserves an initial service instance ID through the distro's default NodeSDK detection", async () => { + delete process.env.OTEL_NODE_RESOURCE_DETECTORS; + vi.stubEnv("OTEL_RESOURCE_ATTRIBUTES", ""); + const sdk = new NodeSDK({ + autoDetectResources: true, + instrumentations: [], + logRecordProcessors: [], + metricReaders: [], + resource: resourceFromAttributes({ + "service.instance.id": "initial-instance", + }), + resourceDetectors: parseResourceDetectorsFromEnvVar(), + spanProcessors: [], + }); + + try { + sdk.start(); + const sdkResource = (sdk as unknown as { _resource: Resource })._resource; + await settle(sdkResource); + expect(sdkResource.attributes["service.instance.id"]).toBe("initial-instance"); + } finally { + await sdk.shutdown(); + } + }); + + it("keeps a configured environment ID stable across repeated detection", async () => { + vi.stubEnv("OTEL_RESOURCE_ATTRIBUTES", "service.instance.id=configured-instance"); + + const first = await settle( + detectResources({ detectors: [serviceInstanceIdDetector, envDetector] }), + ); + const second = await settle( + detectResources({ detectors: [serviceInstanceIdDetector, envDetector] }), + ); + + expect(first.attributes["service.instance.id"]).toBe("configured-instance"); + expect(second.attributes["service.instance.id"]).toBe("configured-instance"); + }); + + it("gives the Kubernetes pod identity precedence when deriving cloud role instance", () => { + const resource = resourceFromAttributes({ + "k8s.pod.name": "orders-api-7b8f9c6d5-x2k9m", + "service.instance.id": "generated-or-vm-instance", + }); + + expect(getCloudRoleInstance(resource)).toBe("orders-api-7b8f9c6d5-x2k9m"); + }); + + it("falls back to service instance ID when Kubernetes pod identity is unavailable", () => { + const resource = resourceFromAttributes({ + "service.instance.id": "configured-instance", + }); + + expect(getCloudRoleInstance(resource)).toBe("configured-instance"); + }); + + it("generates fresh instance IDs and process IDs in separate worker processes", async () => { + const script = ` + const resources = require("@opentelemetry/resources"); + const resource = resources.detectResources({ + detectors: [resources.processDetector, resources.serviceInstanceIdDetector], + }); + Promise.resolve(resource.waitForAsyncAttributes?.()).then(() => { + console.log(JSON.stringify({ + actualPid: process.pid, + detectedPid: resource.attributes["process.pid"], + instanceId: resource.attributes["service.instance.id"], + })); + }); + `; + + const [firstResult, secondResult] = await Promise.all([ + execFileAsync(process.execPath, ["-e", script], { cwd: process.cwd() }), + execFileAsync(process.execPath, ["-e", script], { cwd: process.cwd() }), + ]); + const first = JSON.parse(firstResult.stdout.trim()) as { + actualPid: number; + detectedPid: number; + instanceId: string; + }; + const second = JSON.parse(secondResult.stdout.trim()) as typeof first; + + expect(first.detectedPid).toBe(first.actualPid); + expect(second.detectedPid).toBe(second.actualPid); + expect(first.actualPid).not.toBe(second.actualPid); + expect(first.instanceId).not.toBe(second.instanceId); + }); +}); diff --git a/test/internal/unit/upstream/telemetrySdkContracts.test.ts b/test/internal/unit/upstream/telemetrySdkContracts.test.ts new file mode 100644 index 00000000..606574a5 --- /dev/null +++ b/test/internal/unit/upstream/telemetrySdkContracts.test.ts @@ -0,0 +1,190 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { + context, + defaultTextMapGetter, + defaultTextMapSetter, + SpanKind, + SpanStatusCode, + trace, + TraceFlags, +} from "@opentelemetry/api"; +import { SeverityNumber } from "@opentelemetry/api-logs"; +import { W3CTraceContextPropagator } from "@opentelemetry/core"; +import { + InMemoryLogRecordExporter, + LoggerProvider, + SimpleLogRecordProcessor, +} from "@opentelemetry/sdk-logs"; +import { + AggregationTemporality, + InMemoryMetricExporter, + MeterProvider, + PeriodicExportingMetricReader, +} from "@opentelemetry/sdk-metrics"; +import { + BasicTracerProvider, + InMemorySpanExporter, + SimpleSpanProcessor, +} from "@opentelemetry/sdk-trace-base"; +import { describe, expect, it } from "vitest"; + +describe("OpenTelemetry SDK contracts used by the distro", () => { + it("exports completed spans with attributes, events, links, status, and exceptions", async () => { + const exporter = new InMemorySpanExporter(); + const provider = new BasicTracerProvider({ + spanProcessors: [new SimpleSpanProcessor(exporter)], + }); + const tracer = provider.getTracer("upstream-contracts", "1.0.0"); + const linkedContext = { + traceId: "0123456789abcdef0123456789abcdef", + spanId: "0123456789abcdef", + traceFlags: TraceFlags.SAMPLED, + isRemote: true, + }; + + const span = tracer.startSpan("dependency-operation", { + attributes: { component: "contract-test" }, + kind: SpanKind.CLIENT, + links: [{ context: linkedContext, attributes: { relationship: "dependency" } }], + }); + span.addEvent("request.sent", { attempt: 1 }); + span.recordException(new Error("expected failure")); + span.setStatus({ code: SpanStatusCode.ERROR, message: "failed" }); + span.end(); + await provider.forceFlush(); + + const [exported] = exporter.getFinishedSpans(); + expect(exported).toMatchObject({ + attributes: { component: "contract-test" }, + kind: SpanKind.CLIENT, + name: "dependency-operation", + status: { code: SpanStatusCode.ERROR, message: "failed" }, + }); + expect(exported.instrumentationScope).toMatchObject({ + name: "upstream-contracts", + version: "1.0.0", + }); + expect(exported.events).toEqual( + expect.arrayContaining([ + expect.objectContaining({ name: "request.sent", attributes: { attempt: 1 } }), + expect.objectContaining({ + name: "exception", + attributes: expect.objectContaining({ + "exception.message": "expected failure", + "exception.type": "Error", + }), + }), + ]), + ); + expect(exported.links).toEqual([ + { + context: linkedContext, + attributes: { relationship: "dependency" }, + }, + ]); + + await provider.shutdown(); + }); + + it("collects counter and histogram measurements with attributes", async () => { + const exporter = new InMemoryMetricExporter(AggregationTemporality.CUMULATIVE); + const reader = new PeriodicExportingMetricReader({ + exporter, + exportIntervalMillis: 60_000, + }); + const provider = new MeterProvider({ readers: [reader] }); + const meter = provider.getMeter("upstream-contracts", "1.0.0"); + const counter = meter.createCounter("requests", { unit: "{request}" }); + const histogram = meter.createHistogram("request.duration", { unit: "ms" }); + + counter.add(2, { route: "/orders" }); + histogram.record(125, { route: "/orders" }); + await provider.forceFlush(); + + const metrics = exporter + .getMetrics() + .flatMap((resourceMetrics) => resourceMetrics.scopeMetrics) + .flatMap((scopeMetrics) => scopeMetrics.metrics); + expect(metrics.map((metric) => metric.descriptor.name)).toEqual( + expect.arrayContaining(["requests", "request.duration"]), + ); + for (const metric of metrics) { + expect(metric.dataPoints).toEqual([ + expect.objectContaining({ attributes: { route: "/orders" } }), + ]); + } + + await provider.shutdown(); + }); + + it("exports structured log records with severity, body, and attributes", async () => { + const exporter = new InMemoryLogRecordExporter(); + const provider = new LoggerProvider({ + processors: [new SimpleLogRecordProcessor({ exporter })], + }); + const logger = provider.getLogger("upstream-contracts", "1.0.0"); + + logger.emit({ + attributes: { component: "checkout" }, + body: { orderId: "order-42" }, + severityNumber: SeverityNumber.INFO, + severityText: "INFO", + }); + await provider.forceFlush(); + + expect(exporter.getFinishedLogRecords()).toEqual([ + expect.objectContaining({ + attributes: { component: "checkout" }, + body: { orderId: "order-42" }, + instrumentationScope: expect.objectContaining({ + name: "upstream-contracts", + version: "1.0.0", + }), + severityNumber: SeverityNumber.INFO, + severityText: "INFO", + }), + ]); + + await provider.shutdown(); + }); + + it("round-trips sampled W3C trace context through text-map propagation", () => { + const propagator = new W3CTraceContextPropagator(); + const spanContext = { + traceId: "0123456789abcdef0123456789abcdef", + spanId: "0123456789abcdef", + traceFlags: TraceFlags.SAMPLED, + }; + const carrier: Record = {}; + const sourceContext = trace.setSpanContext(context.active(), spanContext); + + propagator.inject(sourceContext, carrier, defaultTextMapSetter); + const extractedContext = propagator.extract(context.active(), carrier, defaultTextMapGetter); + + expect(carrier).toEqual({ + traceparent: "00-0123456789abcdef0123456789abcdef-0123456789abcdef-01", + }); + expect(trace.getSpanContext(extractedContext)).toEqual({ + ...spanContext, + isRemote: true, + }); + }); + + it.each([ + "", + "not-a-traceparent", + "00-00000000000000000000000000000000-0123456789abcdef-01", + "00-0123456789abcdef0123456789abcdef-0000000000000000-01", + ])("rejects invalid W3C traceparent value %j", (traceparent) => { + const propagator = new W3CTraceContextPropagator(); + const extractedContext = propagator.extract( + context.active(), + { traceparent }, + defaultTextMapGetter, + ); + + expect(trace.getSpanContext(extractedContext)).toBeUndefined(); + }); +}); From 4cd67752369768b56bc0786e97ddd317b0d77196 Mon Sep 17 00:00:00 2001 From: Hector Hernandez <39923391+hectorhdzg@users.noreply.github.com> Date: Wed, 30 Sep 2026 16:57:26 -0700 Subject: [PATCH 2/2] test: satisfy upstream contract checks Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- CHANGELOG.md | 88 +++++++++++++++++++ .../upstream/azureResourceDetectors.test.ts | 51 ++++++----- .../unit/upstream/resourceDetectors.test.ts | 8 +- 3 files changed, 115 insertions(+), 32 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ff00e5d..897ff6bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,94 @@ ## [Unreleased] +### Bugs Fixed +- Default `InvokeAgentScope` spans to `SpanKind.INTERNAL` while preserving explicit span-kind overrides. [#241](https://github.com/microsoft/opentelemetry-distro-javascript/pull/241) + +### Features Added +- Add typed ExecuteTool argument and result schemas with default schema_version: "1.0", collision-safe `extension_data` emitted under `metadata`, and non-throwing validation aligned with the .NET and Python distros. [#240](https://github.com/microsoft/opentelemetry-distro-javascript/pull/240) +- Add manual `sessionId` propagation to `ExecuteToolScope` and `InferenceScope`, plus opt-in custom baggage enrichment for recognized GenAI spans through `BaggageBuilder.customAttribute()` and `customAttributes()`. [#242](https://github.com/microsoft/opentelemetry-distro-javascript/pull/242) +- Add GenAI v1.42 InvokeAgent request, response, cache-token, and provider attribute capture for manual A365 scopes. [#239](https://github.com/microsoft/opentelemetry-distro-javascript/pull/239) + +### Other Changes +- Add upstream compatibility contracts for OpenTelemetry resource detectors, service instance identity precedence, telemetry SDK pipelines, and W3C trace propagation. [#249](https://github.com/microsoft/opentelemetry-distro-javascript/pull/249) +- Add offline SDK throughput and signed memory benchmarks with raw artifacts and explicitly configured named OTLP log result export. +- Consolidate Dependabot updates for Vitest 4.1.11, Hono 4.13.7, qs 6.16.0, fast-uri 3.1.7, actions/deploy-pages 5.0.1, and actions/checkout 7.0.1. +- Document local npm lockfile regeneration for contributors who cannot access the Microsoft package proxy, while retaining the proxy-generated lockfile. [#245](https://github.com/microsoft/opentelemetry-distro-javascript/pull/245) + +## [1.4.0] - 2026-09-08 + +### Features Added +- Add enabled-by-default A365 durable store-and-forward delivery with explicit disable, network-only fallback when durable storage is unavailable, per-application bounded plaintext storage on protected volumes, replay with fresh tokens/current routing, retryable 401 handling, shared `Retry-After` backoff, token isolation, and bounded shutdown drain. [#226](https://github.com/microsoft/opentelemetry-distro-javascript/pull/226) +- Emit `gen_ai.output.type` and available GenAI request parameters on LangChain chat spans. [#213](https://github.com/microsoft/opentelemetry-distro-javascript/pull/213) +- Add an AKS LangChain auto-instrumentation sample with deployment instructions. [#219](https://github.com/microsoft/opentelemetry-distro-javascript/pull/219) + +### Bugs Fixed +- Fix duplicate Bunyan logs, missing HTTP duration metrics, duplicate request filtering, and incorrect performance-counter values. [#212](https://github.com/microsoft/opentelemetry-distro-javascript/pull/212) +- Report the AKS resource detector SDKStats feature only when the current process detects valid AKS cluster metadata, not from customer-provided attributes or inherited feature flags. [#207](https://github.com/microsoft/opentelemetry-distro-javascript/pull/207) + +### Other Changes +- Align Azure Monitor dependencies with `@azure/monitor-opentelemetry` `1.20.0`: raise the `@azure/monitor-opentelemetry-exporter` floor to `1.0.0-beta.45` and update `@azure/opentelemetry-instrumentation-azure-sdk` to `^1.1.0-beta.1`. +- Retain the matching OpenTelemetry core/SDK `2.10.0` / `0.221.0`, API `1.9.1`, semantic conventions `1.43.0`, and instrumentation dependency ranges used by the published Azure Monitor packages. The exporter update removes its older nested OpenTelemetry core/resources `2.9.0` and logs `0.220.0` copies. +- Align sample OpenTelemetry dependencies with the current release line. [#227](https://github.com/microsoft/opentelemetry-distro-javascript/pull/227) +- Update transitive dependencies flagged by Component Governance. [#223](https://github.com/microsoft/opentelemetry-distro-javascript/pull/223) +- Add integration and performance test workflows and require changelog entries in PR validation. [#217](https://github.com/microsoft/opentelemetry-distro-javascript/pull/217), [#224](https://github.com/microsoft/opentelemetry-distro-javascript/pull/224) +- Consolidate Dependabot updates, pin GitHub Actions to full-length commit SHAs, and update CI and sample dependencies. [#216](https://github.com/microsoft/opentelemetry-distro-javascript/pull/216), [#218](https://github.com/microsoft/opentelemetry-distro-javascript/pull/218), [#220](https://github.com/microsoft/opentelemetry-distro-javascript/pull/220), [#221](https://github.com/microsoft/opentelemetry-distro-javascript/pull/221), [#222](https://github.com/microsoft/opentelemetry-distro-javascript/pull/222) + +## [1.3.0] - 2026-08-03 + +### Features Added +- Add top-level `enableSensitiveData` option to capture GenAI message content (prompts, completions, tool arguments/results, system instructions) for LangChain; content is hidden by default [#192](https://github.com/microsoft/opentelemetry-distro-javascript/pull/192) +- Emit `gen_ai.response.finish_reasons` on LangChain chat spans, extracted from the LLM run output [#196](https://github.com/microsoft/opentelemetry-distro-javascript/pull/196) +- Add console log collection via `@opentelemetry/instrumentation-console`; opt in with `instrumentationOptions: { console: { enabled: true } }` and filter by severity with `APPLICATIONINSIGHTS_INSTRUMENTATION_LOGGING_LEVEL` [#206](https://github.com/microsoft/opentelemetry-distro-javascript/pull/206) + +### Other Changes +- Remove the unused `AZURE_MONITOR_DISTRO_VERSION` env var and its constant; the distro reports its version via `MICROSOFT_OPENTELEMETRY_VERSION` [#188](https://github.com/microsoft/opentelemetry-distro-javascript/pull/188) +- **HTTP spans now use stable HTTP semantic conventions only.** `@opentelemetry/instrumentation-http` `0.221.0` drops support for the old (v1.7.0) HTTP semconv and the `OTEL_SEMCONV_STABILITY_OPT_IN` opt-in. Spans now carry `http.request.method`, `http.response.status_code`, `url.full`, `url.path`, `url.scheme`, `server.address`, and `server.port` instead of `http.method`, `http.status_code`, `http.url`, `http.target`, `http.scheme`, `http.host`, and `net.*`. Azure Monitor request/dependency telemetry and Live Metrics are unaffected — the exporter and the distro read both sets of attributes — but custom span processors, samplers, or filters that read the old attribute names must be updated. +- Bump `@azure/monitor-opentelemetry-exporter` floor to `1.0.0-beta.44` +- Bump `@opentelemetry/*` core/SDK packages to `2.10.0` / `0.221.0` and instrumentation packages to their latest releases +- Bump `@azure/*` core packages and promote `@azure/opentelemetry-instrumentation-azure-sdk` to `^1.0.0` +- Adopt the object-based `BatchLogRecordProcessor` / `SimpleLogRecordProcessor` constructors introduced in `@opentelemetry/sdk-logs` `0.221.0` +- Upgrade the ESLint toolchain to ESLint 10 (with an explicit `@eslint/js` dev dependency) to clear transitive `brace-expansion` advisories +- Numerous Dependabot dependency bumps ([#193](https://github.com/microsoft/opentelemetry-distro-javascript/pull/193), [#194](https://github.com/microsoft/opentelemetry-distro-javascript/pull/194), [#195](https://github.com/microsoft/opentelemetry-distro-javascript/pull/195), [#197](https://github.com/microsoft/opentelemetry-distro-javascript/pull/197), [#198](https://github.com/microsoft/opentelemetry-distro-javascript/pull/198), [#199](https://github.com/microsoft/opentelemetry-distro-javascript/pull/199), [#200](https://github.com/microsoft/opentelemetry-distro-javascript/pull/200), [#201](https://github.com/microsoft/opentelemetry-distro-javascript/pull/201), [#202](https://github.com/microsoft/opentelemetry-distro-javascript/pull/202), [#203](https://github.com/microsoft/opentelemetry-distro-javascript/pull/203), [#204](https://github.com/microsoft/opentelemetry-distro-javascript/pull/204), [#205](https://github.com/microsoft/opentelemetry-distro-javascript/pull/205)) + +## [1.2.0] - 2026-07-02 + +### Bugs Fixed +- A365: forward `a365.maxQueueSize`, `a365.scheduledDelayMilliseconds`, `a365.maxExportBatchSize`, and `a365.exporterTimeoutMilliseconds` to the `BatchSpanProcessor` that wraps the `Agent365Exporter`. +- A365: add a configurable timeout to agentic token exchange so an unresponsive STS no longer hangs telemetry exports indefinitely + +### Other Changes +- Raise minimum supported Node.js version to 22, aligning with OpenTelemetry and Azure SDK support policies +- Bump `@azure/monitor-opentelemetry-exporter` floor to `1.0.0-beta.43` +- Bump `@opentelemetry/*` core/SDK packages to `2.8.0` / `0.219.0` + +## [1.1.0] - 2026-05-29 + +### Features Added +- Add network SDKStats (request success) for A365 + OTLP exporters [#145](https://github.com/microsoft/opentelemetry-distro-javascript/pull/145) +- Update message format to align with OTel spec [#155](https://github.com/microsoft/opentelemetry-distro-javascript/pull/155) +- Add `ApplyGuardrailScope` for security guardrail tracing [#157](https://github.com/microsoft/opentelemetry-distro-javascript/pull/157) +- Add main agent attribution propagation for GenAI spans [#158](https://github.com/microsoft/opentelemetry-distro-javascript/pull/158) +- Add `ContextualTokenResolver` with agentic user ID support [#159](https://github.com/microsoft/opentelemetry-distro-javascript/pull/159) + +### Bugs Fixed +- LangChain: split request/response model and emit `gen_ai.response.id` [#127](https://github.com/microsoft/opentelemetry-distro-javascript/pull/127) +- Add product context fallback for subchannels [#133](https://github.com/microsoft/opentelemetry-distro-javascript/pull/133) +- Revert `getCallerBaggagePairs` userId fallback [#140](https://github.com/microsoft/opentelemetry-distro-javascript/pull/140) +- Scope `A365SpanProcessor.onStart` to GenAI spans only [#146](https://github.com/microsoft/opentelemetry-distro-javascript/pull/146) +- Respect `Retry-After` header on 429/503 responses in `Agent365Exporter` [#147](https://github.com/microsoft/opentelemetry-distro-javascript/pull/147) +- LangChain: populate `gen_ai.response.model` for Responses API (`useResponsesApi`) [#151](https://github.com/microsoft/opentelemetry-distro-javascript/pull/151) + +### Other Changes +- Update telemetry SDK name to `microsoft-opentelemetry` [#134](https://github.com/microsoft/opentelemetry-distro-javascript/pull/134) +- Correct repository URLs from Azure to microsoft org and add API docs badge [#136](https://github.com/microsoft/opentelemetry-distro-javascript/pull/136) +- Only redeploy docs when package version changes [#141](https://github.com/microsoft/opentelemetry-distro-javascript/pull/141) +- Rename internal Statsbeat symbols to SdkStats (cleanup) [#152](https://github.com/microsoft/opentelemetry-distro-javascript/pull/152) +- Add `needs-author-feedback` GitHub Actions workflows [#160](https://github.com/microsoft/opentelemetry-distro-javascript/pull/160) +- Bump `@opentelemetry/*` core/SDK packages to 0.218.0 / 2.7.1 and instrumentation packages to latest releases +- Bump `@azure/monitor-opentelemetry-exporter` floor to `1.0.0-beta.42` +- Numerous Dependabot dependency bumps ([#137](https://github.com/microsoft/opentelemetry-distro-javascript/pull/137), [#138](https://github.com/microsoft/opentelemetry-distro-javascript/pull/138), [#139](https://github.com/microsoft/opentelemetry-distro-javascript/pull/139), [#143](https://github.com/microsoft/opentelemetry-distro-javascript/pull/143), [#144](https://github.com/microsoft/opentelemetry-distro-javascript/pull/144), [#148](https://github.com/microsoft/opentelemetry-distro-javascript/pull/148), [#149](https://github.com/microsoft/opentelemetry-distro-javascript/pull/149), [#153](https://github.com/microsoft/opentelemetry-distro-javascript/pull/153)) + ## [1.0.2] - 2026-05-11 ### Bugs Fixed diff --git a/test/internal/unit/upstream/azureResourceDetectors.test.ts b/test/internal/unit/upstream/azureResourceDetectors.test.ts index 56161dc5..88d7eacf 100644 --- a/test/internal/unit/upstream/azureResourceDetectors.test.ts +++ b/test/internal/unit/upstream/azureResourceDetectors.test.ts @@ -45,35 +45,34 @@ async function detect(detector: ResourceDetector) { } function mockVmRequest( - result: - | { body: string; statusCode: number } - | { error: Error }, + result: { body: string; statusCode: number } | { error: Error }, onOptions?: (options: unknown) => void, ) { - return vi.spyOn(http, "request").mockImplementation( - ((options: unknown, callback: (response: IncomingMessage) => void) => { - onOptions?.(options); - const request = new EventEmitter() as ClientRequest; - request.destroy = vi.fn(); - request.end = () => { - queueMicrotask(() => { - if ("error" in result) { - request.emit("error", result.error); - return; - } - - const response = new EventEmitter() as IncomingMessage; - response.statusCode = result.statusCode; - response.setEncoding = vi.fn(); - callback(response); - response.emit("data", result.body); - response.emit("end"); - }); - return request; - }; + return vi.spyOn(http, "request").mockImplementation((( + options: unknown, + callback: (response: IncomingMessage) => void, + ) => { + onOptions?.(options); + const request = new EventEmitter() as ClientRequest; + request.destroy = vi.fn(); + request.end = () => { + queueMicrotask(() => { + if ("error" in result) { + request.emit("error", result.error); + return; + } + + const response = new EventEmitter() as IncomingMessage; + response.statusCode = result.statusCode; + response.setEncoding = vi.fn(); + callback(response); + response.emit("data", result.body); + response.emit("end"); + }); return request; - }) as unknown as typeof http.request, - ); + }; + return request; + }) as unknown as typeof http.request); } describe("@opentelemetry/resource-detector-azure contracts", () => { diff --git a/test/internal/unit/upstream/resourceDetectors.test.ts b/test/internal/unit/upstream/resourceDetectors.test.ts index 30b5d617..ed49619c 100644 --- a/test/internal/unit/upstream/resourceDetectors.test.ts +++ b/test/internal/unit/upstream/resourceDetectors.test.ts @@ -119,8 +119,7 @@ describe("@opentelemetry/resources contracts", () => { it("generates a distinct RFC 4122 service instance ID for each detection", async () => { const first = await detect([serviceInstanceIdDetector]); const second = await detect([serviceInstanceIdDetector]); - const uuidPattern = - /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; + const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; expect(first.attributes["service.instance.id"]).toMatch(uuidPattern); expect(second.attributes["service.instance.id"]).toMatch(uuidPattern); @@ -143,10 +142,7 @@ describe("@opentelemetry/resources contracts", () => { it("preserves matching schema URLs and drops conflicting schema URLs", () => { const original = resourceFromAttributes({ original: true }, { schemaUrl: "schema-v1" }); const matching = resourceFromAttributes({ matching: true }, { schemaUrl: "schema-v1" }); - const conflicting = resourceFromAttributes( - { conflicting: true }, - { schemaUrl: "schema-v2" }, - ); + const conflicting = resourceFromAttributes({ conflicting: true }, { schemaUrl: "schema-v2" }); expect(original.merge(matching).schemaUrl).toBe("schema-v1"); expect(original.merge(conflicting).schemaUrl).toBeUndefined();