EE Usage Monitoring — workload tags + Cloud Monitoring

github github

Why workload tags matter

Every Earth Engine call charges EECU. Without a workload tag every call lands in one anonymous bucket in Cloud Monitoring — you can’t tell which script, which user, which layer, or which feature spent your budget. Tagging is what turns “the project spent 40 EECU-hours this week” into “the SRTM tile layer spent 22, the NLCD area chart spent 14, the elevation inspector spent 4.”

ee.data.setWorkloadTag(...) sets a tag on the SDK; the geeViz proxy default builder passes client-set tags through to EE, and Cloud Monitoring groups by them. Nothing else you do — logging, prints, notebooks — sees the tag; only Cloud Monitoring does.

What this notebook shows

Four progressively richer patterns for tagging, run end-to-end so you can see the exact rows Cloud Monitoring returns:

Example

Pattern

Reversible?

Best for

0

No tags — rely on geeViz’s default

No — a single per-tenant bucket

Baseline / “just want to know what geeViz is spending”

1

ee.data.setWorkloadTag("literal-string")

No — you pick the strings

Notebooks / single-user scripts wanting per-layer attribution

2

eeCreds.setWorkloadTag(**parts) — mint + store + set

YeslookupWorkloadTag(tag) returns parts

Structured attribution + billing rollups

3

Custom workload_tag_builder=... on eeCreds.start()

Whatever your builder stores

Multi-user servers (agent framework pattern)

After each example you’ll Map.view() and poll EEUsageMonitor to watch the tags land.

Under the hood

  1. eeAuth proxy runs locally (in-process by default) between ee and Google’s EE API.

  2. On every request the proxy calls a tag builder. The default passes SDK-set tags through unchanged; custom builders (Example 3) can mint tags from request headers instead.

  3. Cloud Monitoring aggregates EECU per tag on ~1-minute buckets.

  4. EEUsageMonitor.poll(...) pulls the rows for whatever window and grouping you ask for.

Cloud Monitoring lag: fresh tags typically show up within 1–2 minutes. Re-poll if a tag you just fired isn’t there yet — very small compute calls (short reduceRegion on a small AOI) can also fall below EE’s minimum billable threshold and never appear.

Prerequisites

  • pip install geeviz (pulls fastapi, uvicorn, earthengine-api)

  • pip install google-cloud-monitoring

  • Credentials that can call EE and read Cloud Monitoring in the project. Either:

    • earthengine authenticate for personal use

    • Or an SA JSON path in GEEVIZ_DEMO_SA_PATH (SA needs roles/monitoring.viewer on the project + EE access)

  • The GCP project must have the Earth Engine API enabled

Troubleshooting

  • Nothing shows up in monitoring — wait 1–2 minutes and re-poll. Fresh tags don’t appear instantly.

  • ee-proxy__<tenant> shows up instead of your tag — you might be attached to a stale detached proxy that predates a recent geeViz edit. Run from geeViz.eeAuth import eeCreds; eeCreds.restart() to respawn.

  • .getInfo() hangs forever — you probably called eeCreds.stop() which un-inits the SDK. Rerun the proxy start cell.

Example 0 — no workload tag at all

The baseline. You import geeViz, add layers, run compute — no setWorkloadTag anywhere. What lands in Cloud Monitoring?

Since geeViz installs a sensible SDK-level default at proxy start via ee.data.setDefaultWorkloadTag, every untagged call attributes to geeviz__<current-tenant> (e.g. geeviz__ee-persistent) instead of an empty / anonymous bucket. You can immediately tell which geeviz session and which credential the cost came from, even though you never tagged anything explicitly.

Browser-side compute the JS viewer fires (area chart, click-to-query) still lands under the JS viewer’s own default (geeviz---viewer-exports) because that runs in a separate Python-free process.

import time
import datetime as dt

# Import geeView - will auto initialize ee if not already initialized
from geeViz.geeView import Map, ee

# If you edited any geeViz.eeAuth source since the proxy started, run:
#   from geeViz.eeAuth import eeCreds; eeCreds.restart()

Map.clearMap()

# No setWorkloadTag anywhere — the SDK default (installed by
# eeCreds at start time) will attribute untagged calls to
# geeviz__<tenant>.
Map.addLayer(
    ee.Image("USGS/SRTMGL1_003"),
    {"min": 0, "max": 4000, "palette": ["440154", "31688e", "35b779", "fde725"]},
    "SRTM elevation",
)

nlcd = ee.Image("USGS/NLCD_RELEASES/2021_REL/NLCD/2021").select("landcover")
Map.addLayer(nlcd, {"autoViz": True}, "NLCD 2021 land cover")

aoi = ee.Geometry.Point(-105.27, 40.02).buffer(20_000)
nlcd_hist = nlcd.reduceRegion(
    reducer=ee.Reducer.frequencyHistogram(),
    geometry=aoi, scale=90, maxPixels=int(1e9),
).getInfo()
print(f"NLCD class count in AOI: {len(nlcd_hist['landcover'])}")
print(f"Current default tag: {ee.data.getWorkloadTag()!r}")
[geeViz.eeAuth] EE initialized via proxy: http://127.0.0.1:8889/ee-api (tenant_header=X-geeViz-Creds)
geeViz: Earth Engine ready (project='geeviz-geo-agent', source=eeauth-proxy)
Adding layer: SRTM elevation
Adding layer: NLCD 2021 land cover
NLCD class count in AOI: 15
Current default tag: 'geeviz__ee-persistent'
# Open the map. Pan / zoom to trigger tile fetches — attribution
# below reflects the tag that was active when each addLayer was called.
Map.centerObject(aoi, 8)
Map.view()
Starting webmap
Using eeCreds proxy at http://127.0.0.1:8889/ee-api (creds=ee-persistent, mode=attached via default)
geeView URL: http://127.0.0.1:8001/geeView/?v=1786657580900
# Give Cloud Monitoring ~30s to ingest, then run this.
from geeViz.eeAuth import eeCreds
from geeViz.eeAuth.monitoring import EEUsageMonitor

project = eeCreds._entries[eeCreds.current()].project_id
monitor = EEUsageMonitor(project=project, cost_per_eecu_hour=0.40)

n_minutes = 30 # Number of minutes into the past to monitor
group_by_n_seconds = 60 * 5 # 5-minute buckets

now = dt.datetime.now(dt.timezone.utc)
rows = monitor.poll(
    start_time=now - dt.timedelta(minutes=n_minutes),
    end_time=now,
    grouping_seconds=group_by_n_seconds,   
)

print(f"Fetched {len(rows)} rows from Cloud Monitoring\n")

# Column widths: tag=30, time=19, EECU-hours=15 (>15.8f), cost='$'+14=15.
print(f"{'Tag':<30} {'Time':<19} {'EECU-hours':>15} {'Cost (USD)':>15}")
print("-" * 82)
for r in sorted(rows, key=lambda x: -x["bucket_start"].timestamp()):
    print(
        f"{r['workload_tag']:<30} "
        f"{r['bucket_start'].strftime('%Y-%m-%d %H:%M:%S'):<19} "
        f"{r['eecu_hours']:>15.8f} "
        f"${r['cost_usd']:>14.8f}"
    )
Fetched 17 rows from Cloud Monitoring

Tag                            Time                     EECU-hours      Cost (USD)
----------------------------------------------------------------------------------
geeviz__ee-persistent          2026-08-13 21:45:00      0.00087000 $    0.00034800
nlcd_landcover_layer           2026-08-13 21:45:00      0.00207200 $    0.00082900
srtm_dem_layer                 2026-08-13 21:45:00      0.00177000 $    0.00070800
wl_c71d6931e5d6245f            2026-08-13 21:45:00      0.00018000 $    0.00007200
geeviz__ee-persistent          2026-08-13 21:40:00      0.00095600 $    0.00038200
wl_c71d6931e5d6245f            2026-08-13 21:40:00      0.00001000 $    0.00000400
wl_d58799934d95ff09            2026-08-13 21:40:00      0.00001700 $    0.00000700
wl_ddb9d54c4bcf5a89            2026-08-13 21:40:00      0.00000100 $    0.00000000
nlcd_landcover_layer           2026-08-13 21:35:00      0.00000200 $    0.00000100
srtm_dem_layer                 2026-08-13 21:35:00      0.00000300 $    0.00000100
wl_d58799934d95ff09            2026-08-13 21:35:00      0.00007400 $    0.00003000
geeviz__ee-persistent          2026-08-13 21:30:00      0.00099600 $    0.00039800
nlcd_landcover_layer           2026-08-13 21:30:00      0.00005700 $    0.00002300
srtm_dem_layer                 2026-08-13 21:30:00      0.00005400 $    0.00002200
wl_d58799934d95ff09            2026-08-13 21:30:00      0.00020200 $    0.00008100
geeviz__ee-persistent          2026-08-13 21:25:00      0.00485000 $    0.00194000
wl_d58799934d95ff09            2026-08-13 21:25:00      0.00003400 $    0.00001400

Example 1 — ee.data.setWorkloadTag(string)

The simplest way to attribute per-layer / per-call: pick a memorable string, set it before the EE call, and it survives to Cloud Monitoring unchanged (the proxy passes client-set tags through). Tags are not reversible — what you set is what you see — so pick descriptive names.

Map.clearMap()

# Set → addLayer: tile fetches for this layer attribute to srtm_dem_layer.
ee.data.setWorkloadTag("srtm_dem_layer")
Map.addLayer(
    ee.Image("USGS/SRTMGL1_003"),
    {"min": 0, "max": 4000, "palette": ["440154", "31688e", "35b779", "fde725"]},
    "SRTM elevation",
)

# Switch → addLayer: NLCD tiles attribute independently.
ee.data.setWorkloadTag("nlcd_landcover_layer")
nlcd = ee.Image("USGS/NLCD_RELEASES/2021_REL/NLCD/2021").select("landcover")
Map.addLayer(nlcd, {"autoViz": True}, "NLCD 2021 land cover")

# Same pattern for a standalone compute.
aoi = ee.Geometry.Point(-105.27, 40.02).buffer(20_000)
ee.data.setWorkloadTag("nlcd_histogram")
nlcd_hist = nlcd.reduceRegion(
    reducer=ee.Reducer.frequencyHistogram(),
    geometry=aoi, scale=90, maxPixels=int(1e9),
).getInfo()
print(f"NLCD class count in AOI: {len(nlcd_hist['landcover'])}")
Adding layer: SRTM elevation
Adding layer: NLCD 2021 land cover
NLCD class count in AOI: 15
# Open the map. Pan / zoom to trigger tile fetches — attribution
# below reflects the tag that was active when each addLayer was called.
Map.centerObject(aoi, 8)
Map.view()
Starting webmap
Using eeCreds proxy at http://127.0.0.1:8889/ee-api (creds=ee-persistent, mode=attached via default)
geeView URL: http://127.0.0.1:8001/geeView/?v=1786657612270
# Give Cloud Monitoring ~30s to ingest, then run this.
project = eeCreds._entries[eeCreds.current()].project_id
monitor = EEUsageMonitor(project=project, cost_per_eecu_hour=0.40)

n_minutes = 30 # Number of minutes into the past to monitor
group_by_n_seconds = 60 * 5 # 5-minute buckets
now = dt.datetime.now(dt.timezone.utc)
rows = monitor.poll(
    start_time=now - dt.timedelta(minutes=n_minutes),
    end_time=now,
    grouping_seconds=group_by_n_seconds,   
)
print(f"Fetched {len(rows)} rows from Cloud Monitoring\n")

# Column widths: tag=30, time=19, EECU-hours=15 (>15.8f), cost='$'+14=15.
print(f"{'Tag':<30} {'Time':<19} {'EECU-hours':>15} {'Cost (USD)':>15}")
print("-" * 82)
for r in sorted(rows, key=lambda x: -x["bucket_start"].timestamp()):
    print(
        f"{r['workload_tag']:<30} "
        f"{r['bucket_start'].strftime('%Y-%m-%d %H:%M:%S'):<19} "
        f"{r['eecu_hours']:>15.8f} "
        f"${r['cost_usd']:>14.8f}"
    )
total_eecu_hours = sum(r["eecu_hours"] for r in rows)
total_cost_usd = sum(r["cost_usd"] for r in rows)
print(f"Total ee usage for the past {n_minutes} minutes: {total_eecu_hours:.8f} EECU-hours, ${total_cost_usd:.8f} USD")
Fetched 17 rows from Cloud Monitoring

Tag                            Time                     EECU-hours      Cost (USD)
----------------------------------------------------------------------------------
geeviz__ee-persistent          2026-08-13 21:45:00      0.00087000 $    0.00034800
nlcd_landcover_layer           2026-08-13 21:45:00      0.00207200 $    0.00082900
srtm_dem_layer                 2026-08-13 21:45:00      0.00177000 $    0.00070800
wl_c71d6931e5d6245f            2026-08-13 21:45:00      0.00018000 $    0.00007200
geeviz__ee-persistent          2026-08-13 21:40:00      0.00095600 $    0.00038200
wl_c71d6931e5d6245f            2026-08-13 21:40:00      0.00001000 $    0.00000400
wl_d58799934d95ff09            2026-08-13 21:40:00      0.00001700 $    0.00000700
wl_ddb9d54c4bcf5a89            2026-08-13 21:40:00      0.00000100 $    0.00000000
nlcd_landcover_layer           2026-08-13 21:35:00      0.00000200 $    0.00000100
srtm_dem_layer                 2026-08-13 21:35:00      0.00000300 $    0.00000100
wl_d58799934d95ff09            2026-08-13 21:35:00      0.00007400 $    0.00003000
geeviz__ee-persistent          2026-08-13 21:30:00      0.00099600 $    0.00039800
nlcd_landcover_layer           2026-08-13 21:30:00      0.00005700 $    0.00002300
srtm_dem_layer                 2026-08-13 21:30:00      0.00005400 $    0.00002200
wl_d58799934d95ff09            2026-08-13 21:30:00      0.00020200 $    0.00008100
geeviz__ee-persistent          2026-08-13 21:25:00      0.00485000 $    0.00194000
wl_d58799934d95ff09            2026-08-13 21:25:00      0.00003400 $    0.00001400
Total ee usage for the past 30 minutes: 0.01214800 EECU-hours, $0.00486000 USD

Example 2 — eeCreds.setWorkloadTag(**parts) (reversible)

  • Sometimes you need to store a lot of information in the workload tag. Workload tags by default have a 64 character limit and are constrained to alphanumeric and _ . - characters. This can present many limitations if you need to track a lot of information for ee usage.

  • This example provides a method to address this issue. Same shape as Example 1, but tags are deterministic hashes of structured parts (wl_<16hex>) written to a TagStore (SQLite by default at ~/.geeViz/workload_tags.db). After polling monitoring, you can call eeCreds.lookupWorkloadTag(tag) to recover the original parts dict.

Use when you want structured identity in monitoring — e.g. attribute by (user, session, action) and later join tags back to those fields in a billing rollup or admin dashboard.

Map.clearMap()

# Same 3-step pattern as Example 1 — set, addLayer, repeat — but
# eeCreds.setWorkloadTag(**parts) mints wl_<hex> from the parts dict,
# stores tag → parts in the TagStore, AND calls ee.data.setWorkloadTag
# for you. Returns the minted tag string so you can log / correlate.
tag_srtm = eeCreds.setWorkloadTag(user="[email protected]", usage_type="tile_map_service",layer="srtm-elevation", anyFieldName = "$0methingW@Characters")
Map.addLayer(
    ee.Image("USGS/SRTMGL1_003"),
    {"min": 0, "max": 4000, "palette": ["440154", "31688e", "35b779", "fde725"]},
    "SRTM elevation",
)

tag_nlcd = eeCreds.setWorkloadTag(user="[email protected]", layer="nlcd-landcover")
nlcd = ee.Image("USGS/NLCD_RELEASES/2021_REL/NLCD/2021").select("landcover")
Map.addLayer(nlcd, {"autoViz": True}, "NLCD 2021 land cover")

aoi = ee.Geometry.Point(-105.27, 40.02).buffer(20_000)
tag_compute = eeCreds.setWorkloadTag(user="[email protected]", action="nlcd-histogram")
nlcd_hist = nlcd.reduceRegion(
    reducer=ee.Reducer.frequencyHistogram(),
    geometry=aoi, scale=90, maxPixels=int(1e9),
).getInfo()

print(f"SRTM layer   → {tag_srtm}")
print(f"NLCD layer   → {tag_nlcd}")
print(f"compute call → {tag_compute}")
print(f"lookup(compute) → {eeCreds.lookupWorkloadTag(tag_compute)}")
Adding layer: SRTM elevation
Adding layer: NLCD 2021 land cover
SRTM layer   → wl_496453331822373f
NLCD layer   → wl_fb489f834848b136
compute call → wl_a06441647cb4d6a8
lookup(compute) → {'action': 'nlcd-histogram', 'user': '[email protected]'}
# Open the map. Pan / zoom to trigger tile fetches — attribution
# below reflects the tag that was active when each addLayer was called.
Map.centerObject(aoi, 8)
Map.view()
Starting webmap
Using eeCreds proxy at http://127.0.0.1:8889/ee-api (creds=ee-persistent, mode=attached via default)
geeView URL: http://127.0.0.1:8001/geeView/?v=1786658055167
# Give Cloud Monitoring ~30s to ingest, then run this.
from geeViz.eeAuth import eeCreds
from geeViz.eeAuth.monitoring import EEUsageMonitor

project = eeCreds._entries[eeCreds.current()].project_id
monitor = EEUsageMonitor(project=project, cost_per_eecu_hour=0.40)

n_minutes = 30 # Number of minutes into the past to monitor
group_by_n_seconds = 60 * 5 # 5-minute buckets

now = dt.datetime.now(dt.timezone.utc)
rows = monitor.poll(
    start_time=now - dt.timedelta(minutes=n_minutes),
    end_time=now,
    grouping_seconds=group_by_n_seconds,   
)

print(f"Fetched {len(rows)} rows from Cloud Monitoring\n")

# Cleaned up table printing
table_headers = ["Tag", "Time", "EECU-hours", "Cost (USD)", "Creds"]
table_colwidths = [28, 19, 13, 13, 0]  # Last col (creds) flexible width

header_line = (
    f"{table_headers[0]:<{table_colwidths[0]}} "
    f"{table_headers[1]:<{table_colwidths[1]}} "
    f"{table_headers[2]:>{table_colwidths[2]}} "
    f"{table_headers[3]:>{table_colwidths[3]}} "
    f"{table_headers[4]}"
)
print(header_line)
print("-" * (sum(table_colwidths[:-1]) + 4*3 + len(table_headers[4])))

for r in sorted(rows, key=lambda x: -x["eecu_hours"]):
    creds = eeCreds.lookupWorkloadTag(r["workload_tag"]) or "Not in lookup"
    line = (
        f"{r['workload_tag']:<{table_colwidths[0]}} "
        f"{r['bucket_start'].strftime('%Y-%m-%d %H:%M:%S'):<{table_colwidths[1]}} "
        f"{r['eecu_hours']:>{table_colwidths[2]}.8f} "
        f"${r['cost_usd']:>{table_colwidths[3]}.8f} "
        f"{creds}"
    )
    print(line)
Fetched 18 rows from Cloud Monitoring

Tag                          Time                   EECU-hours    Cost (USD) Creds
------------------------------------------------------------------------------------------
geeviz__ee-persistent        2026-08-13 21:25:00    0.00485000 $   0.00194000 Not in lookup
nlcd_landcover_layer         2026-08-13 21:45:00    0.00207200 $   0.00082900 Not in lookup
srtm_dem_layer               2026-08-13 21:45:00    0.00177000 $   0.00070800 Not in lookup
geeviz__ee-persistent        2026-08-13 21:30:00    0.00099600 $   0.00039800 Not in lookup
geeviz__ee-persistent        2026-08-13 21:40:00    0.00095600 $   0.00038200 Not in lookup
geeviz__ee-persistent        2026-08-13 21:45:00    0.00087000 $   0.00034800 Not in lookup
wl_d58799934d95ff09          2026-08-13 21:30:00    0.00020200 $   0.00008100 {'cred': 'ee-persistent', 'pid': 15536, 'src': 'proxy-default', 'tenant': 'ee-persistent'}
wl_c71d6931e5d6245f          2026-08-13 21:45:00    0.00017800 $   0.00007100 {'cred': 'ee-persistent', 'pid': 1772, 'src': 'proxy-default', 'tenant': 'ee-persistent'}
wl_d58799934d95ff09          2026-08-13 21:35:00    0.00007400 $   0.00003000 {'cred': 'ee-persistent', 'pid': 15536, 'src': 'proxy-default', 'tenant': 'ee-persistent'}
nlcd_landcover_layer         2026-08-13 21:30:00    0.00005700 $   0.00002300 Not in lookup
srtm_dem_layer               2026-08-13 21:30:00    0.00005400 $   0.00002200 Not in lookup
wl_d58799934d95ff09          2026-08-13 21:25:00    0.00003400 $   0.00001400 {'cred': 'ee-persistent', 'pid': 15536, 'src': 'proxy-default', 'tenant': 'ee-persistent'}
wl_c71d6931e5d6245f          2026-08-13 21:50:00    0.00003300 $   0.00001300 {'cred': 'ee-persistent', 'pid': 1772, 'src': 'proxy-default', 'tenant': 'ee-persistent'}
wl_d58799934d95ff09          2026-08-13 21:40:00    0.00001700 $   0.00000700 {'cred': 'ee-persistent', 'pid': 15536, 'src': 'proxy-default', 'tenant': 'ee-persistent'}
wl_c71d6931e5d6245f          2026-08-13 21:40:00    0.00001000 $   0.00000400 {'cred': 'ee-persistent', 'pid': 1772, 'src': 'proxy-default', 'tenant': 'ee-persistent'}
srtm_dem_layer               2026-08-13 21:35:00    0.00000300 $   0.00000100 Not in lookup
nlcd_landcover_layer         2026-08-13 21:35:00    0.00000200 $   0.00000100 Not in lookup
wl_ddb9d54c4bcf5a89          2026-08-13 21:40:00    0.00000100 $   0.00000000 {'cred': 'ee-persistent', 'pid': 6628, 'src': 'proxy-default', 'tenant': 'ee-persistent'}

Example 3 — custom workload_tag_builder (proxy-side minting)

Everything above happens on the SDK side: the client sets a tag, the proxy passes it through. For multi-user server contexts (like the geeViz agent framework) that’s the wrong model — every user’s request shares one Python process, so a process-global setWorkloadTag would race between concurrent users.

The alternative: pass a workload_tag_builder=(request, tenant) str callable to eeCreds.start(). The proxy calls it on every incoming request; the builder reads whatever identity headers you attached at request-emit time (Referer, X-Agent-User-Email, IAP header, cookies) and returns a tag. No SDK-side setter needed anywhere.

The builder is a Python closure, so this mode is attached-only — a detached subprocess can’t accept a closure across the process boundary. Call eeCreds.restart() first if a detached subprocess is holding the port.

import time
import datetime as dt
import hashlib

from geeViz.geeView import Map, ee
from geeViz.eeAuth import eeCreds

# ── The custom builder ────────────────────────────────────────
# In production this reads real headers (X-Agent-User-Email, session
# cookie, IAP JWT, etc.). For the notebook demo we hardcode identity
# so every call from THIS process attributes to a fixed user and mint
# a deterministic hash tag.
_DEMO_SECRET = "notebook-demo-secret"
_local_mapping: dict = {}

def _mint_tag(parts: dict) -> str:
    canonical = "|".join(f"{k}={v}" for k, v in sorted(parts.items()))
    h = hashlib.blake2b((canonical + _DEMO_SECRET).encode(), digest_size=8).hexdigest()
    return f"wl_{h}"

def notebook_builder(request, tenant):
    parts = {
        "tenant": tenant or "default",
        "user":   "[email protected]",         # in prod: from request headers
        "src":    "notebook-example-3",
    }
    tag = _mint_tag(parts)
    _local_mapping[tag] = parts             # remember for lookup below
    return tag

# Restart the proxy with our builder installed. restart() = stop() +
# ensure_started() — required because we're changing the builder AND
# because detached subprocesses can't accept closures.
eeCreds.stop()
eeCreds.discover()
eeCreds.start(workload_tag_builder=notebook_builder)

# From here on every request that hits the proxy — Python SDK calls
# AND browser tile fetches from Map.view — attributes via our builder.
# No ee.data.setWorkloadTag calls anywhere.

Map.clearMap()
Map.addLayer(
    ee.Image("USGS/SRTMGL1_003"),
    {"min": 0, "max": 4000, "palette": ["440154", "31688e", "35b779", "fde725"]},
    "SRTM elevation",
)
nlcd = ee.Image("USGS/NLCD_RELEASES/2021_REL/NLCD/2021").select("landcover")
Map.addLayer(nlcd, {"autoViz": True}, "NLCD 2021 land cover")

aoi = ee.Geometry.Point(-105.27, 40.02).buffer(20_000)
_ = nlcd.reduceRegion(
    reducer=ee.Reducer.frequencyHistogram(),
    geometry=aoi, scale=90, maxPixels=int(1e9),
).getInfo()

# The builder was called at least once per request above; peek at what
# it produced.
print(f"Builder minted {len(_local_mapping)} unique tag(s):")
for tag, parts in _local_mapping.items():
    print(f"  {tag}{parts}")
# Open the map. Pan / zoom to trigger tile fetches — attribution
# below reflects the tag that was active when each addLayer was called.
Map.centerObject(aoi, 8)
Map.view()
# Give Cloud Monitoring ~30s to ingest, then run this.
from geeViz.eeAuth import eeCreds
from geeViz.eeAuth.monitoring import EEUsageMonitor

project = eeCreds._entries[eeCreds.current()].project_id
monitor = EEUsageMonitor(project=project, cost_per_eecu_hour=0.40)

now = dt.datetime.now(dt.timezone.utc)
rows = monitor.poll(
    start_time=now - dt.timedelta(minutes=30),
    end_time=now,
    grouping_seconds=60 * 5,   # 5-minute buckets
)
print(f"Fetched {len(rows)} rows from Cloud Monitoring\n")

# Column widths: tag=30, time=19, EECU-hours=15 (>15.8f), cost='$'+14=15.
print(f"{'Tag':<30} {'Time':<19} {'EECU-hours':>15} {'Cost (USD)':>15}")
print("-" * 82)
for r in sorted(rows, key=lambda x: -x["eecu_hours"]):
    print(
        f"{r['workload_tag']:<30} "
        f"{r['bucket_start'].strftime('%Y-%m-%d %H:%M:%S'):<19} "
        f"{r['eecu_hours']:>15.8f} "
        f"${r['cost_usd']:>14.8f}"
    )

# Reverse each row against the builder's local mapping.
print("\nReversibility check (via notebook builder's _local_mapping):")
for r in rows:
    parts = _local_mapping.get(r["workload_tag"])
    if parts:
        print(f"  {r['workload_tag']:22s}{parts}")

Example 4 — multiple tenants + per-tenant attribution

Real-world deployments often use MULTIPLE service accounts / credentials — one per client, one per environment, one per cost center. eeCreds’s addCreds(creds, name=...) registers each under a tenant name; eeCreds.use(name) switches the active tenant for subsequent EE calls (all served by the same proxy on the same port). Every request through the proxy stamps the current tenant in its workload tag so Cloud Monitoring reports break out by tenant.

What this example shows:

  • Register three notional tenants against the same underlying credentials (they’d normally be different SA JSONs) so we don’t need three real SAs to demonstrate the flow.

  • Under each tenant, setWorkloadTag(user=..., action=...) so the tag encodes BOTH the tenant AND per-request identity.

  • Poll EEUsageMonitor and use eeCreds.lookupWorkloadTag(tag) to reverse each row back to its (tenant, user, action) tuple — ready to feed a per-tenant billing rollup.

import time
import datetime as dt

from geeViz.geeView import Map, ee
from geeViz.eeAuth import eeCreds
from geeViz.eeAuth.tags import InMemoryTagStore

# For the demo we register three "tenants" against auto-discovered
# credentials. In production each addCreds() takes a path / dict /
# base64 blob for a distinct SA JSON — e.g.
#   eeCreds.addCreds("acme-prod-sa.json", name="acme")
#   eeCreds.addCreds(BREW_B64,              name="brew")
#   eeCreds.addCreds("cargo-sa.json",       name="cargo")
# We reuse the same discovered creds here so the notebook runs on a
# single credential set without needing multiple real SA files.
eeCreds.stop()
eeCreds.discover()  # picks up whatever's on this machine

# Snap the current cred into three tenant aliases. addCreds is a no-op
# name-with-same-payload when applied to the same underlying data.
_first = next(iter(eeCreds._entries.values()))
for tenant in ("acme", "brew", "cargo"):
    eeCreds.addCreds(_first.data, name=tenant, project=_first.project_id)

# Fresh in-memory store so tags minted below are obvious in the output.
eeCreds.setTagStore(InMemoryTagStore()).setTagSecret("demo-multitenant")
eeCreds.start()
print(f"Registered tenants: {eeCreds.list()}")

# Per-tenant work — pretend each tenant is doing a different query.
Map.clearMap()
tags_by_tenant = {}
for tenant, layer_asset, viz, name in [
    ("acme", "USGS/SRTMGL1_003",
     {"min": 0, "max": 4000}, "SRTM (acme)"),
    ("brew", "USGS/NLCD_RELEASES/2021_REL/NLCD/2021",
     {"autoViz": True}, "NLCD (brew)"),
    ("cargo", "MODIS/061/MOD11A1/2024_06_01",
     {"min": 13000, "max": 16500, "palette": ["blue", "yellow", "red"]},
     "MODIS LST (cargo)"),
]:
    eeCreds.use(tenant)   # subsequent EE calls route via this tenant
    tag = eeCreds.setWorkloadTag(
        tenant=tenant, user="[email protected]", action="compute",
    )
    tags_by_tenant[tenant] = tag
    img = ee.Image(layer_asset).select(0) if "MOD11A1" not in layer_asset else ee.Image(layer_asset).select("LST_Day_1km")
    _ = img.reduceRegion(
        reducer=ee.Reducer.mean(),
        geometry=ee.Geometry.Point(-105.27, 40.02).buffer(20_000),
        scale=1000, maxPixels=int(1e9),
    ).getInfo()
    Map.addLayer(img, viz, name)
    print(f"  {tenant:6s}{tag} → reduceRegion done")
# Open the map. Tile fetches per layer will attribute to whichever
# tenant's tag was active when that layer's addLayer fired (per TAG8).
Map.centerObject(ee.Geometry.Point(-105.27, 40.02).buffer(20_000), 8)
Map.view()
# Give Cloud Monitoring ~30s to ingest, then run this.
from geeViz.eeAuth.monitoring import EEUsageMonitor

project = eeCreds._entries[eeCreds.current()].project_id
monitor = EEUsageMonitor(project=project, cost_per_eecu_hour=0.40)

now = dt.datetime.now(dt.timezone.utc)
rows = monitor.poll(
    start_time=now - dt.timedelta(minutes=30),
    end_time=now,
    grouping_seconds=60 * 5,
)
print(f"Fetched {len(rows)} rows from Cloud Monitoring\n")

print(f"{'Tag':30s} {'Tenant':>8s} {'User':>22s} {'EECU-h':>12s}")
print("-" * 78)
for r in sorted(rows, key=lambda x: -x["eecu_hours"]):
    parts = eeCreds.lookupWorkloadTag(r["workload_tag"]) or {}
    print(
        f"{r['workload_tag']:30s} "
        f"{parts.get('tenant', '-'):>8s} "
        f"{parts.get('user', '-'):>22s} "
        f"{r['eecu_hours']:>12.8f}"
    )

# Per-tenant rollup — group by tenant and sum EECU. Ready to feed a
# billing spreadsheet or invoice generator.
from collections import defaultdict
by_tenant = defaultdict(float)
for r in rows:
    parts = eeCreds.lookupWorkloadTag(r["workload_tag"]) or {}
    t = parts.get("tenant", "unknown")
    by_tenant[t] += r["eecu_hours"]
print(f"\nPer-tenant totals:")
for t, hrs in sorted(by_tenant.items(), key=lambda kv: -kv[1]):
    print(f"  {t:8s}  {hrs:.8f} EECU-h  ${hrs * 0.40:.6f}")

Configuration

import os

# ── Credentials resolution ──────────────────────────────────────────
# Priority order:
#   1. DEMO_SA_PATH — a local "temp SA" JSON path you tuck into your
#      dev machine. Edit the string to match yours (or set the env
#      var GEEVIZ_DEMO_SA_PATH). This is the preferred path so the
#      notebook runs against the same identity you use elsewhere.
#   2. Env vars: GEEVIZ_DEMO_SA_PATH → GEE_SA_JSON_PATH → GOOGLE_APPLICATION_CREDENTIALS
#   3. Empty → cell below falls through to `eeCreds` singleton's
#      auto-discovery (finds ADC and/or earthengine-authenticate
#      credentials — same thing geeViz does when you `import Map`).
DEMO_SA_PATH = r"C:\tmp\your-service-account-json.json"  # edit this

SA_CREDS = ""
if os.path.exists(DEMO_SA_PATH):
    SA_CREDS = DEMO_SA_PATH
else:
    SA_CREDS = (
        os.environ.get("GEEVIZ_DEMO_SA_PATH")
        or os.environ.get("GEE_SA_JSON_PATH")
        or os.environ.get("GOOGLE_APPLICATION_CREDENTIALS")
        or ""
    )

# ── Project resolution ──────────────────────────────────────────────
# Priority order:
#   1. Map.project — set by geeViz's auto-init when you `import
#      geeViz.geeView`. This is the project ee.Initialize picked up
#      from your ADC / EE credentials.
#   2. Env vars: GEE_PROJECT → GOOGLE_CLOUD_PROJECT → PROJECT_ID → GCP_PROJECT
#   3. Placeholder — edit if all else fails
PROJECT = ""
try:
    from geeViz.geeView import Map
    PROJECT = getattr(Map, "project", "") or ""
except Exception:
    pass  # geeViz not initialized yet — env-var / placeholder path handles it

if not PROJECT:
    PROJECT = (
        os.environ.get("GEE_PROJECT")
        or os.environ.get("GOOGLE_CLOUD_PROJECT")
        or os.environ.get("PROJECT_ID")
        or os.environ.get("GCP_PROJECT")
        or "your-project-id"
    )

# Google commercial rate: $0.40/EECU-hour. Set 0 for noncommercial.
COST_PER_EECU_HOUR = 0.40

print(f"PROJECT   = {PROJECT!r}")
print(f"SA_CREDS  = {SA_CREDS!r} (empty → auto-discover ADC / earthengine auth)")

Start the eeAuth proxy in-process

EECreds.start() spins up the proxy as a daemon thread and initializes EE to route through it. It picks a free port automatically.

# Use the singleton `eeCreds` instance — same pattern every other
# geeViz example notebook uses. If you `import geeViz.geeView` (which
# `Map.project` above does), the singleton has already auto-discovered
# whatever ADC / earthengine-authenticate credentials your environment
# has, so we only need to explicitly addCreds when the temp-SA path
# from cell 2 is set and isn't already registered.
from geeViz.eeAuth import eeCreds

_registered = set(eeCreds.list())
print(f"Already discovered tenants: {sorted(_registered) or '(none)'}")

if SA_CREDS and "demo" not in _registered:
    eeCreds.addCreds(SA_CREDS, name="demo", project=PROJECT)
    print(f"Added SA from {SA_CREDS!r} as tenant 'demo'")
elif not _registered:
    # Nothing auto-discovered and no SA path — fall back to explicit ADC.
    eeCreds.addADC(name="demo", project=PROJECT)
    print("Added ADC as tenant 'demo'")

status = eeCreds.start()  # launches proxy + calls ee.Initialize(url=proxy_url)
PROXY_URL = status["proxy_url"]
print(f"Proxy running at {PROXY_URL}")
print(f"EE initialized:  {status.get('ee_initialized')}")

Run some EE work

Every REST call now flows through the proxy, which stamps a workload tag on the outbound EE request. With the default builder that tag is ee-proxy__<tenant-name> — all calls from this notebook attribute together as ee-proxy__demo.

Modest reduceRegion — enough to register measurable EECU without burning quota.

import ee

img = ee.Image("USGS/SRTMGL1_003")
aoi = ee.Geometry.Rectangle([-112.65, 38.55, -112.05, 39.05])  # Utah

stat = img.reduceRegion(
    reducer=ee.Reducer.mean(),
    geometry=aoi,
    scale=90,
    maxPixels=int(1e9),
).getInfo()
print(f"SRTM mean elevation over AOI: {stat}")

modis = ee.ImageCollection("MODIS/061/MCD64A1").filterDate("2024-01-01", "2024-02-01")
print(f"MODIS burn image count Jan 2024: {modis.size().getInfo()}")

Poll Cloud Monitoring

Wait 5–30 min after the EE cell above before running this. Cloud Monitoring ingests EECU points on a lag. Re-run periodically until your tag appears.

EEUsageMonitor.poll() is the exact same call the agent’s puller loop makes every minute inside Cloud Run.

import datetime as dt
from geeViz.eeAuth.monitoring import EEUsageMonitor

monitor = EEUsageMonitor(project=PROJECT, cost_per_eecu_hour=COST_PER_EECU_HOUR)

now = dt.datetime.now(dt.timezone.utc)
start = now - dt.timedelta(hours=0.5)

rows = monitor.poll(start_time=start, end_time=now, grouping_seconds = 60*60,snap_to=True)
print(f"Fetched {len(rows)} tag×bucket rows from Cloud Monitoring\n")

if not rows:
    print("No rows yet — Monitoring lag or no tagged EE activity in the window.")
else:
    print(f"{'workload_tag':30s} {'hour':16s} {'eecu_h':>10s} {'$':>10s}")
    print("-" * 70)
    for r in rows:
        print(
            f"{r['workload_tag']:30s} "
            f"{r['bucket_start'].strftime('%Y-%m-%d %H:%M:%S'):16s} "
            f"{r['eecu_hours']:>10.14f} "
            f"${r['cost_usd']:>9.8f}"
        )

Different bucket sizes with grouping_seconds + snap_to

poll() accepts a grouping_seconds argument (default 3600 = hourly) and a snap_to flag (default True).

  • grouping_seconds — Cloud Monitoring’s alignment period. Any positive int works; common values: 60 (minute), 3600 (hour), 86400 (day), 604800 (week), or something in between like 900 (15 min).

  • snap_to=True — floors start_time and ceils end_time to the nearest multiple of grouping_seconds from the Unix epoch. This is what you want for stable, repeatable polls (the same window always returns the same bucket boundaries). Pass False if you want the raw window and don’t mind sub-bucket edges.

The examples below poll the same time range at different granularities so you can see how the returned bucket_start shifts.

import datetime as dt

# Same 6-hour window for every example.
now = dt.datetime.now(dt.timezone.utc)
window_start = now - dt.timedelta(hours=6)


def _print_rows(label, rows, limit=4):
    if not rows:
        print(f"  {label:>28s}: 0 rows")
        return
    total_h = sum(r["eecu_hours"] for r in rows)
    print(f"  {label:>28s}: {len(rows):>3d} rows  total={total_h:.4f} eecu_h")
    for r in rows[:limit]:
        print(f"    {r['bucket_start'].strftime('%Y-%m-%d %H:%M:%S')}  "
              f"{r['workload_tag'][:24]:24s}  {r['eecu_hours']:.4f}")
    if len(rows) > limit:
        print(f"    … ({len(rows) - limit} more)")


# 1. Default: hourly buckets, snapped to hour boundaries.
_print_rows(
    "hourly (default)",
    monitor.poll(start_time=window_start, end_time=now),
)

# 2. Minute buckets — much finer, more rows.
_print_rows(
    "minute (60s)",
    monitor.poll(start_time=window_start, end_time=now, grouping_seconds=60),
)

# 3. 15-minute buckets — non-standard period, still snaps cleanly to
#    :00, :15, :30, :45.
_print_rows(
    "15-min (900s)",
    monitor.poll(start_time=window_start, end_time=now, grouping_seconds=900),
)

# 4. Daily buckets — window is 6h but snap ceils to a full UTC day.
_print_rows(
    "daily (86400s)",
    monitor.poll(start_time=window_start, end_time=now, grouping_seconds=86400),
)

# 5. snap_to=False — uses the raw window edges. Useful if you already
#    supply pre-aligned bounds or want an ad-hoc slice; may cause partial
#    buckets on the first/last point.
_print_rows(
    "hourly, snap_to=False",
    monitor.poll(start_time=window_start, end_time=now, snap_to=False),
)

setWorkloadTag — one call to mint + store + set on the SDK

The recommended pattern for attributing EE calls is eeCreds.setWorkloadTag(**parts). This does three things atomically:

  1. Mint — deterministically hashes parts + secret into a valid 63-char wl_<hex> tag (via mint_workload_tag).

  2. Store — records tag parts in the configured TagStore (SQLite at ~/.geeViz/workload_tags.db by default, in-memory / Postgres also supported) so eeCreds.lookupWorkloadTag(tag) can recover the parts later.

  3. Set on SDK — calls ee.data.setWorkloadTag(tag) so every subsequent .getInfo(), .getMapId(), export, etc. carries the tag. The proxy’s default builder honours client-set tags → same tag round-trips through the proxy unchanged.

No custom builder needed. Same call works for Python compute AND for tile URLs generated by getMapId (the tag gets baked in, so browser tile fetches inherit it).

from geeViz.eeAuth import eeCreds
from geeViz.eeAuth.tags import InMemoryTagStore

# Use an in-memory store for this notebook demo so the tags we mint are
# obvious in the output (SQLite is the default in production; swap freely).
# Set a stable secret so re-running the notebook mints the SAME tags —
# handy when correlating with Cloud Monitoring across kernel restarts.
eeCreds.setTagStore(InMemoryTagStore()).setTagSecret("demo-secret-please-change")

# Simplest possible attribution — one tag, one call.
tag = eeCreds.setWorkloadTag(user="[email protected]", action="srtm-mean")
print(f"Minted + set on SDK: {tag}")
print(f"ee.data.getWorkloadTag() reports: {ee.data.getWorkloadTag()}")

srtm_mean = (
    ee.Image("USGS/SRTMGL1_003")
    .reduceRegion(reducer=ee.Reducer.mean(), geometry=aoi, scale=90, maxPixels=int(1e9))
    .getInfo()
)
print(f"SRTM mean elevation: {srtm_mean}")

# Recover the parts from just the tag — this is the reversibility guarantee.
print(f"lookupWorkloadTag({tag!r}) → {eeCreds.lookupWorkloadTag(tag)}")

Switching tags between calls

Call setWorkloadTag again with different parts before each block of work. Same call, different parts — each call’s EECU cost lands under a different bucket in Cloud Monitoring.

# Block A — attribute to 'compute'
tag_compute = eeCreds.setWorkloadTag(
    user="[email protected]", action="compute", dataset="nlcd",
)
nlcd = ee.ImageCollection("USGS/NLCD_RELEASES/2021_REL/NLCD").first()
nlcd_hist = nlcd.select("landcover").reduceRegion(
    reducer=ee.Reducer.frequencyHistogram(), geometry=aoi,
    scale=90, maxPixels=int(1e9),
).getInfo()
print(f"[{tag_compute}] NLCD histogram bins: {len(nlcd_hist['landcover'])}")

# Block B — attribute to 'compute' on a different dataset
tag_modis = eeCreds.setWorkloadTag(
    user="[email protected]", action="compute", dataset="modis-burn",
)
burn_count = (
    ee.ImageCollection("MODIS/061/MCD64A1")
    .filterDate("2024-01-01", "2024-02-01")
    .size()
    .getInfo()
)
print(f"[{tag_modis}] MODIS burn image count Jan 2024: {burn_count}")

# Block C — attribute to 'export-preview' (thumbnail URL generation)
tag_thumb = eeCreds.setWorkloadTag(
    user="[email protected]", action="export-preview", dataset="srtm",
)
thumb_url = ee.Image("USGS/SRTMGL1_003").getThumbURL({
    "min": 0, "max": 4000, "region": aoi, "dimensions": 256,
})
print(f"[{tag_thumb}] Thumb URL length: {len(thumb_url)}")

# All three are recoverable through the store.
for t in (tag_compute, tag_modis, tag_thumb):
    print(f"  {t}: {eeCreds.lookupWorkloadTag(t)}")

Map.view() inherits the current tag

Map.view() uses getMapId under the hood, which bakes the CURRENT workload tag into every tile URL it returns. So the tag active when you called addLayer → the tag on every tile fetch the browser makes.

Set a tag, add layers, open the map — the browser tile requests will land in Cloud Monitoring under that tag.

from geeViz.geeView import Map

tag_map = eeCreds.setWorkloadTag(
    user="[email protected]", action="map-view", layer="nlcd-landcover",
)
print(f"Map layers created under tag: {tag_map}")

Map.clearMap()
Map.addLayer(nlcd.select(["landcover"]), {"autoViz": True, "canAreaChart": True}, "NLCD")
Map.centerObject(aoi, 9)
Map.turnOnAutoAreaCharting()
Map.view()

# In the opened map, pan/zoom to trigger tile fetches. Every tile
# request will attribute via `tag_map` — verify below via `monitor.poll`.
print(f"Recoverable identity for tile fetches: {eeCreds.lookupWorkloadTag(tag_map)}")

Per-layer attribution — different tag per layer

getMapId bakes the tag active AT THAT MOMENT into the tile URL. So if you set a different tag before each addLayer, each layer’s tiles attribute to a different bucket in Cloud Monitoring. Useful for distinguishing e.g. “user browsed the burn-severity layer” vs “user browsed the land-cover layer” without them ever leaving the same map.

Map.clearMap()

layer_tags = {}
for lyr_key, image, viz in [
    ("srtm-elevation",
     ee.Image("USGS/SRTMGL1_003"),
     {"min": 0, "max": 4000, "palette": ["440154","31688e","35b779","fde725"]}),
    ("modis-lst",
     ee.ImageCollection("MODIS/061/MOD11A1").filterDate("2024-06-01","2024-06-15").mean().select("LST_Day_1km"),
     {"min": 13000, "max": 16500, "palette": ["blue","yellow","red"]}),
    ("nlcd-landcover",
     nlcd.select("landcover"),
     {"autoViz": True}),
]:
    layer_tags[lyr_key] = eeCreds.setWorkloadTag(
        user="[email protected]", action="map-view-per-layer", layer=lyr_key,
    )
    Map.addLayer(image, viz, lyr_key)
    print(f"  {lyr_key:16s}{layer_tags[lyr_key]}")

Map.centerObject(aoi, 9)
Map.view()

print()
print("Toggle each layer in the map to trigger tile fetches under its own tag.")
print(f"Tags in flight: {len(layer_tags)}")

Fallback default — no setWorkloadTag call, proxy still attributes reversibly

If you never call setWorkloadTag, the proxy’s default builder mints a tag using richer parts than the old ee-proxy__<tenant> shape — tenant, credential subject, process id, and a src=proxy-default marker — and stores the mapping. So even “I forgot to tag” cases produce reversible attribution.

# Explicit clear — SDK-side reset, so the proxy sees no client tag
# and its default builder takes over.
eeCreds.clearWorkloadTag()
print(f"ee.data.getWorkloadTag() after clear: {ee.data.getWorkloadTag()!r} (empty)")

# Do some EE work — the proxy's default builder handles attribution.
count = (
    ee.ImageCollection("LANDSAT/LC09/C02/T1_L2")
    .filterBounds(aoi).filterDate("2024-01-01","2024-04-01").size().getInfo()
)
print(f"Landsat 9 scene count Jan-Mar 2024: {count}")

# The tag was minted proxy-side. Give Cloud Monitoring a moment, then
# poll and inspect — cell 11 does this fully.

Full poll + lookup — join every tag back to identity

Poll Cloud Monitoring across the whole demo window, then for each returned workload_tag use eeCreds.lookupWorkloadTag(tag) to recover the parts. This is the round-trip the agent framework does in production (except with a Postgres-backed TagStore).

import datetime as dt
import time

# Give Cloud Monitoring a beat to ingest the last few calls. Real pullers
# just re-poll on their own cadence and don't need this sleep.
print("Waiting 20s for Cloud Monitoring to catch up...")
time.sleep(20)

now = dt.datetime.now(dt.timezone.utc)
window_start = now - dt.timedelta(hours=1)

rows = monitor.poll(start_time=window_start, end_time=now, grouping_seconds=60)
print(f"Fetched {len(rows)} tag×minute rows\n")

header = f"{'workload_tag':22s} {'eecu_h':>10s}  attribution"
print(header)
print("-" * len(header) * 2)
for r in sorted(rows, key=lambda x: (-x['eecu_hours'], x['workload_tag'])):
    parts = eeCreds.lookupWorkloadTag(r["workload_tag"])
    label = parts if parts else "(not in this process's store)"
    print(f"{r['workload_tag']:22s} {r['eecu_hours']:>10.6f}  {label}")

(Advanced) Custom workload_tag_builder — multi-user servers

setWorkloadTag is process-global on the Python side. That’s perfect for notebooks and single-user scripts. For a multi-user server (like the geeViz agent framework), you want per-REQUEST attribution — the tag comes from headers the client sent, not from a process-global.

For that case, pass workload_tag_builder=(request, tenant) -> str to eeCreds.start(). Your closure gets the raw request and can read whatever identity headers you attach — completely independent of any setWorkloadTag state. The agent’s TenantAwareHttp does exactly this.

# Illustrative — don't run this alongside the earlier cells because
# it replaces the default builder for the whole singleton.
#
# def my_builder(request, tenant):
#     user = request.headers.get("X-Agent-User-Email") or "anonymous"
#     sess = request.headers.get("X-Agent-Session-ID") or "no-session"
#     tag  = mint_workload_tag(
#         {"tenant": tenant, "user": user, "sess": sess},
#         secret=my_secret,
#     )
#     my_store.put(tag, {"tenant": tenant, "user": user, "sess": sess})
#     return tag
#
# eeCreds.stop()
# eeCreds.start(workload_tag_builder=my_builder)
#
# From then on, every request through the proxy — Python OR browser
# tile fetches — attributes via my_builder, and clients don't need
# to call setWorkloadTag at all.
print("See docstring; runnable when you're wiring up a multi-user server.")

How this maps to the agent framework

Notebook step

Agent equivalent

Storage

EECreds.start()

Same, at Cloud Run container startup

Default tag builder

Overridden — agent uses _agent_workload_tag_builder in run_ui.py

create_proxy_app(..., workload_tag_builder=...)

build_proxy_router(..., workload_tag_builder=...) mounted in the ADK FastAPI app

mint() (in-memory dict)

workload_tags_v2.mint_workload_tag()

Postgres ee_workload_tags

unhash()

ee_usage_puller._lookup_workload_tag()

LEFT JOIN ee_workload_tagsusers

EEUsageMonitor(...).poll(...)

Same, called every minute from ee_usage_puller.refresh_ee_usage

Print rows

usage_db.upsert_ee_hours_batch(...) + cdu.record_ee_usage(...)

Postgres ee_usage_hourly + cdu_ledger

If a flow works here, the agent’s version will too — same primitives. If a tag is missing or attribution is wrong here, it will be broken there as well; fix here first, port to geeviz_agent.