Repository: microsoft/ApplicationInsights-node.js (ts)

Issue:
Allow the use of startOperation for non-HTTP functions or expose CorrelationContextManager
SDK version: 1.8.4

I would like to correlate custom traces and custom events in my function app, as currently they all show up in AI with no correlation, with the exception of when I use `context.log` instead of `trackTrace` or `trackEvent`, etc. I've attempted several suggestions from past issues and docs with no real luck. The docs [here](https://github.com/microsoft/ApplicationInsights-node.js#setting-up-auto-correlation-for-azure-functions) suggest using the `wrapWithCorrelationContext` and `startOperation` methods. `startOperation` looks almost exactly like what I need: take the function context and start an AI opertaion based off its data and pass it along, but it must also be passed  an`HttpRequest`. A simplified example of my function using a Kafka trigger:

```
appInsights
    .setup(process.env.APPINSIGHTS_INSTRUMENTATIONKEY)
    .setDistributedTracingMode(appInsights.DistributedTracingModes.AI_AND_W3C)
    .setAutoDependencyCorrelation(true)
    .setAutoCollectRequests(true)
    .setAutoCollectPerformance(true, true)
    .setAutoCollectExceptions(true)
    .setAutoCollectDependencies(true)
    .setAutoCollectConsole(true)
    .setUseDiskRetryCaching(true)
    .setSendLiveMetrics(true)
    .start();
const telemetryClient = appInsights.defaultClient;

const kafkaTrigger: AzureFunction = async (context: Context, event: Buffer): Promise<void> => {
    const correlationContext = appInsights.getCorrelationContext(); // <- null
    
    context.log.info('Incoming event (context log)'); // Shows up in overall function trace in AI
    context.log.info('Another context log'); // Shows up in the same correlated trace as the above message in AI

    // Shows up in its own trace in AI
    telemetryClient.trackTrace({
            message: `Incoming event (trackTrace)`,
            severity: appInsights.Contracts.SeverityLevel.Information,
        });

    // Shows up in its own trace in AI
    telemetryClient.trackTrace({
            message: `Another trace (trackTrace)`,
            severity: appInsights.Contracts.SeverityLevel.Information,
        });
};
```

The correlation context above is `null` from the start of the function. Then the suggested use of `wrapWithCorrelationContext` falls apart:

```
// Default export wrapped with Application Insights FaaS context propagation
export default async function contextPropagatingTrigger(context: Context, event: Buffer): Promise<void> {
    // Start an AI Correlation Context using the provided Function context
    const correlationContext = appInsights.getCorrelationContext(); // <- Still null.

    // Wrap the Function runtime with correlationContext
    return appInsights.wrapWithCorrelationContext(async () => {
        appInsights.startOperation(context); // <- Can't event use this because I don't have an HttpRequest or IncomingMessage

        await kafkaTrigger(context, event);
    }, correlationContext)();
}
```

The issue [here](https://github.com/Microsoft/ApplicationInsights-node.js/issues/401) is basically exactly what I'm trying to solve, and the suggested solution was to set the override tag for operation ID to the invocation ID from the function context, but that was in 2018 and that's not what all other auto-collected traces and `context.log` messages appear to be using anymore. 

The doc [here](https://docs.microsoft.com/en-us/azure/azure-functions/functions-monitoring?tabs=cmd#version-2x-and-later-3) suggests setting the override tag for operation ID to `context.traceContext.traceparent`, but that value appears to be in the format of `00-<operation ID>-<parent ID>-00`. _Almost_ what I need. The operation ID and parent ID within that string _are_ what the auto-collected traces are using. I suppose I could manually parse that myself, but it seems like bad practice since it's guesswork on my part, it could change, and it's essentially what the innards of `CorrelationContextManager.startOperation` (by way of using the `Traceparent` class) is doing anyway. If I can at least re-use the existing logic for parsing `traceparent` to access operation ID and parent ID, that would be very helpful.

Is there some other way that I'm missing to get non-HTTP trigger events and traces correlated?


Pull request description:
feat: allow more generic startOperation calls
Closes #665 

Adds support for a few `startOperation` patterns that don't require an incoming HTTP request object.

- Adds support for `startOperation(context: AzureFunction.Context, operationName: string)`
- Adds support for `startOperaiton(spanContext: SpanContext, operationName: string)`
- Exports some traceId, spanId generator utils

Patch:
diff --git a/AutoCollection/CorrelationContextManager.ts b/AutoCollection/CorrelationContextManager.ts
index 1cf7101..885c551 100644
--- a/AutoCollection/CorrelationContextManager.ts
+++ b/AutoCollection/CorrelationContextManager.ts
@@ -174,20 +174,41 @@ export class CorrelationContextManager {
         this.enabled = true;
     }
 
-    public static startOperation(context: azureFunctionsTypes.Context | (http.IncomingMessage | azureFunctionsTypes.HttpRequest), request?: azureFunctionsTypes.HttpRequest): CorrelationContext | null {
+    public static startOperation(context: azureFunctionsTypes.Context | (http.IncomingMessage | azureFunctionsTypes.HttpRequest) | ISpanContext, request?: azureFunctionsTypes.HttpRequest | string): CorrelationContext | null {
         const traceContext = context && (context as azureFunctionsTypes.Context).traceContext || null;
+        const spanContext = context && (context as ISpanContext).traceId
+            ? context as ISpanContext
+            : null;
         const headers = context && (context as http.IncomingMessage | azureFunctionsTypes.HttpRequest).headers;
 
+        if (spanContext) {
+            const traceparent = new Traceparent(`00-${spanContext.traceId}-${spanContext.spanId}-01`);
+            const tracestate = new Tracestate(spanContext.tracestate);
+            const correlationContext = CorrelationContextManager.generateContextObject(
+                spanContext.traceId,
+                `|${spanContext.traceId}.${spanContext.spanId}.`,
+                typeof request === "string" ? request : "",
+                undefined,
+                traceparent,
+                tracestate,
+            );
+            return correlationContext;
+        }
+
         // AzFunction TraceContext available
         if (traceContext) {
             const traceparent = new Traceparent(traceContext.traceparent);
             const tracestate = new Tracestate(traceContext.tracestate);
-            const parser = new HttpRequestParser(request);
+            const parser = typeof request === "object"
+              ? new HttpRequestParser(request)
+              : null;
             const correlationContext = CorrelationContextManager.generateContextObject(
                 traceparent.traceId,
                 traceparent.parentId,
-                parser.getOperationName({}),
-                parser.getCorrelationContextHeader(),
+                typeof request === "string"
+                    ? request
+                    : parser.getOperationName({}),
+                parser && parser.getCorrelationContextHeader() || undefined,
                 traceparent,
                 tracestate,
             );
diff --git a/Library/Util.ts b/Library/Util.ts
index 176060e..147ebbe 100644
--- a/Library/Util.ts
+++ b/Library/Util.ts
@@ -105,6 +105,10 @@ class Util {
         return oct.substr(0, 8) + oct.substr(9, 4) + "4" + oct.substr(13, 3) + clockSequenceHi + oct.substr(16, 3) + oct.substr(19, 12);
     }
 
+    public static w3cSpanId() {
+        return Util.w3cTraceId().substring(16);
+    }
+
     public static isValidW3CId(id: string): boolean {
         return id.length === 32 && id !== "00000000000000000000000000000000";
     }
diff --git a/applicationinsights.ts b/applicationinsights.ts
index b259e4d..b865805 100644
--- a/applicationinsights.ts
+++ b/applicationinsights.ts
@@ -8,6 +8,11 @@ import AutoCollectHttpRequests = require("./AutoCollection/HttpRequests");
 import CorrelationIdManager = require("./Library/CorrelationIdManager");
 import Logging = require("./Library/Logging");
 import QuickPulseClient = require("./Library/QuickPulseStateManager");
+import Traceparent = require("./Library/Traceparent");
+import Tracestate = require("./Library/Tracestate");
+import HttpRequestParser = require("./AutoCollection/HttpRequestParser");
+import { IncomingMessage } from "http";
+import { ISpanContext } from "diagnostic-channel";
 import * as azureFunctionsTypes from "./Library/Functions";
 
 import { AutoCollectNativePerformance, IDisabledExtendedMetrics } from "./AutoCollection/NativePerformance";
@@ -16,10 +21,6 @@ import { AutoCollectNativePerformance, IDisabledExtendedMetrics } from "./AutoCo
 // They're exposed using "export import" so that types are passed along as expected
 export import TelemetryClient = require("./Library/NodeClient");
 export import Contracts = require("./Declarations/Contracts");
-import Traceparent = require("./Library/Traceparent");
-import Tracestate = require("./Library/Tracestate");
-import HttpRequestParser = require("./AutoCollection/HttpRequestParser");
-import { IncomingMessage } from "http";
 
 export enum DistributedTracingModes {
     /**
@@ -155,9 +156,10 @@ export function getCorrelationContext(): CorrelationContextManager.CorrelationCo
  * **(Experimental!)**
  * Starts a fresh context or propagates the current internal one.
  */
+export function startOperation(context: ISpanContext, name: string): CorrelationContextManager.CorrelationContext | null;
 export function startOperation(context: azureFunctionsTypes.Context, request: azureFunctionsTypes.HttpRequest): CorrelationContextManager.CorrelationContext | null;
 export function startOperation(context: IncomingMessage | azureFunctionsTypes.HttpRequest, request?: never): CorrelationContextManager.CorrelationContext | null;
-export function startOperation(context: azureFunctionsTypes.Context | (IncomingMessage | azureFunctionsTypes.HttpRequest), request?: azureFunctionsTypes.HttpRequest): CorrelationContextManager.CorrelationContext | null {
+export function startOperation(context: azureFunctionsTypes.Context | (IncomingMessage | azureFunctionsTypes.HttpRequest) | (ISpanContext), request?: azureFunctionsTypes.HttpRequest | string): CorrelationContextManager.CorrelationContext | null {
     return CorrelationContextManager.CorrelationContextManager.startOperation(context, request);
 }
 


Test patch:
diff --git a/Tests/AutoCollection/CorrelationContextManager.tests.ts b/Tests/AutoCollection/CorrelationContextManager.tests.ts
index 068d37b..7e37c3c 100644
--- a/Tests/AutoCollection/CorrelationContextManager.tests.ts
+++ b/Tests/AutoCollection/CorrelationContextManager.tests.ts
@@ -4,6 +4,7 @@ import * as azureFunctionTypes from "../../Library/Functions";
 import assert = require("assert");
 import sinon = require("sinon");
 import Traceparent = require("../../Library/Traceparent");
+import { ISpanContext } from "diagnostic-channel";
 
 const customProperties = {
     getProperty(prop: string) {return ""},
@@ -232,6 +233,10 @@ if (CorrelationContextManager.isNodeVersionCompatible()) {
         });
 
         describe("#startOperation()", () => {
+            const spanContext: ISpanContext = {
+                traceId: '5e84aff3af474588a42dcbf3bd1db95f',
+                spanId: '1fc066fb77fa43a3',
+            };
             const functionContext: azureFunctionTypes.TraceContext = {
                 traceparent: "00-5e84aff3af474588a42dcbf3bd1db95f-1fc066fb77fa43a3-00",
                 tracestate: "",
@@ -253,7 +258,7 @@ if (CorrelationContextManager.isNodeVersionCompatible()) {
             };
 
             describe("#Azure Functions", () => {
-                it("should start a new context", () => {
+                it("should start a new context with 2nd arg http request", () => {
                     const context = CorrelationContextManager.startOperation({ traceContext: functionContext } as any, request)
                     const traceparent = new Traceparent(functionContext.traceparent);
 
@@ -263,6 +268,28 @@ if (CorrelationContextManager.isNodeVersionCompatible()) {
                     assert.deepEqual(context.operation.traceparent, traceparent);
                     assert.deepEqual(context.operation.name, "GET /search");
                 });
+
+                it("should start a new context with 2nd arg string", () => {
+                    const context = CorrelationContextManager.startOperation({ traceContext: functionContext } as any, "GET /foo")
+                    const traceparent = new Traceparent(functionContext.traceparent);
+
+                    assert.ok(context.operation);
+                    assert.deepEqual(context.operation.id, traceparent.traceId);
+                    assert.deepEqual(context.operation.parentId, `|${traceparent.traceId}.${traceparent.spanId}.`);
+                    assert.deepEqual(context.operation.traceparent, traceparent);
+                    assert.deepEqual(context.operation.name, "GET /foo");
+                });
+            });
+
+            describe("#SpanContext", () => {
+                it("should start a new context", () => {
+                    const context = CorrelationContextManager.startOperation(spanContext, "GET /example");
+
+                    assert.ok(context.operation);
+                    assert.deepEqual(context.operation.id, spanContext.traceId);
+                    assert.deepEqual(context.operation.parentId, `|${spanContext.traceId}.${spanContext.spanId}.`);
+                    assert.deepEqual(context.operation.name, "GET /example");
+                });
             });
         });
     });