DataDog Tracer on Meteor, has anyone done it before?

Hello I was wondering if anyone has done this to track their meteor apps https://docs.datadoghq.com/tracing/setup/nodejs/ I tried adding it following this guide but haven’t succed as Im not getting anything on the dd instance.

Thanks!

You could start the production build with -r argument, like:

“node -r trace.js bundle/main.js”

and in trace.js:

require(‘dd-trace’).init()

1 Like

Thanks, Im just lost on how to add this if Im building my app on aws pipelines, I have a staged step where I call meteor build build/ --allow-superuser --directory --architecture os.linux.x86_64 after that I build a docker image with this build docker build --build-arg NODE_VERSION=8.11.0 -t $REPOSITORY_URI:latest .
So on those steps where should I add this node -r trace.js bundle/main.js call?

Thanks again!

In the Dockerfile itself where COMMAND or ENTRYPOINT is defined. I don’t know if it’s exactly that line of code and I don’t know if it’s going to work, it’s just the concept using that command line option to preload datadog:

https://nodejs.org/api/cli.html#cli_r_require_module

So please post here once you were successful :wink:

1 Like

Great! thanks again for the help @kaufmae! and sure, as soon as I get it going will post back.

I embarked on this journey last week and have had partial success.

I created my own server-side instrumentation around meteor methods using the opentracing api and it works fine in isolation. Trace id correlation, though, does not, and I suspect that it has something to do with fibers because I seem to get trace ids from the dd-trace-patched mongodb driver mixed up with the trace ids from my manual instrumentation in my dd-trace-patched winston logging calls.

This is my call chain:

registered meteor method invocation
  tracer.startSpan
     error logging wrapper
         code that does actual work, including mongodb queries
     return to logging wrapper
  span.finish()

What I noticed was that the error logging wrapper which uses winston to log json to aws cloudwatch would get the wrong traceid when logging a caught and re-thrown error, in one instance one that belonged to a mongodb span instead of the span that was opened around it.

This made the datadog feature to see log entries for a specific trace not work

If I figure this out I will let you know.

Here’s my instrumentation code in case it helps:

export const outputWithTraceId = (span: opentracing.Span, message: string) => {
  return; // no-op for now
  const context = span.context();
  const traceId = context.toTraceId();
  process.stdout.write(`traceId: ${traceId} - ${message}\n`);
};

const handleError = (span: opentracing.Span, name: string, e: any) => {
  outputWithTraceId(span, `handling error for ${name}`);
  const errorMessage = getErrorMessage(e);
  span.log({
    event: "error",
    message: errorMessage,
    error: e
  }); // span.log does NOT work with datadog so this line does nothing
  span.setTag(opentracing.Tags.ERROR, true);
  span.setTag("errorMessage", errorMessage);
  span.finish();
};

const wrap = <T extends (...args: any[]) => any>(fn: T, name: string) =>
  function wrappedMethod(...args: Parameters<T>) {
    const tracer = opentracing.globalTracer();

    // https://docs.datadoghq.com/tracing/trace_search_and_analytics/?tab=nodejs
    const tags: {
      [key: string]: string | boolean;
    } = {
      [ResourceNameTag]: name,
      [AnalyticsTag]: true
    };
    if (this.userId) {
      tags["user.id"] = this.userId;
    }
    const userSpan = tracer.startSpan("meteorCall", {
      tags
    });

    outputWithTraceId(userSpan, `new span for ${name}`);

    try {
      const result: ReturnType<T> = fn.bind(this)(...args);
      userSpan.log({
        event: "request_end"
      });
      outputWithTraceId(userSpan, `success - finishing for ${name}`);
      userSpan.finish();

      return result;
    } catch (e) {
      handleError(userSpan, name, e);
      throw e;
    }
  };

function wrapAllExistingMethods() {
  const meteor = Meteor as any;
  const defaultServer = meteor.default_server;
  const handlers: { [k: string]: (...args: any[]) => any } =
    defaultServer.method_handlers;
  Object.keys(handlers).forEach(key => {
    const fn = handlers[key];
    const wrapped = wrap(fn, key);
    handlers[key] = wrapped;
  });
}

// First time in, wrap existing methods
wrapAllExistingMethods();

export const wrapMethodsForTracing = <
  T extends { [k in keyof T]: (...args: any[]) => any }
>(
  methods: T
): T => {
  const wrappedMethods = { ...methods };
  for (const name of Object.keys(methods)) {
    const method = methods[name as keyof T];
    wrappedMethods[name] = wrap(method, name);
  }
  return wrappedMethods;
};

1 Like

Did you have any luck with implementing this?

I’ve imported the tracer early on in server code and it catches certain services, but I believe it’s missing quite a bit. Wondering where you wound up on this one.

Hey @typ no luck, tried a lot of stuff and I just gave up, nothing really worked, sorry I don’t have a solution for you.

I’ve seen this presentation: https://www.youtube.com/watch?v=GGBIuDJauYo
by @npvn and Oli from Pathable. Perhaps they could elaborate a bit on how they got the integration working?

1 Like

Hi @batist, we actually don’t use the Node.js tracer, but record and push metrics via the Datadog API instead.

Our metrics include DDP response time (we recently released the script for it here), and all Galaxy internal metrics (which we pull from Galaxy and push to Datadog, using another script that we hope to open source soon).

These metrics help us build a very informative dashboard (as you can see in the presentation) without relying on any Datadog agent.

2 Likes

It works fine for me. I wrap all DDP methods and get nice flame graphs etc.

Setup (once)

import tracer from "dd-trace";
import opentracing from "opentracing";
import { StatsD, Tags } from "hot-shots";
import { ANALYTICS, RESOURCE_NAME } from "dd-trace/ext/tags";

const env = "production"; // or some other value

  const traceEnabled = !!getSettingFromEnvironment("DATADOG_TRACE");
  tracer.init({
    enabled: traceEnabled,
    service: "my app",
    analytics: true,
    logInjection: true,
    tags: { env },
  });
  opentracing.initGlobalTracer(tracer);

export const getTracingScope = () => tracer.scope();

const statsD = new StatsD(
  Meteor.isTest ? { mock: true } : { globalTags: { env } }
);

export const reportTiming = (stat: string, time: number, tags: Tags = []) => {
  statsD.timing(`refapp.${stat}`, time, undefined, tags);
};

export const reportIncrement = (stat: string, tags: Tags = []) => {
  statsD.increment(`refapp.${stat}`, tags);
};

export { Tags, ANALYTICS as AnalyticsTag, RESOURCE_NAME as ResourceNameTag };

and then I wrap all Meteor methods to get both metrics and tracing:

const handleError = (
  span: opentracing.Span,
  errorCounter: string,
  tags: Tags,
  e: any
) => {
  reportIncrement(errorCounter, tags);

  const errorMessage = getErrorMessage(e);
  span.log({
    event: "error",
    message: errorMessage,
    error: e,
  });
  span.setTag(opentracing.Tags.ERROR, true);
  span.setTag("errorMessage", errorMessage);
  span.finish();
};

const wrap = <T extends (...args: any[]) => any>(fn: T, name: string) =>
  function wrappedMethod(
    this: Meteor.MethodThisType,
    ...args: Parameters<T>
  ): ReturnType<T> {
    const timer = startTimer();
    const metricTags = { method: name };
    // https://docs.datadoghq.com/tracing/trace_search_and_analytics/?tab=nodejs
    const tags: {
      [key: string]: string | boolean;
    } = {
      [ResourceNameTag]: name,
      [AnalyticsTag]: true,
    };
    const { ipAddress, userAgent } = getRequestInfo(this);
    tags.ipAddress = ipAddress;
    if (userAgent) {
      tags.userAgent = userAgent;
    }
    if (this.userId) {
      tags["user.id"] = this.userId;
    }
    const tracer = opentracing.globalTracer();
    const userSpan = tracer.startSpan("meteorCall", {
      tags,
    });
    const scope = getTracingScope();
    return scope.activate(userSpan, () => {
      reportIncrement("ddp.count", metricTags);

      try {
        const result: ReturnType<T> = fn.bind(this)(...args);
        userSpan.log({
          event: "request_end",
        });
        userSpan.finish();

        reportIncrement("ddp.success", metricTags);
        timer.reportAndReset("ddp.elapsed", metricTags);

        return result;
      } catch (e) {
        handleError(userSpan, "ddp.error", metricTags, e);
        throw e;
      }
    });
  };

function wrapAllExistingMethods() {
  const handlers = getRegisteredMethods();
  Object.keys(handlers).forEach((key) => {
    const fn = handlers[key];
    const wrapped = wrap(fn, key);
    handlers[key] = wrapped;
  });
}

// First time in, wrap existing methods
wrapAllExistingMethods();

getRegisteredMethods is

/**
 * Returns the object containing all registered methods (login etc)
 */
export const getRegisteredMethods = (): {
  [k: string]: (...args: any[]) => any;
} => {
  const meteor = Meteor as any;
  const defaultServer = meteor.default_server;
  return defaultServer.method_handlers;
};

Are you still able to use dd-trace-js? For me, the tracer’s init crashes immediately. I opened an issue with Datadog, and they say it’s due to incompatibility with Meteor: Getting `TypeError: Cannot convert undefined or null to object` error when starting Meteor app · Issue #2090 · DataDog/dd-trace-js · GitHub

1 Like

Do you use DD for Frontend or Backend or both?