Getting Started

Connect Firebase App Hosting

Forward Firebase App Hosting request logs to Honeylog using Cloud Logging and Pub/Sub.

Forward your website’s request logs to Honeylog using Google Cloud Logging and Pub/Sub. Complete every step in your browser using the Google Cloud Console and the JavaScript below.

Logs follow this path: Firebase App Hosting → Cloud Logging → Pub/Sub topic → push subscription → Honeylog.

This guide covers Firebase App Hosting. Classic Firebase Hosting, which commonly uses .web.app domains, is a different service. A Cloud Run filter is included at the end of this guide.

#1. Collect your settings

Your App Hosting website must already be running and producing request logs. In the Google Cloud Console, select the project that contains your website.

Create or open the site in Honeylog where you want to record these requests. Collect the following values:

Setting What to use
Honeylog site domain The domain registered in Honeylog, without https://, a path, or a trailing slash. For example: www.example.com.
Ingestion secret The ingestion secret for that Honeylog site. This is not an account API key or a Google credential.
Ingestion endpoint The ingestion URL supplied by Honeylog, including /events or /api/events. This is not your Firebase website URL.
Google project ID The project ID, such as my-project-123, rather than its display name.
App Hosting backend ID The backend ID shown in Firebase App Hosting and in the request logs.

The Honeylog domain can differ from the technical .hosted.app domain. The script assigns events to the Honeylog site while preserving the original request URL. The secret must belong to the site configured in the script. Use a dedicated log sink for the backend you want to monitor.

You need permission to create Pub/Sub topics and subscriptions, create a Cloud Logging sink, and grant the sink the Pub/Sub Publisher role on its destination topic.

#2. Create a Pub/Sub topic

Open Pub/Sub → Topics → Create topic in the Google Cloud Console.

Field Value
Topic ID honeylog-requests
Create a default subscription Off
Schema None
Topic transforms None

Leave the other fields at their defaults and click Create. If prompted, enable the Pub/Sub API in the Console.

The full topic name is projects/YOUR_PROJECT_ID/topics/honeylog-requests. Use this same topic throughout the remaining steps.

#3. Configure a push subscription

Open Pub/Sub → Subscriptions → Create subscription.

Field Value
Subscription ID honeylog-requests-push
Topic The honeylog-requests topic from step 2
Delivery type Push
Endpoint URL Your Honeylog ingestion endpoint from step 1
Enable authentication Off
Payload unwrapping On
Write metadata On
Acknowledgment deadline 60 seconds
Message retention duration 7 days
Expiration Never expire
Retry policy Exponential backoff: minimum 10 seconds, maximum 600 seconds
Subscription filter Empty

Both Payload unwrapping and Write metadata must be enabled. Unwrapping sends the JSON produced by the script as the HTTP request body. Write metadata sends the authentication attributes as HTTP headers.

The script authenticates requests to Honeylog using your site’s ingestion secret. Keep Google push authentication disabled.

Before completing subscription creation, add the transform in the next step. If the subscription already exists, open it and click Edit.

#4. Add the JavaScript transform

In the subscription configuration, open Transforms → Add a transform.

Field Value
Transform type JavaScript UDF
Function name honeylogNormalize
Disable transform Unchecked
Code The entire script below, including all three helper functions

Replace only the two values at the beginning of the script:

const site = "YOUR_HONEYLOG_SITE_DOMAIN";
const secret = "YOUR_HONEYLOG_INGESTION_SECRET";

Keep the quotation marks. Paste your ingestion secret exactly as supplied by Honeylog, without decoding or converting it.

The script preserves the visitor IP, user agent, URL, method, original log timestamp and, when present, HTTP status, response size, referrer, and latency. It excludes common static assets such as images, CSS, and JavaScript. Requests for sitemap.xml and robots.txt are kept.

#Complete script

function honeylogNormalize(message, metadata) {
  const site = "YOUR_HONEYLOG_SITE_DOMAIN";
  const secret = "YOUR_HONEYLOG_INGESTION_SECRET";

  const log = JSON.parse(message.data);
  const http = log.httpRequest;

  if (!http || !log.timestamp || !log.resource || !log.logName) {
    throw new Error("Expected a Google request log");
  }

  if (
    typeof http.remoteIp !== "string" || !http.remoteIp.trim() ||
    typeof http.userAgent !== "string" || !http.userAgent.trim() ||
    typeof http.requestMethod !== "string" ||
    !/^[A-Za-z]{1,16}$/.test(http.requestMethod)
  ) {
    throw new Error("Missing original visitor IP, user agent or request method");
  }

  const url = typeof http.requestUrl === "string" &&
    /^https?:\/\/([^/?#]+)(\/[^?#]*)?(?:[?#].*)?$/i.exec(http.requestUrl);

  if (!url) {
    throw new Error("Invalid request URL");
  }

  if (
    /\.(?:css|js|mjs|map|png|jpe?g|gif|webp|avif|svg|ico|woff2?|ttf|otf|eot|mp4|webm|mp3|wav|pdf)$/i
      .test(url[2] || "/")
  ) {
    return null;
  }

  if (
    typeof log.timestamp !== "string" ||
    !Number.isFinite(Date.parse(log.timestamp))
  ) {
    throw new Error("Invalid log timestamp");
  }

  const event = {
    ip: http.remoteIp,
    ua: http.userAgent,
    n: "pageview",
    u: http.requestUrl,
    d: site,
    timestamp: log.timestamp.replace(
      /(\.\d{6})\d+(Z|[+-]\d{2}:\d{2})$/,
      "$1$2"
    ),
    method: http.requestMethod.toUpperCase()
  };

  if (http.referer) {
    event.r = http.referer;
  }

  if (http.status !== undefined) {
    if (
      !Number.isInteger(http.status) ||
      http.status < 100 ||
      http.status > 599
    ) {
      throw new Error("Invalid HTTP status");
    }

    event.status_code = http.status;
  }

  if (http.responseSize !== undefined) {
    const size = http.responseSize;

    if (
      (typeof size !== "number" && typeof size !== "string") ||
      !/^\d+$/.test(String(size)) ||
      !Number.isSafeInteger(Number(size))
    ) {
      throw new Error("Invalid responseSize");
    }

    event.bytes_sent = Number(size);
  }

  if (http.latency !== undefined) {
    if (
      typeof http.latency !== "string" ||
      !/^\d+(?:\.\d+)?s$/.test(http.latency)
    ) {
      throw new Error("Invalid latency");
    }

    const ms = Math.round(Number(http.latency.slice(0, -1)) * 1000);

    if (!Number.isSafeInteger(ms)) {
      throw new Error("Invalid latency");
    }

    event.response_time = ms;
  }

  // Signature timestamp: current time, not the log timestamp.
  const timestamp = String(Math.floor(Date.now() / 1000));

  // Distinguishes signatures from separate executions within the same second.
  // Stays outside the events and does not become a pageview property.
  const nonce =
    Date.now().toString(36) + "-" +
    Math.random().toString(36).slice(2) + "-" +
    Math.random().toString(36).slice(2);

  const body = JSON.stringify({
    events: [event],
    delivery_nonce: nonce
  });

  message.data = body;
  message.attributes = {
    "Content-Type": "application/json",
    "X-Honeylog-Site": site,
    "X-Honeylog-Timestamp": timestamp,
    "X-Honeylog-Signature": honeylogHmacSha256(
      secret,
      timestamp + "." + body
    )
  };

  return message;
}

function honeylogUtf8(text) {
  const out = [];

  for (let i = 0; i < text.length; i++) {
    let c = text.charCodeAt(i);

    if (c >= 0xd800 && c <= 0xdbff) {
      const next = text.charCodeAt(i + 1);

      if (next >= 0xdc00 && next <= 0xdfff) {
        c = 0x10000 + ((c - 0xd800) << 10) + next - 0xdc00;
        i++;
      } else {
        c = 0xfffd;
      }
    } else if (c >= 0xdc00 && c <= 0xdfff) {
      c = 0xfffd;
    }

    if (c < 0x80) {
      out.push(c);
    } else if (c < 0x800) {
      out.push(
        0xc0 | (c >>> 6),
        0x80 | (c & 63)
      );
    } else if (c < 0x10000) {
      out.push(
        0xe0 | (c >>> 12),
        0x80 | ((c >>> 6) & 63),
        0x80 | (c & 63)
      );
    } else {
      out.push(
        0xf0 | (c >>> 18),
        0x80 | ((c >>> 12) & 63),
        0x80 | ((c >>> 6) & 63),
        0x80 | (c & 63)
      );
    }
  }

  return out;
}

function honeylogSha256(bytes) {
  const k = [
    0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,
    0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5,
    0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,
    0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174,
    0xe49b69c1,0xefbe4786,0x0fc19dc6,0x240ca1cc,
    0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da,
    0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,
    0xc6e00bf3,0xd5a79147,0x06ca6351,0x14292967,
    0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,
    0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85,
    0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,
    0xd192e819,0xd6990624,0xf40e3585,0x106aa070,
    0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,
    0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3,
    0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,
    0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2
  ];

  const h = [
    0x6a09e667,0xbb67ae85,0x3c6ef372,0xa54ff53a,
    0x510e527f,0x9b05688c,0x1f83d9ab,0x5be0cd19
  ];

  const data = bytes.slice();
  const low = (bytes.length * 8) >>> 0;
  const high = Math.floor(bytes.length / 0x20000000);

  data.push(0x80);

  while (data.length % 64 !== 56) {
    data.push(0);
  }

  for (let shift = 24; shift >= 0; shift -= 8) {
    data.push((high >>> shift) & 255);
  }

  for (let shift = 24; shift >= 0; shift -= 8) {
    data.push((low >>> shift) & 255);
  }

  const rotate = (x, n) => (x >>> n) | (x << (32 - n));
  const w = new Array(64);

  for (let offset = 0; offset < data.length; offset += 64) {
    for (let i = 0; i < 16; i++) {
      const j = offset + i * 4;

      w[i] = (
        (data[j] << 24) |
        (data[j + 1] << 16) |
        (data[j + 2] << 8) |
        data[j + 3]
      ) >>> 0;
    }

    for (let i = 16; i < 64; i++) {
      const x = w[i - 15];
      const y = w[i - 2];

      w[i] = (
        w[i - 16] +
        (rotate(x, 7) ^ rotate(x, 18) ^ (x >>> 3)) +
        w[i - 7] +
        (rotate(y, 17) ^ rotate(y, 19) ^ (y >>> 10))
      ) >>> 0;
    }

    let [a,b,c,d,e,f,g,z] = h;

    for (let i = 0; i < 64; i++) {
      const t1 = (
        z +
        (rotate(e, 6) ^ rotate(e, 11) ^ rotate(e, 25)) +
        ((e & f) ^ (~e & g)) +
        k[i] +
        w[i]
      ) >>> 0;

      const t2 = (
        (rotate(a, 2) ^ rotate(a, 13) ^ rotate(a, 22)) +
        ((a & b) ^ (a & c) ^ (b & c))
      ) >>> 0;

      z = g;
      g = f;
      f = e;
      e = (d + t1) >>> 0;
      d = c;
      c = b;
      b = a;
      a = (t1 + t2) >>> 0;
    }

    const state = [a,b,c,d,e,f,g,z];

    for (let i = 0; i < 8; i++) {
      h[i] = (h[i] + state[i]) >>> 0;
    }
  }

  const out = [];

  for (const word of h) {
    for (let shift = 24; shift >= 0; shift -= 8) {
      out.push((word >>> shift) & 255);
    }
  }

  return out;
}

function honeylogHmacSha256(secret, text) {
  let key = honeylogUtf8(secret);

  if (key.length > 64) {
    key = honeylogSha256(key);
  }

  const inner = [];
  const outer = [];

  for (let i = 0; i < 64; i++) {
    const b = key[i] || 0;
    inner.push(b ^ 0x36);
    outer.push(b ^ 0x5c);
  }

  return honeylogSha256(
    outer.concat(
      honeylogSha256(
        inner.concat(honeylogUtf8(text))
      )
    )
  ).map(b => b.toString(16).padStart(2, "0")).join("");
}

#5. Test the transform

Click Validate to check the code, then Test transforms to execute it.

Paste the following JSON into Input message. This is a sample: you can leave its project, backend, and URL unchanged for this test. Leave the input attributes empty.

{
  "timestamp": "2026-09-17T19:40:25.082921Z",
  "logName": "projects/example-project/logs/firebaseapphosting.googleapis.com%2Frequests",
  "resource": {
    "type": "firebaseapphosting.googleapis.com/Backend",
    "labels": {
      "backend_id": "example-backend",
      "resource_container": "projects/example-project",
      "location": "global"
    }
  },
  "httpRequest": {
    "requestMethod": "GET",
    "requestUrl": "https://example-backend.example.hosted.app/honeylog-test",
    "remoteIp": "203.0.113.10",
    "userAgent": "Mozilla/5.0 HoneylogSetupTest/1.0",
    "status": 200,
    "responseSize": "1024",
    "latency": "0.125s"
  }
}

Use plain JSON, without Base64 encoding. requestUrl must contain a plain URL, without Markdown link formatting.

Click Test. The output should contain:

  • A JSON message with an events array containing the sample request, and a delivery_nonce field.
  • The attributes Content-Type, X-Honeylog-Site, X-Honeylog-Timestamp, and X-Honeylog-Signature.
  • Your configured Honeylog domain in X-Honeylog-Site.
  • A 64-character hexadecimal signature in X-Honeylog-Signature.

This test executes the script; it does not send a request to Honeylog. If it succeeds, click Create or Save on the subscription.

Keep the transform on the subscription, so the signature is generated near delivery time. Do not add the same script to the topic.

#6. Forward Cloud Logging requests to the topic

Open Logging → Logs Router → Create sink.

Field Value
Sink name honeylog-requests
Sink destination service Cloud Pub/Sub topic
Destination The honeylog-requests topic from step 2
Inclusion filter The filter below, with your project and backend IDs
Exclusion filters None

Replace YOUR_PROJECT_ID and YOUR_BACKEND_ID in this filter:

resource.type="firebaseapphosting.googleapis.com/Backend"
logName="projects/YOUR_PROJECT_ID/logs/firebaseapphosting.googleapis.com%2Frequests"
resource.labels.backend_id="YOUR_BACKEND_ID"
httpRequest:*

To find the exact values, open Logging → Logs Explorer and search for:

resource.type="firebaseapphosting.googleapis.com/Backend"
httpRequest:*

Open a request log for your website and inspect logName, resource.labels.backend_id, and httpRequest.requestUrl. If no logs appear, visit your website again and change the search time range to Last hour.

Click Create sink.

#Verify publishing permission

If the Console has already granted the sink permission to publish to the topic, no additional grant is needed. Otherwise:

  1. Open the sink details and copy its Writer identity.
  2. Open Pub/Sub → Topics → honeylog-requests → Permissions.
  3. Click Grant access.
  4. Paste the writer identity’s email into the principal field. If it starts with serviceAccount:, use only the email address.
  5. Assign Pub/Sub Publisher and save.

This permission lets Cloud Logging publish messages to Pub/Sub. It is separate from authentication of HTTP requests to Honeylog.

#7. Verify delivery

The sink forwards new matching logs. It does not automatically export logs created before the sink was configured.

  1. Open a new path on your website, such as https://YOUR_WEBSITE/honeylog-test-1. A 404 response is also useful for testing log forwarding.

  2. In Logs Explorer, select Last hour and search for:

    httpRequest.requestUrl:"/honeylog-test-1"
    
  3. Once the log appears, open Pub/Sub → Subscriptions → honeylog-requests-push → Metrics. Check the push request count and response codes. A request accepted by the Honeylog ingestion endpoint returns HTTP 202.

  4. Open your site in Honeylog and look for the event with the path you just visited.

Delivery takes some time: the request must appear in Cloud Logging, be forwarded, and then be processed by Honeylog. Allow a few minutes. If it does not arrive, use the troubleshooting table below.

If your Honeylog site records only bot traffic, an ordinary browser visit may be filtered out. For testing, enable recording of non-bot traffic using the available site settings, or ask Honeylog support to check the filter.

#Troubleshooting

Problem What to check
No request in Logs Explorer Selected Google project, correct backend, and time range. Confirm that the site uses Firebase App Hosting and generate a new visit.
The log exists but there are no push deliveries Sink destination and inclusion filter, the writer identity’s Pub/Sub Publisher role, and the subscription’s topic.
invalid_argument Run Test transforms on the subscription with the actual request log JSON and read the complete error. Check that the entire script and all helper functions were pasted.
auth_required_401 or HTTP 401 First check Payload unwrapping on and Write metadata on. Then check site, the ingestion secret for that site, and the Honeylog endpoint. Google push authentication must remain off.
Empty output or null in the transform test Static asset requests are intentionally filtered. Try an HTML path, /robots.txt, or /sitemap.xml.
Expected a Google request log The input must be the original request log JSON with httpRequest, timestamp, resource, and logName. Use the request log inclusion filter.
Missing original visitor IP, user agent or request method Check that the log contains httpRequest.remoteIp, httpRequest.userAgent, and httpRequest.requestMethod.
HTTP 202 but no visible event Check the Honeylog site configured in the script, the displayed time range, and any bot-only filter. If the issue persists, contact support with the request time and path.

When contacting support, include the complete error, push endpoint URL, and unwrapping and metadata settings. Do not include your site’s ingestion secret.

Pub/Sub can redeliver messages, so this integration can produce duplicate events. For persistent failures, Google also supports configuring a dead-letter topic from the subscription Console.

#Alternative: a Cloud Run service

For a Cloud Run service, use the same Pub/Sub configuration and JavaScript. In step 6, use this filter instead, replacing the project ID and service name:

resource.type="cloud_run_revision"
logName="projects/YOUR_PROJECT_ID/logs/run.googleapis.com%2Frequests"
resource.labels.service_name="YOUR_CLOUD_RUN_SERVICE"
httpRequest:*

First confirm in Logs Explorer that requests contain the visitor IP and user agent. If the service is behind a CDN or proxy, Cloud Run logs may describe backend requests and omit requests served from cache. For Firebase App Hosting, use the main filter in this guide.

#Google documentation