Skip to main content

Overview

This guide builds a dedicated Upsun Cloud project that receives logs over HTTPS, parses them with Vector, and stores them in OpenSearch for search and analysis. Optionally, Vector also writes a compressed copy to Upsun object storage so you can keep OpenSearch retention short and archive cheaply. The project accepts two kinds of logs:
  • Fastly CDN logs, streamed from one or more Fastly services through an HTTPS logging endpoint, in a structured JSON format.
  • Upsun application and service logs, forwarded from one or many Upsun projects through the httplog log forwarding integration.
Everything runs inside one Upsun project with two apps: Vector receives logs, and OpenSearch Dashboards gives you a browser UI to search them. The router terminates TLS for both. OpenSearch and object storage are reachable only on the project’s internal network. Single project or many? The setup is the same either way. Each source project or Fastly service points at the same ingestion URL, and Vector tags every event with its source so you can filter by project, environment, or Fastly service in OpenSearch.

Prerequisites

You need the following before you start:
  • An Upsun organization where you can create projects, and the Upsun CLI installed and logged in (upsun login).
  • Project admin access on every Upsun project whose logs you want to forward. Real-time log forwarding is a paid feature on Upsun, so confirm it’s enabled for those projects.
  • An Upsun project served through Fastly. Upsun Support adds the Fastly logging endpoint for you through a ticket (Step 6).
  • A domain or subdomain for the ingestion endpoint, such as logs.example.com, that you can point at Upsun.
  • git and curl on your workstation.
Plan for volume. OpenSearch is the largest cost driver. Estimate daily log volume first: a busy Fastly service can produce several GB of JSON per day, and OpenSearch disk usage depends on your mappings, replicas, and indexed fields. The optional object storage archive, near the end of this guide, exists to keep this number small.

Step 1: Create the Flex project

Create an empty project in the same region as the projects and Fastly POPs you’ll send the most logs from. That keeps latency and egress down.
The CLI prompts for the organization, title, region, and default branch. It looks like this:
Clone the new project using the project ID from the output:
The output looks like this:
Then move into the new directory with cd log-ingestion. The clone already contains a .upsun/local/ folder. That’s the CLI’s local cache for this project, so keep it out of git before you add anything else:
By the end of this guide the repository will look like this:
Create the folders and empty files now, from the project root:
The challenge file answers Fastly’s ownership check for HTTPS logging endpoints (see Step 6).

Step 2: Define apps, services, and routes

The project has two apps and one service. Both apps use the default OpenSearch relationship, and mounts keep their data writable at runtime. The build hooks download pinned releases of Vector, OpenSearch Dashboards, and Caddy. Their archives are kept in $PLATFORM_CACHE_DIR to reuse them across builds. Create .upsun/config.yaml:
.upsun/config.yaml
Key points:
  • socket_family: tcp makes Upsun hand each app a $PORT to listen on. Both configs read it from the environment.
  • locations on the Vector app serve the static Fastly challenge file directly and pass every other request to Vector.
  • authentication.enabled: true turns on OpenSearch credentials. Upsun exposes them to both apps as OPENSEARCH_USERNAME and OPENSEARCH_PASSWORD, alongside OPENSEARCH_HOST and OPENSEARCH_PORT.
  • Object storage isn’t used by default. If you add the optional archive, you enable it with a resources command, and Upsun adds the OBJECT_STORAGE_* variables to the Vector app automatically.
Create dashboards/supervisor.conf to manage both processes:
dashboards/supervisor.conf
Upsun’s web start command runs Supervisor in the foreground. Upsun restarts Supervisor if it exits; Supervisor starts and restarts both Caddy and Dashboards. Both children inherit the environment prepared by the start command, including Upsun’s NODE_OPTIONS heap setting. Their output goes to the app log. Create dashboards/opensearch_dashboards.yml:
dashboards/opensearch_dashboards.yml
Then create dashboards/Caddyfile. Caddy listens on the Upsun port, asks for a username and password, and passes authenticated requests to Dashboards:
dashboards/Caddyfile
Why Caddy rewrites the Authorization header. Upsun protects OpenSearch with plain HTTP basic auth, not the OpenSearch Security plugin. Dashboards uses opensearch.username only for its own startup tasks. For requests made on behalf of a person, it forwards that request’s Authorization header. Caddy checks the person’s Dashboards password, then swaps in the OpenSearch credentials, so every OpenSearch call is authenticated. Without the swap, Dashboards loads but every page returns 401 from OpenSearch.
How access works. Opening https://dashboards.logs.example.com/ makes your browser ask for the DASHBOARDS_USER username and password (you set them in Step 4). The ingestion endpoint isn’t affected. Dashboards itself has no second login: it talks to OpenSearch with the platform-generated OPENSEARCH_USERNAME and OPENSEARCH_PASSWORD, so anyone with the HTTP auth password can see every log. Share it accordingly.

Step 3: Configure Vector

Vector listens on one HTTPS route and sorts traffic by URL path. Fastly posts to /fastly, and each Upsun project posts to /upsun/<project-id>. Vector checks HTTP basic auth on every request. First, store the ingestion credentials as sensitive project variables so they never live in git:
These are separate from the Dashboards credentials (Step 4) and from the OPENSEARCH_USERNAME and OPENSEARCH_PASSWORD that Vector sends on every OpenSearch call. Then create vector/vector.yaml:
vector/vector.yaml
What this does:
  • One source, many senders. Upsun gives each app a single port, so one http_server source takes all traffic, and the route transform splits it by path.
  • Per-source parsing. Fastly’s time field has no time zone, so it’s parsed as UTC into @timestamp. Upsun events get an upsun_project field from Upsun’s own container.project metadata, which is how you tell projects apart.
  • Daily indices. Events land in logs-fastly-YYYY.MM.DD and logs-upsun-YYYY.MM.DD. Daily indices make retention a matter of deleting old indices (see Security, retention, and cost).
  • Disk buffer. If OpenSearch is slow or restarting, Vector queues up to 1 GiB on disk instead of dropping logs.
parse_upsun reads the event time from Upsun’s timestamp_ns field. It also spots Nginx lines, which arrive as <container> nginx: <log line>, and parses them into an nginx object (nginx.client, nginx.path, nginx.status, …) with log_file set to access.log or error.log. Keeping them in their own object stops them clashing with Upsun’s host and timestamp fields, which OpenSearch would otherwise reject as a mapping conflict. Vector 0.57 and later. Two defaults changed in 0.57. First, ${VAR} references in the config are no longer filled in unless Vector starts with --dangerously-allow-env-var-interpolation, which the start command in Step 2 includes. Every credential and port here comes from Upsun environment variables, so leave it on. Second, index names and S3 key prefixes built from templates must start with fixed text, which is why they begin with logs- and logs/.

Step 4: Deploy and size resources

Commit and push. The first deploy builds both apps and provisions OpenSearch:
Then give OpenSearch enough memory and disk. The defaults are too small for log workloads:
Give Vector disk for its buffer. The Vector app’s data mount defaults to 512 MB, but the OpenSearch sink reserves a 1 GiB disk buffer. Vector refuses to start if the buffers don’t fit (Mountpoint '/app/data' has total capacity of … bytes), which shows up as a 502 on the ingestion URL. 2 GB covers the default config. If you add the optional archive sink, which has its own 1 GiB buffer, use vector:3072. Use this table as a starting point, then adjust from real usage with upsun metrics: Above about 50 GB a day, shorten hot retention, add the optional object storage archive (pattern A), or talk to your account team about a larger setup. Match the Dashboards version. Dashboards refuses to connect to a different OpenSearch version. Check what Upsun runs, then set OSD_VERSION in .upsun/config.yaml to the same number and push again:
Add your domain (optional). Vector’s route is https://{default}/ and Dashboards is https://dashboards.{default}/. Set your ingestion hostname as the default domain and add a CNAME for each hostname, pointing at the target that upsun domain:add prints:
Or skip the domain. A custom domain isn’t required. Run upsun url --primary to open the primary route, or upsun url --pipe to list the generated URLs, which look like this:
Use them wherever this guide says logs.example.com or dashboards.logs.example.com, including the Fastly endpoint and challenge URL. They change only if the environment is renamed or recreated.

Set the Dashboards password

Caddy checks a username and password stored as project variables. The app hashes the password each time it starts, so you only ever handle the plain value:
Creating the variables redeploys the environment. Until both exist, the Dashboards URL returns an error. Then open https://dashboards.logs.example.com/ and, at the browser’s login prompt, enter admin and the password from DASH_PASS. To change it later, run upsun variable:update --level project --value "<new password>" env:DASHBOARDS_PASSWORD. Upsun’s environment-level HTTP access control in the Console isn’t used here: it applies to every route, so it would also block Fastly and your log senders.

Step 5: Verify the pipeline

Prove the whole chain works with a sample event before you connect Fastly or any Upsun project. Come back to parts 3 and 4 once real logs are flowing.
  1. Send a test event. This should return 200:
A 401 means the credentials are wrong. A 404 means the path didn’t match a route. 2. Check that indices exist:
  1. Inspect raw events (optional). To see exactly what Fastly or Upsun sends before you adjust the parsing, add this to the top of vector.yaml, push, then tap the stream:
  1. Explore in Dashboards. Open https://dashboards.logs.example.com/app/home and sign in at the browser prompt. If you see a Dashboards 404 page ({"statusCode":404,"error":"Not Found"}), the browser requested the wrong path; open /app/home directly. On first load, Dashboards may offer a “New! Enhanced Discover experience” popup. Click Dismiss: this guide uses the standard Discover, and those settings aren’t needed. On the “Welcome to OpenSearch Dashboards” screen, click Explore on my own (Add data only offers sample datasets).
Go to Dashboards Management > Index patterns, and create logs-fastly-* and logs-upsun-*, both with @timestamp as the time field. Then try these in Discover:

Step 6: Stream Fastly logs

Fastly sends logs to an HTTPS endpoint only after the endpoint proves it expects them, then posts batches of newline-delimited JSON.

Publish the ownership challenge

Fastly fetches https://logs.example.com/.well-known/fastly/logging/challenge and looks for the SHA-256 hash of your service ID. Put one hash per line, one line per Fastly service:
Use shasum -a 256, which works on macOS and Linux. sha256sum doesn’t exist on macOS, and the command above would silently write an empty file. Use the service ID (for example SU1Z0isxPaozGVKXdv0eY), not the service name. Don’t know your Fastly service ID? Upsun Support can tell you. Until then, put a single * in the file, which accepts any Fastly service; Vector’s password check still protects the endpoint. Confirm it’s served without auth: curl https://logs.example.com/.well-known/fastly/logging/challenge.

Request the HTTPS logging endpoint

Upsun manages the Fastly service in front of your project, so Upsun Support adds the logging endpoint for you. Once the challenge file is live, open a ticket from the Upsun Console with the text below, filled in with your values. Generate the header value first:
Then use this as the ticket body:
Replace logs.example.com with your ingestion hostname (or the generated platformsh.site URL). The header value is a credential: the ticket is private to your organization, but rotate INGEST_PASSWORD if it’s ever shared more widely.
This is the log format to paste at the end of the ticket. Keep it on one line, because each newline starts a new event. Replace <your-service-name> with a readable name for the site:
About the fields:
  • time is the request start in UTC. Vector copies it into @timestamp.
  • service.id identifies the Fastly service, so many services can share one endpoint and you can filter on it.
  • ja3_fingerprint and ja4_fingerprint are only filled on the first Fastly hop (the edge). On shield nodes they’re empty and server.is_shield is true.
Once Support confirms the new version is active, logs usually appear within a minute. Check with Step 5, part 2. For each additional Fastly service or project, add its challenge hash and open another ticket with the same settings.

Step 7: Forward logs from Upsun projects

Each source project gets an httplog log forwarding integration that posts to its own path on the ingestion endpoint. Use the source project’s own ID in the path. Upsun also stamps the project ID on every event, and Vector stores it as upsun_project. Forwarding sends everything that goes through the system journal of each app container. On Upsun Cloud that includes Nginx’s access and error logs, so one integration per project covers the usual /var/log files: This works the same for PHP (Drupal, WordPress, Magento), Node.js, Python, and decoupled projects. Nothing needs to change in the source app’s code or .upsun/config.yaml.

One project

Forwarding starts after the redeploy. Repeat the redeploy for any other environment you want to ship.

Many projects

List the project IDs in a file, one per line, and loop:
Plan requirements. You need project admin access on each source project, and forwarding only works on Grid projects (Dedicated uses rsyslog instead). Real-time log forwarding is billed separately; on Upsun Fixed it’s part of the Observability Suite. Check with your account team if integration:add refuses the httplog type. Why not syslog? Upsun also supports syslog forwarding, but syslog needs a raw TCP or TLS port. Upsun apps are only reachable over HTTP(S) through the router, so this project accepts httplog only.

What arrives and how to find it

Each forwarded event carries Upsun’s metadata. The useful fields are: Nginx lines are parsed by parse_upsun (Step 3). Access lines get log_file: "access.log" plus nginx.client, nginx.method, nginx.path, nginx.protocol, nginx.status, nginx.size, nginx.referer and nginx.agent. Error lines get log_file: "error.log" plus nginx.message, and nginx.severity, nginx.client, nginx.request and nginx.host when the line includes them. After the first events arrive, refresh the index pattern so the new fields show up: go to Dashboards Management → Index patterns, open logs-upsun-* and click the refresh icon. Then try these in Discover: Add container.service, nginx.client, nginx.method, nginx.path, nginx.status and nginx.agent as columns, then Save the search for one-click access. Requests served from Fastly’s cache never reach the app, so they aren’t in the app’s access log. Use logs-fastly-* for the full edge view.

Other file logs

httplog doesn’t forward logs your app writes to its own files, or post-start.log. Send those messages to stdout or syslog instead: Drupal’s syslog module, WordPress’s error_log() with error_log = syslog, or Python’s and Node.js’s standard loggers writing to stdout. They then arrive through the same integration with no extra setup.

Security, retention, and cost

Security

  • Send logs over https://. The router terminates TLS; the examples use HTTP on the private network for OpenSearch and object storage.
  • Rotate the ingest password. Update env:INGEST_PASSWORD (this redeploys), then update the Fastly header and re-run integration:update on each source project. Expect a short gap in logs.
  • One Dashboards login. Anyone with the Dashboards password can see every log, so share it only with people who need it. To give someone separate credentials, add another user hash line to the Caddyfile’s basic_auth block.
  • The challenge file is public on purpose. It holds only hashes of service IDs.
Three sets of credentials: OpenSearch has no authentication by default on Upsun, so keep authentication.enabled: true even though it’s only reachable internally.

Retention

Add this cron job to the vector app in .upsun/config.yaml. It deletes OpenSearch indices older than 14 days:
Upsun’s OpenSearch also supports the Index State Management (ISM) plugin. If you enable it on the service, an ISM delete policy on logs-* can replace the opensearch-retention cron.

Cost

  • OpenSearch disk is the biggest line. Shorten hot retention before adding disk.
  • Drop fields you never query. A del(.field) in the parse_fastly transform shrinks every document.
  • Sample noisy traffic. Vector’s sample transform can keep, say, 1 in 10 successful static-asset hits while keeping every error.
  • Object storage is billed on allocation. 512 GB is the minimum, so compare the allocated storage cost with your expected OpenSearch retention before enabling an archive.
  • Avoid long-lived preview environments. A branch of this project clones OpenSearch and runs a second ingest stack. Point senders only at main.

Optional: Archive logs to object storage

Upsun object storage is an S3-compatible bucket attached to one app. Compressed JSON in a bucket costs far less per GB than OpenSearch disk, so it’s a good home for logs you must keep but rarely search. Choose one of two patterns:

Enable the bucket

Object storage is sized in 512 GB steps (minimum 512 GB, maximum 10 TB) and billed on allocated size, not usage. Attach it to the Vector app:
The bucket is named after the app (vector). Upsun injects OBJECT_STORAGE_HOST, OBJECT_STORAGE_PORT, OBJECT_STORAGE_ACCESS_KEY, OBJECT_STORAGE_SECRET_KEY and OBJECT_STORAGE_REGION into the Vector app. The endpoint is internal only, so no one outside the project can reach it.

Add the archive sink

Raise the Vector app’s disk for the second buffer (upsun resources:set --disk vector:3072), then append this sink to vector/vector.yaml:
For pattern B, also delete the opensearch sink, the dashboards app, the opensearch service, the dashboards.{default} route and the opensearch relationship from the Vector app.

Things to know

  • No lifecycle rules. Upsun object storage doesn’t support lifecycle policies, versioning, object lock, server-side encryption or bucket policies. Expire old objects with the cron job below.
  • Deletion is permanent. Setting the size to 0 deletes the bucket and everything in it on the next deployment.
  • Reading archives. Use upsun ssh -A vector and the AWS CLI with --endpoint-url "http://$OBJECT_STORAGE_HOST:$OBJECT_STORAGE_PORT" to list or download files. To search old logs, download a day’s prefix and load it into your tool of choice, or re-ingest it into OpenSearch.

Expire old archives

Add the AWS CLI and a second cron job to the vector app, next to opensearch-retention. This one deletes the archive day that just aged out, here 400 days:
The job removes one day per run. If it misses a day, delete that prefix by hand.

Troubleshooting

Start with the Vector app log: upsun log -A vector app --tail. Expected and harmless:
  • Transform "route_by_path._unmatched" has no consumers at Vector startup. Requests to paths other than /fastly or /upsun/<id> are dropped on purpose.
  • A new index takes a few seconds to appear. Vector batches events before sending them. If _cat/indices is empty right after the first test event, wait 10 to 20 seconds and check again.
  • vector tap says the API isn’t reachable. The api: block must start at column 0 in vector.yaml, level with data_dir:, and you must push after adding it.
Dashboards shows a generic Error ... at Fetch and then 502s. Check upsun log -A dashboards app. JavaScript heap out of memory means Dashboards ran out of memory, usually on a Discover search over a long time range. Upsun supplies the Node heap setting through NODE_OPTIONS, and Supervisor restarts Dashboards if it exits. If it keeps happening, raise the app’s size, for example upsun resources:set --size dashboards:2, and keep Discover time ranges short. Upsun logs stop after the ingestion endpoint was down. If Vector was stopped or failing for a while, for example during a bad config push, source projects can stop forwarding even after it recovers. Today’s logs-upsun-* docs.count stays flat. Redeploy each source project to restart forwarding: upsun redeploy -p <source-project-id> -e main.

Sources

Last modified on October 13, 2026