Collect Sentry logs

Supported in:

This document explains how to ingest Sentry logs to Google Security Operations using Amazon S3. Sentry produces operational data in the form of events, issues, performance monitoring data, and error tracking information. This integration lets you send these logs to Google SecOps for analysis and monitoring, providing visibility into application errors, performance issues, and user interactions within your Sentry-monitored applications.

Before you begin

Make sure you have the following prerequisites:

  • A Google SecOps instance.
  • Privileged access to Sentrytenant (Auth Token with API scopes).
  • Privileged access to AWS(S3, Identity and Access Management (IAM), Lambda, EventBridge).

Collect Sentry prerequisites (IDs, API keys, org IDs, tokens)

  1. Sign in to Sentry.
  2. Find your Organization slug:
    • Go to Settings > Organization > Settings > Organization ID(the slug appears next to the org name).
  3. Create an Auth Token:
    • Go to Settings > Developer Settings > Personal Tokens.
    • Click Create New
    • Scopes (minimum): org:read , project:read , event:read .
    • Copy the token value (shown once). This is used as: Authorization: Bearer <token> .
  4. (If self-hosted): Note your base URL(for example, https://<your-domain> ); otherwise use https://sentry.io .

Configure AWS S3 bucket and IAM for Google SecOps

  1. Create Amazon S3 bucketfollowing this user guide: Creating a bucket
  2. Save bucket Nameand Regionfor future reference (for example, sentry-logs ).
  3. Create a Userfollowing this user guide: Creating an IAM user .
  4. Select the created User.
  5. Select Security credentialstab.
  6. Click Create Access Keyin section Access Keys.
  7. Select Third-party serviceas Use case.
  8. Click Next.
  9. Optional: Add a description tag.
  10. Click Create access key.
  11. Click Download CSV fileto save the Access Keyand Secret Access Keyfor future reference.
  12. Click Done.
  13. Select Permissionstab.
  14. Click Add permissionsin section Permissions policies.
  15. Select Add permissions.
  16. Select Attach policies directly.
  17. Search for AmazonS3FullAccesspolicy.
  18. Select the policy.
  19. Click Next.
  20. Click Add permissions.

Configure the IAM policy and role for S3 uploads

  1. In the AWS console, go to IAM > Policies.
  2. Click Create policy > JSON tab.
  3. Copy and paste the following policy.
  4. Policy JSON(replace sentry-logs if you entered a different bucket name):

      { 
      
     "Version" 
     : 
      
     "2012-10-17" 
     , 
      
     "Statement" 
     : 
      
     [ 
      
     { 
      
     "Sid" 
     : 
      
     "AllowPutObjects" 
     , 
      
     "Effect" 
     : 
      
     "Allow" 
     , 
      
     "Action" 
     : 
      
     "s3:PutObject" 
     , 
      
     "Resource" 
     : 
      
     "arn:aws:s3:::sentry-logs/*" 
      
     }, 
      
     { 
      
     "Sid" 
     : 
      
     "AllowGetStateObject" 
     , 
      
     "Effect" 
     : 
      
     "Allow" 
     , 
      
     "Action" 
     : 
      
     "s3:GetObject" 
     , 
      
     "Resource" 
     : 
      
     "arn:aws:s3:::sentry-logs/sentry/events/state.json" 
      
     } 
      
     ] 
     } 
     
    
  5. Click Next > Create policy.

  6. Go to IAM > Roles > Create role > AWS service > Lambda.

  7. Attach the newly created policy.

  8. Name the role WriteSentryToS3Role and click Create role.

Create the Lambda function

  1. In the AWS Console, go to Lambda > Functions > Create function.
  2. Click Author from scratch.
  3. Provide the following configuration details:

    Setting Value
    Name sentry_to_s3
    Runtime Python 3.13
    Architecture x86_64
    Execution role WriteSentryToS3Role
  4. After the function is created, open the Codetab, delete the stub and paste the following code ( sentry_to_s3.py ).

      #!/usr/bin/env python3 
     # Lambda: Pull Sentry project events (raw JSON) to S3 using Link "previous" cursor for duplicate-safe polling 
     import 
      
     os 
     , 
      
     json 
     , 
      
     time 
     from 
      
     urllib.request 
      
     import 
     Request 
     , 
     urlopen 
     from 
      
     urllib.parse 
      
     import 
     urlencode 
     , 
     urlparse 
     , 
     parse_qs 
     import 
      
     boto3 
     ORG 
     = 
     os 
     . 
     environ 
     [ 
     "SENTRY_ORG" 
     ] 
     . 
     strip 
     () 
     TOKEN 
     = 
     os 
     . 
     environ 
     [ 
     "SENTRY_AUTH_TOKEN" 
     ] 
     . 
     strip 
     () 
     S3_BUCKET 
     = 
     os 
     . 
     environ 
     [ 
     "S3_BUCKET" 
     ] 
     S3_PREFIX 
     = 
     os 
     . 
     environ 
     . 
     get 
     ( 
     "S3_PREFIX" 
     , 
     "sentry/events/" 
     ) 
     STATE_KEY 
     = 
     os 
     . 
     environ 
     . 
     get 
     ( 
     "STATE_KEY" 
     , 
     "sentry/events/state.json" 
     ) 
     BASE 
     = 
     os 
     . 
     environ 
     . 
     get 
     ( 
     "SENTRY_API_BASE" 
     , 
     "https://sentry.io" 
     ) 
     . 
     rstrip 
     ( 
     "/" 
     ) 
     MAX_PROJECTS 
     = 
     int 
     ( 
     os 
     . 
     environ 
     . 
     get 
     ( 
     "MAX_PROJECTS" 
     , 
     "100" 
     )) 
     MAX_PAGES_PER_PROJECT 
     = 
     int 
     ( 
     os 
     . 
     environ 
     . 
     get 
     ( 
     "MAX_PAGES_PER_PROJECT" 
     , 
     "5" 
     )) 
     s3 
     = 
     boto3 
     . 
     client 
     ( 
     "s3" 
     ) 
     HDRS 
     = 
     { 
     "Authorization" 
     : 
     f 
     "Bearer 
     { 
     TOKEN 
     } 
     " 
     , 
     "Accept" 
     : 
     "application/json" 
     , 
     "User-Agent" 
     : 
     "chronicle-s3-sentry-lambda/1.0" 
     } 
     def 
      
     _get_state 
     () 
     - 
    > dict 
     : 
     try 
     : 
     obj 
     = 
     s3 
     . 
     get_object 
     ( 
     Bucket 
     = 
     S3_BUCKET 
     , 
     Key 
     = 
     STATE_KEY 
     ) 
     raw 
     = 
     obj 
     [ 
     "Body" 
     ] 
     . 
     read 
     () 
     return 
     json 
     . 
     loads 
     ( 
     raw 
     ) 
     if 
     raw 
     else 
     { 
     "projects" 
     : 
     {}} 
     except 
     Exception 
     : 
     return 
     { 
     "projects" 
     : 
     {}} 
     def 
      
     _put_state 
     ( 
     state 
     : 
     dict 
     ): 
     s3 
     . 
     put_object 
     ( 
     Bucket 
     = 
     S3_BUCKET 
     , 
     Key 
     = 
     STATE_KEY 
     , 
     Body 
     = 
     json 
     . 
     dumps 
     ( 
     state 
     , 
     separators 
     = 
     ( 
     "," 
     , 
     ":" 
     )) 
     . 
     encode 
     ( 
     "utf-8" 
     )) 
     def 
      
     _req 
     ( 
     path 
     : 
     str 
     , 
     params 
     : 
     dict 
     | 
     None 
     = 
     None 
     ): 
     url 
     = 
     f 
     " 
     { 
     BASE 
     }{ 
     path 
     } 
     " 
     if 
     params 
     : 
     url 
     = 
     f 
     " 
     { 
     url 
     } 
     ? 
     { 
     urlencode 
     ( 
     params 
     ) 
     } 
     " 
     req 
     = 
     Request 
     ( 
     url 
     , 
     method 
     = 
     "GET" 
     , 
     headers 
     = 
     HDRS 
     ) 
     with 
     urlopen 
     ( 
     req 
     , 
     timeout 
     = 
     60 
     ) 
     as 
     r 
     : 
     data 
     = 
     json 
     . 
     loads 
     ( 
     r 
     . 
     read 
     () 
     . 
     decode 
     ( 
     "utf-8" 
     )) 
     link 
     = 
     r 
     . 
     headers 
     . 
     get 
     ( 
     "Link" 
     ) 
     return 
     data 
     , 
     link 
     def 
      
     _parse_link 
     ( 
     link_header 
     : 
     str 
     | 
     None 
     ): 
      
     """Return (prev_cursor, prev_has_more, next_cursor, next_has_more).""" 
     if 
     not 
     link_header 
     : 
     return 
     None 
     , 
     False 
     , 
     None 
     , 
     False 
     prev_cursor 
     , 
     next_cursor 
     = 
     None 
     , 
     None 
     prev_more 
     , 
     next_more 
     = 
     False 
     , 
     False 
     parts 
     = 
     [ 
     p 
     . 
     strip 
     () 
     for 
     p 
     in 
     link_header 
     . 
     split 
     ( 
     "," 
     )] 
     for 
     p 
     in 
     parts 
     : 
     if 
     "<" 
     not 
     in 
     p 
     or 
     ">" 
     not 
     in 
     p 
     : 
     continue 
     url 
     = 
     p 
     . 
     split 
     ( 
     "<" 
     , 
     1 
     )[ 
     1 
     ] 
     . 
     split 
     ( 
     ">" 
     , 
     1 
     )[ 
     0 
     ] 
     rel 
     = 
     "previous" 
     if 
     'rel="previous"' 
     in 
     p 
     else 
     ( 
     "next" 
     if 
     'rel="next"' 
     in 
     p 
     else 
     None 
     ) 
     has_more 
     = 
     'results="true"' 
     in 
     p 
     try 
     : 
     q 
     = 
     urlparse 
     ( 
     url 
     ) 
     . 
     query 
     cur 
     = 
     parse_qs 
     ( 
     q 
     ) 
     . 
     get 
     ( 
     "cursor" 
     , 
     [ 
     None 
     ])[ 
     0 
     ] 
     except 
     Exception 
     : 
     cur 
     = 
     None 
     if 
     rel 
     == 
     "previous" 
     : 
     prev_cursor 
     , 
     prev_more 
     = 
     cur 
     , 
     has_more 
     elif 
     rel 
     == 
     "next" 
     : 
     next_cursor 
     , 
     next_more 
     = 
     cur 
     , 
     has_more 
     return 
     prev_cursor 
     , 
     prev_more 
     , 
     next_cursor 
     , 
     next_more 
     def 
      
     _write_page 
     ( 
     project_slug 
     : 
     str 
     , 
     payload 
     : 
     object 
     , 
     page_idx 
     : 
     int 
     ) 
     - 
    > str 
     : 
     ts 
     = 
     time 
     . 
     gmtime 
     () 
     key 
     = 
     f 
     " 
     { 
     S3_PREFIX 
     . 
     rstrip 
     ( 
     '/' 
     ) 
     } 
     / 
     { 
     time 
     . 
     strftime 
     ( 
     '%Y/%m/ 
     %d 
     ' 
     , 
      
     ts 
     ) 
     } 
     /sentry- 
     { 
     project_slug 
     } 
     - 
     { 
     page_idx 
     : 
     05d 
     } 
     .json" 
     s3 
     . 
     put_object 
     ( 
     Bucket 
     = 
     S3_BUCKET 
     , 
     Key 
     = 
     key 
     , 
     Body 
     = 
     json 
     . 
     dumps 
     ( 
     payload 
     , 
     separators 
     = 
     ( 
     "," 
     , 
     ":" 
     )) 
     . 
     encode 
     ( 
     "utf-8" 
     )) 
     return 
     key 
     def 
      
     list_projects 
     ( 
     max_projects 
     : 
     int 
     ): 
     projects 
     , 
     cursor 
     = 
     [], 
     None 
     while 
     len 
     ( 
     projects 
     ) 
    < max_projects 
     : 
     params 
     = 
     { 
     "cursor" 
     : 
     cursor 
     } 
     if 
     cursor 
     else 
     {} 
     data 
     , 
     link 
     = 
     _req 
     ( 
     f 
     "/api/0/organizations/ 
     { 
     ORG 
     } 
     /projects/" 
     , 
     params 
     ) 
     for 
     p 
     in 
     data 
     : 
     slug 
     = 
     p 
     . 
     get 
     ( 
     "slug" 
     ) 
     if 
     slug 
     : 
     projects 
     . 
     append 
     ( 
     slug 
     ) 
     if 
     len 
     ( 
     projects 
     ) 
    > = 
     max_projects 
     : 
     break 
     # advance pagination 
     _ 
     , 
     _ 
     , 
     next_cursor 
     , 
     next_more 
     = 
     _parse_link 
     ( 
     link 
     ) 
     cursor 
     = 
     next_cursor 
     if 
     next_more 
     else 
     None 
     if 
     not 
     next_more 
     : 
     break 
     return 
     projects 
     def 
      
     fetch_project_events 
     ( 
     project_slug 
     : 
     str 
     , 
     start_prev_cursor 
     : 
     str 
     | 
     None 
     ): 
     # If we have a stored "previous" cursor, poll forward (newer) until no more results. 
     # If not (first run), fetch the latest page, then optionally follow "next" (older) for initial backfill up to the limit. 
     pages 
     = 
     0 
     total 
     = 
     0 
     latest_prev_cursor_to_store 
     = 
     None 
     def 
      
     _one 
     ( 
     cursor 
     : 
     str 
     | 
     None 
     ): 
     nonlocal 
     pages 
     , 
     total 
     , 
     latest_prev_cursor_to_store 
     params 
     = 
     { 
     "cursor" 
     : 
     cursor 
     } 
     if 
     cursor 
     else 
     {} 
     data 
     , 
     link 
     = 
     _req 
     ( 
     f 
     "/api/0/projects/ 
     { 
     ORG 
     } 
     / 
     { 
     project_slug 
     } 
     /events/" 
     , 
     params 
     ) 
     _write_page 
     ( 
     project_slug 
     , 
     data 
     , 
     pages 
     ) 
     total 
     += 
     len 
     ( 
     data 
     ) 
     if 
     isinstance 
     ( 
     data 
     , 
     list 
     ) 
     else 
     0 
     prev_c 
     , 
     prev_more 
     , 
     next_c 
     , 
     next_more 
     = 
     _parse_link 
     ( 
     link 
     ) 
     # capture the most recent "previous" cursor observed to store for the next run 
     latest_prev_cursor_to_store 
     = 
     prev_c 
     or 
     latest_prev_cursor_to_store 
     pages 
     += 
     1 
     return 
     prev_c 
     , 
     prev_more 
     , 
     next_c 
     , 
     next_more 
     if 
     start_prev_cursor 
     : 
     # Poll new pages toward "previous" until no more 
     cur 
     = 
     start_prev_cursor 
     while 
     pages 
    < MAX_PAGES_PER_PROJECT 
     : 
     prev_c 
     , 
     prev_more 
     , 
     _ 
     , 
     _ 
     = 
     _one 
     ( 
     cur 
     ) 
     if 
     not 
     prev_more 
     : 
     break 
     cur 
     = 
     prev_c 
     else 
     : 
     # First run: start at newest, then (optionally) backfill a few older pages 
     prev_c 
     , 
     _ 
     , 
     next_c 
     , 
     next_more 
     = 
     _one 
     ( 
     None 
     ) 
     cur 
     = 
     next_c 
     while 
     next_more 
     and 
     pages 
    < MAX_PAGES_PER_PROJECT 
     : 
     _ 
     , 
     _ 
     , 
     next_c 
     , 
     next_more 
     = 
     _one 
     ( 
     cur 
     ) 
     cur 
     = 
     next_c 
     return 
     { 
     "project" 
     : 
     project_slug 
     , 
     "pages" 
     : 
     pages 
     , 
     "written" 
     : 
     total 
     , 
     "store_prev_cursor" 
     : 
     latest_prev_cursor_to_store 
     } 
     def 
      
     lambda_handler 
     ( 
     event 
     = 
     None 
     , 
     context 
     = 
     None 
     ): 
     state 
     = 
     _get_state 
     () 
     state 
     . 
     setdefault 
     ( 
     "projects" 
     , 
     {}) 
     projects 
     = 
     list_projects 
     ( 
     MAX_PROJECTS 
     ) 
     summary 
     = 
     [] 
     for 
     slug 
     in 
     projects 
     : 
     start_prev 
     = 
     state 
     [ 
     "projects" 
     ] 
     . 
     get 
     ( 
     slug 
     , 
     {}) 
     . 
     get 
     ( 
     "prev_cursor" 
     ) 
     res 
     = 
     fetch_project_events 
     ( 
     slug 
     , 
     start_prev 
     ) 
     if 
     res 
     . 
     get 
     ( 
     "store_prev_cursor" 
     ): 
     state 
     [ 
     "projects" 
     ][ 
     slug 
     ] 
     = 
     { 
     "prev_cursor" 
     : 
     res 
     [ 
     "store_prev_cursor" 
     ]} 
     summary 
     . 
     append 
     ( 
     res 
     ) 
     _put_state 
     ( 
     state 
     ) 
     return 
     { 
     "ok" 
     : 
     True 
     , 
     "projects" 
     : 
     len 
     ( 
     projects 
     ), 
     "summary" 
     : 
     summary 
     } 
     if 
     __name__ 
     == 
     "__main__" 
     : 
     print 
     ( 
     lambda_handler 
     ()) 
     
    
  5. Go to Configuration > Environment variables.

  6. Click Edit > Add new environment variable.

  7. Enter the environment variables provided in the following table, replacing the example values with your values.

    Environment variables

    Key Example Value Description
    S3_BUCKET
    sentry-logs S3 bucket name where data will be stored.
    S3_PREFIX
    sentry/events/ Optional S3 prefix (subfolder) for objects.
    STATE_KEY
    sentry/events/state.json Optional state/checkpoint file key.
    SENTRY_ORG
    your-org-slug Sentry organization slug.
    SENTRY_AUTH_TOKEN
    sntrys_************************ Sentry Auth Token with org:read, project:read, event:read.
    SENTRY_API_BASE
    https://sentry.io Sentry API base URL (self-hosted: https://<your-domain> ).
    MAX_PROJECTS
    100 Maximum number of projects to process.
    MAX_PAGES_PER_PROJECT
    5 Maximum pages per project per execution.
  8. After the function is created, stay on its page (or open Lambda > Functions > your-function).

  9. Select the Configurationtab.

  10. In the General configurationpanel click Edit.

  11. Change Timeoutto 5 minutes (300 seconds)and click Save.

Create an EventBridge schedule

  1. Go to Amazon EventBridge > Scheduler > Create schedule.
  2. Provide the following configuration details:
    • Recurring schedule: Rate( 1 hour ).
    • Target: Your Lambda function sentry_to_s3 .
    • Name: sentry-1h .
  3. Click Create schedule.

(Optional) Create read-only IAM user & keys for Google SecOps

  1. In the AWS Console, go to IAM > Users.
  2. Click Add users.
  3. Provide the following configuration details:
    • User: Enter secops-reader .
    • Access type: Select Access key — Programmatic access.
  4. Click Create user.
  5. Attach minimal read policy (custom): Users > secops-reader > Permissions > Add permissions > Attach policies directly > Create policy.
  6. JSON:

      { 
      
     "Version" 
     : 
      
     "2012-10-17" 
     , 
      
     "Statement" 
     : 
      
     [ 
      
     { 
      
     "Effect" 
     : 
      
     "Allow" 
     , 
      
     "Action" 
     : 
      
     [ 
     "s3:GetObject" 
     ], 
      
     "Resource" 
     : 
      
     "arn:aws:s3:::sentry-logs/*" 
      
     }, 
      
     { 
      
     "Effect" 
     : 
      
     "Allow" 
     , 
      
     "Action" 
     : 
      
     [ 
     "s3:ListBucket" 
     ], 
      
     "Resource" 
     : 
      
     "arn:aws:s3:::sentry-logs" 
      
     } 
      
     ] 
     } 
     
    
  7. Name = secops-reader-policy .

  8. Click Create policy > search/select > Next > Add permissions.

  9. Create access key for secops-reader : Security credentials > Access keys.

  10. Click Create access key.

  11. Download the .CSV . (You'll paste these values into the feed).

Configure a feed in Google SecOps to ingest Sentry logs

  1. Go to SIEM Settings > Feeds.
  2. Click + Add New Feed.
  3. In the Feed namefield, enter a name for the feed (for example, Sentry Logs ).
  4. Select Amazon S3 V2as the Source type.
  5. Select Sentryas the Log type.
  6. Click Next.
  7. Specify values for the following input parameters:
    • S3 URI: s3://sentry-logs/sentry/events/
    • Source deletion options: Select deletion option according to your preference.
    • Maximum File Age: Include files modified in the last number of days. Default is 180 days.
    • Access Key ID: User access key with access to the S3 bucket.
    • Secret Access Key: User secret key with access to the S3 bucket.
    • Asset namespace: The asset namespace .
    • Ingestion labels: The label applied to the events from this feed.
  8. Click Next.
  9. Review your new feed configuration in the Finalizescreen, and then click Submit.

Need more help? Get answers from Community members and Google SecOps professionals.

Create a Mobile Website
View Site in Mobile | Classic
Share by: