azure-kusto-ingest 3.3.0 → 3.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,12 +1,18 @@
1
1
  {
2
2
  "name": "azure-kusto-ingest",
3
- "version": "3.3.0",
3
+ "version": "3.4.0",
4
4
  "description": "Azure Data Explorer Ingestion SDK",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
7
+ "scripts": {
8
+ "build": "tsc"
9
+ },
7
10
  "engines": {
8
11
  "node": ">= 14.0.0"
9
12
  },
13
+ "publishConfig": {
14
+ "access": "public"
15
+ },
10
16
  "tags": [
11
17
  "azure",
12
18
  "kusto",
@@ -17,40 +23,41 @@
17
23
  "azure",
18
24
  "kusto"
19
25
  ],
20
- "scripts": {
21
- "build": "npm link ../azure-kusto-data && tsc -b",
22
- "prepublish": "npm run build",
23
- "example": "npm run build && node example.js",
24
- "lint": "npx eslint -c .eslintrc.js --ext .ts .",
25
- "test": "npm run build && mocha --parallel --recursive test/*.js",
26
- "e2e": "npm run build && mocha --parallel --timeout 240000 test/e2eTests/e2eTest.js",
27
- "allTests": "npm run build && mocha --parallel --timeout 240000 --recursive",
28
- "testPipeline": "npm run build && nyc --reporter lcovonly mocha --timeout 240000 --recursive --reporter mocha-junit-reporter ",
29
- "format": "npx prettier --write .",
30
- "checkFormat": "npx prettier --check --end-of-line lf ."
31
- },
32
26
  "repository": {
33
27
  "type": "git",
34
28
  "url": "https://github.com/Azure/azure-kusto-node.git",
35
29
  "directory": "azure-kusto-ingest"
36
30
  },
31
+ "files": [
32
+ "source/**/*.js",
33
+ "source/**/*.d.ts",
34
+ "index.js",
35
+ "index.d.ts",
36
+ "tsconfig.tsbuildinfo",
37
+ "example.js"
38
+ ],
37
39
  "author": "",
38
40
  "license": "ISC",
39
41
  "dependencies": {
40
- "@azure/storage-blob": "12.1.2",
41
- "@azure/storage-queue": "12.0.5",
42
- "@types/node": "^14.14.13",
43
- "@types/sinon": "^9.0.9",
42
+ "@azure/storage-blob": "^12.11.0",
43
+ "@azure/storage-queue": "^12.10.0",
44
+ "@types/node": "^18.6.4",
45
+ "@types/sinon": "^10.0.13",
44
46
  "@types/stream-array": "^1.1.0",
45
47
  "@types/stream-to-array": "^2.3.0",
46
- "@types/uuid": "^8.3.0",
48
+ "@types/uuid": "^8.3.4",
47
49
  "@types/uuid-validate": "0.0.1",
48
- "azure-kusto-data": "file:../azure-kusto-data",
49
- "moment": "^2.22.2",
50
- "stream-array": "^1.1.0",
50
+ "azure-kusto-data": "^3.4.0",
51
+ "moment": "^2.29.4",
52
+ "stream-array": "^1.1.2",
51
53
  "stream-to-array": "^2.3.0",
54
+ "tmp-promise": "^3.0.3",
55
+ "ts-node": "^10.9.1",
52
56
  "uuid": "^8.3.2",
53
57
  "uuid-validate": "0.0.3"
54
58
  },
55
- "gitHead": "c0b1e333ceddca341e05b3e0add4ef939d1bcea3"
59
+ "gitHead": "7133736d0867b15b126c62e14dd72bbaa69bf905",
60
+ "devDependencies": {
61
+ "@types/tmp": "^0.2.3"
62
+ }
56
63
  }
@@ -3,7 +3,6 @@
3
3
  // Licensed under the MIT License.
4
4
  Object.defineProperty(exports, "__esModule", { value: true });
5
5
  exports.W3CLogFileMapping = exports.OrcColumnMapping = exports.ParquetColumnMapping = exports.SStreamColumnMapping = exports.ApacheAvroColumnMapping = exports.AvroColumnMapping = exports.JsonColumnMapping = exports.CsvColumnMapping = exports.ColumnMapping = exports.ConstantTransformation = exports.FieldTransformation = void 0;
6
- /* eslint-disable max-classes-per-file -- We want all the Column Mappings to be in this file */
7
6
  /* eslint-disable @typescript-eslint/ban-types -- We legitimately want to use {} as a "any non-nullable type" */
8
7
  const ingestionProperties_1 = require("./ingestionProperties");
9
8
  var FieldTransformation;
@@ -12,9 +12,13 @@ export declare class FileDescriptor {
12
12
  size: number | null;
13
13
  sourceId: string;
14
14
  zipped: boolean;
15
- constructor(filePath: string, sourceId?: string | null, size?: number | null);
15
+ compressionType: CompressionType;
16
+ cleanupTmp?: () => Promise<void>;
17
+ constructor(filePath: string, sourceId?: string | null, size?: number | null, compressionType?: CompressionType);
16
18
  _gzip(): Promise<string>;
17
19
  prepare(): Promise<string>;
20
+ private calculateSize;
21
+ cleanup(): Promise<void>;
18
22
  }
19
23
  export declare class StreamDescriptor {
20
24
  readonly stream: Readable;
@@ -15,12 +15,13 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  exports.BlobDescriptor = exports.StreamDescriptor = exports.FileDescriptor = exports.CompressionType = void 0;
18
- /* eslint-disable max-classes-per-file -- We want all the Descriptors to be in this file */
19
18
  const uuid_1 = require("uuid");
20
19
  const uuid_validate_1 = __importDefault(require("uuid-validate"));
21
20
  const zlib_1 = __importDefault(require("zlib"));
22
21
  const path_1 = __importDefault(require("path"));
23
22
  const fs_1 = __importDefault(require("fs"));
23
+ const tmp_promise_1 = require("tmp-promise");
24
+ const util_1 = require("util");
24
25
  var CompressionType;
25
26
  (function (CompressionType) {
26
27
  CompressionType["ZIP"] = ".zip";
@@ -37,19 +38,22 @@ const getSourceId = (sourceId) => {
37
38
  return (0, uuid_1.v4)();
38
39
  };
39
40
  class FileDescriptor {
40
- constructor(filePath, sourceId = null, size = null) {
41
+ constructor(filePath, sourceId = null, size = null, compressionType = CompressionType.None) {
41
42
  this.filePath = filePath;
43
+ this.compressionType = compressionType;
42
44
  this.name = path_1.default.basename(this.filePath);
43
45
  this.extension = path_1.default.extname(this.filePath).toLowerCase();
44
46
  this.size = size;
45
- this.zipped = this.extension === ".gz" || this.extension === ".zip";
47
+ this.zipped = compressionType !== CompressionType.None || this.extension === ".gz" || this.extension === ".zip";
46
48
  this.sourceId = getSourceId(sourceId);
47
49
  }
48
50
  _gzip() {
49
51
  return __awaiter(this, void 0, void 0, function* () {
52
+ const { path, cleanup } = yield (0, tmp_promise_1.file)({ postfix: ".gz", keep: false });
53
+ this.cleanupTmp = cleanup;
50
54
  const zipper = zlib_1.default.createGzip();
51
55
  const input = fs_1.default.createReadStream(this.filePath, { autoClose: true });
52
- const output = fs_1.default.createWriteStream(this.filePath + ".gz");
56
+ const output = fs_1.default.createWriteStream(path);
53
57
  yield new Promise((resolve, reject) => {
54
58
  input
55
59
  .pipe(zipper)
@@ -61,22 +65,34 @@ class FileDescriptor {
61
65
  resolve(null);
62
66
  });
63
67
  });
64
- return this.filePath + ".gz";
68
+ return path;
65
69
  });
66
70
  }
67
71
  prepare() {
68
72
  return __awaiter(this, void 0, void 0, function* () {
69
73
  if (this.zipped) {
70
- if (this.size == null || this.size <= 0) {
71
- this.size = fs_1.default.statSync(this.filePath).size * 11;
72
- }
74
+ const estimatedCompressionModifier = 11;
75
+ yield this.calculateSize(estimatedCompressionModifier);
73
76
  return this.filePath;
74
77
  }
75
- yield this._gzip();
78
+ const path = yield this._gzip();
79
+ yield this.calculateSize();
80
+ return path;
81
+ });
82
+ }
83
+ calculateSize(modifier = 1) {
84
+ return __awaiter(this, void 0, void 0, function* () {
76
85
  if (this.size == null || this.size <= 0) {
77
- this.size = fs_1.default.statSync(this.filePath).size;
86
+ const asyncStat = (0, util_1.promisify)(fs_1.default.stat);
87
+ this.size = (yield asyncStat(this.filePath)).size * modifier;
88
+ }
89
+ });
90
+ }
91
+ cleanup() {
92
+ return __awaiter(this, void 0, void 0, function* () {
93
+ if (this.cleanupTmp) {
94
+ yield this.cleanupTmp();
78
95
  }
79
- return this.filePath + ".gz";
80
96
  });
81
97
  }
82
98
  }
@@ -22,6 +22,7 @@ const ingestionBlobInfo_1 = __importDefault(require("./ingestionBlobInfo"));
22
22
  const storage_queue_1 = require("@azure/storage-queue");
23
23
  const storage_blob_1 = require("@azure/storage-blob");
24
24
  const abstractKustoClient_1 = require("./abstractKustoClient");
25
+ const path_1 = __importDefault(require("path"));
25
26
  class KustoIngestClient extends abstractKustoClient_1.AbstractKustoClient {
26
27
  constructor(kcsb, defaultProps) {
27
28
  super(defaultProps);
@@ -40,7 +41,7 @@ class KustoIngestClient extends abstractKustoClient_1.AbstractKustoClient {
40
41
  throw new Error("Failed to get containers");
41
42
  }
42
43
  const container = containers[Math.floor(Math.random() * containers.length)];
43
- const containerClient = new storage_blob_1.ContainerClient(container.getSASConnectionString(), container.objectName);
44
+ const containerClient = new storage_blob_1.ContainerClient(container.uri);
44
45
  return containerClient.getBlockBlobClient(blobName);
45
46
  });
46
47
  }
@@ -59,11 +60,16 @@ class KustoIngestClient extends abstractKustoClient_1.AbstractKustoClient {
59
60
  return __awaiter(this, void 0, void 0, function* () {
60
61
  const props = this._getMergedProps(ingestionProperties);
61
62
  const descriptor = file instanceof descriptors_1.FileDescriptor ? file : new descriptors_1.FileDescriptor(file);
62
- const fileToUpload = yield descriptor.prepare();
63
- const blobName = `${props.database}__${props.table}__${descriptor.sourceId}__${fileToUpload}`;
64
- const blockBlobClient = yield this._getBlockBlobClient(blobName);
65
- yield blockBlobClient.uploadFile(fileToUpload);
66
- return this.ingestFromBlob(new descriptors_1.BlobDescriptor(blockBlobClient.url, descriptor.size, descriptor.sourceId), props);
63
+ try {
64
+ const fileToUpload = yield descriptor.prepare();
65
+ const blobName = `${props.database}__${props.table}__${descriptor.sourceId}__${descriptor.name}__.${path_1.default.extname(fileToUpload)}`;
66
+ const blockBlobClient = yield this._getBlockBlobClient(blobName);
67
+ yield blockBlobClient.uploadFile(fileToUpload);
68
+ return this.ingestFromBlob(new descriptors_1.BlobDescriptor(blockBlobClient.url, descriptor.size, descriptor.sourceId), props);
69
+ }
70
+ finally {
71
+ yield descriptor.cleanup();
72
+ }
67
73
  });
68
74
  }
69
75
  ingestFromBlob(blob, ingestionProperties) {
@@ -76,7 +82,7 @@ class KustoIngestClient extends abstractKustoClient_1.AbstractKustoClient {
76
82
  }
77
83
  const authorizationContext = yield this.resourceManager.getAuthorizationContext();
78
84
  const queueDetails = queues[Math.floor(Math.random() * queues.length)];
79
- const queueClient = new storage_queue_1.QueueClient(queueDetails.getSASConnectionString(), queueDetails.objectName);
85
+ const queueClient = new storage_queue_1.QueueClient(queueDetails.uri);
80
86
  const ingestionBlobInfo = new ingestionBlobInfo_1.default(descriptor, props, authorizationContext);
81
87
  const ingestionBlobInfoJson = JSON.stringify(ingestionBlobInfo);
82
88
  const encoded = Buffer.from(ingestionBlobInfoJson).toString("base64");
@@ -3,7 +3,6 @@
3
3
  // Licensed under the MIT License.
4
4
  Object.defineProperty(exports, "__esModule", { value: true });
5
5
  exports.IngestionProperties = exports.ReportMethod = exports.ReportLevel = exports.ValidationPolicy = exports.ValidationImplications = exports.ValidationOptions = exports.dataFormatMappingKind = exports.IngestionMappingKind = exports.DataFormat = void 0;
6
- /* eslint-disable max-classes-per-file -- the main class is IngestionProperties, ValidationPolicy is a tiny class */
7
6
  const errors_1 = require("./errors");
8
7
  /**
9
8
  * Data formats supported for Kusto ingestion.
@@ -1,13 +1,8 @@
1
1
  import { Client } from "azure-kusto-data";
2
2
  import moment from "moment";
3
3
  export declare class ResourceURI {
4
- readonly storageAccountName: string;
5
- readonly objectType: string;
6
- readonly objectName: string;
7
- readonly sas: string;
8
- constructor(storageAccountName: string, objectType: string, objectName: string, sas: string);
9
- static fromURI(uri: string): ResourceURI;
10
- getSASConnectionString(): string;
4
+ readonly uri: string;
5
+ constructor(uri: string);
11
6
  }
12
7
  export declare class IngestClientResources {
13
8
  readonly securedReadyForAggregationQueues: ResourceURI[] | null;
@@ -24,6 +19,8 @@ export declare class ResourceManager {
24
19
  ingestClientResourcesLastUpdate: moment.Moment | null;
25
20
  authorizationContext: string | null;
26
21
  authorizationContextLastUpdate: moment.Moment | null;
22
+ private baseSleepTimeSecs;
23
+ private baseJitterSecs;
27
24
  constructor(kustoClient: Client);
28
25
  refreshIngestClientResources(): Promise<IngestClientResources>;
29
26
  getIngestClientResourcesFromService(): Promise<IngestClientResources>;
@@ -15,30 +15,13 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  exports.ResourceManager = exports.IngestClientResources = exports.ResourceURI = void 0;
18
+ const azure_kusto_data_1 = require("azure-kusto-data");
19
+ const retry_1 = require("./retry");
18
20
  const moment_1 = __importDefault(require("moment"));
19
- const URI_FORMAT = /https:\/\/(\w+).(queue|blob|table).core.windows.net\/([\w,-]+)\?(.*)/;
21
+ const ATTEMPT_COUNT = 4;
20
22
  class ResourceURI {
21
- constructor(storageAccountName, objectType, objectName, sas) {
22
- this.storageAccountName = storageAccountName;
23
- this.objectType = objectType;
24
- this.objectName = objectName;
25
- this.sas = sas;
26
- }
27
- static fromURI(uri) {
28
- const match = URI_FORMAT.exec(uri);
29
- if (match == null || match.length < 5) {
30
- throw Error(`Failed to create ResourceManager from URI - invalid uri (${uri})`);
31
- }
32
- return new ResourceURI(match[1], match[2], match[3], match[4]);
33
- }
34
- getSASConnectionString() {
35
- if (this.objectType === "queue") {
36
- return `QueueEndpoint=https://${this.storageAccountName}.queue.core.windows.net/;SharedAccessSignature=${this.sas}`;
37
- }
38
- if (this.objectType === "blob") {
39
- return `BlobEndpoint=https://${this.storageAccountName}.blob.core.windows.net/;SharedAccessSignature=${this.sas}`;
40
- }
41
- throw new Error(`Can't make the current object type (${this.objectType}) to connection string`);
23
+ constructor(uri) {
24
+ this.uri = uri;
42
25
  }
43
26
  }
44
27
  exports.ResourceURI = ResourceURI;
@@ -58,6 +41,8 @@ exports.IngestClientResources = IngestClientResources;
58
41
  class ResourceManager {
59
42
  constructor(kustoClient) {
60
43
  this.kustoClient = kustoClient;
44
+ this.baseSleepTimeSecs = 1;
45
+ this.baseJitterSecs = 1;
61
46
  this.refreshPeriod = moment_1.default.duration(1, "h");
62
47
  this.ingestClientResources = null;
63
48
  this.ingestClientResourcesLastUpdate = null;
@@ -79,9 +64,21 @@ class ResourceManager {
79
64
  }
80
65
  getIngestClientResourcesFromService() {
81
66
  return __awaiter(this, void 0, void 0, function* () {
82
- const response = yield this.kustoClient.execute("NetDefaultDB", ".get ingestion resources");
83
- const table = response.primaryResults[0];
84
- return new IngestClientResources(this.getResourceByName(table, "SecuredReadyForAggregationQueue"), this.getResourceByName(table, "FailedIngestionsQueue"), this.getResourceByName(table, "SuccessfulIngestionsQueue"), this.getResourceByName(table, "TempStorage"));
67
+ const retry = new retry_1.ExponentialRetry(ATTEMPT_COUNT, this.baseSleepTimeSecs, this.baseJitterSecs);
68
+ while (retry.shouldTry()) {
69
+ try {
70
+ const response = yield this.kustoClient.execute("NetDefaultDB", ".get ingestion resources");
71
+ const table = response.primaryResults[0];
72
+ return new IngestClientResources(this.getResourceByName(table, "SecuredReadyForAggregationQueue"), this.getResourceByName(table, "FailedIngestionsQueue"), this.getResourceByName(table, "SuccessfulIngestionsQueue"), this.getResourceByName(table, "TempStorage"));
73
+ }
74
+ catch (error) {
75
+ if (!(error instanceof azure_kusto_data_1.KustoDataErrors.ThrottlingError)) {
76
+ throw error;
77
+ }
78
+ yield retry.backoff();
79
+ }
80
+ }
81
+ throw new Error(`Failed to get ingestion resources from server - the request was throttled ${ATTEMPT_COUNT} times.`);
85
82
  });
86
83
  }
87
84
  getResourceByName(table, resourceName) {
@@ -89,7 +86,7 @@ class ResourceManager {
89
86
  for (const row of table.rows()) {
90
87
  const typedRow = row;
91
88
  if (typedRow.ResourceTypeName === resourceName) {
92
- result.push(ResourceURI.fromURI(typedRow.StorageRoot));
89
+ result.push(new ResourceURI(typedRow.StorageRoot));
93
90
  }
94
91
  }
95
92
  return result;
@@ -110,12 +107,24 @@ class ResourceManager {
110
107
  }
111
108
  getAuthorizationContextFromService() {
112
109
  return __awaiter(this, void 0, void 0, function* () {
113
- const response = yield this.kustoClient.execute("NetDefaultDB", ".get kusto identity token");
114
- const next = response.primaryResults[0].rows().next();
115
- if (next.done) {
116
- throw new Error("Failed to get authorization context - got empty results");
110
+ const retry = new retry_1.ExponentialRetry(ATTEMPT_COUNT, this.baseSleepTimeSecs, this.baseJitterSecs);
111
+ while (retry.shouldTry()) {
112
+ try {
113
+ const response = yield this.kustoClient.execute("NetDefaultDB", ".get kusto identity token");
114
+ const next = response.primaryResults[0].rows().next();
115
+ if (next.done) {
116
+ throw new Error("Failed to get authorization context - got empty results");
117
+ }
118
+ return next.value.toJSON().AuthorizationContext;
119
+ }
120
+ catch (error) {
121
+ if (!(error instanceof azure_kusto_data_1.KustoDataErrors.ThrottlingError)) {
122
+ throw error;
123
+ }
124
+ yield retry.backoff();
125
+ }
117
126
  }
118
- return next.value.toJSON().AuthorizationContext;
127
+ throw new Error(`Failed to get identity token from server - the request was throttled ${ATTEMPT_COUNT} times.`);
119
128
  });
120
129
  }
121
130
  getIngestionQueues() {
package/source/retry.js CHANGED
@@ -30,10 +30,14 @@ class ExponentialRetry {
30
30
  if (!this.shouldTry()) {
31
31
  throw new Error("Max retries exceeded");
32
32
  }
33
- const base = this.sleepBaseSecs * Math.pow(2, this.currentAttempt);
33
+ this.currentAttempt++;
34
+ if (!this.shouldTry()) {
35
+ // This was the last retry - no need to sleep
36
+ return;
37
+ }
38
+ const base = this.sleepBaseSecs * Math.pow(2, this.currentAttempt - 1);
34
39
  const jitter = Math.floor(this.maxJitterSecs * Math.random());
35
40
  yield (0, exports.sleep)(1000 * (base + jitter));
36
- this.currentAttempt++;
37
41
  });
38
42
  }
39
43
  shouldTry() {
package/source/status.js CHANGED
@@ -3,7 +3,6 @@
3
3
  // Licensed under the MIT License.
4
4
  Object.defineProperty(exports, "__esModule", { value: true });
5
5
  exports.KustoIngestStatusQueues = exports.StatusMessage = void 0;
6
- /* eslint-disable max-classes-per-file -- We want all the Status related classes in this file */
7
6
  const statusQ_1 = require("./statusQ");
8
7
  class StatusMessage {
9
8
  constructor(raw, obj, extraProps) {
package/source/statusQ.js CHANGED
@@ -12,7 +12,6 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
12
12
  };
13
13
  Object.defineProperty(exports, "__esModule", { value: true });
14
14
  exports.StatusQueue = void 0;
15
- /* eslint-disable max-classes-per-file -- QueueDetails is a very small class, so we don't need it in a different file */
16
15
  const storage_queue_1 = require("@azure/storage-queue");
17
16
  class QueueDetails {
18
17
  constructor(name, service) {
@@ -36,11 +35,14 @@ class StatusQueue {
36
35
  }
37
36
  _getQServices(queuesDetails) {
38
37
  return queuesDetails.map((q) => {
39
- const sasConnectionString = q.getSASConnectionString();
40
- if (!sasConnectionString) {
38
+ const fullUri = q.uri;
39
+ if (!fullUri) {
41
40
  throw new Error("Empty or null connection string");
42
41
  }
43
- return new QueueDetails(q.objectName, new storage_queue_1.QueueClient(sasConnectionString, q.objectName));
42
+ // chop off sas
43
+ const indexOfSas = q.uri.indexOf("?");
44
+ const name = indexOfSas > 0 ? q.uri.substring(0, indexOfSas) : q.uri;
45
+ return new QueueDetails(name, new storage_queue_1.QueueClient(fullUri));
44
46
  });
45
47
  }
46
48
  isEmpty() {