-
Notifications
You must be signed in to change notification settings - Fork 811
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
feat(diag-logger): introduce a new global level api.diag for internal diagnostic logging #1880
Conversation
@@ -13,7 +13,7 @@ const setups = [ | |||
}, | |||
{ | |||
name: 'BasicTracerProvider', | |||
provider: new BasicTracerProvider({ logger }) | |||
provider: new BasicTracerProvider({ logger: diagLogger }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As a DiagLogger implements the same functions as Logger, this is just updating to show usage and provide a simple concrete compatibility example.
}; | ||
|
||
// DiagLogger implementation | ||
const theFuncs: Array<keyof DiagLogger> = [ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using an embedded array to avoid a for..in loop that could lead to incorrect functionality if the end-user is using some library that decorates objects. e.g. like ember.js and prototype.js
Codecov Report
@@ Coverage Diff @@
## main #1880 +/- ##
==========================================
+ Coverage 92.73% 92.91% +0.17%
==========================================
Files 172 176 +4
Lines 5964 6072 +108
Branches 1268 1289 +21
==========================================
+ Hits 5531 5642 +111
+ Misses 433 430 -3
|
type Get<T> = (version: number) => T; | ||
type OtelGlobal = Partial<{ | ||
[GLOBAL_CONTEXT_MANAGER_API_KEY]: Get<ContextManager>; | ||
[GLOBAL_PROPAGATION_API_KEY]: Get<TextMapPropagator>; | ||
[GLOBAL_TRACE_API_KEY]: Get<TracerProvider>; | ||
[GLOBAL_DIAG_LOGGER_API_KEY]: Get<DiagAPI>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Created the DiagAPI as the global instance, although this could have some unexpected side effects (which I think is also true for context, propagation and trace.
Specific Scenario:
An application includes multiple "components" with each component importing and using OT... As these are registered on the global instance there will be unexpected interactions between the different components usage of the global API's. This is probably mostly an issue for browser based scenario's vs server side node based etc.
The solution to the above would be to introduce some form of API context that is used and passed around to all API operations etc.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In this case, a mismatched API version should call a noop implementation. This is what the other API types do
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Which it will as when DiagAPI registers itself it supplies a NoopDiagAPI fallback.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But the scenario I'm pointing out is that you could have many components using the SAME version of the API, so the Noop doesn't come into play here as everything is registered as a global.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm I wonder if a simpler solution would be to prevent different signals from using different API versions. This is what is proposed in #1772
function _consoleFunc(funcName: keyof Console): DiagLogFunction { | ||
return function () { | ||
const orgArguments = arguments; | ||
if (console) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code for handling #1847
08cac50
to
99650d0
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks this is a much more manageable PR. Looks like you really spent a lot of time filling out the featureset. My only major concern is the inclusion of "forced" logs, which IMO should not be allowed. I understand the usecase, but for a tracing system I would expect INFO
level logs to be used as startup logs and anything that happens during operation to be TRACE
level, which would also solve this usecase. The risk of the feature is that a forced log may break a user application if they are not expecting an instrumentation to call it.
/** General debug log message */ | ||
DEBUG = 3, | ||
|
||
/** Detailed trace level logging should only be used for development, should only be set |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a way we can cause this to fail lint if used?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure I understand why? -- I did see bart's comment on renaming to VERBOSE which makes a lot of sense to avoid overloading "Trace" yet again.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To avoid letting it get checked in. If it fails lint it will fail CI. It says right in the docstring it should only be used for development.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I understand what your talking about, I don't think we want to block this level of message being added... The comment is that when being "used" it is not recommended that they deploy to a production system with this setting as it would (possible) cause a bucket load of internal logging...
* useful for logging component startup and version information without causing additional | ||
* general informational messages when the logging level is set to DiagLogLevel.WARN or lower. | ||
*/ | ||
public forcedInfo!: DiagLogFunction; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems quite dangerous. We should at the very least mention that this should not be used in any core or instrumentation components and that this is meant for the end user only.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not really, the end-user should not be using the DiagLogger as it's for the API, SDK or Contrib packages as it's a diagnostic logger for OT to use... And the DiagLogger will/should only be used by end-users when during development to diagnose why something isn't working.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I really dont like this, i don't think any of the component should be able to force log something. If i have a specific level in place to make diagostics i don't expect having log coming where i explicitly dont want them, even if its to give me some informations (that in my case i dont want).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
By default the logger is a Noop, and the diagnostic logger is for us to help end-users (and most likely component implementers) diagnose and find why something in OT is not working. Based on history it is extremely useful to not just "know" what version of the component they are using but also to have it validated within any log / output from this that they will provide (because we will be requesting this). Which is also why I added the description of when this should be used.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I had the same concern and it was addressed at the SIG. By default nothing will be logged, even the "forcedInfo." Maybe it is a bad name and we should call it "startupInfo" or something one level higher than info? Then users can decide if they want all info, or startup info only.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Then users can decide if they want all info, or startup info only.
I don't see how they can do this since there are no level for this "forcedInfo" (or "startupInfo") ? Maybe i'm missing something ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not exactly the purpose, but I think a reasonable compromise -- so it would be between WARN and INFO..
Potentially called "STARTUPINFO"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Then users can decide if they want all info, or startup info only.
I don't see how they can do this since there are no level for this "forcedInfo" (or "startupInfo") ? Maybe i'm missing something ?
The current implementation logs it as INFO level even if the configured log level is WARN or ERROR. I'm suggesting we add a STARTUPINFO level which would live between INFO and WARN instead.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I fine with this
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Personally, I don't really see the value in INFO level logs in the diagnostic logger. During normal operation the sdk/api shouldn't log. But, as stated below, it is easier to include levels now we may not need later than it is to add them later.
type Get<T> = (version: number) => T; | ||
type OtelGlobal = Partial<{ | ||
[GLOBAL_CONTEXT_MANAGER_API_KEY]: Get<ContextManager>; | ||
[GLOBAL_PROPAGATION_API_KEY]: Get<TextMapPropagator>; | ||
[GLOBAL_TRACE_API_KEY]: Get<TracerProvider>; | ||
[GLOBAL_DIAG_LOGGER_API_KEY]: Get<DiagAPI>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In this case, a mismatched API version should call a noop implementation. This is what the other API types do
In general this seems very full featured, but I wonder if we really need such a full featured logger. Telemetry should aim to be as lightweight as possible and logging should really only be used in unusual scenarios. A few things that stick out at me:
|
@@ -61,7 +61,7 @@ meter.createBatchObserver((observerBatchResult) => { | |||
}); | |||
}, { | |||
maxTimeoutUpdateMS: 500, | |||
logger: new ConsoleLogger(LogLevel.DEBUG) | |||
logger: diagLogLevelFilter(DiagLogLevel.DEBUG, new DiagConsoleLogger()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
for me diagLogLevelFilter
is confusing. I would move this functionality to chaining
logger: new DiagConsoleLogger().setLogLevel(DiagConsoleLogger.LOG_LEVEL.DEBUG)
or if we don't want to have static LOG_LEVEL
logger: new DiagConsoleLogger().setLogLevel(DiagLogLevel.DEBUG)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we follow the same factory model (as with the NoopLogger) this would become (perhaps)
logger: createLogLevelDiagLogger(DiagLogLevel.DEBUG, new DiagConsoleLogger())
The DiagnosticAPI does already of a setLogLevel() to enforce a global default logging level. As part of the SDK there is a Global LOG_LEVEL but it hangs off environment so that s a different discussion.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Additionally, once we only use diag this will become
diag.setLogger(new DiagConsoleLogger());
diag.setLogLevel(DiagLogLevel.DEBUG);
And then it would need to be added here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
tbh it looks strange that you still have to create a new DiagConsoleLogger()
which you then have to put to the "factory" to create a new LogLevelDiagLogger
. Why do I have to create a 2 instances of 2 different classes to be able to have a logger with desired log level.
Things like
logger: new ConsoleLogger(LogLevel.DEBUG)
is self explanatory easy and intuitive, why can't we have
one of:
logger: createDiagConsoleLogger(DiagLogLevel.DEBUG)
// or
logger: new DiagConsoleLogger(DiagLogLevel.DEBUG)
// or
logger: new DiagConsoleLogger().setLogLevel(DiagLogLevel.DEBUG)
comparing to
logger: createLogLevelDiagLogger(DiagLogLevel.DEBUG, new DiagConsoleLogger())
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is part of the design to reduce code duplication, doing it this way NO DiagLogger (except the DiagLogger returned by createLogLevelDiagLogger()) needs to add any code to enforce log level filtering.
So ALL log level filtering is in a single location and anyone who wants to create their own DiagLogger (file, database, event logger, etc) just doesn't need to worry about it.
The only other solution to achieve the same would be to introduce a Base class that everyone would need to extend and this has a couple of implications, not just on size but the actual function implementation of the Base class would need to be prototype level OR we force every implementor to save / call the base instance level functions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And generally, aren't we also saying that the code moving forward (and unless absolutely necessary) would not create it's own instances and instead would use the api.diag
instance -- this will be a migration process and
diag.setLogger(new DiagConsoleLogger());
diag.setLogLevel(DiagLogLevel.DEBUG);
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Personally, I think this distinction makes sense and it is similar to previous loggers i've seen. I think confusion may primarily come from the naming. What I have seen in the past is LoggingDestination
or LogSink
. Here's a very simplified example to help explain what is going on and why it is implemented this way.
// A LogSink is a destination where logs will be sent.
// A LogSink can assume any logs it receives have already had all appropriate filtering applied (e.g. log level filtering)
interface LogSink {
debug(...args): void;
info(...args): void;
warn(...args): void;
error(...args): void;
critical(...args): void;
}
enum LogLevel {
DEBUG = 0,
INFO = 10,
WARN = 20,
ERROR = 30,
CRITICAL = 40,
}
// Logger class is what users call log methods on.
// It is in charge of doing any required filtering and sending it to the configured sink.
class Logger {
constructor(
private level: LogLevel,
private sink: LogSink,
) { }
debug(...args) {
if (this.level < LogLevel.DEBUG)
this.sink.debug(...args)
}
info(...args) {
if (this.level < LogLevel.INFO)
this.sink.info(...args)
}
warn(...args) {
if (this.level < LogLevel.WARN)
this.sink.warn(...args)
}
error(...args) {
if (this.level < LogLevel.ERROR)
this.sink.error(...args)
}
critical(...args) {
if (this.level < LogLevel.CRITICAL)
this.sink.critical(...args)
}
}
// console sink does not need to do any filtering. this is already done
class ConsoleLogSink implements LogSink {
debug(...args) {
console.log(...args);
};
info(...args) {
console.log(...args);
};
warn(...args) {
console.error(...args);
};
error(...args) {
console.error(...args);
};
critical(...args) {
console.error(...args);
};
}
// file sink does not need to do any filtering. this is already done
class FileLogSink implements LogSink {
constructor(private filename: string) { }
debug(...args) {
fs.appendFileSync(this.filename, format(...args))
};
info(...args) {
fs.appendFileSync(this.filename, format(...args))
};
warn(...args) {
fs.appendFileSync(this.filename, format(...args))
};
error(...args) {
fs.appendFileSync(this.filename, format(...args))
};
critical(...args) {
fs.appendFileSync(this.filename, format(...args))
};
}
// User can now use either logging sink/destination without them both having to implement filtering based on log levels
const fileLogger = new Logger(LogLevel.INFO, new FileLogSink("info.log"));
const consoleLogger = new Logger(LogLevel.CRITICAL, new ConsoleLogSink());
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thx the "sink" approach / explanation is clear and straightforward
f1ca657
to
9847508
Compare
Note: Created new issue #1903 for considering how we add a "is logging level enabled" check. |
b3d7bfc
to
b681850
Compare
@open-telemetry/javascript-maintainers @open-telemetry/javascript-approvers please review PRs that affect the API. I want to merge what we have so I can move the code to the API repository this week. |
* useful for logging component startup and version information without causing additional | ||
* general informational messages when the logging level is set to DiagLogLevel.WARN or lower. | ||
*/ | ||
public forcedInfo!: DiagLogFunction; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I really dont like this, i don't think any of the component should be able to force log something. If i have a specific level in place to make diagostics i don't expect having log coming where i explicitly dont want them, even if its to give me some informations (that in my case i dont want).
* of the levels are defined to match the original values from the initial LogLevel to avoid | ||
* compatibility/migration issues for any implementation that assume the numeric ordering. | ||
*/ | ||
export enum DiagLogLevel { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why do we have so many different level ? Can't we use error/warn/info/debug like most logger do ? As a end user i directly give the library that i use to log (pino in my case) to otel, having some many levels (that i dont personally think we really need) means that i will need to do some code to translate those into level my logger understand.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The comment on the number of logging levels depends on your history, most of the logging systems I have used (and created) actually have more than this. .Net, log4j.
As a diagnostic logger, especially for a system where we don't want to publish new version regularly (esp once we become stable) it is extremely useful to have multiple levels to diagnose internal failures for different scenarios -- without needing to debug with the end-user or release a "special" version with extra debugging (diagnostic logging) included.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The comment on the number of logging levels depends on your history, most of the logging systems I have used (and created) actually have more than this. .Net, log4j.
I understand but don't think its a valid point, the goal is not have a many level as possible.
As a diagnostic logger, especially for a system where we don't want to publish new version regularly (esp once we become stable) it is extremely useful to have multiple levels to diagnose internal failures for different scenarios -- without needing to debug with the end-user or release a "special" version with extra debugging (diagnostic logging) included.
Again i understand that we need to avoid publishing the api often but i stand my point: its about having just what we need.
For exemple, why differenciate terminal/critial and error which is generic enough, the level doesn't provide much (specially since it will be mostly likely serialized as a number) what is important is the error message.
Also what the difference between verbose and debug ? currently debug is used for what you describe as verbose
I don't see the point of having a lot of level "in case we might need them", specially since my original problem is still there, the less level we have the more likely it can be plugged with whatever logger the end user is already using
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I had the same thought about the number of log levels but came to the same conclusion. It may be difficult to add levels later if we need them and this gives us a fair bit of control when trying to debug. One change I might make is to make the levels 10, 20, 30, ...
instead of 1, 2, 3, ...
. This would make it much easier to insert a log level between existing levels (like startupinfo) later as level 25 or 35 or something without breaking deployed loggers.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why differenciate terminal/critial and error
I think I added this as the description on the levels, terminal and critical are important level events
Also what the difference between verbose and debug
Again see the descriptions, for history verbose
was originally called trace
I don't see the point of having a lot of level "in case we might need them"
They are not "in case", when used properly they are extremely useful.
the less level we have the more likely it can be plugged with whatever logger the end user is already using
This was considered and the LogLevel logger handles this here as part of the followup PR to remove the Logger (which will be a breaking change) this can either be removed or retained to support this scenario.
One change I might make is to make the levels
10, 20, 30, ...
I did originally consider this (as part of the pre-decessor PR to this one), however, to keep backward compatibility the numeric value of the existing LogLevel and DiagLogLevel where matched -- As this would be a breaking change, I'm happy to make this change as part of the followup breaking PR.
Timing wise, my plan was to publish the followup breaking change PR after we publish the next API release, so that this can be "waiting" for the next release and in the meantime I can also take the task to update Contrib.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Which timeline wise (I'm assuming) would be after @dyladan has created the new Api Repo.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Adding ranges (0..10..20..) as there was no "space" for the Startup level, this will break numeric compatibility between LogLevel and DiagLogLevel -- this WILL cuase some confusion and compatibility issues with the Environment OTEL_LOG_LEVEL -- But this will be "fixed" as part of the followup PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But, as stated below, it is easier to include levels now we may not need later than it is to add them later.
(answering here for @dyladan comment here) : I think this drill down to a different opinion about software design, i would prefer have basic levels (like said here) and design so we can add more later (0..10..20 are a good way to do this) only if we fill the need for more.
Anyway if the consensus is more to have a lot in the first place i don't mind.
… diagnostic logging (Part 1) open-telemetry#1877
summary of changes - rename trace -> verbose - make log level NONE block all logging - use factory name for Noop logger - refactor tests to use arrays -- reducing test size - remove diagLogAdapterfix: address review changes summary - rename trace -> verbose - make log level NONE block all logging - use factory name for Noop logger - refactor tests to use arrays -- reducing test size - remove diagLogAdapter
- fix issue where the global diag level affected trailing tests
summary of changes - removed forcedInfo -> startupInfo - change DiagLogLevel range add STARTUP - removed explicit numeric values on Core.LogLevel
b681850
to
a4cb4ba
Compare
… diagnostic logging (open-telemetry#1880) Co-authored-by: Daniel Dyla <[email protected]>
… diagnostic logging (open-telemetry#1880) Co-authored-by: Daniel Dyla <[email protected]>
Which problem is this PR solving?
Short description of the changes
introduces new api level object, classes and functions
api.diag
global loggerapi.DiagLogLevel
- Also provides extended diagnostic levels for internal diagnostic logging.api.DiagConsoleLogger
- A ConsoleLogger includes fixes for [Environments] ConsoleLogger does not handle all console environments #1847api.noopDiagLogger(): DiagLogger
- A replacement for api.NoopLogger (from [API Suggestion] Provide getLogger helper and remove NoopLogger class implementation #1754)api.diagLogLevelFilter(): DiagLogger
(helper to wrap anyDiagLogger
instance with aDiagLogLevel
filtering)Tags the following as deprecated to be removed in a future PR for [API] Introduce a new global level api.diag for internal Diagnostic Logging (Part 2) #1878
Replaces internal self contained usages which do not affect compatibility or functionality, also updated the examples.