Skip to content
This repository has been archived by the owner on Oct 3, 2023. It is now read-only.

feat: add mechanism to load internal module files to patch #52

Merged

Conversation

fabiogomessilva
Copy link
Member

Sometimes, there is a need to patch internal module files that are not exported by default. This PR aims to add a mechanism to make it possible to load internal files from a module and make it available to a plugin.

this.setPluginContext(moduleExports, tracer, version);
applyPatch(
moduleExports: any, tracer: modelTypes.Tracer, version: string,
basedir?: string): any {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

basedir is optional here but is passed to functions that require it to have a value later. I would suggest making it required here as well.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It will not be used by all implementations, just the ones that need to patch internal files that are not exported. The parameter as optional does not affect current implementations.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TS optionality doesn't refer to whether it is required to be used, but whether it is required to be given a value. In the latter case it is not optional because some plugins just won't work if you don't pass it.

I understand that you'd have to update the plugins to add a basedir argument -- if you agree that it should be required then it should be done in this PR.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Method loadPlugins from PluginLoader is always passing this parameter value.

 result = plugin.applyPatch(exports, this.tracer, version, basedir);

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The implementations that will require this parameter will have it available.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you might be confounding optional assignment vs. optional usage:

optional assignment

// it is OK for us not to pass a value for `basedir`.
plugin.applyPatch(exports, this.tracer, version);

optional usage

class MyPlugin extends BasePlugin {
  applyPatch(exports, tracer, version, basedir) {
    // it is OK for us not to use `basedir`.
  }
}

The syntax basedir? suggests that basedir's assignment is optional. But as we have both said, the real intent that we want to convey is that its usage is optional. (There is no syntax for this; the only way to show this intent is to document it.)

/**
* Maps a name (key) representing a internal file module and its exports
*/
export type ModuleExports = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The type name here should be more descriptive. Something like ModuleExportsMapping or something like that. But I wonder if you really need this type to be defined explicitly, because it's so broad. (Why not just define it locally in base-plugin.ts.)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree. Done !

if (this.internalFileList) {
Object.keys(this.internalFileList).map(versionRange => {
if (semver.satisfies(this.version, versionRange)) {
result = this.loadInternalModuleFiles(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should you warn if multiple versions match? It sounds like users would be depending on implementation-private behavior.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If multiple versions match, the last one will prevail.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a logger warning? It should be brought to the user's attention because it is mostly a bug for someone to user overlapping version ranges.

let result: types.ModuleExports = null;
this.logger.debug('loadInternalFiles %o', this.internalFileList);
if (this.internalFileList) {
Object.keys(this.internalFileList).map(versionRange => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prefer forEach over map if you are not using the return value.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

/** a logger */
protected logger: Logger;
/** list of internal files that need patch and are not exported by default */
protected internalFileList: types.PluginInternalFiles;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is readonly, right?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

basedir: string): types.ModuleExports {
const extraModules: types.ModuleExports = {};
if (extraModulesList) {
Object.keys(extraModulesList).map(modulename => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use a camelcase variable name (modulename).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

try {
this.logger.debug('loading File %s', extraModulesList[modulename]);
extraModules[modulename] =
require(path.join(basedir, extraModulesList[modulename]));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not certain that this will work in all cases. What does basedir usually look like -- is it absolute or relative?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The basedir parameter is passed to the callback of the hook function from lib "require-in-the-middle", it is an absolute path.

this.moduleExports = moduleExports;
this.tracer = tracer;
this.version = version;
this.basedir = basedir;
this.logger = tracer.logger || logger.logger();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not just tracer.logger?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree.

@@ -20,18 +20,36 @@ import * as shimmer from 'shimmer';


export class SimpleModulePlugin extends classes.BasePlugin {
protected internalFileList: types.PluginInternalFiles = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This module doesn't seem very simple anymore. Can you have a separate instrumentation fixture for something that requires an internal module to be patched.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A new one was created.

const extraFile = 'src/extra-module';
const extraModule = require(path.join(indexPath, extraFile));
assert.strictEqual(extraModule.name(), 'patched-' + extraModuleName);
assert.strictEqual(extraModule.value(), 121);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

extraModule is already not user-visible. I would rather see a test based around a module that uses the module exports of an internal file.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think if the internal file export is accessible, whats the point to load it this way?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It does not simulate a real user-visible behavior. Based on this test alone, it is not clear that if the internal module's exports are patched, it will be visible to the user through behavior changes in simple-module.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test improved.

@kjin
Copy link
Contributor

kjin commented Jun 18, 2018

Overall, this is much better than before. I think it's good to not expose details about module internals to users.

Copy link
Contributor

@kjin kjin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this will be good to land after these comments. I still think moduleExports does not belong as a field on Plugin... let's address that in a separate PR.

* applyPatch() and applyUnpatch()
*
* GoF Template Method Pattern - this is the variant part of the pattern
* Each plugin should implement his own version
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wasn't aware that this pattern had a specific name, TIL. However, I don't think this necessary belongs in docs, it may be confusing to users. Not everyone has read GoF.

I would just mention that this method will be called when applyPluginPatch is called.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Design Patterns are meant to be a kind of universal language, I think it is not confusing at all, it adds more information and makes things clear. I think our audience is pretty much aware of them.

if (this.internalFileList) {
Object.keys(this.internalFileList).map(versionRange => {
if (semver.satisfies(this.version, versionRange)) {
result = this.loadInternalModuleFiles(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a logger warning? It should be brought to the user's attention because it is mostly a bug for someone to user overlapping version ranges.

*/
// tslint:disable:no-any
applyPatch(
applyPluginPatch(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: enable and disable to distinguish this from applyPatch and applyUnpatch.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the public interface [enable/disable]PluginPatch ? or the protected methods [enable/disable]Patch ?

Copy link
Contributor

@kjin kjin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re: naming, I meant just call the functions enable and disable.

if (this.internalFileList) {
this.logger.debug('loadInternalFiles %o', this.internalFileList);
Object.keys(this.internalFileList).forEach(versionRange => {
if (semver.satisfies(this.version, versionRange)) {
if (result) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not to be pedantic, but this can yield strange results if result just happens to be a falsey value (0 or ''). For correctness' sake I would just make overlap a counter (or otherwise just print the warning right in this function).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refactory done.

* applyPatch() and applyUnpatch()
*
* GoF Template Method Pattern - this is the variant part of the pattern
* Each plugin should implement his own version.
Copy link
Contributor

@kjin kjin Jun 19, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would not consider this universal language. I did not know what this was referring to and had to look it up. It might be true that I am not the common case, but it is better not to assume something like this about the audience. After all, it is possible to understand the concept without knowing its name.

Feel free to document the design pattern at the top of the file, but I do not think it belongs here.

Another note, we should not use gender pronouns in code, especially when referring to impersonal objects. Please use its.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

... document the design pattern at the top of the file ...

At the top, it loses context. I have improved the message.

Copy link
Contributor

@kjin kjin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. I will file a separate issue about the concern related to loading multiple modules with different versions.

@kjin kjin merged commit 3a613a7 into census-instrumentation:master Jun 19, 2018
kjin pushed a commit to kjin/opencensus-node that referenced this pull request Aug 24, 2018
…strumentation#52)

* feat: add mechanism to load internal module files to patch

* refactor: change Plugin Interface and BasePlugin to implement GoF Template Method

* refactor: change http plugin to new interface

* refactor: change https plugin to new interface

*  refactor: change http2 plugin to new interface

* refactor: change mongodb plugin to new interface

* refactor: improve load internal file test case

* refactor: rename methods of Plugin interface
kjin pushed a commit to kjin/opencensus-node that referenced this pull request Aug 24, 2018
…strumentation#52)

* feat: add mechanism to load internal module files to patch

* refactor: change Plugin Interface and BasePlugin to implement GoF Template Method

* refactor: change http plugin to new interface

* refactor: change https plugin to new interface

*  refactor: change http2 plugin to new interface

* refactor: change mongodb plugin to new interface

* refactor: improve load internal file test case

* refactor: rename methods of Plugin interface
kjin pushed a commit to kjin/opencensus-node that referenced this pull request Aug 24, 2018
…strumentation#52)

* feat: add mechanism to load internal module files to patch

* refactor: change Plugin Interface and BasePlugin to implement GoF Template Method

* refactor: change http plugin to new interface

* refactor: change https plugin to new interface

*  refactor: change http2 plugin to new interface

* refactor: change mongodb plugin to new interface

* refactor: improve load internal file test case

* refactor: rename methods of Plugin interface
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants