Repository: serverless/serverless-azure-functions
Pull request #419 (2020-03-06 22:47:39)

Upgrade windows & mac agents per AzDO removing older agents support
<!--
1. Please check out and follow our Contributing Guidelines: https://github.com/serverless/serverless-azure-functions/CONTRIBUTING.md
2. Do not remove any section of the template. If something is not applicable leave it empty but leave it in the PR
3. Please follow the template, otherwise we'll have to ask you to update it and it will take longer until your PR is merged
-->

## What did you implement:

1. Updating old Mac & Windows agent as they're being [removed on 3/23](https://devblogs.microsoft.com/devops/removing-older-images-in-azure-pipelines-hosted-pools/)

2. add dependabot config so it target `dev` instead of `master

<!--
Briefly describe the feature if no issue exists for this PR
-->

## How did you implement it:

* Update pipelines to use new agents

<!--
If this is a nontrivial change please briefly describe your implementation so its easy for us to understand and review your code.
-->

## How can we verify it:

<!--
Add any applicable config, commands, screenshots or other resources
to make it easy for us to verify this works. The easier you make it for us
to review a PR, the faster we can review and merge it.

Examples:
* serverless.yml - Fully functioning to easily deploy changes
* Screenshots - Showing the difference between your output and the master
* Cloud Configuration - List cloud resources and show that the correct configuration is in place
* Other - Anything else that comes to mind to help us evaluate
-->

## Todos:

_**Note: Run `npm run test:ci` to run all validation checks on proposed changes**_

- [x] Ensure there are no lint errors.  
       **Validate via `npm run lint`**  
       _Note: Some reported issues can be automatically fixed by running `npm run lint:fix`_
- [x] Write tests and confirm existing functionality is not broken.  
       **Validate via `npm test`**
- [x] Write documentation
- [x] Provide verification config / commands / resources
- [x] Enable "Allow edits from maintainers" for this PR
- [x] Update the messages below

**_Is this ready for review?:_** YES  
**_Is it a breaking change?:_** NO



Patch:
diff --git a/.dependabot/config.yml b/.dependabot/config.yml
new file mode 100644
index 0000000..ba0f200
--- /dev/null
+++ b/.dependabot/config.yml
@@ -0,0 +1,6 @@
+version: 1
+update_configs:
+  - package_manager: "javascript"
+    directory: "/"
+    update_schedule: "live"
+    target_branch: "dev"
diff --git a/README.md b/README.md
index b29df66..c28fbd8 100644
--- a/README.md
+++ b/README.md
@@ -216,6 +216,15 @@ The getting started walkthrough illustrates the interactive login experience, wh
 - **[Visit our sample repos](docs/examples/samples.md) for full projects with different use cases**
 - [Configuring API Management](docs/examples/apim.md) that sits in front of function app
 
+### Logging Verbosity
+
+You can set the logging verbosity with the `--verbose` flag (`-v` for short). If the `--verbose` flag is set with no value, logging will be as verbose as possible (debug mode). You can also provide a value with the flag to set the verbosity to a specific level:
+
+- `-v error` - Only error messages printed
+- `-v warn` - Only error and warning messages printed
+- `-v info` - Only error, warning and info messages printed
+- `-v debug` - All messages printed
+
 ### Contributing
 
 Please create issues in this repo for any problems or questions you find. Before sending a PR for any major changes, please create an issue to discuss.
diff --git a/package-lock.json b/package-lock.json
index c86690a..17a4b68 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
 {
   "name": "serverless-azure-functions",
-  "version": "1.0.2-19",
+  "version": "1.0.2-20",
   "lockfileVersion": 1,
   "requires": true,
   "dependencies": {
diff --git a/package.json b/package.json
index 8621269..8082757 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "serverless-azure-functions",
-  "version": "1.0.2-19",
+  "version": "1.0.2-20",
   "description": "Provider plugin for the Serverless Framework v1.x which adds support for Azure Functions.",
   "license": "MIT",
   "main": "./lib/index.js",
diff --git a/src/plugins/azureBasePlugin.ts b/src/plugins/azureBasePlugin.ts
index f68b9fe..820811c 100644
--- a/src/plugins/azureBasePlugin.ts
+++ b/src/plugins/azureBasePlugin.ts
@@ -3,11 +3,13 @@ import { ServerlessAzureConfig, ServerlessCliCommand,
   ServerlessCommandMap, ServerlessHookMap, ServerlessObject } from "../models/serverless";
 import { Guard } from "../shared/guard";
 import { Utils } from "../shared/utils";
+import { LoggingService, LogLevel } from "../services/loggingService";
 
 export abstract class AzureBasePlugin<TOptions=Serverless.Options> {
 
   public hooks: ServerlessHookMap
   protected config: ServerlessAzureConfig;
+  protected loggingService: LoggingService;
   protected commands: ServerlessCommandMap;
   protected processedCommands: ServerlessCliCommand[];
 
@@ -17,11 +19,48 @@ export abstract class AzureBasePlugin<TOptions=Serverless.Options> {
   ) {
     Guard.null(serverless);
     this.config = serverless.service as any;
+    this.loggingService = new LoggingService(serverless, options as any);
     this.processedCommands = (serverless as any as ServerlessObject).processedInput.commands;
   }
 
-  protected log(message: string) {
-    this.serverless.cli.log(message);
+  /**
+   * Log message to Serverless CLI
+   * @param message Message to log
+   */
+  protected log(message: string, logLevel?: LogLevel) {
+    this.loggingService.log(message, logLevel);
+  }
+
+  /**
+   * Log error message to Serverless CLI
+   * @param message Error message to log
+   */
+  protected error(message: string) {
+    this.loggingService.error(message);
+  }
+
+  /**
+   * Log warning message to Serverless CLI
+   * @param message Warning message to log
+   */
+  protected warn(message: string) {
+    this.loggingService.warn(message);
+  }
+
+  /**
+   * Log info message to Serverless CLI
+   * @param message Info message to log
+   */
+  protected info(message: string) {
+    this.loggingService.info(message);
+  }
+
+  /**
+   * Log debug message to Serverless CLI
+   * @param message Debug message to log
+   */
+  protected debug(message: string) {
+    this.loggingService.debug(message);
   }
 
   protected getOption(key: string, defaultValue?: any): string {
diff --git a/src/services/baseService.ts b/src/services/baseService.ts
index 71ace8c..4a2137d 100644
--- a/src/services/baseService.ts
+++ b/src/services/baseService.ts
@@ -4,11 +4,12 @@ import fs from "fs";
 import request from "request";
 import Serverless from "serverless";
 import { StorageAccountResource } from "../armTemplates/resources/storageAccount";
-import { ServerlessAzureConfig, ServerlessAzureOptions, ServerlessLogOptions } from "../models/serverless";
+import { ServerlessAzureConfig, ServerlessAzureOptions } from "../models/serverless";
 import { constants } from "../shared/constants";
 import { Guard } from "../shared/guard";
 import { Utils } from "../shared/utils";
 import { ConfigService } from "./configService";
+import { LoggingService, LogLevel } from "./loggingService";
 
 export abstract class BaseService {
   protected baseUrl: string;
@@ -21,6 +22,7 @@ export abstract class BaseService {
   protected storageAccountName: string;
   protected config: ServerlessAzureConfig;
   protected configService: ConfigService;
+  protected loggingService: LoggingService;
 
   protected constructor(
     protected serverless: Serverless,
@@ -29,6 +31,8 @@ export abstract class BaseService {
   ) {
     Guard.null(serverless);
     this.configService = new ConfigService(serverless, options);
+    this.loggingService = new LoggingService(serverless, options);
+
     this.config = this.configService.getConfig();
 
     this.baseUrl = "https://management.azure.com";
@@ -105,9 +109,41 @@ export abstract class BaseService {
    * Log message to Serverless CLI
    * @param message Message to log
    */
-  protected log(message: string, options?: ServerlessLogOptions, entity?: string) {
-    (this.serverless.cli.log as any)(message, entity, options);
-  }  
+  protected log(message: string, logLevel?: LogLevel) {
+    this.loggingService.log(message, logLevel);
+  }
+
+  /**
+   * Log error message to Serverless CLI
+   * @param message Error message to log
+   */
+  protected error(message: string) {
+    this.loggingService.error(message);
+  }
+
+  /**
+   * Log warning message to Serverless CLI
+   * @param message Warning message to log
+   */
+  protected warn(message: string) {
+    this.loggingService.warn(message);
+  }
+
+  /**
+   * Log info message to Serverless CLI
+   * @param message Info message to log
+   */
+  protected info(message: string) {
+    this.loggingService.info(message);
+  }
+
+  /**
+   * Log debug message to Serverless CLI
+   * @param message Debug message to log
+   */
+  protected debug(message: string) {
+    this.loggingService.debug(message);
+  }
 
   protected prettyPrint(object: any) {
     this.log(this.stringify(object));
diff --git a/src/services/loggingService.ts b/src/services/loggingService.ts
new file mode 100644
index 0000000..b1f3c86
--- /dev/null
+++ b/src/services/loggingService.ts
@@ -0,0 +1,85 @@
+import Serverless from "serverless";
+import { ServerlessAzureOptions } from "../models/serverless";
+import { Utils } from "../shared/utils";
+
+/**
+ * Log Level for service, in order from least verbose to most
+ */
+export enum LogLevel {
+  /** Only log error messages */
+  ERROR = 1,
+  /** Only log warnings and error messages */
+  WARN = 2,
+  /** Only log info, warnings and error messages */
+  INFO = 3,
+  /** Log everything */
+  DEBUG = 4
+}
+
+export class LoggingService {
+  /** Logging level for service. Specified by 'verbose' or 'v' flag in Serverless Options.
+   * Defaults to 'info' */
+  private logLevel: LogLevel;
+
+  public constructor(private serverless: Serverless, private options: ServerlessAzureOptions) {
+    
+    // Check for 'verbose' or 'v'. Would use the sls shortcuts at the plugin level, 
+    // but this will be utilized across the plugins, so we check for both
+    const logLevelStr = Utils.get(
+      options,
+      "verbose",
+      Utils.get(options, "v", "info")).toLowerCase();
+    
+    this.logLevel = Utils.get({
+      "error": LogLevel.ERROR,
+      "warn": LogLevel.WARN,
+      "info": LogLevel.INFO,
+      "debug": LogLevel.DEBUG,
+      "": LogLevel.DEBUG
+    }, logLevelStr, LogLevel.INFO);
+  }
+
+  /**
+   * Logs any message with a level (error, warn, info, debug) less than or equal
+   * to the logging level set in the constructor (defaults to info)
+   * @param message Message to log
+   * @param logLevel Log Level
+   */
+  public log(message: string, logLevel: LogLevel = LogLevel.INFO) {
+    if (logLevel <= this.logLevel) {
+      this.serverless.cli.log(message);
+    }
+  }
+
+  /**
+   * Log an error message with prefix '[ERROR] '
+   * @param message Error message
+   */
+  public error(message: string) {
+    this.log(`[ERROR] ${message}`, LogLevel.ERROR);
+  }
+
+  /**
+   * Log a warning message with prefix '[WARN] '
+   * @param message Warning message
+   */
+  public warn(message: string) {
+    this.log(`[WARN] ${message}`, LogLevel.WARN);
+  }
+  
+  /**
+   * Log an info message. Does not include any prefix, as info is default behavior
+   * @param message Info message
+   */
+  public info(message: string) {
+    this.log(message, LogLevel.INFO);
+  }
+
+  /**
+   * Log a debug message prefix '[DEBUG] '
+   * @param message Debug message
+   */
+  public debug(message: string) {
+    this.log(`[DEBUG] ${message}`, LogLevel.DEBUG);
+  }
+}


Test patch:
diff --git a/pipelines/templates/unit-tests.yml b/pipelines/templates/unit-tests.yml
index 76ed1eb..134bf79 100644
--- a/pipelines/templates/unit-tests.yml
+++ b/pipelines/templates/unit-tests.yml
@@ -5,9 +5,9 @@ jobs:
         linux:
           imageName: "ubuntu-16.04"
         mac:
-          imageName: "macos-10.13"
+          imageName: "macOS-10.15"
         windows:
-          imageName: "vs2017-win2016"
+          imageName: "windows-2019"
     displayName: "${{ parameters.name }}"
     pool:
       vmImage: $(imageName)
diff --git a/src/plugins/azureBasePlugin.test.ts b/src/plugins/azureBasePlugin.test.ts
index e3a8d54..d174f58 100644
--- a/src/plugins/azureBasePlugin.test.ts
+++ b/src/plugins/azureBasePlugin.test.ts
@@ -2,6 +2,9 @@ import { AzureBasePlugin } from "./azureBasePlugin";
 import Serverless from "serverless";
 import { MockFactory } from "../test/mockFactory";
 
+jest.mock("../services/loggingService");
+import { LoggingService } from "../services/loggingService";
+
 class MockPlugin extends AzureBasePlugin {
   public constructor(serverless: Serverless, options: Serverless.Options) {
     super(serverless, options);
@@ -14,6 +17,22 @@ class MockPlugin extends AzureBasePlugin {
   public getServerlessOption(key: string, defaultValue?: any) {
     return this.getOption(key, defaultValue);
   }
+
+  public err(message: string) {
+    this.error(message);
+  }
+
+  public war(message: string) {
+    this.warn(message);
+  }
+
+  public inf(message: string) {
+    this.info(message);
+  }
+
+  public deb(message: string) {
+    this.debug(message);
+  }
 }
 
 describe("Azure Base Plugin", () => {
@@ -31,7 +50,7 @@ describe("Azure Base Plugin", () => {
 
   it("logs a message", () => {
     plugin.logMessage("test message");
-    expect(sls.cli.log).lastCalledWith("test message");
+    expect(LoggingService.prototype.log).lastCalledWith("test message", undefined);
   });
 
   it("gets an option", () => {
@@ -40,4 +59,20 @@ describe("Azure Base Plugin", () => {
     expect(plugin.getServerlessOption("key3")).toBeUndefined();
     expect(plugin.getServerlessOption("key3", "myValue")).toEqual("myValue");
   });
+
+  it("calls LoggingService", () => {
+    const mockPlugin = new MockPlugin(sls, {} as any);
+    
+    mockPlugin.err("error");
+    expect(LoggingService.prototype.error).toBeCalledWith("error");
+
+    mockPlugin.war("warn");
+    expect(LoggingService.prototype.warn).toBeCalledWith("warn");
+
+    mockPlugin.inf("info");
+    expect(LoggingService.prototype.info).toBeCalledWith("info");
+
+    mockPlugin.deb("debug");
+    expect(LoggingService.prototype.debug).toBeCalledWith("debug");
+  });
 })
diff --git a/src/plugins/func/azureFuncPlugin.test.ts b/src/plugins/func/azureFuncPlugin.test.ts
index 5b01606..6f3d5d2 100644
--- a/src/plugins/func/azureFuncPlugin.test.ts
+++ b/src/plugins/func/azureFuncPlugin.test.ts
@@ -44,11 +44,9 @@ describe("Azure Func Plugin", () => {
       const plugin = new AzureFuncPlugin(sls, options);
       await invokeHook(plugin, "func:add:add");
 
-      expect(sls.cli.log).toBeCalledWith(
-        "Need to provide a name of function to add",
-        undefined,
-        undefined
-      )
+      expect(sls.cli.log).lastCalledWith(
+        "Need to provide a name of function to add"
+      );
     });
 
     it("returns with pre-existing function", async () => {
@@ -102,10 +100,8 @@ describe("Azure Func Plugin", () => {
       const options = MockFactory.createTestServerlessOptions();
       const plugin = new AzureFuncPlugin(sls, options);
       await invokeHook(plugin, "func:remove:remove");
-      expect(sls.cli.log).toBeCalledWith(
-        "Need to provide a name of function to remove",
-        undefined,
-        undefined
+      expect(sls.cli.log).lastCalledWith(
+        "Need to provide a name of function to remove"
       )
     });
 
@@ -115,10 +111,8 @@ describe("Azure Func Plugin", () => {
       options["name"] = "myNonExistingFunction";
       const plugin = new AzureFuncPlugin(sls, options);
       await invokeHook(plugin, "func:remove:remove");
-      expect(sls.cli.log).toBeCalledWith(
-        "Function myNonExistingFunction does not exist",
-        undefined,
-        undefined
+      expect(sls.cli.log).lastCalledWith(
+        "Function myNonExistingFunction does not exist"
       );
     });
 
diff --git a/src/services/baseService.test.ts b/src/services/baseService.test.ts
index 90be581..b02bd38 100644
--- a/src/services/baseService.test.ts
+++ b/src/services/baseService.test.ts
@@ -5,6 +5,9 @@ import { ServerlessAzureOptions } from "../models/serverless";
 import { MockFactory } from "../test/mockFactory";
 import { BaseService } from "./baseService";
 
+jest.mock("./loggingService");
+import { LoggingService, LogLevel } from "./loggingService";
+
 jest.mock("axios", () => jest.fn());
 import axios from "axios";
 
@@ -34,6 +37,22 @@ class MockService extends BaseService {
       deploymentName: this.deploymentName,
     };
   }
+
+  public err(message: string) {
+    this.error(message);
+  }
+
+  public war(message: string) {
+    this.warn(message);
+  }
+
+  public inf(message: string) {
+    this.info(message);
+  }
+
+  public deb(message: string) {
+    this.debug(message);
+  }
 }
 
 describe("Base Service", () => {
@@ -141,4 +160,20 @@ describe("Base Service", () => {
 
     readStreamSpy.mockRestore();
   });
+
+  it("calls LoggingService", () => {
+    const mockService = new MockService(serverless);
+    
+    mockService.err("error");
+    expect(LoggingService.prototype.error).toBeCalledWith("error");
+
+    mockService.war("warn");
+    expect(LoggingService.prototype.warn).toBeCalledWith("warn");
+
+    mockService.inf("info");
+    expect(LoggingService.prototype.info).toBeCalledWith("info");
+
+    mockService.deb("debug");
+    expect(LoggingService.prototype.debug).toBeCalledWith("debug");
+  });
 });
diff --git a/src/services/funcService.test.ts b/src/services/funcService.test.ts
index c36fdd7..a90a96a 100644
--- a/src/services/funcService.test.ts
+++ b/src/services/funcService.test.ts
@@ -41,7 +41,7 @@ describe("Azure Func Service", () => {
       const service = createService(sls, options);
       await service.add();
 
-      expect(sls.cli.log).toBeCalledWith("Need to provide a name of function to add", undefined, undefined);
+      expect(sls.cli.log).lastCalledWith("Need to provide a name of function to add");
     });
 
     it("returns with pre-existing function", async () => {
@@ -94,11 +94,7 @@ describe("Azure Func Service", () => {
       const options = MockFactory.createTestServerlessOptions();
       const service = createService(sls, options);
       await service.remove();
-      expect(sls.cli.log).toBeCalledWith(
-        "Need to provide a name of function to remove",
-        undefined,
-        undefined
-      )
+      expect(sls.cli.log).lastCalledWith("Need to provide a name of function to remove");
     });
 
     it("returns with non-existing function", async () => {
@@ -107,10 +103,7 @@ describe("Azure Func Service", () => {
       options["name"] = "myNonExistingFunction";
       const service = createService(sls, options);
       await service.remove();
-      expect(sls.cli.log).toBeCalledWith("Function myNonExistingFunction does not exist",
-        undefined,
-        undefined
-      );
+      expect(sls.cli.log).lastCalledWith("Function myNonExistingFunction does not exist");
     });
 
     it("deletes directory and updates serverless.yml", async () => {
diff --git a/src/services/loggingService.test.ts b/src/services/loggingService.test.ts
new file mode 100644
index 0000000..313ed34
--- /dev/null
+++ b/src/services/loggingService.test.ts
@@ -0,0 +1,55 @@
+import { LoggingService } from "./loggingService";
+import Serverless from "serverless";
+import { MockFactory } from "../test/mockFactory";
+
+describe("Logging Service", () => {
+  let loggingService: LoggingService;
+
+  function createService(sls?: Serverless, options?: any) {
+    sls = sls || MockFactory.createTestServerless();
+    return new LoggingService(sls, options || {});
+  }
+
+  it("logs info, warning and error by default, does not log debug", () => {
+    const sls = MockFactory.createTestServerless();
+    loggingService = createService(sls);
+    loggingService.debug("Hello");
+    expect(sls.cli.log).not.toBeCalled();
+    loggingService.info("Info");
+    expect(sls.cli.log).lastCalledWith("Info");
+    loggingService.warn("Warning");
+    expect(sls.cli.log).lastCalledWith("[WARN] Warning");
+    loggingService.error("Error");
+    expect(sls.cli.log).lastCalledWith("[ERROR] Error");
+  });
+
+  it("logs warning and error if set to warn, does not log info or debug", () => {
+    const sls = MockFactory.createTestServerless();
+    loggingService = createService(sls, {"verbose": "warn"});
+    loggingService.info("Hello");
+    loggingService.debug("Hello");
+    expect(sls.cli.log).not.toBeCalled();
+    loggingService.warn("Warning");
+    expect(sls.cli.log).lastCalledWith("[WARN] Warning");
+    loggingService.error("Error");
+    expect(sls.cli.log).lastCalledWith("[ERROR] Error");
+  });
+
+  it("logs only error if set to error, does not log warn, info or debug", () => {
+    const sls = MockFactory.createTestServerless();
+    loggingService = createService(sls, {"verbose": "error"});
+    loggingService.warn("Hello");
+    loggingService.info("Hello");
+    loggingService.debug("Hello");
+    expect(sls.cli.log).not.toBeCalled();
+    loggingService.error("Error");
+    expect(sls.cli.log).lastCalledWith("[ERROR] Error");
+  });
+
+  it("empty string as option value sets logging level to debug", () => {
+    const sls = MockFactory.createTestServerless();
+    loggingService = createService(sls, {"verbose": ""});
+    loggingService.debug("Debug");
+    expect(sls.cli.log).toBeCalledWith("[DEBUG] Debug");
+  })
+});