@causa/workspace-google 0.16.0 → 0.17.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.
Files changed (31) hide show
  1. package/README.md +8 -0
  2. package/dist/functions/event-topic/index.d.ts +1 -0
  3. package/dist/functions/event-topic/index.js +1 -0
  4. package/dist/functions/event-topic/query-events-bigquery.call.d.ts +4 -0
  5. package/dist/functions/event-topic/query-events-bigquery.call.js +41 -0
  6. package/dist/functions/event-topic/query-events-bigquery.d.ts +10 -0
  7. package/dist/functions/event-topic/query-events-bigquery.js +17 -0
  8. package/dist/functions/google-firestore/index.d.ts +1 -0
  9. package/dist/functions/google-firestore/index.js +1 -0
  10. package/dist/functions/google-firestore/query-records.call.d.ts +3 -0
  11. package/dist/functions/google-firestore/query-records.call.js +50 -0
  12. package/dist/functions/google-firestore/query-records.d.ts +18 -0
  13. package/dist/functions/google-firestore/query-records.js +22 -0
  14. package/dist/functions/google-spanner/index.d.ts +1 -0
  15. package/dist/functions/google-spanner/index.js +1 -0
  16. package/dist/functions/google-spanner/query-records.call.d.ts +3 -0
  17. package/dist/functions/google-spanner/query-records.call.js +66 -0
  18. package/dist/functions/google-spanner/query-records.d.ts +23 -0
  19. package/dist/functions/google-spanner/query-records.js +27 -0
  20. package/dist/functions/index.js +5 -4
  21. package/dist/functions/service-container/index.d.ts +1 -0
  22. package/dist/functions/service-container/index.js +1 -0
  23. package/dist/functions/service-container/query-logs-cloud-run.call.d.ts +4 -0
  24. package/dist/functions/service-container/query-logs-cloud-run.call.js +36 -0
  25. package/dist/functions/service-container/query-logs-cloud-run.d.ts +12 -0
  26. package/dist/functions/service-container/query-logs-cloud-run.js +18 -0
  27. package/dist/services/index.d.ts +1 -0
  28. package/dist/services/index.js +1 -0
  29. package/dist/services/logging.d.ts +16 -0
  30. package/dist/services/logging.js +20 -0
  31. package/package.json +15 -14
package/README.md CHANGED
@@ -62,6 +62,14 @@ Backfilling is supported when `google.pubSub` is set as the `events.broker`. Tem
62
62
 
63
63
  When no source is specified, the default is to fetch events to backfill from the BigQuery dataset configured in `google.pubSub.bigQueryStorage`. A custom BigQuery table can also be set as source using the `bq://<projectId>.<datasetId>.<tableId>` format. It should have the `data` and `attributes` columns.
64
64
 
65
+ ### Querying
66
+
67
+ This module implements the `DatabaseQueryRecords`, `ServiceContainerQueryLogs`, and `EventTopicQueryEvents` workspace functions, which expose read-only access to data stored in or emitted by deployed services:
68
+
69
+ - `DatabaseQueryRecords` is supported for the `google.spanner` and `google.firestore` engines. For Spanner, the function takes a database name and a SQL query, runs it on the configured Spanner instance, and returns the matching rows (capped to 100000). For Firestore, the query is the path to a single document, fetched from the default database (or the specified one). The document data is returned as a single-element array. An empty array is returned when the document does not exist.
70
+ - `ServiceContainerQueryLogs` is supported when `serviceContainer.platform` is `google.cloudRun`. It queries Cloud Logging for log entries emitted by the service's Cloud Run revisions. By default, only the last hour of logs and up to 1 000 entries are returned.
71
+ - `EventTopicQueryEvents` is supported when `events.broker` is `google.pubSub`, `events.format` is `json`, and `google.pubSub.bigQueryStorage.rawEventsDatasetId` is set. It reads events from the topic's BigQuery raw events table. Same defaults as for log queries: last hour and up to 1 000 events.
72
+
65
73
  ### Secrets backend
66
74
 
67
75
  This module implements the `google.secretManager` secret backend, allowing fetching secrets from the Google Secret Manager service. Here are some example of how secrets with the `google.secretManager` backend should be defined:
@@ -7,3 +7,4 @@ export { EventTopicBrokerDeleteTriggerResourceForServiceAccount } from './broker
7
7
  export { EventTopicBrokerGetTopicIdForPubSub } from './broker-get-topic-id-pubsub.js';
8
8
  export { EventTopicBrokerPublishEventsForGoogle } from './broker-publish-events-google.js';
9
9
  export { EventTopicCreateBackfillSourceForBigQuery } from './create-backfill-source-bigquery.js';
10
+ export { EventTopicQueryEventsForBigQuery } from './query-events-bigquery.js';
@@ -7,3 +7,4 @@ export { EventTopicBrokerDeleteTriggerResourceForServiceAccount } from './broker
7
7
  export { EventTopicBrokerGetTopicIdForPubSub } from './broker-get-topic-id-pubsub.js';
8
8
  export { EventTopicBrokerPublishEventsForGoogle } from './broker-publish-events-google.js';
9
9
  export { EventTopicCreateBackfillSourceForBigQuery } from './create-backfill-source-bigquery.js';
10
+ export { EventTopicQueryEventsForBigQuery } from './query-events-bigquery.js';
@@ -0,0 +1,4 @@
1
+ import type { WorkspaceContext } from '@causa/workspace';
2
+ import type { QueriedEvent } from '@causa/workspace-core';
3
+ import type { EventTopicQueryEventsForBigQuery } from './query-events-bigquery.js';
4
+ export default function call(this: EventTopicQueryEventsForBigQuery, context: WorkspaceContext): Promise<QueriedEvent[]>;
@@ -0,0 +1,41 @@
1
+ import { BigQueryService } from '../../services/index.js';
2
+ /**
3
+ * The default duration applied to `from` when it is not set, in milliseconds.
4
+ */
5
+ const DEFAULT_FROM_OFFSET = 60 * 60 * 1000;
6
+ /**
7
+ * The default value for `limit` when it is not set.
8
+ */
9
+ const DEFAULT_LIMIT = 1000;
10
+ export default async function call(context) {
11
+ const from = this.from ?? new Date(Date.now() - DEFAULT_FROM_OFFSET);
12
+ const limit = this.limit ?? DEFAULT_LIMIT;
13
+ const googleConf = context.asConfiguration();
14
+ const projectId = googleConf.getOrThrow('google.project');
15
+ const rawEventsDatasetId = googleConf.getOrThrow('google.pubSub.bigQueryStorage.rawEventsDatasetId');
16
+ const tableName = this.topic.replace(/[-\.]/g, '_');
17
+ const tableId = `${projectId}.${rawEventsDatasetId}.${tableName}`;
18
+ const conditions = [`publish_time >= @from`];
19
+ const params = { from: from.toISOString() };
20
+ if (this.to) {
21
+ conditions.push(`publish_time < @to`);
22
+ params.to = this.to.toISOString();
23
+ }
24
+ if (this.filter) {
25
+ conditions.push(`(${this.filter})`);
26
+ }
27
+ const query = `
28
+ SELECT publish_time, data, attributes
29
+ FROM \`${tableId}\`
30
+ WHERE ${conditions.join(' AND ')}
31
+ ORDER BY publish_time ASC
32
+ LIMIT ${limit}`;
33
+ context.logger.debug(`Querying BigQuery for events:\n${query}`);
34
+ const bigQuery = context.service(BigQueryService).bigQuery;
35
+ const [rows] = await bigQuery.query({ query, params });
36
+ return rows.map((row) => ({
37
+ timestamp: new Date(row.publish_time.value),
38
+ attributes: row.attributes ? JSON.parse(row.attributes) : {},
39
+ data: JSON.parse(row.data.toString('utf8')),
40
+ }));
41
+ }
@@ -0,0 +1,10 @@
1
+ import { WorkspaceContext } from '@causa/workspace';
2
+ import { EventTopicQueryEvents, type QueriedEvent } from '@causa/workspace-core';
3
+ /**
4
+ * Implements {@link EventTopicQueryEvents} for Pub/Sub topics whose events are stored in BigQuery via a BigQuery
5
+ * subscription. The events are read from the dataset configured at `google.pubSub.bigQueryStorage.rawEventsDatasetId`.
6
+ */
7
+ export declare class EventTopicQueryEventsForBigQuery extends EventTopicQueryEvents {
8
+ _call(context: WorkspaceContext): Promise<QueriedEvent[]>;
9
+ _supports(context: WorkspaceContext): boolean;
10
+ }
@@ -0,0 +1,17 @@
1
+ import { callDeferred, WorkspaceContext } from '@causa/workspace';
2
+ import { EventTopicQueryEvents, } from '@causa/workspace-core';
3
+ /**
4
+ * Implements {@link EventTopicQueryEvents} for Pub/Sub topics whose events are stored in BigQuery via a BigQuery
5
+ * subscription. The events are read from the dataset configured at `google.pubSub.bigQueryStorage.rawEventsDatasetId`.
6
+ */
7
+ export class EventTopicQueryEventsForBigQuery extends EventTopicQueryEvents {
8
+ async _call(context) {
9
+ return await callDeferred(this, context, import.meta.url);
10
+ }
11
+ _supports(context) {
12
+ const conf = context.asConfiguration();
13
+ return (conf.get('events.broker') === 'google.pubSub' &&
14
+ conf.get('events.format') === 'json' &&
15
+ !!conf.get('google.pubSub.bigQueryStorage.rawEventsDatasetId'));
16
+ }
17
+ }
@@ -1 +1,2 @@
1
+ export { DatabaseQueryRecordsForFirestore } from './query-records.js';
1
2
  export { GoogleFirestoreMergeRules } from './merge-rules.js';
@@ -1 +1,2 @@
1
+ export { DatabaseQueryRecordsForFirestore } from './query-records.js';
1
2
  export { GoogleFirestoreMergeRules } from './merge-rules.js';
@@ -0,0 +1,3 @@
1
+ import type { WorkspaceContext } from '@causa/workspace';
2
+ import type { DatabaseQueryRecordsForFirestore } from './query-records.js';
3
+ export default function call(this: DatabaseQueryRecordsForFirestore, context: WorkspaceContext): Promise<any[]>;
@@ -0,0 +1,50 @@
1
+ import { deleteApp, initializeApp } from 'firebase-admin/app';
2
+ import { DocumentReference, GeoPoint, getFirestore, Timestamp, } from 'firebase-admin/firestore';
3
+ import { randomUUID } from 'node:crypto';
4
+ export default async function call(context) {
5
+ if (!this.query) {
6
+ throw new Error(`The 'query' input is required for the 'google.firestore' engine.`);
7
+ }
8
+ const googleConf = context.asConfiguration();
9
+ const projectId = googleConf.getOrThrow('google.project');
10
+ const database = this.database ?? '(default)';
11
+ context.logger.debug(`🗃️ Fetching Firestore document '${this.query}' from database '${database}' in project '${projectId}'.`);
12
+ const app = initializeApp({ projectId }, randomUUID());
13
+ try {
14
+ const firestore = getFirestore(app, database);
15
+ const snapshot = await firestore.doc(this.query).get();
16
+ return snapshot.exists ? [toJson(snapshot.data())] : [];
17
+ }
18
+ finally {
19
+ await deleteApp(app);
20
+ }
21
+ }
22
+ /**
23
+ * Recursively converts Firestore-specific types to JSON-friendly equivalents:
24
+ * - {@link Timestamp} → {@link Date}
25
+ * - {@link GeoPoint} → `{ latitude, longitude }`
26
+ * - {@link DocumentReference} → its `path`
27
+ *
28
+ * Other values (including `Buffer`) are returned as-is.
29
+ */
30
+ function toJson(value) {
31
+ if (value === null || typeof value !== 'object') {
32
+ return value;
33
+ }
34
+ if (value instanceof Timestamp) {
35
+ return value.toDate();
36
+ }
37
+ if (value instanceof GeoPoint) {
38
+ return { latitude: value.latitude, longitude: value.longitude };
39
+ }
40
+ if (value instanceof DocumentReference) {
41
+ return value.path;
42
+ }
43
+ if (Buffer.isBuffer(value)) {
44
+ return value;
45
+ }
46
+ if (Array.isArray(value)) {
47
+ return value.map(toJson);
48
+ }
49
+ return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, toJson(v)]));
50
+ }
@@ -0,0 +1,18 @@
1
+ import { WorkspaceContext } from '@causa/workspace';
2
+ import { DatabaseQueryRecords } from '@causa/workspace-core';
3
+ /**
4
+ * The engine identifier handled by this implementation.
5
+ */
6
+ export declare const FIRESTORE_ENGINE = "google.firestore";
7
+ /**
8
+ * Implements {@link DatabaseQueryRecords} for Firestore.
9
+ *
10
+ * The `database` input is optional and defaults to the default Firestore database. The `query` input is the path to a
11
+ * single document (e.g. `users/abc`).
12
+ *
13
+ * Returns an array containing a single document's data, or an empty array if the document does not exist.
14
+ */
15
+ export declare class DatabaseQueryRecordsForFirestore extends DatabaseQueryRecords {
16
+ _call(context: WorkspaceContext): Promise<any[]>;
17
+ _supports(): boolean;
18
+ }
@@ -0,0 +1,22 @@
1
+ import { callDeferred, WorkspaceContext } from '@causa/workspace';
2
+ import { DatabaseQueryRecords } from '@causa/workspace-core';
3
+ /**
4
+ * The engine identifier handled by this implementation.
5
+ */
6
+ export const FIRESTORE_ENGINE = 'google.firestore';
7
+ /**
8
+ * Implements {@link DatabaseQueryRecords} for Firestore.
9
+ *
10
+ * The `database` input is optional and defaults to the default Firestore database. The `query` input is the path to a
11
+ * single document (e.g. `users/abc`).
12
+ *
13
+ * Returns an array containing a single document's data, or an empty array if the document does not exist.
14
+ */
15
+ export class DatabaseQueryRecordsForFirestore extends DatabaseQueryRecords {
16
+ async _call(context) {
17
+ return await callDeferred(this, context, import.meta.url);
18
+ }
19
+ _supports() {
20
+ return this.engine === FIRESTORE_ENGINE;
21
+ }
22
+ }
@@ -1,2 +1,3 @@
1
1
  export { GoogleSpannerListDatabases } from './list-databases.js';
2
+ export { DatabaseQueryRecordsForSpanner } from './query-records.js';
2
3
  export { GoogleSpannerWriteDatabases } from './write-databases.js';
@@ -1,2 +1,3 @@
1
1
  export { GoogleSpannerListDatabases } from './list-databases.js';
2
+ export { DatabaseQueryRecordsForSpanner } from './query-records.js';
2
3
  export { GoogleSpannerWriteDatabases } from './write-databases.js';
@@ -0,0 +1,3 @@
1
+ import type { WorkspaceContext } from '@causa/workspace';
2
+ import { type DatabaseQueryRecordsForSpanner } from './query-records.js';
3
+ export default function call(this: DatabaseQueryRecordsForSpanner, context: WorkspaceContext): Promise<any[]>;
@@ -0,0 +1,66 @@
1
+ import { Numeric, PGNumeric, Spanner, SpannerDate, } from '@google-cloud/spanner';
2
+ import { SPANNER_QUERY_RECORDS_LIMIT, } from './query-records.js';
3
+ export default async function call(context) {
4
+ if (!this.database) {
5
+ throw new Error(`The 'database' input is required for the 'google.spanner' engine.`);
6
+ }
7
+ if (!this.query) {
8
+ throw new Error(`The 'query' input is required for the 'google.spanner' engine.`);
9
+ }
10
+ const googleConf = context.asConfiguration();
11
+ const projectId = googleConf.getOrThrow('google.project');
12
+ const instanceName = googleConf.getOrThrow('google.spanner.instance.name');
13
+ context.logger.debug(`🗃️ Querying Spanner database '${this.database}' in instance '${instanceName}' (project '${projectId}').`);
14
+ const spanner = new Spanner({ projectId });
15
+ let database;
16
+ try {
17
+ database = spanner.instance(instanceName).database(this.database);
18
+ const rows = [];
19
+ const stream = database.runStream({
20
+ sql: this.query,
21
+ json: true,
22
+ jsonOptions: { wrapNumbers: false, wrapStructs: false },
23
+ });
24
+ for await (const row of stream) {
25
+ if (rows.length >= SPANNER_QUERY_RECORDS_LIMIT) {
26
+ context.logger.warn(`⚠️ Spanner query exceeded the hard limit of ${SPANNER_QUERY_RECORDS_LIMIT} rows. Results were truncated.`);
27
+ break;
28
+ }
29
+ rows.push(toJson(row));
30
+ }
31
+ return rows;
32
+ }
33
+ finally {
34
+ await database?.close();
35
+ spanner.close();
36
+ }
37
+ }
38
+ /**
39
+ * Recursively converts Spanner-specific types in a row to JSON-friendly equivalents:
40
+ * - {@link Numeric} / {@link PGNumeric} (NUMERIC / PG_NUMERIC) → their string value.
41
+ * - {@link SpannerDate} (DATE) → `YYYY-MM-DD` string.
42
+ * - Other `Date` subclasses such as `PreciseDate` (TIMESTAMP) → plain {@link Date} instances.
43
+ *
44
+ * `Buffer` values are returned as-is.
45
+ */
46
+ function toJson(value) {
47
+ if (value === null || typeof value !== 'object') {
48
+ return value;
49
+ }
50
+ if (value instanceof Numeric || value instanceof PGNumeric) {
51
+ return value.value;
52
+ }
53
+ if (value instanceof SpannerDate) {
54
+ return value.toJSON();
55
+ }
56
+ if (value instanceof Date) {
57
+ return new Date(value.getTime());
58
+ }
59
+ if (Buffer.isBuffer(value)) {
60
+ return value;
61
+ }
62
+ if (Array.isArray(value)) {
63
+ return value.map(toJson);
64
+ }
65
+ return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, toJson(v)]));
66
+ }
@@ -0,0 +1,23 @@
1
+ import { WorkspaceContext } from '@causa/workspace';
2
+ import { DatabaseQueryRecords } from '@causa/workspace-core';
3
+ /**
4
+ * The engine identifier handled by this implementation.
5
+ */
6
+ export declare const SPANNER_ENGINE = "google.spanner";
7
+ /**
8
+ * The maximum number of rows that will be returned by a single query.
9
+ * Acts as a safety mechanism to avoid loading huge result sets in memory.
10
+ */
11
+ export declare const SPANNER_QUERY_RECORDS_LIMIT = 100000;
12
+ /**
13
+ * Implements {@link DatabaseQueryRecords} for Spanner databases.
14
+ *
15
+ * The Spanner instance and GCP project are read from the workspace configuration. The `database` input is the name of
16
+ * the database within the instance, and `query` is a regular SQL statement.
17
+ *
18
+ * Results are streamed and capped to {@link SPANNER_QUERY_RECORDS_LIMIT} rows.
19
+ */
20
+ export declare class DatabaseQueryRecordsForSpanner extends DatabaseQueryRecords {
21
+ _call(context: WorkspaceContext): Promise<any[]>;
22
+ _supports(): boolean;
23
+ }
@@ -0,0 +1,27 @@
1
+ import { callDeferred, WorkspaceContext } from '@causa/workspace';
2
+ import { DatabaseQueryRecords } from '@causa/workspace-core';
3
+ /**
4
+ * The engine identifier handled by this implementation.
5
+ */
6
+ export const SPANNER_ENGINE = 'google.spanner';
7
+ /**
8
+ * The maximum number of rows that will be returned by a single query.
9
+ * Acts as a safety mechanism to avoid loading huge result sets in memory.
10
+ */
11
+ export const SPANNER_QUERY_RECORDS_LIMIT = 100_000;
12
+ /**
13
+ * Implements {@link DatabaseQueryRecords} for Spanner databases.
14
+ *
15
+ * The Spanner instance and GCP project are read from the workspace configuration. The `database` input is the name of
16
+ * the database within the instance, and `query` is a regular SQL statement.
17
+ *
18
+ * Results are streamed and capped to {@link SPANNER_QUERY_RECORDS_LIMIT} rows.
19
+ */
20
+ export class DatabaseQueryRecordsForSpanner extends DatabaseQueryRecords {
21
+ async _call(context) {
22
+ return await callDeferred(this, context, import.meta.url);
23
+ }
24
+ _supports() {
25
+ return this.engine === SPANNER_ENGINE;
26
+ }
27
+ }
@@ -1,16 +1,17 @@
1
1
  import { CausaListConfigurationSchemasForGoogle } from './causa/index.js';
2
2
  import { EmulatorStartForFirebaseStorage, EmulatorStartForFirestore, EmulatorStartForIdentityPlatform, EmulatorStartForPubSub, EmulatorStartForSpanner, EmulatorStopForFirebaseStorage, EmulatorStopForFirestore, EmulatorStopForIdentityPlatform, EmulatorStopForPubSub, EmulatorStopForSpanner, } from './emulator/index.js';
3
- import { EventTopicBrokerCreateTopicForPubSub, EventTopicBrokerCreateTriggerForCloudRun, EventTopicBrokerDeleteTopicForPubSub, EventTopicBrokerDeleteTriggerResourceForCloudRunInvokerRole, EventTopicBrokerDeleteTriggerResourceForPubSubSubscription, EventTopicBrokerDeleteTriggerResourceForServiceAccount, EventTopicBrokerGetTopicIdForPubSub, EventTopicBrokerPublishEventsForGoogle, EventTopicCreateBackfillSourceForBigQuery, } from './event-topic/index.js';
3
+ import { EventTopicBrokerCreateTopicForPubSub, EventTopicBrokerCreateTriggerForCloudRun, EventTopicBrokerDeleteTopicForPubSub, EventTopicBrokerDeleteTriggerResourceForCloudRunInvokerRole, EventTopicBrokerDeleteTriggerResourceForPubSubSubscription, EventTopicBrokerDeleteTriggerResourceForServiceAccount, EventTopicBrokerGetTopicIdForPubSub, EventTopicBrokerPublishEventsForGoogle, EventTopicCreateBackfillSourceForBigQuery, EventTopicQueryEventsForBigQuery, } from './event-topic/index.js';
4
4
  import { GoogleAppCheckGenerateToken } from './google-app-check/index.js';
5
5
  import { GoogleFirebaseStorageMergeRules } from './google-firebase-storage/index.js';
6
- import { GoogleFirestoreMergeRules } from './google-firestore/index.js';
6
+ import { DatabaseQueryRecordsForFirestore, GoogleFirestoreMergeRules, } from './google-firestore/index.js';
7
7
  import { GoogleIdentityPlatformGenerateCustomToken, GoogleIdentityPlatformGenerateToken, } from './google-identity-platform/index.js';
8
8
  import { GooglePubSubWriteTopics } from './google-pubsub/index.js';
9
9
  import { GoogleServicesEnable } from './google-services/index.js';
10
- import { GoogleSpannerListDatabases, GoogleSpannerWriteDatabases, } from './google-spanner/index.js';
10
+ import { DatabaseQueryRecordsForSpanner, GoogleSpannerListDatabases, GoogleSpannerWriteDatabases, } from './google-spanner/index.js';
11
11
  import { ProjectGetArtefactDestinationForCloudFunctions, ProjectGetArtefactDestinationForCloudRun, ProjectPushArtefactForCloudFunctions, } from './project/index.js';
12
12
  import { SecretFetchForGoogleAccessToken, SecretFetchForGoogleSecretManager, } from './secret/index.js';
13
+ import { ServiceContainerQueryLogsForCloudRun } from './service-container/index.js';
13
14
  import { TypeScriptGetDecoratorRendererForGoogleFirestore, TypeScriptGetDecoratorRendererForGoogleSpanner, } from './typescript/index.js';
14
15
  export function registerFunctions(context) {
15
- context.registerFunctionImplementations(CausaListConfigurationSchemasForGoogle, EmulatorStartForFirebaseStorage, EmulatorStartForFirestore, EmulatorStartForIdentityPlatform, EmulatorStartForPubSub, EmulatorStartForSpanner, EmulatorStopForFirebaseStorage, EmulatorStopForFirestore, EmulatorStopForIdentityPlatform, EmulatorStopForPubSub, EmulatorStopForSpanner, EventTopicBrokerCreateTopicForPubSub, EventTopicBrokerCreateTriggerForCloudRun, EventTopicBrokerDeleteTopicForPubSub, EventTopicBrokerDeleteTriggerResourceForCloudRunInvokerRole, EventTopicBrokerDeleteTriggerResourceForPubSubSubscription, EventTopicBrokerDeleteTriggerResourceForServiceAccount, EventTopicBrokerGetTopicIdForPubSub, EventTopicBrokerPublishEventsForGoogle, EventTopicCreateBackfillSourceForBigQuery, GoogleAppCheckGenerateToken, GoogleFirebaseStorageMergeRules, GoogleFirestoreMergeRules, GoogleIdentityPlatformGenerateCustomToken, GoogleIdentityPlatformGenerateToken, GooglePubSubWriteTopics, GoogleServicesEnable, GoogleSpannerListDatabases, GoogleSpannerWriteDatabases, ProjectGetArtefactDestinationForCloudFunctions, ProjectGetArtefactDestinationForCloudRun, ProjectPushArtefactForCloudFunctions, SecretFetchForGoogleAccessToken, SecretFetchForGoogleSecretManager, TypeScriptGetDecoratorRendererForGoogleFirestore, TypeScriptGetDecoratorRendererForGoogleSpanner);
16
+ context.registerFunctionImplementations(CausaListConfigurationSchemasForGoogle, DatabaseQueryRecordsForFirestore, DatabaseQueryRecordsForSpanner, EmulatorStartForFirebaseStorage, EmulatorStartForFirestore, EmulatorStartForIdentityPlatform, EmulatorStartForPubSub, EmulatorStartForSpanner, EmulatorStopForFirebaseStorage, EmulatorStopForFirestore, EmulatorStopForIdentityPlatform, EmulatorStopForPubSub, EmulatorStopForSpanner, EventTopicBrokerCreateTopicForPubSub, EventTopicBrokerCreateTriggerForCloudRun, EventTopicBrokerDeleteTopicForPubSub, EventTopicBrokerDeleteTriggerResourceForCloudRunInvokerRole, EventTopicBrokerDeleteTriggerResourceForPubSubSubscription, EventTopicBrokerDeleteTriggerResourceForServiceAccount, EventTopicBrokerGetTopicIdForPubSub, EventTopicBrokerPublishEventsForGoogle, EventTopicCreateBackfillSourceForBigQuery, EventTopicQueryEventsForBigQuery, GoogleAppCheckGenerateToken, GoogleFirebaseStorageMergeRules, GoogleFirestoreMergeRules, GoogleIdentityPlatformGenerateCustomToken, GoogleIdentityPlatformGenerateToken, GooglePubSubWriteTopics, GoogleServicesEnable, GoogleSpannerListDatabases, GoogleSpannerWriteDatabases, ProjectGetArtefactDestinationForCloudFunctions, ProjectGetArtefactDestinationForCloudRun, ProjectPushArtefactForCloudFunctions, SecretFetchForGoogleAccessToken, SecretFetchForGoogleSecretManager, ServiceContainerQueryLogsForCloudRun, TypeScriptGetDecoratorRendererForGoogleFirestore, TypeScriptGetDecoratorRendererForGoogleSpanner);
16
17
  }
@@ -0,0 +1 @@
1
+ export { ServiceContainerQueryLogsForCloudRun } from './query-logs-cloud-run.js';
@@ -0,0 +1 @@
1
+ export { ServiceContainerQueryLogsForCloudRun } from './query-logs-cloud-run.js';
@@ -0,0 +1,4 @@
1
+ import type { WorkspaceContext } from '@causa/workspace';
2
+ import type { QueriedLogEntry } from '@causa/workspace-core';
3
+ import type { ServiceContainerQueryLogsForCloudRun } from './query-logs-cloud-run.js';
4
+ export default function call(this: ServiceContainerQueryLogsForCloudRun, context: WorkspaceContext): Promise<QueriedLogEntry[]>;
@@ -0,0 +1,36 @@
1
+ import { LoggingService } from '../../services/index.js';
2
+ /**
3
+ * The default duration applied to `from` when it is not set, in milliseconds.
4
+ */
5
+ const DEFAULT_FROM_OFFSET = 60 * 60 * 1000;
6
+ /**
7
+ * The default value for `limit` when it is not set.
8
+ */
9
+ const DEFAULT_LIMIT = 1000;
10
+ export default async function call(context) {
11
+ const from = this.from ?? new Date(Date.now() - DEFAULT_FROM_OFFSET);
12
+ const limit = this.limit ?? DEFAULT_LIMIT;
13
+ const filterParts = [
14
+ `resource.type="cloud_run_revision"`,
15
+ `resource.labels.service_name=~"${this.service}"`,
16
+ `timestamp>="${from.toISOString()}"`,
17
+ ];
18
+ if (this.to) {
19
+ filterParts.push(`timestamp<"${this.to.toISOString()}"`);
20
+ }
21
+ if (this.filter) {
22
+ filterParts.push(this.filter);
23
+ }
24
+ const filter = filterParts.join('\n');
25
+ const loggingService = context.service(LoggingService);
26
+ context.logger.debug(`Querying Cloud Logging for service '${this.service}' in project '${loggingService.projectId}' with filter:\n${filter}`);
27
+ const [pageEntries] = await loggingService.logging.getEntries({
28
+ filter,
29
+ orderBy: 'timestamp asc',
30
+ maxResults: limit,
31
+ });
32
+ return pageEntries.map((entry) => ({
33
+ timestamp: entry.metadata.timestamp,
34
+ message: entry.toStructuredJSON(),
35
+ }));
36
+ }
@@ -0,0 +1,12 @@
1
+ import { WorkspaceContext } from '@causa/workspace';
2
+ import { type QueriedLogEntry, ServiceContainerQueryLogs } from '@causa/workspace-core';
3
+ /**
4
+ * Implements {@link ServiceContainerQueryLogs} for services deployed on Cloud Run.
5
+ *
6
+ * Queries the Cloud Logging API for entries emitted by `cloud_run_revision` resources whose `service_name` label
7
+ * matches the input `service`. The optional `from` / `to` time range, `filter`, and `limit` are forwarded to the API.
8
+ */
9
+ export declare class ServiceContainerQueryLogsForCloudRun extends ServiceContainerQueryLogs {
10
+ _call(context: WorkspaceContext): Promise<QueriedLogEntry[]>;
11
+ _supports(context: WorkspaceContext): boolean;
12
+ }
@@ -0,0 +1,18 @@
1
+ import { callDeferred, WorkspaceContext } from '@causa/workspace';
2
+ import { ServiceContainerQueryLogs, } from '@causa/workspace-core';
3
+ /**
4
+ * Implements {@link ServiceContainerQueryLogs} for services deployed on Cloud Run.
5
+ *
6
+ * Queries the Cloud Logging API for entries emitted by `cloud_run_revision` resources whose `service_name` label
7
+ * matches the input `service`. The optional `from` / `to` time range, `filter`, and `limit` are forwarded to the API.
8
+ */
9
+ export class ServiceContainerQueryLogsForCloudRun extends ServiceContainerQueryLogs {
10
+ async _call(context) {
11
+ return await callDeferred(this, context, import.meta.url);
12
+ }
13
+ _supports(context) {
14
+ return (context
15
+ .asConfiguration()
16
+ .get('serviceContainer.platform') === 'google.cloudRun');
17
+ }
18
+ }
@@ -8,6 +8,7 @@ export { FirebaseEmulatorService } from './firebase-emulator.js';
8
8
  export { GcloudEmulatorService } from './gcloud-emulator.js';
9
9
  export { GoogleApisService } from './google-apis.js';
10
10
  export { IamService } from './iam.js';
11
+ export { LoggingService } from './logging.js';
11
12
  export { PubSubService } from './pubsub.js';
12
13
  export { ResourceManagerService } from './resource-manager.js';
13
14
  export { GoogleSecretManagerService } from './secret-manager.js';
@@ -7,6 +7,7 @@ export { FirebaseEmulatorService } from './firebase-emulator.js';
7
7
  export { GcloudEmulatorService } from './gcloud-emulator.js';
8
8
  export { GoogleApisService } from './google-apis.js';
9
9
  export { IamService } from './iam.js';
10
+ export { LoggingService } from './logging.js';
10
11
  export { PubSubService } from './pubsub.js';
11
12
  export { ResourceManagerService } from './resource-manager.js';
12
13
  export { GoogleSecretManagerService } from './secret-manager.js';
@@ -0,0 +1,16 @@
1
+ import { WorkspaceContext } from '@causa/workspace';
2
+ import { Logging } from '@google-cloud/logging';
3
+ /**
4
+ * A service that provides access to Cloud Logging.
5
+ */
6
+ export declare class LoggingService {
7
+ /**
8
+ * The Cloud Logging client configured with the GCP project ID.
9
+ */
10
+ readonly logging: Logging;
11
+ /**
12
+ * The GCP project ID.
13
+ */
14
+ readonly projectId: string;
15
+ constructor(context: WorkspaceContext);
16
+ }
@@ -0,0 +1,20 @@
1
+ import { WorkspaceContext } from '@causa/workspace';
2
+ import { Logging } from '@google-cloud/logging';
3
+ /**
4
+ * A service that provides access to Cloud Logging.
5
+ */
6
+ export class LoggingService {
7
+ /**
8
+ * The Cloud Logging client configured with the GCP project ID.
9
+ */
10
+ logging;
11
+ /**
12
+ * The GCP project ID.
13
+ */
14
+ projectId;
15
+ constructor(context) {
16
+ const googleConf = context.asConfiguration();
17
+ this.projectId = googleConf.getOrThrow('google.project');
18
+ this.logging = new Logging({ projectId: this.projectId });
19
+ }
20
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@causa/workspace-google",
3
- "version": "0.16.0",
3
+ "version": "0.17.0",
4
4
  "description": "The Causa workspace module providing many functionalities related to GCP and its services.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -36,18 +36,19 @@
36
36
  },
37
37
  "dependencies": {
38
38
  "@causa/cli": ">= 0.7.0 < 1.0.0",
39
- "@causa/workspace": ">= 0.24.0 < 1.0.0",
40
- "@causa/workspace-core": ">= 0.32.0 < 1.0.0",
39
+ "@causa/workspace": ">= 0.24.1 < 1.0.0",
40
+ "@causa/workspace-core": ">= 0.34.0 < 1.0.0",
41
41
  "@causa/workspace-typescript": ">= 0.22.0 < 1.0.0",
42
- "@google-cloud/apikeys": "^2.2.1",
43
- "@google-cloud/bigquery": "^8.1.1",
44
- "@google-cloud/iam-credentials": "^4.2.1",
42
+ "@google-cloud/apikeys": "^2.2.2",
43
+ "@google-cloud/bigquery": "^8.3.0",
44
+ "@google-cloud/iam-credentials": "^4.2.2",
45
+ "@google-cloud/logging": "^11.2.1",
45
46
  "@google-cloud/pubsub": "^5.3.0",
46
- "@google-cloud/resource-manager": "^6.2.1",
47
- "@google-cloud/run": "^3.2.0",
48
- "@google-cloud/secret-manager": "^6.1.1",
49
- "@google-cloud/service-usage": "^4.2.1",
50
- "@google-cloud/spanner": "^8.6.0",
47
+ "@google-cloud/resource-manager": "^6.2.2",
48
+ "@google-cloud/run": "^3.2.1",
49
+ "@google-cloud/secret-manager": "^6.1.2",
50
+ "@google-cloud/service-usage": "^4.2.2",
51
+ "@google-cloud/spanner": "^8.7.0",
51
52
  "@google-cloud/storage": "^7.19.0",
52
53
  "class-validator": "^0.15.1",
53
54
  "firebase": "^12.12.1",
@@ -61,20 +62,20 @@
61
62
  "quicktype-core": "^23.2.6"
62
63
  },
63
64
  "devDependencies": {
64
- "@swc/core": "^1.15.30",
65
+ "@swc/core": "^1.15.33",
65
66
  "@swc/jest": "^0.2.39",
66
67
  "@tsconfig/node22": "^22.0.5",
67
68
  "@types/jest": "^30.0.0",
68
69
  "@types/micromatch": "^4.0.10",
69
70
  "@types/node": "^20.19.39",
70
71
  "copyfiles": "^2.4.1",
71
- "eslint": "^10.2.1",
72
+ "eslint": "^10.3.0",
72
73
  "eslint-config-prettier": "^10.1.8",
73
74
  "eslint-plugin-prettier": "^5.5.5",
74
75
  "jest": "^30.3.0",
75
76
  "jest-extended": "^7.0.0",
76
77
  "rimraf": "^6.1.3",
77
78
  "typescript": "^6.0.3",
78
- "typescript-eslint": "^8.59.0"
79
+ "typescript-eslint": "^8.59.1"
79
80
  }
80
81
  }