Open24

NewRelic「are not instrumented」を計装して解決したい

seoinkseoink

前提

  • Node.js
  • Typescript
  • gRPC

gRPCのMethodの単位までは見えているけど、

Application code in (WebTransaction/WebFrameworkUri/gRPC//Service/Method)

以下の詳細は見えない

Deeper visibility is not available because these classes and methods are not instrumented with the NodeJS Agents current configuration. Consult our documentation to find out how to add custom metrics to your app.

seoinkseoink

他の未計装の表示(Uninstrumented time)

We detected a gap that may indicate an opportunity to add deeper instrumentation for this entity.
See our docs

seoinkseoink

チュートリアルを発見したので読んでみます

https://newrelic.github.io/node-newrelic/tutorial-Webframework-Simple.html

ExpressっぽいWebサーバーに計装を施す流れ。
以下は機能一覧:

  • /api/users: userのリストをJSONで返す
  • /home: HTMLをレンダリングして返す
  • 他: 認証などのミドルウェアがある
seoinkseoink

計装関数

Webフレームワークを計装するために、NewRelicにフレームワークのどのメソッドを計装するか、どのようなメトリクスやデータを収集するかを定義する 計装関数 を作る必要がある。
それを newrelic.instrumentWebframework() でエージェントに登録すると、適宜呼び出してくれるようになる。
(計装関数は、計装されたアプリ内に書いてネストすることもできるっぽい)

こういう計装関数を書いて

function instrumentMyWebFramework(shim, myModule, moduleName) {

こうやって登録する

import newrelic from "newrelic"
newrelic.instrumentWebframework('my-web-framework', instrumentMyWebFramework)

ここで、計装関数の定義には shim: WebFrameworkShim が必要になる。
なんだいそれは…

seoinkseoink

計装の基礎

https://newrelic.github.io/node-newrelic/tutorial-Instrumentation-Basics.html
順番が前後してる感じがするけど、shimのことを知りたかったら読めと書いてたので、読んでいく。

計装の目的

  1. サーバーで起こっていることの詳細な情報をユーザーに提供すること
    • 計装されたものが多いほどグラフが詳細になる
  2. トランザクションコンテキストを維持すること

カスタム計装関数をNewRelicに追加する

APIを通して、エージェントにモジュールと計装関数の紐づけを登録しておくと、エージェントがよしなに読み込んでくれるようになる。
このとき。計装対象のモジュールがNodeに読み込まれる前に、登録しておく必要がある。

import newrelic from "newrelic"
newrelic.instrumentWebframework('my-web-framework', instrumentMyWebFramework)

// 別ファイル
import myWebFramework from "my-web-framework"

instrumentWebframework()といったAPIは Shim を返す(今回はWebFrameworkShim)

計装関数のデバッグ中は、APIの第3引数で計装関数由来のエラーを握りつぶすとよい

seoinkseoink

instrumentWebframeworkに与える計装関数を定義するためにshimが必要になるからと寄り道をしたはずなのに、instrumentWebframeworkshimを返すとか言われてちょっと混乱してる。
なんか間違ってるかな…

seoinkseoink

いろいろ読んでたけど、WebTransaction/WebFrameworkUri/gRPC//Service/Methodまでは追えているのだから、やるべきはトランザクション内でインストゥルメンテーションを拡張することかもしれない

seoinkseoink

もしかして全てのメソッド実行にSegmentを与えないといい感じにならないんだろうか?
そんなことある…?

Segmentを簡単に作れるデコレータ作ってる人を見つけた。
もしそうだったらこれを参考にさせてもらうかも
https://zenn.dev/odan/articles/fe28a747c5b949

seoinkseoink

ひとつひとつセグメントとしてマークするのは流石に手間だし、社内に横展開するためにもミドルウェアっぽく使えるようにしたい…
どうせやるならOpenTelemetryでやるか〜という気持ちです。

一旦はサポートに連絡してみてここまでが合ってるか、既存のミドルウェアはないかを確認してみる。

seoinkseoink

というか、そもそも計装は見たいところにすべきで、ひとつひとつやって然るべきな気がしてきました。
でもアプリケーションコードの中にNewRelicがゴリゴリ入るのも嫌だし、どうにかならないものかなぁ
レイヤードで実装していたら、ドメイン層にNewRelicの知識が入るのとか抵抗ありますね…

seoinkseoink

デコレータでいい感じに計装したい!
上の記事を一旦コピペでやってみたけどいい感じに挙動してくれないので、ちょっとデコレータに詳しくなろうかな

seoinkseoink

MethodDecoratorの第三引数descriptor: PropertyDescriptor
discriptor.valueにはデコレータを付けたメソッドが丸ごと入っているみたい。

class Adding {
  baseNumber: number;
  
  constructor(baseNumber: number) {
    this.baseNumber = baseNumber;
  }

  @multiply(2)
  add(plus: number) {
    return (this.baseNumber += plus);
  }
}

function multiply(num: number) {
  return (
    target: Object,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) => {
    console.log("method");
    const addFunc = descriptor.value;
    console.log(addFunc);
    descriptor.value = function(...args: any) {
      const result = addFunc.apply(this, args);
      return result * num;
    };
  };
}

const adding = new Adding(1);
console.log(adding.add(1));
[LOG]: "method" 
[LOG]: add(plus) {
        return (this.baseNumber += plus);
    } 
[LOG]: 4 
seoinkseoink

デコレータを付けたメソッド含めてテストを動かす。
Jestのエラーが出た

 FAIL  test/useCase/UseCaseTest.test.ts
  ● Test suite failed to run

    Cannot find module './event-sources' from 'node_modules/newrelic/lib/serverless/aws-lambda.js'

    Require stack:
      node_modules/newrelic/lib/serverless/aws-lambda.js
      node_modules/newrelic/api.js
      node_modules/newrelic/stub_api.js
      node_modules/newrelic/index.js
      src/external/newrelic/SegmentDecorator.ts
      src/useCase/UseCase.ts
      test/useCase/UseCaseTest.test.ts


    However, Jest was able to find:
        './event-sources.json'

    You might want to include a file extension in your import, or update your 'moduleFileExtensions', which is currently ['ts', 'js'].

    See https://jestjs.io/docs/configuration#modulefileextensions-arraystring

    > 1 | import newrelic from "newrelic"
        |                      ^
      2 |
      3 | /**
      4 |  * NewRelic上で確認するためのSegmentを付与するデコレータ

ESMに対応してないとかで、importするとき気をつけないといけない
https://github.com/newrelic/node-newrelic/issues/344

seoinkseoink

結合テストを動かすと、DIされてるRepositoryの実体が落ちる?

seoinkseoink

DIしなかったら通った
(ふつうにnewrelicのstartSegmentでラップした)

seoinkseoink

https://zenn.dev/odan/articles/fe28a747c5b949
↑参考にさせてもらったやつ

デコレータ
import newrelic from "newrelic"

/**
 * NewRelic上で確認するためのSegmentを付与するデコレータ
 * 
 * @returns MethodDecorator
 */
export function Segment(): MethodDecorator {
  return (
    _: any,
    methodName: string | symbol,
    descriptor: PropertyDescriptor
  ) => {
    const originalMethod = descriptor.value;
    descriptor.value = (...args: any) => {
      return newrelic.startSegment(methodName.toString(), true, () => {
        return originalMethod.apply(...args);
      });
    };
  };
}

seoinkseoink

こういうエラーが出るのは、MethodDecoratorで関数をreturnしていて、元の関数とthisが変わってしまってるから説ある

こういうエラー
TypeError: Cannot read properties of undefined (reading transaction)

at Number.findOneByPk (infrastructure/entity/repository/TypeOrmEntityRepository.ts:21:23)
at src/external/newrelic/SegmentDecorator.ts:17:31