Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(winston-transport): Add attribute serialization #2352

Open
wants to merge 4 commits into
base: main
Choose a base branch
from

Conversation

hectorhdzg
Copy link
Member

@hectorhdzg hectorhdzg commented Jul 24, 2024

Which problem is this PR solving?

Fixes #2351

Short description of the changes

Added serialization of Winston parameters to ensure supported types are passed through.

@hectorhdzg hectorhdzg requested a review from a team July 24, 2024 18:03
Copy link

codecov bot commented Jul 24, 2024

Codecov Report

Attention: Patch coverage is 95.45455% with 1 line in your changes missing coverage. Please review.

Project coverage is 90.26%. Comparing base (dfb2dff) to head (0afb696).
Report is 278 commits behind head on main.

Files with missing lines Patch % Lines
packages/winston-transport/src/utils.ts 95.45% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2352      +/-   ##
==========================================
- Coverage   90.97%   90.26%   -0.72%     
==========================================
  Files         146      150       +4     
  Lines        7492     7167     -325     
  Branches     1502     1509       +7     
==========================================
- Hits         6816     6469     -347     
- Misses        676      698      +22     
Files with missing lines Coverage Δ
packages/winston-transport/src/utils.ts 97.22% <95.45%> (ø)

... and 75 files with indirect coverage changes

@hectorhdzg
Copy link
Member Author

@trentm we need to do similar thing in other instrumentation like Bunyan and Pino, just wanted to get some feedback about this issue, also it would be good to have serialization code shared between all of these, so maybe this could be added as instrumentation package utils or something, let me know what you think

@trentm
Copy link
Contributor

trentm commented Jul 31, 2024

@hectorhdzg I haven't looked closely yet, but some notes.

Any can also be a map, array, byte-array, right? https://opentelemetry.io/docs/specs/otel/logs/data-model/#type-any I.e. we don't need to serialize it down to just bool | str | number.

@blumamir
Copy link
Member

While we're at it, I noticed that the spec says that type any can be:

An array (a list) of any values,

But the code of @opentelemetry/api-logs only allows arrays of primitive types here.

Should we also add AnyValueArray to the api-logs and to

export type AnyValue = AttributeValue | AnyValueMap;

?

@blumamir
Copy link
Member

blumamir commented Aug 7, 2024

@hectorhdzg I haven't looked closely yet, but some notes.

Any can also be a map, array, byte-array, right? https://opentelemetry.io/docs/specs/otel/logs/data-model/#type-any I.e. we don't need to serialize it down to just bool | str | number.

@trentm @hectorhdzg

Please note this pr in core which I opened relating to this comment.

Looks like the types for AnyValue in api-logs should be extended to allow more options to comply with the spec

@trentm
Copy link
Contributor

trentm commented Aug 8, 2024

@hectorhdzg I tweaked your patch with this to play with it:

diff --git a/packages/winston-transport/src/utils.ts b/packages/winston-transport/src/utils.ts
index e7eddc66..5461ea4c 100644
--- a/packages/winston-transport/src/utils.ts
+++ b/packages/winston-transport/src/utils.ts
@@ -68,7 +68,11 @@ export function emitLogRecord(
   const attributes: LogAttributes = {};
   for (const key in splat) {
     if (Object.prototype.hasOwnProperty.call(splat, key)) {
-      attributes[key] = serializeAttribute(splat[key]);
+      if (process.env.USE_HECTOR_PR === 'true') {
+        attributes[key] = serializeAttribute(splat[key]);
+      } else {
+        attributes[key] = splat[key];
+      }
     }
   }
   const logRecord: LogRecord = {

Then I ran it with this script. Mostly this is the example from the @opentelemetry/instrumentation-winston README.

use-instr-winston.js

const { NodeTracerProvider } = require('@opentelemetry/sdk-trace-node');
const logsAPI = require('@opentelemetry/api-logs');
const {
    LoggerProvider,
    SimpleLogRecordProcessor,
    ConsoleLogRecordExporter,
} = require('@opentelemetry/sdk-logs');
const { WinstonInstrumentation } = require('@opentelemetry/instrumentation-winston');
const { registerInstrumentations } = require('@opentelemetry/instrumentation');
const { OTLPLogExporter } = require('@opentelemetry/exporter-logs-otlp-proto');

const tracerProvider = new NodeTracerProvider();
tracerProvider.register();

const loggerProvider = new LoggerProvider();
var exporter = new ConsoleLogRecordExporter();
// exporter = new OTLPLogExporter();
loggerProvider.addLogRecordProcessor(
    new SimpleLogRecordProcessor(exporter)
);
logsAPI.logs.setGlobalLoggerProvider(loggerProvider);

registerInstrumentations({
    instrumentations: [ new WinstonInstrumentation() ],
});

const winston = require('winston');
const logger = winston.createLogger({
    transports: [new winston.transports.Console()],
})
logger.info('the message', {
  aNum: 42,
  aStr: 'str',
  aUint8Array: new Uint8Array([4,5,6]),
  aInt8Array: new Int8Array([-7,8,9]),
  anObj: {
    numField: 43,
    strField: 'str',
    uint8ArrayField: new Uint8Array([10,11,12]),
  }
});

Without your patch:

% node use-instr-winston.js
{"aInt8Array":{"0":-7,"1":8,"2":9},"aNum":42,"aStr":"str","aUint8Array":{"0":4,"1":5,"2":6},"anObj":{"numField":43,"strField":"str","uint8ArrayField":{"0":10,"1":11,"2":12}},"level":"info","message":"the message"}
{
  resource: {
    attributes: {
      'service.name': 'unknown_service:node',
      'telemetry.sdk.language': 'nodejs',
      'telemetry.sdk.name': 'opentelemetry',
      'telemetry.sdk.version': '1.25.1'
    }
  },
  timestamp: 1723161181754000,
  traceId: undefined,
  spanId: undefined,
  traceFlags: undefined,
  severityText: 'info',
  severityNumber: 9,
  body: 'the message',
  attributes: {
    aNum: 42,
    aStr: 'str',
    aUint8Array: Uint8Array(3) [ 4, 5, 6 ],
    aInt8Array: Int8Array(3) [ -7, 8, 9 ],
    anObj: {
      numField: 43,
      strField: 'str',
      uint8ArrayField: Uint8Array(3) [ 10, 11, 12 ]
    }
  }
}

With your patch:

% USE_HECTOR_PR=true node use-instr-winston.js
{"aInt8Array":{"0":-7,"1":8,"2":9},"aNum":42,"aStr":"str","aUint8Array":{"0":4,"1":5,"2":6},"anObj":{"numField":43,"strField":"str","uint8ArrayField":{"0":10,"1":11,"2":12}},"level":"info","message":"the message"}
{
  resource: {
    attributes: {
      'service.name': 'unknown_service:node',
      'telemetry.sdk.language': 'nodejs',
      'telemetry.sdk.name': 'opentelemetry',
      'telemetry.sdk.version': '1.25.1'
    }
  },
  timestamp: 1723161127451000,
  traceId: undefined,
  spanId: undefined,
  traceFlags: undefined,
  severityText: 'info',
  severityNumber: 9,
  body: 'the message',
  attributes: {
    aNum: 42,
    aStr: 'str',
    aUint8Array: Uint8Array(3) [ 4, 5, 6 ],
    aInt8Array: Int8Array(3) [ -7, 8, 9 ],
    anObj: '{"numField":43,"strField":"str","uint8ArrayField":{"0":10,"1":11,"2":12}}'
  }
}

I don't think that JSON.stringify() of anObj into a string is desired for serialization.

(I haven't played with an Error instance yet. I need to refresh my memory on the various ways Winston handles given Error instances.)

@trentm
Copy link
Contributor

trentm commented Aug 9, 2024

From the linked issue:

Winston instrumentation is allowing JavaScript objects to be used as attributes.

The Any type allows AnyValueMap, which is what the current serializer (e.g. in otlp-transformer) will do with a field with typeof v === 'object'. I think that is fine?

Or perhaps there is a need to guard against things like:

logger.info('try to break it', {
  os: require('os')
});

where some of those attributes are functions, which are clearly not allowed.

FWIW, that currently sort of works. When using an OTLP exporter, the receiving OTLP end gets a ExportLogsServiceRequest structure where the function attributes (e.g. os.arch) are encoded with an empty value (KeyValue { key: 'arch', value: AnyValue {} } in protobuf speak).

full `ExportLogsServiceRequest` dump
ExportLogsServiceRequest {
  resourceLogs: [
    ResourceLogs {
      scopeLogs: [
        ScopeLogs {
          logRecords: [
            LogRecord {
              attributes: [
                KeyValue {
                  key: 'os',
                  value: AnyValue {
                    kvlistValue: KeyValueList {
                      values: [
                        KeyValue { key: 'arch', value: AnyValue {} },
                        KeyValue { key: 'availableParallelism', value: AnyValue {} },
                        KeyValue { key: 'cpus', value: AnyValue {} },
                        KeyValue { key: 'endianness', value: AnyValue {} },
                        KeyValue { key: 'freemem', value: AnyValue {} },
                        KeyValue { key: 'getPriority', value: AnyValue {} },
                        KeyValue { key: 'homedir', value: AnyValue {} },
                        KeyValue { key: 'hostname', value: AnyValue {} },
                        KeyValue { key: 'loadavg', value: AnyValue {} },
                        KeyValue { key: 'networkInterfaces', value: AnyValue {} },
                        KeyValue { key: 'platform', value: AnyValue {} },
                        KeyValue { key: 'release', value: AnyValue {} },
                        KeyValue { key: 'setPriority', value: AnyValue {} },
                        KeyValue { key: 'tmpdir', value: AnyValue {} },
                        KeyValue { key: 'totalmem', value: AnyValue {} },
                        KeyValue { key: 'type', value: AnyValue {} },
                        KeyValue { key: 'userInfo', value: AnyValue {} },
                        KeyValue { key: 'uptime', value: AnyValue {} },
                        KeyValue { key: 'version', value: AnyValue {} },
                        KeyValue { key: 'machine', value: AnyValue {} },
                        KeyValue {
                          key: 'constants',
                          value: AnyValue {
                            kvlistValue: KeyValueList {
                              values: [
                                KeyValue { key: 'UV_UDP_REUSEADDR', value: AnyValue { intValue: Long { low: 4, high: 0, unsigned: false } } },
                                KeyValue {
                                  key: 'dlopen',
                                  value: AnyValue {
                                    kvlistValue: KeyValueList {
                                      values: [
                                        KeyValue { key: 'RTLD_LAZY', value: AnyValue { intValue: Long { low: 1, high: 0, unsigned: false } } },
                                        KeyValue { key: 'RTLD_NOW', value: AnyValue { intValue: Long { low: 2, high: 0, unsigned: false } } },
                                        KeyValue { key: 'RTLD_GLOBAL', value: AnyValue { intValue: Long { low: 8, high: 0, unsigned: false } } },
                                        KeyValue { key: 'RTLD_LOCAL', value: AnyValue { intValue: Long { low: 4, high: 0, unsigned: false } } }
                                      ]
                                    }
                                  }
                                },
                                KeyValue {
                                  key: 'errno',
                                  value: AnyValue {
                                    kvlistValue: KeyValueList {
                                      values: [
                                        KeyValue { key: 'E2BIG', value: AnyValue { intValue: Long { low: 7, high: 0, unsigned: false } } },
                                        KeyValue { key: 'EACCES', value: AnyValue { intValue: Long { low: 13, high: 0, unsigned: false } } },
                                        KeyValue { key: 'EADDRINUSE', value: AnyValue { intValue: Long { low: 48, high: 0, unsigned: false } } },
                                        KeyValue { key: 'EADDRNOTAVAIL', value: AnyValue { intValue: Long { low: 49, high: 0, unsigned: false } } },
                                        KeyValue { key: 'EAFNOSUPPORT', value: AnyValue { intValue: Long { low: 47, high: 0, unsigned: false } } },
                                        KeyValue { key: 'EAGAIN', value: AnyValue { intValue: Long { low: 35, high: 0, unsigned: false } } },
                                        KeyValue { key: 'EALREADY', value: AnyValue { intValue: Long { low: 37, high: 0, unsigned: false } } },
                                        KeyValue { key: 'EBADF', value: AnyValue { intValue: Long { low: 9, high: 0, unsigned: false } } },
                                        KeyValue { key: 'EBADMSG', value: AnyValue { intValue: Long { low: 94, high: 0, unsigned: false } } },
                                        KeyValue { key: 'EBUSY', value: AnyValue { intValue: Long { low: 16, high: 0, unsigned: false } } },
                                        KeyValue { key: 'ECANCELED', value: AnyValue { intValue: Long { low: 89, high: 0, unsigned: false } } },
                                        KeyValue { key: 'ECHILD', value: AnyValue { intValue: Long { low: 10, high: 0, unsigned: false } } },
                                        KeyValue { key: 'ECONNABORTED', value: AnyValue { intValue: Long { low: 53, high: 0, unsigned: false } } },
                                        KeyValue { key: 'ECONNREFUSED', value: AnyValue { intValue: Long { low: 61, high: 0, unsigned: false } } },
                                        KeyValue { key: 'ECONNRESET', value: AnyValue { intValue: Long { low: 54, high: 0, unsigned: false } } },
                                        KeyValue { key: 'EDEADLK', value: AnyValue { intValue: Long { low: 11, high: 0, unsigned: false } } },
                                        KeyValue { key: 'EDESTADDRREQ', value: AnyValue { intValue: Long { low: 39, high: 0, unsigned: false } } },
                                        KeyValue { key: 'EDOM', value: AnyValue { intValue: Long { low: 33, high: 0, unsigned: false } } },
                                        KeyValue { key: 'EDQUOT', value: AnyValue { intValue: Long { low: 69, high: 0, unsigned: false } } },
                                        KeyValue { key: 'EEXIST', value: AnyValue { intValue: Long { low: 17, high: 0, unsigned: false } } },
                                        KeyValue { key: 'EFAULT', value: AnyValue { intValue: Long { low: 14, high: 0, unsigned: false } } },
                                        KeyValue { key: 'EFBIG', value: AnyValue { intValue: Long { low: 27, high: 0, unsigned: false } } },
                                        KeyValue { key: 'EHOSTUNREACH', value: AnyValue { intValue: Long { low: 65, high: 0, unsigned: false } } },
                                        KeyValue { key: 'EIDRM', value: AnyValue { intValue: Long { low: 90, high: 0, unsigned: false } } },
                                        KeyValue { key: 'EILSEQ', value: AnyValue { intValue: Long { low: 92, high: 0, unsigned: false } } },
                                        KeyValue { key: 'EINPROGRESS', value: AnyValue { intValue: Long { low: 36, high: 0, unsigned: false } } },
                                        KeyValue { key: 'EINTR', value: AnyValue { intValue: Long { low: 4, high: 0, unsigned: false } } },
                                        KeyValue { key: 'EINVAL', value: AnyValue { intValue: Long { low: 22, high: 0, unsigned: false } } },
                                        KeyValue { key: 'EIO', value: AnyValue { intValue: Long { low: 5, high: 0, unsigned: false } } },
                                        KeyValue { key: 'EISCONN', value: AnyValue { intValue: Long { low: 56, high: 0, unsigned: false } } },
                                        KeyValue { key: 'EISDIR', value: AnyValue { intValue: Long { low: 21, high: 0, unsigned: false } } },
                                        KeyValue { key: 'ELOOP', value: AnyValue { intValue: Long { low: 62, high: 0, unsigned: false } } },
                                        KeyValue { key: 'EMFILE', value: AnyValue { intValue: Long { low: 24, high: 0, unsigned: false } } },
                                        KeyValue { key: 'EMLINK', value: AnyValue { intValue: Long { low: 31, high: 0, unsigned: false } } },
                                        KeyValue { key: 'EMSGSIZE', value: AnyValue { intValue: Long { low: 40, high: 0, unsigned: false } } },
                                        KeyValue { key: 'EMULTIHOP', value: AnyValue { intValue: Long { low: 95, high: 0, unsigned: false } } },
                                        KeyValue { key: 'ENAMETOOLONG', value: AnyValue { intValue: Long { low: 63, high: 0, unsigned: false } } },
                                        KeyValue { key: 'ENETDOWN', value: AnyValue { intValue: Long { low: 50, high: 0, unsigned: false } } },
                                        KeyValue { key: 'ENETRESET', value: AnyValue { intValue: Long { low: 52, high: 0, unsigned: false } } },
                                        KeyValue { key: 'ENETUNREACH', value: AnyValue { intValue: Long { low: 51, high: 0, unsigned: false } } },
                                        KeyValue { key: 'ENFILE', value: AnyValue { intValue: Long { low: 23, high: 0, unsigned: false } } },
                                        KeyValue { key: 'ENOBUFS', value: AnyValue { intValue: Long { low: 55, high: 0, unsigned: false } } },
                                        KeyValue { key: 'ENODATA', value: AnyValue { intValue: Long { low: 96, high: 0, unsigned: false } } },
                                        KeyValue { key: 'ENODEV', value: AnyValue { intValue: Long { low: 19, high: 0, unsigned: false } } },
                                        KeyValue { key: 'ENOENT', value: AnyValue { intValue: Long { low: 2, high: 0, unsigned: false } } },
                                        KeyValue { key: 'ENOEXEC', value: AnyValue { intValue: Long { low: 8, high: 0, unsigned: false } } },
                                        KeyValue { key: 'ENOLCK', value: AnyValue { intValue: Long { low: 77, high: 0, unsigned: false } } },
                                        KeyValue { key: 'ENOLINK', value: AnyValue { intValue: Long { low: 97, high: 0, unsigned: false } } },
                                        KeyValue { key: 'ENOMEM', value: AnyValue { intValue: Long { low: 12, high: 0, unsigned: false } } },
                                        KeyValue { key: 'ENOMSG', value: AnyValue { intValue: Long { low: 91, high: 0, unsigned: false } } },
                                        KeyValue { key: 'ENOPROTOOPT', value: AnyValue { intValue: Long { low: 42, high: 0, unsigned: false } } },
                                        KeyValue { key: 'ENOSPC', value: AnyValue { intValue: Long { low: 28, high: 0, unsigned: false } } },
                                        KeyValue { key: 'ENOSR', value: AnyValue { intValue: Long { low: 98, high: 0, unsigned: false } } },
                                        KeyValue { key: 'ENOSTR', value: AnyValue { intValue: Long { low: 99, high: 0, unsigned: false } } },
                                        KeyValue { key: 'ENOSYS', value: AnyValue { intValue: Long { low: 78, high: 0, unsigned: false } } },
                                        KeyValue { key: 'ENOTCONN', value: AnyValue { intValue: Long { low: 57, high: 0, unsigned: false } } },
                                        KeyValue { key: 'ENOTDIR', value: AnyValue { intValue: Long { low: 20, high: 0, unsigned: false } } },
                                        KeyValue { key: 'ENOTEMPTY', value: AnyValue { intValue: Long { low: 66, high: 0, unsigned: false } } },
                                        KeyValue { key: 'ENOTSOCK', value: AnyValue { intValue: Long { low: 38, high: 0, unsigned: false } } },
                                        KeyValue { key: 'ENOTSUP', value: AnyValue { intValue: Long { low: 45, high: 0, unsigned: false } } },
                                        KeyValue { key: 'ENOTTY', value: AnyValue { intValue: Long { low: 25, high: 0, unsigned: false } } },
                                        KeyValue { key: 'ENXIO', value: AnyValue { intValue: Long { low: 6, high: 0, unsigned: false } } },
                                        KeyValue { key: 'EOPNOTSUPP', value: AnyValue { intValue: Long { low: 102, high: 0, unsigned: false } } },
                                        KeyValue { key: 'EOVERFLOW', value: AnyValue { intValue: Long { low: 84, high: 0, unsigned: false } } },
                                        KeyValue { key: 'EPERM', value: AnyValue { intValue: Long { low: 1, high: 0, unsigned: false } } },
                                        KeyValue { key: 'EPIPE', value: AnyValue { intValue: Long { low: 32, high: 0, unsigned: false } } },
                                        KeyValue { key: 'EPROTO', value: AnyValue { intValue: Long { low: 100, high: 0, unsigned: false } } },
                                        KeyValue { key: 'EPROTONOSUPPORT', value: AnyValue { intValue: Long { low: 43, high: 0, unsigned: false } } },
                                        KeyValue { key: 'EPROTOTYPE', value: AnyValue { intValue: Long { low: 41, high: 0, unsigned: false } } },
                                        KeyValue { key: 'ERANGE', value: AnyValue { intValue: Long { low: 34, high: 0, unsigned: false } } },
                                        KeyValue { key: 'EROFS', value: AnyValue { intValue: Long { low: 30, high: 0, unsigned: false } } },
                                        KeyValue { key: 'ESPIPE', value: AnyValue { intValue: Long { low: 29, high: 0, unsigned: false } } },
                                        KeyValue { key: 'ESRCH', value: AnyValue { intValue: Long { low: 3, high: 0, unsigned: false } } },
                                        KeyValue { key: 'ESTALE', value: AnyValue { intValue: Long { low: 70, high: 0, unsigned: false } } },
                                        KeyValue { key: 'ETIME', value: AnyValue { intValue: Long { low: 101, high: 0, unsigned: false } } },
                                        KeyValue { key: 'ETIMEDOUT', value: AnyValue { intValue: Long { low: 60, high: 0, unsigned: false } } },
                                        KeyValue { key: 'ETXTBSY', value: AnyValue { intValue: Long { low: 26, high: 0, unsigned: false } } },
                                        KeyValue { key: 'EWOULDBLOCK', value: AnyValue { intValue: Long { low: 35, high: 0, unsigned: false } } },
                                        KeyValue { key: 'EXDEV', value: AnyValue { intValue: Long { low: 18, high: 0, unsigned: false } } }
                                      ]
                                    }
                                  }
                                },
                                KeyValue {
                                  key: 'signals',
                                  value: AnyValue {
                                    kvlistValue: KeyValueList {
                                      values: [
                                        KeyValue { key: 'SIGHUP', value: AnyValue { intValue: Long { low: 1, high: 0, unsigned: false } } },
                                        KeyValue { key: 'SIGINT', value: AnyValue { intValue: Long { low: 2, high: 0, unsigned: false } } },
                                        KeyValue { key: 'SIGQUIT', value: AnyValue { intValue: Long { low: 3, high: 0, unsigned: false } } },
                                        KeyValue { key: 'SIGILL', value: AnyValue { intValue: Long { low: 4, high: 0, unsigned: false } } },
                                        KeyValue { key: 'SIGTRAP', value: AnyValue { intValue: Long { low: 5, high: 0, unsigned: false } } },
                                        KeyValue { key: 'SIGABRT', value: AnyValue { intValue: Long { low: 6, high: 0, unsigned: false } } },
                                        KeyValue { key: 'SIGIOT', value: AnyValue { intValue: Long { low: 6, high: 0, unsigned: false } } },
                                        KeyValue { key: 'SIGBUS', value: AnyValue { intValue: Long { low: 10, high: 0, unsigned: false } } },
                                        KeyValue { key: 'SIGFPE', value: AnyValue { intValue: Long { low: 8, high: 0, unsigned: false } } },
                                        KeyValue { key: 'SIGKILL', value: AnyValue { intValue: Long { low: 9, high: 0, unsigned: false } } },
                                        KeyValue { key: 'SIGUSR1', value: AnyValue { intValue: Long { low: 30, high: 0, unsigned: false } } },
                                        KeyValue { key: 'SIGSEGV', value: AnyValue { intValue: Long { low: 11, high: 0, unsigned: false } } },
                                        KeyValue { key: 'SIGUSR2', value: AnyValue { intValue: Long { low: 31, high: 0, unsigned: false } } },
                                        KeyValue { key: 'SIGPIPE', value: AnyValue { intValue: Long { low: 13, high: 0, unsigned: false } } },
                                        KeyValue { key: 'SIGALRM', value: AnyValue { intValue: Long { low: 14, high: 0, unsigned: false } } },
                                        KeyValue { key: 'SIGTERM', value: AnyValue { intValue: Long { low: 15, high: 0, unsigned: false } } },
                                        KeyValue { key: 'SIGCHLD', value: AnyValue { intValue: Long { low: 20, high: 0, unsigned: false } } },
                                        KeyValue { key: 'SIGCONT', value: AnyValue { intValue: Long { low: 19, high: 0, unsigned: false } } },
                                        KeyValue { key: 'SIGSTOP', value: AnyValue { intValue: Long { low: 17, high: 0, unsigned: false } } },
                                        KeyValue { key: 'SIGTSTP', value: AnyValue { intValue: Long { low: 18, high: 0, unsigned: false } } },
                                        KeyValue { key: 'SIGTTIN', value: AnyValue { intValue: Long { low: 21, high: 0, unsigned: false } } },
                                        KeyValue { key: 'SIGTTOU', value: AnyValue { intValue: Long { low: 22, high: 0, unsigned: false } } },
                                        KeyValue { key: 'SIGURG', value: AnyValue { intValue: Long { low: 16, high: 0, unsigned: false } } },
                                        KeyValue { key: 'SIGXCPU', value: AnyValue { intValue: Long { low: 24, high: 0, unsigned: false } } },
                                        KeyValue { key: 'SIGXFSZ', value: AnyValue { intValue: Long { low: 25, high: 0, unsigned: false } } },
                                        KeyValue { key: 'SIGVTALRM', value: AnyValue { intValue: Long { low: 26, high: 0, unsigned: false } } },
                                        KeyValue { key: 'SIGPROF', value: AnyValue { intValue: Long { low: 27, high: 0, unsigned: false } } },
                                        KeyValue { key: 'SIGWINCH', value: AnyValue { intValue: Long { low: 28, high: 0, unsigned: false } } },
                                        KeyValue { key: 'SIGIO', value: AnyValue { intValue: Long { low: 23, high: 0, unsigned: false } } },
                                        KeyValue { key: 'SIGINFO', value: AnyValue { intValue: Long { low: 29, high: 0, unsigned: false } } },
                                        KeyValue { key: 'SIGSYS', value: AnyValue { intValue: Long { low: 12, high: 0, unsigned: false } } }
                                      ]
                                    }
                                  }
                                },
                                KeyValue {
                                  key: 'priority',
                                  value: AnyValue {
                                    kvlistValue: KeyValueList {
                                      values: [
                                        KeyValue { key: 'PRIORITY_LOW', value: AnyValue { intValue: Long { low: 19, high: 0, unsigned: false } } },
                                        KeyValue { key: 'PRIORITY_BELOW_NORMAL', value: AnyValue { intValue: Long { low: 10, high: 0, unsigned: false } } },
                                        KeyValue { key: 'PRIORITY_NORMAL', value: AnyValue { intValue: Long { low: 0, high: 0, unsigned: false } } },
                                        KeyValue { key: 'PRIORITY_ABOVE_NORMAL', value: AnyValue { intValue: Long { low: -7, high: -1, unsigned: false } } },
                                        KeyValue { key: 'PRIORITY_HIGH', value: AnyValue { intValue: Long { low: -14, high: -1, unsigned: false } } },
                                        KeyValue { key: 'PRIORITY_HIGHEST', value: AnyValue { intValue: Long { low: -20, high: -1, unsigned: false } } }
                                      ]
                                    }
                                  }
                                }
                              ]
                            }
                          }
                        },
                        KeyValue { key: 'EOL', value: AnyValue { stringValue: '\n' } },
                        KeyValue { key: 'devNull', value: AnyValue { stringValue: '/dev/null' } }
                      ]
                    }
                  }
                }
              ],
              timeUnixNano: Long { low: -851487488, high: 401204852, unsigned: true },
              severityNumber: 9,
              severityText: 'info',
              body: AnyValue { stringValue: 'try to break it' },
              droppedAttributesCount: 0,
              observedTimeUnixNano: Long { low: -851487488, high: 401204852, unsigned: true }
            }
          ],
          scope: InstrumentationScope { attributes: [], name: '@opentelemetry/winston-transport', version: '0.5.0' }
        }
      ],
      resource: Resource {
        attributes: [
          KeyValue { key: 'service.name', value: AnyValue { stringValue: 'unknown_service:node' } },
          KeyValue { key: 'telemetry.sdk.language', value: AnyValue { stringValue: 'nodejs' } },
          KeyValue { key: 'telemetry.sdk.name', value: AnyValue { stringValue: 'opentelemetry' } },
          KeyValue { key: 'telemetry.sdk.version', value: AnyValue { stringValue: '1.25.1' } }
        ],
        droppedAttributesCount: 0
      }
    }
  ]
}

@blumamir
Copy link
Member

where some of those attributes are functions, which are clearly not allowed.

I checked what winston logs to console in this case, and looks like it filters out properties which are functions:

// logger.js

const winston = require('winston');

// Create a Winston logger instance
const logger = winston.createLogger({
  level: 'info',
  format: winston.format.simple(),
  transports: [
    new winston.transports.Console() // Log to the console
  ],
});

// Example usage of the logger
logger.info('This is an info message', require('os'));

module.exports = logger;

Output:

yarn exec ts-node winston.ts
yarn exec v1.22.19
{"EOL":"\n","constants":{"UV_UDP_REUSEADDR":4,"dlopen":{"RTLD_GLOBAL":8,"RTLD_LAZY":1,"RTLD_LOCAL":4,"RTLD_NOW":2},"errno":{"E2BIG":7,"EACCES":13,"EADDRINUSE":48,"EADDRNOTAVAIL":49,"EAFNOSUPPORT":47,"EAGAIN":35,"EALREADY":37,"EBADF":9,"EBADMSG":94,"EBUSY":16,"ECANCELED":89,"ECHILD":10,"ECONNABORTED":53,"ECONNREFUSED":61,"ECONNRESET":54,"EDEADLK":11,"EDESTADDRREQ":39,"EDOM":33,"EDQUOT":69,"EEXIST":17,"EFAULT":14,"EFBIG":27,"EHOSTUNREACH":65,"EIDRM":90,"EILSEQ":92,"EINPROGRESS":36,"EINTR":4,"EINVAL":22,"EIO":5,"EISCONN":56,"EISDIR":21,"ELOOP":62,"EMFILE":24,"EMLINK":31,"EMSGSIZE":40,"EMULTIHOP":95,"ENAMETOOLONG":63,"ENETDOWN":50,"ENETRESET":52,"ENETUNREACH":51,"ENFILE":23,"ENOBUFS":55,"ENODATA":96,"ENODEV":19,"ENOENT":2,"ENOEXEC":8,"ENOLCK":77,"ENOLINK":97,"ENOMEM":12,"ENOMSG":91,"ENOPROTOOPT":42,"ENOSPC":28,"ENOSR":98,"ENOSTR":99,"ENOSYS":78,"ENOTCONN":57,"ENOTDIR":20,"ENOTEMPTY":66,"ENOTSOCK":38,"ENOTSUP":45,"ENOTTY":25,"ENXIO":6,"EOPNOTSUPP":102,"EOVERFLOW":84,"EPERM":1,"EPIPE":32,"EPROTO":100,"EPROTONOSUPPORT":43,"EPROTOTYPE":41,"ERANGE":34,"EROFS":30,"ESPIPE":29,"ESRCH":3,"ESTALE":70,"ETIME":101,"ETIMEDOUT":60,"ETXTBSY":26,"EWOULDBLOCK":35,"EXDEV":18},"priority":{"PRIORITY_ABOVE_NORMAL":-7,"PRIORITY_BELOW_NORMAL":10,"PRIORITY_HIGH":-14,"PRIORITY_HIGHEST":-20,"PRIORITY_LOW":19,"PRIORITY_NORMAL":0},"signals":{"SIGABRT":6,"SIGALRM":14,"SIGBUS":10,"SIGCHLD":20,"SIGCONT":19,"SIGFPE":8,"SIGHUP":1,"SIGILL":4,"SIGINFO":29,"SIGINT":2,"SIGIO":23,"SIGIOT":6,"SIGKILL":9,"SIGPIPE":13,"SIGPROF":27,"SIGQUIT":3,"SIGSEGV":11,"SIGSTOP":17,"SIGSYS":12,"SIGTERM":15,"SIGTRAP":5,"SIGTSTP":18,"SIGTTIN":21,"SIGTTOU":22,"SIGURG":16,"SIGUSR1":30,"SIGUSR2":31,"SIGVTALRM":26,"SIGWINCH":28,"SIGXCPU":24,"SIGXFSZ":25}},"devNull":"/dev/null","level":"info","message":"This is an info message"}

Under the hood, it uses the safe-stable-stringify pacakge which contains a switch case where functions are not being handled thus dropped

I support dropping these function properties in otlp-transformations, but this should probably not be a blocker for this PR which is about recording them into attributes correctly. we can open an issue and work on this separately now or later.

@hectorhdzg
Copy link
Member Author

hectorhdzg commented Aug 12, 2024

@trentm @blumamir maybe we can just reuse toAttributes and toAnyValue methods included in otlp-transformer, I was not aware of these, maybe moving these to core package and all Log Instrumentations can reuse it, does that sound good to you? we will not use JSON.stringify in objects, because we will have Map<string, any> instead, we can JSON stringify as needed when exporting.

@trentm
Copy link
Contributor

trentm commented Aug 16, 2024

maybe moving these to core package and all Log Instrumentations can reuse it, does that sound good to you?

I don't yet see the harm in allowing whatever values through and letting the OTLP serializer handle it.

I do see some potential harm in fully validating that log record attributes are a valid type: It could ahve a significant perf impact. Because arrays and maps are allowed, it would require recursing through the full object tree and checking every key and array element's type. That full object walk would then be done again in the serializer/otlp-transformer.

On the SIG call, Dan mentioned considering a depth limit in the otlp-transformer. We also briefly talked about a circular-ref check in the transformer -- though that is a chunk of work.

@hectorhdzg Are there specific issues that you were hitting in Log Record usage that you are able to post so we can weight options?

@hectorhdzg
Copy link
Member Author

@trentm originally this issue came with JS Errors not being able to serialize with JSON.stringify and causing the attribute to be not useful in our Azure Exporter, there were some internal discussions about which part of the code is responsible for ensuring the attributes are correct types, like dictionaries instead of any generic object, objects with circular dependencies, etc. As this is mentioned in OTel spec I was trying to ensure the attributes are correct, in our case we have tons of customers not using otlp-transformer so they don't get this serialization out of the box, we can add serialization in our side, we already have some though, but just wondering which is the correct place to have that so everyone get the advantage of it.

@trentm
Copy link
Contributor

trentm commented Aug 20, 2024

we have tons of customers not using otlp-transformer

Oh, so an export mechanism other that OTLP is typically being used? Can you provide details on that exporter? (Not necessarily needed here, though, I'm mostly curious.)

originally this issue came with JS Errors not being able to serialize with JSON.stringify and causing the attribute to be not useful in our Azure Exporter,

Any specifics on that would be helpful -- though is possibly beside the point.

In this mostly-unrelated package (that I've worked on) that does formatting of Winston output to "ECS logging" format (a structured format that dictates some of the fields) here is a long comment describing the various scenarios I'd hit with a Winston logger and Error instances being passed to it, along with some notes on how the formatter should handle those.
https://github.com/elastic/ecs-logging-nodejs/blob/main/packages/ecs-winston-format/index.js#L59-L91

I wonder if the case of handing JS Errors could, at least partially, be handled separate from some general solution that needs to walk full object trees.

there were some internal discussions about which part of the code is responsible for ensuring the attributes are correct types,
like dictionaries instead of any generic object,

Dictionaries vs generic objects isn't a thing that I think we'll want to distinguish for JS. It is a subjective distinction in JS.

objects with circular dependencies

Circular refs: Yes, this one is interesting to discuss. Dan suggested punting the responsibility back to the user for this on the last JS SIG call. I'm a little less sure. The main Node.js logging libs have settled on handling circular refs gracefully for the user -- mostly via the safe-stable-stringify dep. If we wanted to handle circular refs in OTel JS, I kinda think it should be the responsibility of the exporter (e.g., otlp-transformer for the OTLP exporters). For the Console exporter, the internals of console.log handle this already.

etc.

Other cases to perhaps consider:

  • TypeArray classes other than Uint8Array
  • Date?
  • BigInt?
  • others?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Winston LogRecord attributes include unsupported types
3 participants