|
| 1 | +import { z } from "zod"; |
| 2 | +import { tool } from "../../tool.js"; |
| 3 | +import { toContent } from "../../util.js"; |
| 4 | +import { Backend, getBackend, getTraffic, listBuilds, Traffic } from "../../../gcp/apphosting.js"; |
| 5 | +import { last } from "../../../utils.js"; |
| 6 | +import { FirebaseError } from "../../../error.js"; |
| 7 | +import { fetchServiceLogs } from "../../../gcp/run.js"; |
| 8 | +import { listEntries } from "../../../gcp/cloudlogging.js"; |
| 9 | + |
| 10 | +export const fetch_logs = tool( |
| 11 | + { |
| 12 | + name: "fetch_logs", |
| 13 | + description: |
| 14 | + "Fetches the most recent logs for a specified App Hosting backend. If `buildLogs` is specified, the logs from the build process for the latest build are returned. The most recent logs are listed first.", |
| 15 | + inputSchema: z.object({ |
| 16 | + buildLogs: z |
| 17 | + .boolean() |
| 18 | + .default(false) |
| 19 | + .describe( |
| 20 | + "If specified, the logs for the most recent build will be returned instead of the logs for the service. The build logs are returned 'in order', to be read from top to bottom.", |
| 21 | + ), |
| 22 | + backendId: z.string().describe("The ID of the backend for which to fetch logs."), |
| 23 | + location: z |
| 24 | + .string() |
| 25 | + .describe( |
| 26 | + "The specific region for the backend. By default, if a backend is uniquely named across all locations, that one will be used.", |
| 27 | + ), |
| 28 | + }), |
| 29 | + annotations: { |
| 30 | + title: "Fetch logs for App Hosting backends and builds.", |
| 31 | + readOnlyHint: true, |
| 32 | + }, |
| 33 | + _meta: { |
| 34 | + requiresAuth: true, |
| 35 | + requiresProject: true, |
| 36 | + }, |
| 37 | + }, |
| 38 | + async ({ buildLogs, backendId, location } = {}, { projectId }) => { |
| 39 | + projectId ||= ""; |
| 40 | + location ||= ""; |
| 41 | + if (!backendId) { |
| 42 | + return toContent(`backendId must be specified.`); |
| 43 | + } |
| 44 | + const backend = await getBackend(projectId, location, backendId); |
| 45 | + const traffic = await getTraffic(projectId, location, backendId); |
| 46 | + const data: Backend & { traffic: Traffic } = { ...backend, traffic }; |
| 47 | + |
| 48 | + if (buildLogs) { |
| 49 | + const builds = await listBuilds(projectId, location, backendId); |
| 50 | + builds.builds.sort( |
| 51 | + (a, b) => new Date(a.createTime).getTime() - new Date(b.createTime).getTime(), |
| 52 | + ); |
| 53 | + const build = last(builds.builds); |
| 54 | + const r = new RegExp(`region=${location}/([0-9a-f-]+)?`); |
| 55 | + const match = r.exec(build.buildLogsUri ?? ""); |
| 56 | + if (!match) { |
| 57 | + throw new FirebaseError("Unable to determine the build ID."); |
| 58 | + } |
| 59 | + const buildId = match[1]; |
| 60 | + // Thirty days ago makes sure we get any saved data within the default retention period. |
| 61 | + const thirtyDaysAgo = new Date(); |
| 62 | + thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30); |
| 63 | + const timestampFilter = `timestamp >= "${thirtyDaysAgo.toISOString()}"`; |
| 64 | + const filter = `resource.type="build" resource.labels.build_id="${buildId}" ${timestampFilter}`; |
| 65 | + const entries = await listEntries(projectId, filter, 100, "asc"); |
| 66 | + if (!Array.isArray(entries) || !entries.length) { |
| 67 | + return toContent("No logs found."); |
| 68 | + } |
| 69 | + return toContent(entries); |
| 70 | + } |
| 71 | + |
| 72 | + const serviceName = last(data.managedResources)?.runService.service; |
| 73 | + if (!serviceName) { |
| 74 | + throw new FirebaseError("Unable to get service name from managedResources."); |
| 75 | + } |
| 76 | + const serviceId = last(serviceName.split("/")); |
| 77 | + const logs = await fetchServiceLogs(projectId, serviceId); |
| 78 | + return toContent(logs); |
| 79 | + }, |
| 80 | +); |
0 commit comments