Skip to content

Instantly share code, notes, and snippets.

@polotek
Last active August 3, 2021 00:13
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save polotek/0c79c3b8b61c3bb8ace7408cf74728a5 to your computer and use it in GitHub Desktop.
Save polotek/0c79c3b8b61c3bb8ace7408cf74728a5 to your computer and use it in GitHub Desktop.
How to use the honeycomb javascript libraries

Using node callback

beeline.startAsyncSpan({
    task: "writing a file",
    filePath,
}, span => {
  writeFile(filePath, fileContents, (err, fileData) => {
    if(err) {
      beeline.addContext({fileError: err.toString()});
    }
    beeline.finishSpan(span);
  });
});

Using promises

beeline.startAsyncSpan({
    task: "writing a file",
    filePath,
}, span => {
  writeFile(filePath, fileContents)
    .then(fileData => {
      beeline.finishSpan(span);
    })
    .catch(err => {
      beeline.addContext({fileError: err.toString()});
      beeline.finishSpan(span);
    });
});

If your code runs on newer js engines, you can use finally() with promises and avoid the repetition.

beeline.startAsyncSpan({
    task: "writing a file",
    filePath,
}, span => {
  writeFile(filePath, fileContents)
    .catch(err => { beeline.addContext({fileError: err.toString()}); })
    .finally(() => { beeline.finishSpan(span); });
});

Using async/await

beeline.startAsyncSpan({
    task: "writing a file",
    filePath,
}, async (span) => {
  try {
    const fileData = await writeFile(filePath, fileContents);
  } catch(err) {
    beeline.addContext({fileError: err.toString()});
  } finally {
    beeline.finishSpan(span);
  }
});

So we have a wrapper around beeline in one of our node projects. I added this function that makes async spans a little nicer to use. You should also be able to just add it as a method on a subclass of the beeline client.

withAsyncSpan(spanData: any, spanFn: Function): Promise<any> {
  return new Promise((resolve, reject) => {

    const value = this.startAsyncSpan(spanData, (span: any) => {
      let innerValue;
      try {
        innerValue = spanFn(span);
      } catch(err) {
        // catch errors here and update the span
        this.addContext({error: `${err}`});

        // re-throw here so the calling function can
        // decide to do something about the error
        throw err;
      } finally {
        // If it's not a promise and the spanFn throws
        // this is our only chance to finish the span!
        if(!isPromise(innerValue)) {
          this.finishSpan(span);
        }
      }

      if(isPromise(innerValue)) {
        innerValue
          .catch((err: Error) => {
            // catch errors here and update the span
            this.addContext({error: `${err}`});
            throw err;
          })
          .finally(() => {
            this.finishSpan(span);
          });
      }

      return innerValue;
    });

    // Now that we have the return value we just forward it
    if(isPromise(value)) {
      value
        .then(resolve)
        .catch(reject);
    } else {
      resolve(value);
    }
  });
}

function isPromise(p: any) {
  return p && typeof p.then === 'function';
}

With startAsyncSpan you have to be responsible for finishing the span properly yourself. This method enables you to use async/await or just return a promise, and the span will be finished properly. You can still add context to the span inside your function. Here's a contrived usage example usage.

await observability.withAsyncSpan({
  name: 'Get widget',
  type: 'app'
}, async (span) => {
  const widget = await this.getWidget();
  if(widget.spinner) {
    span.addContext({widget_type: 'spinner'});
  }
  return widget;
});

There may be edge case problems with this. It needs tests. Not to mention the usage is pretty ugly IMO. But I've been using it, and it has been working very well so far.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment