From 7d515d56d5cabde2b307beba8d9993e6ed48906d Mon Sep 17 00:00:00 2001 From: Adam Cowley Date: Fri, 7 Nov 2025 13:57:03 +0000 Subject: [PATCH 1/2] update drivers courses to past tests --- asciidoc/courses/drivers-go/course.adoc | 1 + .../1-driver/lessons/1-driver-lifecycle/lesson.adoc | 6 ++---- .../1-driver/lessons/3-execute-query/lesson.adoc | 6 ++---- .../courses/drivers-go/modules/1-driver/module.adoc | 11 ++++++++--- .../lessons/1-type-system/lesson.adoc | 4 +--- .../lessons/3-dates-and-times/lesson.adoc | 7 ++----- .../lessons/5-spatial-types/lesson.adoc | 4 +--- .../modules/2-handling-results/module.adoc | 12 ++++++++++-- .../lessons/1-transaction-management/lesson.adoc | 4 +--- .../lessons/3-error-handling/lesson.adoc | 4 +--- .../drivers-go/modules/3-in-production/module.adoc | 11 ++++++++++- asciidoc/courses/drivers-java/course.adoc | 1 + .../1-driver/lessons/1-driver-lifecycle/lesson.adoc | 4 +--- .../1-driver/lessons/3-execute-query/lesson.adoc | 6 ++---- .../1-driver/lessons/4-object-mapping/lesson.adoc | 5 ++++- .../drivers-java/modules/1-driver/module.adoc | 11 +++++++---- .../lessons/1-type-system/lesson.adoc | 4 +--- .../lessons/3-dates-and-times/lesson.adoc | 7 ++----- .../lessons/5-spatial-types/lesson.adoc | 6 ++---- .../modules/2-handling-results/module.adoc | 12 ++++++++++-- .../lessons/1-transaction-management/lesson.adoc | 4 +--- .../lessons/3-error-handling/lesson.adoc | 5 +---- .../lessons/4c-handling-errors/lesson.adoc | 2 +- .../modules/3-in-production/module.adoc | 13 +++++++++++-- asciidoc/courses/drivers-python/course.adoc | 1 + .../1-driver/lessons/1-driver-lifecycle/lesson.adoc | 5 +---- .../1-driver/lessons/3-execute-query/lesson.adoc | 6 ++---- .../drivers-python/modules/1-driver/module.adoc | 11 ++++++++--- .../lessons/1-type-system/lesson.adoc | 7 +++---- .../lessons/3-dates-and-times/lesson.adoc | 8 ++------ .../lessons/5-spatial-types/lesson.adoc | 7 +------ .../modules/2-handling-results/module.adoc | 12 ++++++++++-- .../lessons/1-transaction-management/lesson.adoc | 5 +---- .../lessons/3-error-handling/lesson.adoc | 6 +----- .../lessons/4c-handling-errors/lesson.adoc | 2 +- .../modules/3-in-production/module.adoc | 11 ++++++++++- 36 files changed, 124 insertions(+), 107 deletions(-) diff --git a/asciidoc/courses/drivers-go/course.adoc b/asciidoc/courses/drivers-go/course.adoc index 429378942..716a1a352 100644 --- a/asciidoc/courses/drivers-go/course.adoc +++ b/asciidoc/courses/drivers-go/course.adoc @@ -4,6 +4,7 @@ :caption: Learn how to interact with Neo4j from Go using the Neo4j Go Driver :next: app-go :key-points: Driver life cycle, Installing and instantiation, Read and write transactions, Best practices +:duration: 1 hour // tag::config[] :go-version: 1.21 :slides: true diff --git a/asciidoc/courses/drivers-go/modules/1-driver/lessons/1-driver-lifecycle/lesson.adoc b/asciidoc/courses/drivers-go/modules/1-driver/lessons/1-driver-lifecycle/lesson.adoc index 4e3ad4bd9..354351cf9 100644 --- a/asciidoc/courses/drivers-go/modules/1-driver/lessons/1-driver-lifecycle/lesson.adoc +++ b/asciidoc/courses/drivers-go/modules/1-driver/lessons/1-driver-lifecycle/lesson.adoc @@ -107,7 +107,7 @@ if err != nil { [NOTE] .Verify Connectivity ===== -The `VerifyConnectivity()` method will link:https://neo4j.com/docs/go-manual/current/errors.html[return an error^] if the connection cannot be made. +The `VerifyConnectivity()` method will link:https://neo4j.com/docs/go-manual/current/connect/[return an error^] if the connection cannot be made. ===== @@ -227,10 +227,8 @@ driver.Close(ctx) // ---- -[.next.discrete] -== Check your understanding -link:../2c-create-driver-instance/[Take challenge,role=btn] +read::Take challenge[] [.summary] == Lesson Summary diff --git a/asciidoc/courses/drivers-go/modules/1-driver/lessons/3-execute-query/lesson.adoc b/asciidoc/courses/drivers-go/modules/1-driver/lessons/3-execute-query/lesson.adoc index 0a85d60a9..e771dfd90 100644 --- a/asciidoc/courses/drivers-go/modules/1-driver/lessons/3-execute-query/lesson.adoc +++ b/asciidoc/courses/drivers-go/modules/1-driver/lessons/3-execute-query/lesson.adoc @@ -1,5 +1,5 @@ = Executing Cypher statements -:type: lesson +:type: lesson :slides: true :minutes: 10 :order: 3 @@ -154,10 +154,8 @@ result, err := neo4j.ExecuteQuery(ctx, driver, [TIP] You can also use `neo4j.ExecuteQueryWithWritersRouting` to explicitly invoke write mode. -[.next.discrete] -== Check your understanding +read::Run your first query[] -link:../4c-your-first-query/[Run your first query,role=btn] [.summary] == Summary diff --git a/asciidoc/courses/drivers-go/modules/1-driver/module.adoc b/asciidoc/courses/drivers-go/modules/1-driver/module.adoc index b0c708a91..3321bba9f 100644 --- a/asciidoc/courses/drivers-go/modules/1-driver/module.adoc +++ b/asciidoc/courses/drivers-go/modules/1-driver/module.adoc @@ -1,10 +1,15 @@ = The Driver :order: 1 -In this module, you'll learn the fundamentals of working with Neo4j in Go applications. You will start by learning how to install the Neo4j Go Driver and use it to connect to a Neo4j database instance. +In this module, you'll learn the fundamentals of working with Neo4j in Go applications. You will start by learning how to install the Neo4j Go Driver and use it to connect to a Neo4j database instance. -You'll then write your first query using the driver's `ExecuteQuery()` method and learn how to process the results returned from the database. +Throughout this module, you will explore: + +* Installing the Neo4j Go Driver +* Creating driver instances and verifying connectivity +* Executing your first Cypher queries using the `ExecuteQuery()` method +* Understanding the driver lifecycle and how to properly manage connections By the end of this module, you'll have the foundation needed to start building Go applications with Neo4j. -link:1-driver-lifecycle/[Go to the first lesson,role="btn"] +link:./1-driver-lifecycle/[Go to the first lesson,role="btn"] diff --git a/asciidoc/courses/drivers-go/modules/2-handling-results/lessons/1-type-system/lesson.adoc b/asciidoc/courses/drivers-go/modules/2-handling-results/lessons/1-type-system/lesson.adoc index 0084dcf18..2943a5d26 100644 --- a/asciidoc/courses/drivers-go/modules/2-handling-results/lessons/1-type-system/lesson.adoc +++ b/asciidoc/courses/drivers-go/modules/2-handling-results/lessons/1-type-system/lesson.adoc @@ -200,10 +200,8 @@ A path is a sequence of nodes and relationships and is returned as a `neo4j.Path ==== -[.next.discrete] -== Check your understanding +read::Advance to challenge[] -link:../2c-accessing-graph-types/[Advance to challenge,role=btn] [.summary] == Summary diff --git a/asciidoc/courses/drivers-go/modules/2-handling-results/lessons/3-dates-and-times/lesson.adoc b/asciidoc/courses/drivers-go/modules/2-handling-results/lessons/3-dates-and-times/lesson.adoc index fc9d60713..53a81d788 100644 --- a/asciidoc/courses/drivers-go/modules/2-handling-results/lessons/3-dates-and-times/lesson.adoc +++ b/asciidoc/courses/drivers-go/modules/2-handling-results/lessons/3-dates-and-times/lesson.adoc @@ -1,5 +1,5 @@ = Dates and times -:type: lesson +:type: lesson :order: 3 [.slide.discrete] @@ -140,10 +140,7 @@ For time arithmetic in Go, use standard `time.Duration` values directly. ==== -[.next.discrete] -== Check your understanding - -link:../4c-working-with-dates-and-times/[Advance to challenge,role=btn] +read::Advance to challenge[] [.summary] diff --git a/asciidoc/courses/drivers-go/modules/2-handling-results/lessons/5-spatial-types/lesson.adoc b/asciidoc/courses/drivers-go/modules/2-handling-results/lessons/5-spatial-types/lesson.adoc index 5ad6d8863..3798192e6 100644 --- a/asciidoc/courses/drivers-go/modules/2-handling-results/lessons/5-spatial-types/lesson.adoc +++ b/asciidoc/courses/drivers-go/modules/2-handling-results/lessons/5-spatial-types/lesson.adoc @@ -204,10 +204,8 @@ if err == nil && len(result.Records) > 0 { ---- ==== -[.next.discrete] -== Check your understanding +read::Advance to challenge[] -link:../6c-using-spatial-types/[Advance to challenge,role=btn] [.summary] == Lesson Summary diff --git a/asciidoc/courses/drivers-go/modules/2-handling-results/module.adoc b/asciidoc/courses/drivers-go/modules/2-handling-results/module.adoc index d976728fd..74a3f8e91 100644 --- a/asciidoc/courses/drivers-go/modules/2-handling-results/module.adoc +++ b/asciidoc/courses/drivers-go/modules/2-handling-results/module.adoc @@ -3,7 +3,15 @@ In the previous module, you learned how to install the driver, connect to an instance, and run your first query. -In this module, you will learn how to handle the results of a query. -You will learn the types of data returned by the driver and how to work with them in your application. +In this module, you will learn how to handle the results of a query. The Neo4j Go Driver returns data in various formats, and understanding how to work with these types is essential for building robust applications. + +You will learn about: + +* Working with graph types (Nodes, Relationships, and Paths) +* Handling temporal types (dates and times) +* Using spatial types (points and distances) +* Transforming query results into different formats + +By the end of this module, you'll be able to work confidently with all the data types returned by Neo4j queries. link:./1-type-system/[Advance to the next lesson,role=btn] diff --git a/asciidoc/courses/drivers-go/modules/3-in-production/lessons/1-transaction-management/lesson.adoc b/asciidoc/courses/drivers-go/modules/3-in-production/lessons/1-transaction-management/lesson.adoc index ee6a67aae..89e100e8c 100644 --- a/asciidoc/courses/drivers-go/modules/3-in-production/lessons/1-transaction-management/lesson.adoc +++ b/asciidoc/courses/drivers-go/modules/3-in-production/lessons/1-transaction-management/lesson.adoc @@ -245,10 +245,8 @@ fmt.Printf("Results available after %d ms and consumed after %d ms\n", ---- -[.next.discrete] -== Check your understanding +read::Advance to next lesson[] -link:../2c-write-transaction/[Advance to the next lesson,role=btn] [.summary] == Lesson Summary diff --git a/asciidoc/courses/drivers-go/modules/3-in-production/lessons/3-error-handling/lesson.adoc b/asciidoc/courses/drivers-go/modules/3-in-production/lessons/3-error-handling/lesson.adoc index 9f9c4f37e..291fd97ff 100644 --- a/asciidoc/courses/drivers-go/modules/3-in-production/lessons/3-error-handling/lesson.adoc +++ b/asciidoc/courses/drivers-go/modules/3-in-production/lessons/3-error-handling/lesson.adoc @@ -94,10 +94,8 @@ func executeWithRetry(ctx context.Context, driver neo4j.Driver, query string, pa ---- ==== -[.next.discrete] -== Check your understanding +read::Advance to challenge[] -link:../4c-handling-errors/[Advance to challenge,role=btn] [.summary] == Summary diff --git a/asciidoc/courses/drivers-go/modules/3-in-production/module.adoc b/asciidoc/courses/drivers-go/modules/3-in-production/module.adoc index fef602efb..0fcf1f9ce 100644 --- a/asciidoc/courses/drivers-go/modules/3-in-production/module.adoc +++ b/asciidoc/courses/drivers-go/modules/3-in-production/module.adoc @@ -3,6 +3,15 @@ So far you have learned how to connect to Neo4j, execute Cypher queries and handle the results returned. -In this module, you will learn everything you need to know to put your application into production. +In this module, you will learn everything you need to know to put your application into production. You'll discover how to manage transactions effectively, handle errors gracefully, and implement best practices that ensure your application is robust and performant. + +This module covers: + +* Managing database transactions for better performance +* Writing efficient transactions for data modifications +* Handling errors gracefully in production applications +* Implementing best practices for robust Neo4j applications + +By the end of this module, you'll have the knowledge and confidence to deploy your Neo4j Go application to production. link:./1-transaction-management/[Advance to the next lesson,role=btn] diff --git a/asciidoc/courses/drivers-java/course.adoc b/asciidoc/courses/drivers-java/course.adoc index 9021fa2e8..4de797ddc 100644 --- a/asciidoc/courses/drivers-java/course.adoc +++ b/asciidoc/courses/drivers-java/course.adoc @@ -4,6 +4,7 @@ :caption: Learn how to interact with Neo4j using the Neo4j Java Driver :next: app-java :key-points: Driver life cycle, Installing and instantiation, Read and write transactions, Best practices +:duration: 1 hour // tag::config[] :slides: true :repository: neo4j-graphacademy/drivers-java diff --git a/asciidoc/courses/drivers-java/modules/1-driver/lessons/1-driver-lifecycle/lesson.adoc b/asciidoc/courses/drivers-java/modules/1-driver/lessons/1-driver-lifecycle/lesson.adoc index 65fe60777..5e7bd51d4 100644 --- a/asciidoc/courses/drivers-java/modules/1-driver/lessons/1-driver-lifecycle/lesson.adoc +++ b/asciidoc/courses/drivers-java/modules/1-driver/lessons/1-driver-lifecycle/lesson.adoc @@ -155,10 +155,8 @@ try ( ---- -[.next.discrete] -== Check your understanding +read::Take challenge[] -link:../2c-create-driver-instance/[Take challenge,role=btn] [.summary] == Lesson Summary diff --git a/asciidoc/courses/drivers-java/modules/1-driver/lessons/3-execute-query/lesson.adoc b/asciidoc/courses/drivers-java/modules/1-driver/lessons/3-execute-query/lesson.adoc index 3f8114e95..3710a8c6b 100644 --- a/asciidoc/courses/drivers-java/modules/1-driver/lessons/3-execute-query/lesson.adoc +++ b/asciidoc/courses/drivers-java/modules/1-driver/lessons/3-execute-query/lesson.adoc @@ -1,5 +1,5 @@ = Executing Cypher statements -:type: lesson +:type: lesson :slides: true :minutes: 10 :order: 4 @@ -110,10 +110,8 @@ var result = driver.executableQuery(cypher) .execute(); ---- -[.next.discrete] -== Check your understanding +read::Take challenge[] -link:../4c-your-first-query/[Take the challenge,role=btn] [.summary] == Summary diff --git a/asciidoc/courses/drivers-java/modules/1-driver/lessons/4-object-mapping/lesson.adoc b/asciidoc/courses/drivers-java/modules/1-driver/lessons/4-object-mapping/lesson.adoc index 1931a349d..e5ce73942 100644 --- a/asciidoc/courses/drivers-java/modules/1-driver/lessons/4-object-mapping/lesson.adoc +++ b/asciidoc/courses/drivers-java/modules/1-driver/lessons/4-object-mapping/lesson.adoc @@ -1,5 +1,5 @@ = Mapping Results to Java Objects -:type: lesson +:type: lesson :slides: true :minutes: 15 :order: 5 @@ -150,6 +150,9 @@ System.out.println(movie); ---- ==== +read::Mark as completed[] + + [.summary] == Summary diff --git a/asciidoc/courses/drivers-java/modules/1-driver/module.adoc b/asciidoc/courses/drivers-java/modules/1-driver/module.adoc index 13bbca9d8..190cc7a13 100644 --- a/asciidoc/courses/drivers-java/modules/1-driver/module.adoc +++ b/asciidoc/courses/drivers-java/modules/1-driver/module.adoc @@ -1,12 +1,15 @@ = The Driver :order: 1 -In this module, you'll learn the fundamentals of working with Neo4j in Java applications. +In this module, you'll learn the fundamentals of working with Neo4j in Java applications. You will start by learning how to install the Neo4j Java Driver and use it to connect to a Neo4j database instance. -You will setup an environment and learn how to reference the Neo4j Java Driver and use it to connect to a Neo4j database instance. +Throughout this module, you will explore: -You'll then write your first query using the driver's `executableQuery()` method and learn how to process the results returned from the database. +* Installing the Neo4j Java Driver +* Creating driver instances and verifying connectivity +* Executing your first Cypher queries using the `executableQuery()` method +* Understanding the driver lifecycle and how to properly manage connections By the end of this module, you'll have the foundation needed to start building Java applications with Neo4j. -link:0-setup/[Setup your environment,role="btn"] +link:./0-setup/[Setup your environment,role="btn"] diff --git a/asciidoc/courses/drivers-java/modules/2-handling-results/lessons/1-type-system/lesson.adoc b/asciidoc/courses/drivers-java/modules/2-handling-results/lessons/1-type-system/lesson.adoc index 272aea5ca..cf2d7cb2d 100644 --- a/asciidoc/courses/drivers-java/modules/2-handling-results/lessons/1-type-system/lesson.adoc +++ b/asciidoc/courses/drivers-java/modules/2-handling-results/lessons/1-type-system/lesson.adoc @@ -200,10 +200,8 @@ records.forEach(r -> { ==== -[.next.discrete] -== Check your understanding +read::Advance to challenge[] -link:../2c-accessing-graph-types/[Advance to challenge,role=btn] [.summary] == Summary diff --git a/asciidoc/courses/drivers-java/modules/2-handling-results/lessons/3-dates-and-times/lesson.adoc b/asciidoc/courses/drivers-java/modules/2-handling-results/lessons/3-dates-and-times/lesson.adoc index 807eaf465..603caa6fc 100644 --- a/asciidoc/courses/drivers-java/modules/2-handling-results/lessons/3-dates-and-times/lesson.adoc +++ b/asciidoc/courses/drivers-java/modules/2-handling-results/lessons/3-dates-and-times/lesson.adoc @@ -1,5 +1,5 @@ = Dates and times -:type: lesson +:type: lesson :order: 3 [.slide.discrete] @@ -167,10 +167,7 @@ You can use the `duration.between` method to calculate the duration between two ==== -[.next.discrete] -== Check your understanding - -link:../4c-working-with-dates-and-times/[Advance to challenge,role=btn] +read::Advance to challenge[] [.summary] diff --git a/asciidoc/courses/drivers-java/modules/2-handling-results/lessons/5-spatial-types/lesson.adoc b/asciidoc/courses/drivers-java/modules/2-handling-results/lessons/5-spatial-types/lesson.adoc index 1c7551a59..c9d9ee60e 100644 --- a/asciidoc/courses/drivers-java/modules/2-handling-results/lessons/5-spatial-types/lesson.adoc +++ b/asciidoc/courses/drivers-java/modules/2-handling-results/lessons/5-spatial-types/lesson.adoc @@ -174,12 +174,10 @@ var distance = result.records().get(0).get("distance").asDouble(); System.out.println(distance); ---- -[.next.discrete] -== Check your understanding +==== -link:../6c-using-spatial-types/[Advance to challenge,role=btn] +read::Advance to challenge[] -==== [.summary] == Lesson Summary diff --git a/asciidoc/courses/drivers-java/modules/2-handling-results/module.adoc b/asciidoc/courses/drivers-java/modules/2-handling-results/module.adoc index ee086ad94..0bdbd8e8f 100644 --- a/asciidoc/courses/drivers-java/modules/2-handling-results/module.adoc +++ b/asciidoc/courses/drivers-java/modules/2-handling-results/module.adoc @@ -3,8 +3,16 @@ In the previous module, you learned how to install the driver, connect to an instance, and run your first query. -In this module, you will learn how to handle the results of a query. -You will learn the types of data returned by the driver and how to work with them in your application. +In this module, you will learn how to handle the results of a query. The Neo4j Java Driver returns data in various formats, and understanding how to work with these types is essential for building robust applications. + +You will learn about: + +* Working with graph types (Nodes, Relationships, and Paths) +* Handling temporal types (dates and times) +* Using spatial types (points and distances) +* Transforming query results into different formats + +By the end of this module, you'll be able to work confidently with all the data types returned by Neo4j queries. link:./1-type-system/[Advance to the next lesson,role=btn] diff --git a/asciidoc/courses/drivers-java/modules/3-in-production/lessons/1-transaction-management/lesson.adoc b/asciidoc/courses/drivers-java/modules/3-in-production/lessons/1-transaction-management/lesson.adoc index d3169c9b0..bd41df73d 100644 --- a/asciidoc/courses/drivers-java/modules/3-in-production/lessons/1-transaction-management/lesson.adoc +++ b/asciidoc/courses/drivers-java/modules/3-in-production/lessons/1-transaction-management/lesson.adoc @@ -173,10 +173,8 @@ try (var session = driver.session()) { ==== -[.next.discrete] -== Check your understanding +read::Advance to next lesson[] -link:../2c-write-transaction/[Advance to the next lesson,role=btn] [.summary] == Lesson Summary diff --git a/asciidoc/courses/drivers-java/modules/3-in-production/lessons/3-error-handling/lesson.adoc b/asciidoc/courses/drivers-java/modules/3-in-production/lessons/3-error-handling/lesson.adoc index 732731b47..a4dab7468 100644 --- a/asciidoc/courses/drivers-java/modules/3-in-production/lessons/3-error-handling/lesson.adoc +++ b/asciidoc/courses/drivers-java/modules/3-in-production/lessons/3-error-handling/lesson.adoc @@ -96,10 +96,7 @@ try (var session = driver.session()) { ==== -[.next.discrete] -== Check your understanding - -link:../4c-handling-errors/[Advance to challenge,role="btn transcript-only"] +read::Advance to challenge[] [.summary] diff --git a/asciidoc/courses/drivers-java/modules/3-in-production/lessons/4c-handling-errors/lesson.adoc b/asciidoc/courses/drivers-java/modules/3-in-production/lessons/4c-handling-errors/lesson.adoc index 59753b0aa..b4857e8f7 100644 --- a/asciidoc/courses/drivers-java/modules/3-in-production/lessons/4c-handling-errors/lesson.adoc +++ b/asciidoc/courses/drivers-java/modules/3-in-production/lessons/4c-handling-errors/lesson.adoc @@ -21,7 +21,7 @@ It does not check for existing users with the same email address, and instead re include::./questions/1-constraint-error.adoc[leveloffset=+1] -[summary] +[.summary] == Lesson Summary In this challenge you demonstrated how to handle errors in a real-world scenario. diff --git a/asciidoc/courses/drivers-java/modules/3-in-production/module.adoc b/asciidoc/courses/drivers-java/modules/3-in-production/module.adoc index 90d8d2b99..3c843128d 100644 --- a/asciidoc/courses/drivers-java/modules/3-in-production/module.adoc +++ b/asciidoc/courses/drivers-java/modules/3-in-production/module.adoc @@ -1,8 +1,17 @@ = Best practices :order: 3 -You have learned how to connect to Neo4j, execute Cypher queries and handle the results returned. +So far you have learned how to connect to Neo4j, execute Cypher queries and handle the results returned. -In this module, you will learn everything you need to know to put your application into production. +In this module, you will learn everything you need to know to put your application into production. You'll discover how to manage transactions effectively, handle errors gracefully, and implement best practices that ensure your application is robust and performant. + +This module covers: + +* Managing database transactions for better performance +* Writing efficient transactions for data modifications +* Handling errors gracefully in production applications +* Implementing best practices for robust Neo4j applications + +By the end of this module, you'll have the knowledge and confidence to deploy your Neo4j Java application to production. link:./1-transaction-management/[Advance to the next lesson,role=btn] diff --git a/asciidoc/courses/drivers-python/course.adoc b/asciidoc/courses/drivers-python/course.adoc index 71319580e..c5a4a7507 100644 --- a/asciidoc/courses/drivers-python/course.adoc +++ b/asciidoc/courses/drivers-python/course.adoc @@ -4,6 +4,7 @@ :caption: Learn how to interact with Neo4j from Python using the Neo4j Python Driver :next: app-python :key-points: Driver life cycle, Installing and instantiation, Read and write transactions, Best practices +:duration: 1 hour // tag::config[] :python-version: 3.12.4 :slides: true diff --git a/asciidoc/courses/drivers-python/modules/1-driver/lessons/1-driver-lifecycle/lesson.adoc b/asciidoc/courses/drivers-python/modules/1-driver/lessons/1-driver-lifecycle/lesson.adoc index 3ffd46f40..b1a7154d0 100644 --- a/asciidoc/courses/drivers-python/modules/1-driver/lessons/1-driver-lifecycle/lesson.adoc +++ b/asciidoc/courses/drivers-python/modules/1-driver/lessons/1-driver-lifecycle/lesson.adoc @@ -135,10 +135,7 @@ with GraphDatabase.driver(NEO4J_URI, auth=(NEO4J_USERNAME, NEO4J_PASSWORD)) as d ---- -[.next.discrete] -== Check your understanding - -link:../2c-create-driver-instance/[Take challenge,role=btn] +read::Take challenge[] [.summary] == Lesson Summary diff --git a/asciidoc/courses/drivers-python/modules/1-driver/lessons/3-execute-query/lesson.adoc b/asciidoc/courses/drivers-python/modules/1-driver/lessons/3-execute-query/lesson.adoc index 643ed95c4..4e19f342f 100644 --- a/asciidoc/courses/drivers-python/modules/1-driver/lessons/3-execute-query/lesson.adoc +++ b/asciidoc/courses/drivers-python/modules/1-driver/lessons/3-execute-query/lesson.adoc @@ -1,5 +1,5 @@ = Executing Cypher statements -:type: lesson +:type: lesson :slides: true :minutes: 10 :order: 3 @@ -136,10 +136,8 @@ driver.execute_query( [TIP] You can also pass `r` for read mode and `w` to explicitly invoke write mode. -[.next.discrete] -== Check your understanding -link:../4c-your-first-query/[Run your first query,role=btn] +read::Mark as completed[] [.summary] == Summary diff --git a/asciidoc/courses/drivers-python/modules/1-driver/module.adoc b/asciidoc/courses/drivers-python/modules/1-driver/module.adoc index be80d7916..19e011114 100644 --- a/asciidoc/courses/drivers-python/modules/1-driver/module.adoc +++ b/asciidoc/courses/drivers-python/modules/1-driver/module.adoc @@ -1,10 +1,15 @@ = The Driver :order: 1 -In this module, you'll learn the fundamentals of working with Neo4j in Python applications. You will start by learning how to install the Neo4j Python Driver and use it to connect to a Neo4j database instance. +In this module, you'll learn the fundamentals of working with Neo4j in Python applications. You will start by learning how to install the Neo4j Python Driver and use it to connect to a Neo4j database instance. -You'll then write your first query using the driver's `execute_query()` method and learn how to process the results returned from the database. +Throughout this module, you will explore: + +* Installing the Neo4j Python Driver +* Creating driver instances and verifying connectivity +* Executing your first Cypher queries using the `execute_query()` method +* Understanding the driver lifecycle and how to properly manage connections By the end of this module, you'll have the foundation needed to start building Python applications with Neo4j. -link:1-driver-lifecycle/[Go to the first lesson,role="btn"] +link:./1-driver-lifecycle/[Go to the first lesson,role="btn"] diff --git a/asciidoc/courses/drivers-python/modules/2-handling-results/lessons/1-type-system/lesson.adoc b/asciidoc/courses/drivers-python/modules/2-handling-results/lessons/1-type-system/lesson.adoc index a1e8a5c39..4d3ba6aa5 100644 --- a/asciidoc/courses/drivers-python/modules/2-handling-results/lessons/1-type-system/lesson.adoc +++ b/asciidoc/courses/drivers-python/modules/2-handling-results/lessons/1-type-system/lesson.adoc @@ -69,6 +69,8 @@ The following code snippet finds all movies with the specified title and returns .Return Nodes and Relationships [source,python,role=ncopy,subs="attributes+",indent=0] ---- +movie = "Toy Story" + records, summary, keys = driver.execute_query(""" MATCH path = (person:Person)-[actedIn:ACTED_IN]->(movie:Movie {title: $title}) RETURN path, person, actedIn, movie @@ -190,10 +192,7 @@ Use `iter(path)` to iterate over the relationships in a path. ==== -[.next.discrete] -== Check your understanding - -link:../2c-accessing-graph-types/[Advance to challenge,role=btn] +read::Advance to challenge[] [.summary] == Summary diff --git a/asciidoc/courses/drivers-python/modules/2-handling-results/lessons/3-dates-and-times/lesson.adoc b/asciidoc/courses/drivers-python/modules/2-handling-results/lessons/3-dates-and-times/lesson.adoc index 1d875b919..a2ec75a0a 100644 --- a/asciidoc/courses/drivers-python/modules/2-handling-results/lessons/3-dates-and-times/lesson.adoc +++ b/asciidoc/courses/drivers-python/modules/2-handling-results/lessons/3-dates-and-times/lesson.adoc @@ -1,5 +1,5 @@ = Dates and times -:type: lesson +:type: lesson :order: 3 [.slide.discrete] @@ -131,11 +131,7 @@ You can use the `duration.between` method to calculate the duration between two ==== -[.next.discrete] -== Check your understanding - -link:../4c-working-with-dates-and-times/[Advance to challenge,role=btn] - +read::Advance to challenge[] [.summary] == Lesson Summary diff --git a/asciidoc/courses/drivers-python/modules/2-handling-results/lessons/5-spatial-types/lesson.adoc b/asciidoc/courses/drivers-python/modules/2-handling-results/lessons/5-spatial-types/lesson.adoc index efa442966..af5a8e833 100644 --- a/asciidoc/courses/drivers-python/modules/2-handling-results/lessons/5-spatial-types/lesson.adoc +++ b/asciidoc/courses/drivers-python/modules/2-handling-results/lessons/5-spatial-types/lesson.adoc @@ -161,12 +161,7 @@ print(distance) # 12.727922061357855 ---- ==== -[.next.discrete] -== Check your understanding - -link:../6c-using-spatial-types/[Advance to challenge,role=btn] - - +read::Advance to challenge[] [.summary] == Lesson Summary diff --git a/asciidoc/courses/drivers-python/modules/2-handling-results/module.adoc b/asciidoc/courses/drivers-python/modules/2-handling-results/module.adoc index ee086ad94..6ee54b3e9 100644 --- a/asciidoc/courses/drivers-python/modules/2-handling-results/module.adoc +++ b/asciidoc/courses/drivers-python/modules/2-handling-results/module.adoc @@ -3,8 +3,16 @@ In the previous module, you learned how to install the driver, connect to an instance, and run your first query. -In this module, you will learn how to handle the results of a query. -You will learn the types of data returned by the driver and how to work with them in your application. +In this module, you will learn how to handle the results of a query. The Neo4j Python Driver returns data in various formats, and understanding how to work with these types is essential for building robust applications. + +You will learn about: + +* Working with graph types (Nodes, Relationships, and Paths) +* Handling temporal types (dates and times) +* Using spatial types (points and distances) +* Transforming query results into different formats + +By the end of this module, you'll be able to work confidently with all the data types returned by Neo4j queries. link:./1-type-system/[Advance to the next lesson,role=btn] diff --git a/asciidoc/courses/drivers-python/modules/3-in-production/lessons/1-transaction-management/lesson.adoc b/asciidoc/courses/drivers-python/modules/3-in-production/lessons/1-transaction-management/lesson.adoc index ef3617276..4ef1b28dd 100644 --- a/asciidoc/courses/drivers-python/modules/3-in-production/lessons/1-transaction-management/lesson.adoc +++ b/asciidoc/courses/drivers-python/modules/3-in-production/lessons/1-transaction-management/lesson.adoc @@ -159,10 +159,7 @@ with driver.session() as session: ==== -[.next.discrete] -== Check your understanding - -link:../2c-write-transaction/[Advance to the next lesson,role=btn] +read::Advance to next lesson[] [.summary] == Lesson Summary diff --git a/asciidoc/courses/drivers-python/modules/3-in-production/lessons/3-error-handling/lesson.adoc b/asciidoc/courses/drivers-python/modules/3-in-production/lessons/3-error-handling/lesson.adoc index a5bded235..3058e4f5d 100644 --- a/asciidoc/courses/drivers-python/modules/3-in-production/lessons/3-error-handling/lesson.adoc +++ b/asciidoc/courses/drivers-python/modules/3-in-production/lessons/3-error-handling/lesson.adoc @@ -98,11 +98,7 @@ def create_user(tx, name, email): ==== -[.next.discrete] -== Check your understanding - -link:../4c-handling-errors/[Advance to challenge,role="btn transcript-only"] - +read::Advance to challenge[] [.summary] == Summary diff --git a/asciidoc/courses/drivers-python/modules/3-in-production/lessons/4c-handling-errors/lesson.adoc b/asciidoc/courses/drivers-python/modules/3-in-production/lessons/4c-handling-errors/lesson.adoc index 1050dfe03..78292ca51 100644 --- a/asciidoc/courses/drivers-python/modules/3-in-production/lessons/4c-handling-errors/lesson.adoc +++ b/asciidoc/courses/drivers-python/modules/3-in-production/lessons/4c-handling-errors/lesson.adoc @@ -21,7 +21,7 @@ It does not check for existing users with the same email address, and instead re include::./questions/1-constraint-error.adoc[leveloffset=+1] -[summary] +[.summary] == Lesson Summary In this challenge you demonstrated how to handle errors in a real-world scenario. diff --git a/asciidoc/courses/drivers-python/modules/3-in-production/module.adoc b/asciidoc/courses/drivers-python/modules/3-in-production/module.adoc index fef602efb..db3672094 100644 --- a/asciidoc/courses/drivers-python/modules/3-in-production/module.adoc +++ b/asciidoc/courses/drivers-python/modules/3-in-production/module.adoc @@ -3,6 +3,15 @@ So far you have learned how to connect to Neo4j, execute Cypher queries and handle the results returned. -In this module, you will learn everything you need to know to put your application into production. +In this module, you will learn everything you need to know to put your application into production. You'll discover how to manage transactions effectively, handle errors gracefully, and implement best practices that ensure your application is robust and performant. + +This module covers: + +* Managing database transactions for better performance +* Writing efficient transactions for data modifications +* Handling errors gracefully in production applications +* Implementing best practices for robust Neo4j applications + +By the end of this module, you'll have the knowledge and confidence to deploy your Neo4j Python application to production. link:./1-transaction-management/[Advance to the next lesson,role=btn] From a0d9ffe25f0ca11844b863bead8e3598f3bc8e1e Mon Sep 17 00:00:00 2001 From: Adam Cowley Date: Fri, 7 Nov 2025 19:20:37 +0000 Subject: [PATCH 2/2] initial structure --- .cursor/technical-lesson-review.mdc | 86 +++- .../courses/aura-administration/banner.png | Bin 0 -> 72247 bytes .../courses/aura-administration/course.adoc | 54 ++ .../aura-administration/illustration.svg | 24 + .../lessons/1-snapshot-management/lesson.adoc | 183 +++++++ .../questions/1-snapshot-schedule.adoc | 33 ++ .../lessons/2-backup-strategies/lesson.adoc | 127 +++++ .../lessons/2-snapshot-challenge/lesson.adoc | 30 ++ .../questions/1-snapshot-recovery.adoc | 33 ++ .../lessons/3-restore-operations/lesson.adoc | 142 ++++++ .../lessons/4-backup-challenge/lesson.adoc | 32 ++ .../questions/1-test-environment.adoc | 35 ++ .../lessons/6-restore-challenge/lesson.adoc | 41 ++ .../questions/1-restore-decision.adoc | 45 ++ .../modules/1-backup-restore/module.adoc | 30 ++ .../lessons/1-metrics-overview/lesson.adoc | 135 +++++ .../questions/1-dashboard-navigation.adoc | 31 ++ .../lessons/2-cpu-usage/lesson.adoc | 152 ++++++ .../2-cpu-usage/questions/1-cpu-action.adoc | 33 ++ .../lessons/3-storage-query-rate/lesson.adoc | 206 ++++++++ .../questions/1-storage-query.adoc | 33 ++ .../lessons/4-cpu-challenge/lesson.adoc | 47 ++ .../questions/1-cpu-crisis.adoc | 55 ++ .../lessons/6-storage-challenge/lesson.adoc | 68 +++ .../questions/1-storage-emergency.adoc | 58 +++ .../2-monitoring-resources/module.adoc | 28 + .../lessons/1-heap-memory/lesson.adoc | 215 ++++++++ .../questions/1-heap-threshold.adoc | 39 ++ .../lessons/10-oom-challenge/lesson.adoc | 189 +++++++ .../questions/1-oom-triage.adoc | 105 ++++ .../lessons/2-heap-challenge/lesson.adoc | 48 ++ .../questions/1-heap-pressure.adoc | 42 ++ .../lessons/2-page-cache/lesson.adoc | 288 +++++++++++ .../questions/1-cache-hit-ratio.adoc | 37 ++ .../lessons/3-bolt-connections/lesson.adoc | 302 +++++++++++ .../questions/1-connection-patterns.adoc | 39 ++ .../lessons/4-cache-challenge/lesson.adoc | 51 ++ .../questions/1-cache-performance.adoc | 50 ++ .../lessons/4-garbage-collection/lesson.adoc | 325 ++++++++++++ .../questions/1-gc-indicators.adoc | 41 ++ .../6-connection-challenge/lesson.adoc | 64 +++ .../questions/1-connection-leak.adoc | 63 +++ .../lessons/8-gc-challenge/lesson.adoc | 62 +++ .../8-gc-challenge/questions/1-gc-impact.adoc | 64 +++ .../lessons/9-oom-errors/lesson.adoc | 477 ++++++++++++++++++ .../questions/1-oom-response.adoc | 45 ++ .../modules/3-monitoring-instance/module.adoc | 30 ++ .../lessons/1-store-size/lesson.adoc | 292 +++++++++++ .../questions/1-store-compaction.adoc | 43 ++ .../lessons/2-query-metrics/lesson.adoc | 314 ++++++++++++ .../questions/1-query-latency.adoc | 46 ++ .../lessons/2-store-challenge/lesson.adoc | 61 +++ .../questions/1-compaction-decision.adoc | 57 +++ .../lessons/3-transactions/lesson.adoc | 296 +++++++++++ .../lessons/4-query-challenge/lesson.adoc | 59 +++ .../questions/1-percentile-analysis.adoc | 68 +++ .../6-transaction-challenge/lesson.adoc | 76 +++ .../questions/1-rollback-analysis.adoc | 87 ++++ .../lessons/7-checkpoints-replan/lesson.adoc | 313 ++++++++++++ .../8-checkpoint-challenge/lesson.adoc | 97 ++++ .../questions/1-replan-investigation.adoc | 82 +++ .../modules/4-monitoring-database/module.adoc | 28 + .../lessons/1-query-log-overview/lesson.adoc | 231 +++++++++ .../lessons/2-filtering-queries/lesson.adoc | 323 ++++++++++++ .../lessons/3-query-log-challenge/lesson.adoc | 66 +++ .../questions/1-prioritization.adoc | 77 +++ .../4-optimizing-performance/lesson.adoc | 437 ++++++++++++++++ .../6-optimization-challenge/lesson.adoc | 114 +++++ .../questions/1-optimization-strategy.adoc | 94 ++++ .../modules/5-query-logs/module.adoc | 28 + .../lessons/1-metrics-integration/lesson.adoc | 330 ++++++++++++ .../lessons/2-security-logs/lesson.adoc | 278 ++++++++++ .../modules/6-advanced-monitoring/module.adoc | 26 + .../courses/aura-administration/summary.adoc | 256 ++++++++++ .../courses/banners/aura-administration.png | Bin 0 -> 72247 bytes .../illustrations/aura-administration.svg | 24 + tests/qa.test.js | 474 ++++++++--------- 77 files changed, 8760 insertions(+), 234 deletions(-) create mode 100644 asciidoc/courses/aura-administration/banner.png create mode 100644 asciidoc/courses/aura-administration/course.adoc create mode 100644 asciidoc/courses/aura-administration/illustration.svg create mode 100644 asciidoc/courses/aura-administration/modules/1-backup-restore/lessons/1-snapshot-management/lesson.adoc create mode 100644 asciidoc/courses/aura-administration/modules/1-backup-restore/lessons/1-snapshot-management/questions/1-snapshot-schedule.adoc create mode 100644 asciidoc/courses/aura-administration/modules/1-backup-restore/lessons/2-backup-strategies/lesson.adoc create mode 100644 asciidoc/courses/aura-administration/modules/1-backup-restore/lessons/2-snapshot-challenge/lesson.adoc create mode 100644 asciidoc/courses/aura-administration/modules/1-backup-restore/lessons/2-snapshot-challenge/questions/1-snapshot-recovery.adoc create mode 100644 asciidoc/courses/aura-administration/modules/1-backup-restore/lessons/3-restore-operations/lesson.adoc create mode 100644 asciidoc/courses/aura-administration/modules/1-backup-restore/lessons/4-backup-challenge/lesson.adoc create mode 100644 asciidoc/courses/aura-administration/modules/1-backup-restore/lessons/4-backup-challenge/questions/1-test-environment.adoc create mode 100644 asciidoc/courses/aura-administration/modules/1-backup-restore/lessons/6-restore-challenge/lesson.adoc create mode 100644 asciidoc/courses/aura-administration/modules/1-backup-restore/lessons/6-restore-challenge/questions/1-restore-decision.adoc create mode 100644 asciidoc/courses/aura-administration/modules/1-backup-restore/module.adoc create mode 100644 asciidoc/courses/aura-administration/modules/2-monitoring-resources/lessons/1-metrics-overview/lesson.adoc create mode 100644 asciidoc/courses/aura-administration/modules/2-monitoring-resources/lessons/1-metrics-overview/questions/1-dashboard-navigation.adoc create mode 100644 asciidoc/courses/aura-administration/modules/2-monitoring-resources/lessons/2-cpu-usage/lesson.adoc create mode 100644 asciidoc/courses/aura-administration/modules/2-monitoring-resources/lessons/2-cpu-usage/questions/1-cpu-action.adoc create mode 100644 asciidoc/courses/aura-administration/modules/2-monitoring-resources/lessons/3-storage-query-rate/lesson.adoc create mode 100644 asciidoc/courses/aura-administration/modules/2-monitoring-resources/lessons/3-storage-query-rate/questions/1-storage-query.adoc create mode 100644 asciidoc/courses/aura-administration/modules/2-monitoring-resources/lessons/4-cpu-challenge/lesson.adoc create mode 100644 asciidoc/courses/aura-administration/modules/2-monitoring-resources/lessons/4-cpu-challenge/questions/1-cpu-crisis.adoc create mode 100644 asciidoc/courses/aura-administration/modules/2-monitoring-resources/lessons/6-storage-challenge/lesson.adoc create mode 100644 asciidoc/courses/aura-administration/modules/2-monitoring-resources/lessons/6-storage-challenge/questions/1-storage-emergency.adoc create mode 100644 asciidoc/courses/aura-administration/modules/2-monitoring-resources/module.adoc create mode 100644 asciidoc/courses/aura-administration/modules/3-monitoring-instance/lessons/1-heap-memory/lesson.adoc create mode 100644 asciidoc/courses/aura-administration/modules/3-monitoring-instance/lessons/1-heap-memory/questions/1-heap-threshold.adoc create mode 100644 asciidoc/courses/aura-administration/modules/3-monitoring-instance/lessons/10-oom-challenge/lesson.adoc create mode 100644 asciidoc/courses/aura-administration/modules/3-monitoring-instance/lessons/10-oom-challenge/questions/1-oom-triage.adoc create mode 100644 asciidoc/courses/aura-administration/modules/3-monitoring-instance/lessons/2-heap-challenge/lesson.adoc create mode 100644 asciidoc/courses/aura-administration/modules/3-monitoring-instance/lessons/2-heap-challenge/questions/1-heap-pressure.adoc create mode 100644 asciidoc/courses/aura-administration/modules/3-monitoring-instance/lessons/2-page-cache/lesson.adoc create mode 100644 asciidoc/courses/aura-administration/modules/3-monitoring-instance/lessons/2-page-cache/questions/1-cache-hit-ratio.adoc create mode 100644 asciidoc/courses/aura-administration/modules/3-monitoring-instance/lessons/3-bolt-connections/lesson.adoc create mode 100644 asciidoc/courses/aura-administration/modules/3-monitoring-instance/lessons/3-bolt-connections/questions/1-connection-patterns.adoc create mode 100644 asciidoc/courses/aura-administration/modules/3-monitoring-instance/lessons/4-cache-challenge/lesson.adoc create mode 100644 asciidoc/courses/aura-administration/modules/3-monitoring-instance/lessons/4-cache-challenge/questions/1-cache-performance.adoc create mode 100644 asciidoc/courses/aura-administration/modules/3-monitoring-instance/lessons/4-garbage-collection/lesson.adoc create mode 100644 asciidoc/courses/aura-administration/modules/3-monitoring-instance/lessons/4-garbage-collection/questions/1-gc-indicators.adoc create mode 100644 asciidoc/courses/aura-administration/modules/3-monitoring-instance/lessons/6-connection-challenge/lesson.adoc create mode 100644 asciidoc/courses/aura-administration/modules/3-monitoring-instance/lessons/6-connection-challenge/questions/1-connection-leak.adoc create mode 100644 asciidoc/courses/aura-administration/modules/3-monitoring-instance/lessons/8-gc-challenge/lesson.adoc create mode 100644 asciidoc/courses/aura-administration/modules/3-monitoring-instance/lessons/8-gc-challenge/questions/1-gc-impact.adoc create mode 100644 asciidoc/courses/aura-administration/modules/3-monitoring-instance/lessons/9-oom-errors/lesson.adoc create mode 100644 asciidoc/courses/aura-administration/modules/3-monitoring-instance/lessons/9-oom-errors/questions/1-oom-response.adoc create mode 100644 asciidoc/courses/aura-administration/modules/3-monitoring-instance/module.adoc create mode 100644 asciidoc/courses/aura-administration/modules/4-monitoring-database/lessons/1-store-size/lesson.adoc create mode 100644 asciidoc/courses/aura-administration/modules/4-monitoring-database/lessons/1-store-size/questions/1-store-compaction.adoc create mode 100644 asciidoc/courses/aura-administration/modules/4-monitoring-database/lessons/2-query-metrics/lesson.adoc create mode 100644 asciidoc/courses/aura-administration/modules/4-monitoring-database/lessons/2-query-metrics/questions/1-query-latency.adoc create mode 100644 asciidoc/courses/aura-administration/modules/4-monitoring-database/lessons/2-store-challenge/lesson.adoc create mode 100644 asciidoc/courses/aura-administration/modules/4-monitoring-database/lessons/2-store-challenge/questions/1-compaction-decision.adoc create mode 100644 asciidoc/courses/aura-administration/modules/4-monitoring-database/lessons/3-transactions/lesson.adoc create mode 100644 asciidoc/courses/aura-administration/modules/4-monitoring-database/lessons/4-query-challenge/lesson.adoc create mode 100644 asciidoc/courses/aura-administration/modules/4-monitoring-database/lessons/4-query-challenge/questions/1-percentile-analysis.adoc create mode 100644 asciidoc/courses/aura-administration/modules/4-monitoring-database/lessons/6-transaction-challenge/lesson.adoc create mode 100644 asciidoc/courses/aura-administration/modules/4-monitoring-database/lessons/6-transaction-challenge/questions/1-rollback-analysis.adoc create mode 100644 asciidoc/courses/aura-administration/modules/4-monitoring-database/lessons/7-checkpoints-replan/lesson.adoc create mode 100644 asciidoc/courses/aura-administration/modules/4-monitoring-database/lessons/8-checkpoint-challenge/lesson.adoc create mode 100644 asciidoc/courses/aura-administration/modules/4-monitoring-database/lessons/8-checkpoint-challenge/questions/1-replan-investigation.adoc create mode 100644 asciidoc/courses/aura-administration/modules/4-monitoring-database/module.adoc create mode 100644 asciidoc/courses/aura-administration/modules/5-query-logs/lessons/1-query-log-overview/lesson.adoc create mode 100644 asciidoc/courses/aura-administration/modules/5-query-logs/lessons/2-filtering-queries/lesson.adoc create mode 100644 asciidoc/courses/aura-administration/modules/5-query-logs/lessons/3-query-log-challenge/lesson.adoc create mode 100644 asciidoc/courses/aura-administration/modules/5-query-logs/lessons/3-query-log-challenge/questions/1-prioritization.adoc create mode 100644 asciidoc/courses/aura-administration/modules/5-query-logs/lessons/4-optimizing-performance/lesson.adoc create mode 100644 asciidoc/courses/aura-administration/modules/5-query-logs/lessons/6-optimization-challenge/lesson.adoc create mode 100644 asciidoc/courses/aura-administration/modules/5-query-logs/lessons/6-optimization-challenge/questions/1-optimization-strategy.adoc create mode 100644 asciidoc/courses/aura-administration/modules/5-query-logs/module.adoc create mode 100644 asciidoc/courses/aura-administration/modules/6-advanced-monitoring/lessons/1-metrics-integration/lesson.adoc create mode 100644 asciidoc/courses/aura-administration/modules/6-advanced-monitoring/lessons/2-security-logs/lesson.adoc create mode 100644 asciidoc/courses/aura-administration/modules/6-advanced-monitoring/module.adoc create mode 100644 asciidoc/courses/aura-administration/summary.adoc create mode 100644 public/img/courses/banners/aura-administration.png create mode 100644 public/img/courses/illustrations/aura-administration.svg diff --git a/.cursor/technical-lesson-review.mdc b/.cursor/technical-lesson-review.mdc index e02057e96..70b5bcf2d 100644 --- a/.cursor/technical-lesson-review.mdc +++ b/.cursor/technical-lesson-review.mdc @@ -16,6 +16,24 @@ If you change something, make sure you explain _why_ the change has been made. * [ ] Titles should always be followed by two line breaks. * [ ] The "== Check your understanding" header should be preceded with `[.quiz]` on the previous line. +* [ ] There must always be a blank line between a paragraph and a list (ordered or unordered). + +**Example - Incorrect:** + +```asciidoc +Here is some text in a paragraph. +* List item 1 +* List item 2 +``` + +**Example - Correct:** + +```asciidoc +Here is some text in a paragraph. + +* List item 1 +* List item 2 +``` ## Links @@ -33,11 +51,12 @@ A lesson may be read-only, or include questions. Any questions are stored in th * If a lesson has questions, it must not have a "Mark as read" button (`read::{text}[]`). Remove the read button. * For each .adoc file in the `questions/` folder, make sure that the file is included. +* **There can only be ONE bullet list in a question** - the answer choices. Avoid bullet lists in the scenario/question text. Numbered lists are acceptable for presenting data, but bullet lists should be avoided in the question setup. ### How to include a question -Use the include macro, making sure there is a +Use the include macro: ```asciidoc include::questions/[file-name].adoc[leveloffset=+1] @@ -45,6 +64,71 @@ include::questions/[file-name].adoc[leveloffset=+1] ``` +### Challenge Lessons + +Challenge lessons (`:type: challenge`) should follow a specific structure to avoid duplicating content: + +**In the lesson.adoc file:** +1. Present the scenario with all context needed +2. Include a "The Challenge" section with the question prompt +3. Include the question file directly (no `[.quiz]` or `== Check Your Understanding` header needed) +4. Provide a summary with key takeaways + +**Structure:** + +```asciidoc += Challenge Title +:type: challenge +:order: 2 + + +== Scenario + +Present the full scenario with all context, constraints, and information the user needs. + + +== The Challenge + +State the challenge question (e.g., "What is the BEST course of action?") + + +include::questions/1-question.adoc[leveloffset=+1] + + +[.summary] +== Summary + +Provide key takeaways and lessons learned. +``` + +**In the question file:** + +The question file should contain ONLY the question title, answer options, hint, and solution. Do NOT duplicate the scenario from the lesson.adoc file. + +```asciidoc +[.question] += Question Title + +What is the best approach? + +* [ ] Option A +* [x] Option B (correct) +* [ ] Option C + +[TIP,role=hint] +.Hint +==== +Hint text +==== + +[TIP,role=solution] +.Solution +==== +Explanation of correct answer +==== +``` + + ## Slides diff --git a/asciidoc/courses/aura-administration/banner.png b/asciidoc/courses/aura-administration/banner.png new file mode 100644 index 0000000000000000000000000000000000000000..0c51a337abad98d73097de5d1eb075a805e0a18f GIT binary patch literal 72247 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 ep`RA;-<9v4M_R(O + + + + + + + + + + + + + + + + + + + + + + + diff --git a/asciidoc/courses/aura-administration/modules/1-backup-restore/lessons/1-snapshot-management/lesson.adoc b/asciidoc/courses/aura-administration/modules/1-backup-restore/lessons/1-snapshot-management/lesson.adoc new file mode 100644 index 000000000..2dfa8d1e2 --- /dev/null +++ b/asciidoc/courses/aura-administration/modules/1-backup-restore/lessons/1-snapshot-management/lesson.adoc @@ -0,0 +1,183 @@ += Managing Snapshots +:type: lesson +:order: 1 + + +[.discrete] +== Introduction + +The data in your Aura instance can be backed up, exported, and restored using snapshots. + +A **snapshot** is a copy of the data in an instance at a specific point in time. + +In this lesson, you will learn about snapshot types, how to access and manage them, and the retention policies for different Aura tiers. + + +== Snapshot Types + +Aura provides two types of snapshots to help you protect your data and maintain operational flexibility. + + +=== Scheduled Snapshots + +Scheduled snapshots run automatically based on your Aura tier without manual intervention. + +The frequency depends on your tier. For instances running the latest version of Neo4j, **AuraDB Professional** and **AuraDS Professional** create snapshots once per day. **AuraDB Business Critical** creates snapshots every hour. **AuraDB Virtual Dedicated Cloud** creates both daily full snapshots and hourly differential snapshots. **AuraDS Enterprise** creates daily snapshots. + +If you're running Virtual Dedicated Cloud instances on Neo4j 4.x, you'll get scheduled snapshots every 6 hours. + +[NOTE] +.Full vs. Differential Snapshots +==== +Understanding the difference between snapshot types is important for your recovery strategy. A **full snapshot** captures your entire database at a specific point in time - it's complete and self-contained. A **differential snapshot**, on the other hand, only records the changes since the last full snapshot, making it more efficient but dependent on the full snapshot that came before it. + +Keep in mind that differential snapshots are not exportable and cannot be used to create new instances. They're designed purely for restore operations on your existing instance. +==== + + +=== On Demand Snapshots + +On demand snapshots are manually triggered using the **Take snapshot** button in the Aura console. You can create them at any time. + +For AuraDB Free instances, on demand snapshots are the only snapshot type available. + + +== Accessing Snapshots + +To view and manage snapshots: + +. Navigate to your Aura instance card in the console +. Click the more menu icon (**…**) on the instance card +. Select **Inspect** +. Select the **Snapshots** tab + +The Snapshots tab displays a list of available snapshots with their creation time, type, and available actions. + +Use the **Show exportable only** toggle to filter snapshots by whether they can be exported. + + +== Snapshot Actions + +The more menu (**…**) next to each snapshot provides access to three actions: export, create instance, and restore. + + +=== Export + +Exporting downloads a snapshot as a backup file for offline storage or use with other Neo4j installations. **AuraDB instances** export as `.backup` files (latest version) or `.dump` files (version 4.x). **AuraDS instances** export as `.tar` files. + +Exported files provide data recovery options independent of Aura's infrastructure. + +[IMPORTANT] +.Exportability Restrictions +==== +There are some limits to be aware of. For Virtual Dedicated Cloud instances running Neo4j latest version, you can only export one full snapshot per day. This is by design to balance data protection with system resources. + +Remember that differential backups cannot be exported at all - only full snapshots can be downloaded. +==== + + +=== Create Instance from Snapshot + +Creating a new instance from a snapshot produces an independent instance populated with data from the snapshot point in time. The original instance remains unchanged. + +This enables testing changes, debugging production issues in isolation, and providing realistic test data without affecting production. + + +=== Restore + +The restore action (accessed via the arrow icon next to the more menu) reverts the instance to the state captured in the snapshot. + +[WARNING] +==== +Restoring from a snapshot **overwrites** all current data in the instance, replacing it entirely with the snapshot data. The operation is destructive and irreversible. + +The console requires confirmation before proceeding. To verify snapshot data without overwriting current data, create a new instance from the snapshot instead. +==== + + +== Snapshot Retention + +Different tiers offer different retention policies: + +[cols="1,1,1,1", options="header"] +|=== +|Tier +|Restorable Days +|Exportable Days +|Total Retention + +|AuraDB Free +|N/A (on-demand only) +|N/A (on-demand only) +|N/A + +|AuraDB Professional +|7 +|7 +|7 + +|AuraDB Business Critical +|30 +|30 (full only) +|30 + +|AuraDB Virtual Dedicated Cloud (latest) +|60 (full) +|60 (full) +|90 + +|AuraDB Virtual Dedicated Cloud (4.x) +|60 (long), 7 (short) +|14 (long), 7 (short) +|90 + +|AuraDS Professional +|7 +|7 +|30 + +|AuraDS Enterprise +|14 +|7 +|90 +|=== + +[NOTE] +.On-Demand Snapshot Retention +==== +On-demand snapshots are restorable and exportable for the same period as the total retention for your tier. +==== + + +== Best Practices + +Effective snapshot management requires consistent operational procedures: + +Take an on-demand snapshot before making major database changes. While automatic snapshots run on schedule, a snapshot immediately before a risky operation provides a specific recovery point. + +Test restore procedures regularly. Restore a snapshot to a test instance and verify data integrity. This confirms that recovery processes function correctly before they're needed in production. + +Download snapshots as backup files for critical data. This provides recovery options independent of Aura's infrastructure. + +Use the **Show exportable only** filter in the Snapshots tab to identify downloadable snapshots. Differential backups are not exportable. + +Understand your tier's retention policies, including the difference between restorable and exportable retention periods. Professional tier provides 7 days for both. Higher tiers have different periods for full versus differential backups. + +Create new instances from snapshots for testing or troubleshooting rather than restoring to production instances. This maintains production stability during investigation. + + +[.quiz] +== Check Your Understanding + +include::questions/1-snapshot-schedule.adoc[leveloffset=+1] + + +[.summary] +== Summary + +You learned how to manage snapshots for your Aura instances. + +You learned about automatic snapshot schedules, retention policies, and how to access the Snapshots page in the Aura console. + +In the next lesson, you will learn how to use snapshots to create test environments from production data. + diff --git a/asciidoc/courses/aura-administration/modules/1-backup-restore/lessons/1-snapshot-management/questions/1-snapshot-schedule.adoc b/asciidoc/courses/aura-administration/modules/1-backup-restore/lessons/1-snapshot-management/questions/1-snapshot-schedule.adoc new file mode 100644 index 000000000..0186db37a --- /dev/null +++ b/asciidoc/courses/aura-administration/modules/1-backup-restore/lessons/1-snapshot-management/questions/1-snapshot-schedule.adoc @@ -0,0 +1,33 @@ +[.question] += Snapshot Frequency + +Your team is running an Aura Professional instance with critical business data. + +How frequently are automatic snapshots created for this instance? + +* [ ] Hourly + +* [x] Daily + +* [ ] Weekly + +* [ ] Only on demand + +[TIP,role=hint] +.Hint +==== +Different Aura tiers have different snapshot schedules. Review the snapshot schedules for each tier covered in this lesson. +==== + +[TIP,role=solution] +.Solution +==== +**Daily** is correct. + +Aura Professional instances have automatic daily snapshots with 7-day retention. + +Business Critical and Virtual Dedicated Cloud (latest version) have hourly differential snapshots plus daily full snapshots. Virtual Dedicated Cloud 4.x instances have snapshots every 6 hours. + +Aura Free does not support automatic snapshots - only on-demand snapshots. +==== + diff --git a/asciidoc/courses/aura-administration/modules/1-backup-restore/lessons/2-backup-strategies/lesson.adoc b/asciidoc/courses/aura-administration/modules/1-backup-restore/lessons/2-backup-strategies/lesson.adoc new file mode 100644 index 000000000..8933b8d35 --- /dev/null +++ b/asciidoc/courses/aura-administration/modules/1-backup-restore/lessons/2-backup-strategies/lesson.adoc @@ -0,0 +1,127 @@ += Backup Strategies for Testing +:type: lesson +:order: 3 +:optional: true + + +[.discrete] +== Introduction + +One of the most valuable uses of snapshots is creating test environments that mirror your production data. + +In this lesson, you will learn how to use snapshots to create test instances for development, troubleshooting, and performance testing. + + +== Why Use Production Data for Testing? + +Production data contains the scale, complexity, edge cases, and patterns that synthetic test data cannot replicate. + +Testing with production data enables accurate reproduction of production issues, validation of queries and data model changes under realistic conditions, and measurable performance optimization. Development teams can work with actual data patterns, relationships, and scale. Upgrades and migrations can be tested in conditions that match production. + +[WARNING] +.Data privacy compliance +==== +Production data used in test environments must comply with data privacy regulations including GDPR, CCPA, and industry-specific requirements. + +If production data contains personally identifiable information (PII), customer data, or other sensitive information, anonymize it before using it for testing. +==== + + +== Creating Test Instances from Snapshots + +You can create a new instance from any exportable snapshot: + +. Navigate to your Aura instance card in the console +. Click the more menu icon (**…**) on the instance card +. Select **Inspect** +. Select the **Snapshots** tab +. Find the snapshot you want to use +. Click the more menu (**…**) next to that snapshot +. Select **Create instance from snapshot** +. Configure the new instance settings: + * Instance name + * Instance size (can be smaller than production for testing) + * Region + * Cloud provider + +. Review and create the instance +. Download the credentials for the new instance + +This creates an independent instance with a copy of your data at the snapshot time. + + +[NOTE] +.Exportable Snapshots Only +==== +You can only create instances from exportable snapshots. + +Use the **Show exportable only** toggle to filter the snapshot list. + +Remember that differential backups cannot be used to create new instances. +==== + + +== Managing Test Instances + +Test instances require management to control costs and maintain organization. + +Use clear naming conventions. Names like `test-prod-copy-2024-11-07` identify the instance type, source, and creation date, simplifying inventory management and deletion decisions. + +Test instances can be smaller than production instances. Unless testing performance at production scale, a smaller instance reduces costs without affecting test validity. + +Delete test instances when testing is complete. Retaining unnecessary instances increases costs. Snapshots remain available if the instance is needed again. + +Document the source snapshot and creation date for each test instance. This information is necessary for debugging and understanding the data state. + + +== Testing Scenarios + +Common scenarios for using production snapshots: + +=== Troubleshooting Production Issues + +. Create a test instance from a recent snapshot +. Reproduce the issue in the test environment +. Test potential fixes without affecting production +. Deploy the verified fix to production + +=== Performance Testing + +. Create a test instance from a production snapshot +. Run performance tests with production data volumes +. Test query optimizations and index changes +. Validate improvements before applying to production + +=== Data Model Changes + +. Create a test instance from a production snapshot +. Test schema changes and data migrations +. Validate data integrity after changes +. Document the migration procedure for production deployment + + +== Cost Considerations + +Test instances incur the same billing costs as production instances. + +Choose instance sizes appropriate for testing requirements. Smaller instances reduce costs when production-scale performance testing is not required. + +Delete instances immediately when testing is complete. Idle test instances generate ongoing costs. + +Use time-boxed testing periods with defined start and end dates. This establishes accountability for instance cleanup. + +Plan test schedules around your tier's snapshot retention periods. Professional tier retention is 7 days. Ensure snapshots remain available for the planned testing window. + + +read::Continue to next lesson[] + + +[.summary] +== Summary + +You learned how to use snapshots to create test environments from production data. + +You learned how to create new instances from exportable snapshots and best practices for managing test instances while controlling costs. + +In the next challenge, you will apply this knowledge to recover from an accidental data deletion scenario. + diff --git a/asciidoc/courses/aura-administration/modules/1-backup-restore/lessons/2-snapshot-challenge/lesson.adoc b/asciidoc/courses/aura-administration/modules/1-backup-restore/lessons/2-snapshot-challenge/lesson.adoc new file mode 100644 index 000000000..cc74b95c7 --- /dev/null +++ b/asciidoc/courses/aura-administration/modules/1-backup-restore/lessons/2-snapshot-challenge/lesson.adoc @@ -0,0 +1,30 @@ += Snapshot Strategy +:type: challenge +:order: 2 + + +== Scenario + +You are the database administrator for an e-commerce platform running on Aura Professional. + +The development team has reported that they accidentally deleted critical product category data in production at 2:15 PM today. + +Your instance has automatic daily snapshots enabled with 7-day retention. + +The last successful snapshot was taken at 1:00 AM this morning, before the accidental deletion occurred. + + +include::questions/1-snapshot-recovery.adoc[leveloffset=+1] + + +[.summary] +== Summary + +When accidental data deletion occurs, the best approach is to create a new instance from the most recent snapshot before the incident. + +This preserves your current state while giving you a recovery option. + +For Business Critical tier with hourly differential snapshots, you would have backups much closer to the incident time, minimizing data loss. + +This scenario highlights the importance of understanding your snapshot schedule and retention policy for your tier. + diff --git a/asciidoc/courses/aura-administration/modules/1-backup-restore/lessons/2-snapshot-challenge/questions/1-snapshot-recovery.adoc b/asciidoc/courses/aura-administration/modules/1-backup-restore/lessons/2-snapshot-challenge/questions/1-snapshot-recovery.adoc new file mode 100644 index 000000000..1a9d235e4 --- /dev/null +++ b/asciidoc/courses/aura-administration/modules/1-backup-restore/lessons/2-snapshot-challenge/questions/1-snapshot-recovery.adoc @@ -0,0 +1,33 @@ +[.question] += Recovering from Data Deletion + +What is the best course of action to recover from this situation? + +* [ ] Wait for tonight's automatic snapshot and restore from that + +* [x] Create a new instance from the 1:00 AM snapshot and manually replay changes + +* [ ] Upgrade to Business Critical tier for hourly snapshots first + +* [ ] Ask developers to recreate the deleted data manually + +[TIP,role=hint] +.Hint +==== +Consider what data you have available right now in your snapshots, and what actions you can take immediately to restore service. +==== + +[TIP,role=solution] +.Solution +==== +**Create a new instance from the 1:00 AM snapshot and manually replay changes** is correct. + +This is the best immediate action because you have a snapshot from before the deletion (1:00 AM), creating a new instance preserves your current production instance, you can compare the two instances to identify what was deleted, you can then manually restore the missing data to production, and this minimizes risk while giving you recovery options. + +Why the alternatives don't work: Waiting for tonight's snapshot means 24+ hours of downtime (unacceptable), upgrading tiers doesn't help recover already-deleted data, and manual recreation is error-prone and time-consuming without a reference. + +**Note**: The new instance will have data as it was at 1:00 AM. Changes made between 1:00 AM and 2:15 PM (13 hours) won't be in that snapshot. You may need to replay those changes from application logs. + +For future prevention, consider upgrading to Business Critical tier for hourly snapshots if this type of incident poses significant business risk. +==== + diff --git a/asciidoc/courses/aura-administration/modules/1-backup-restore/lessons/3-restore-operations/lesson.adoc b/asciidoc/courses/aura-administration/modules/1-backup-restore/lessons/3-restore-operations/lesson.adoc new file mode 100644 index 000000000..1aefb8072 --- /dev/null +++ b/asciidoc/courses/aura-administration/modules/1-backup-restore/lessons/3-restore-operations/lesson.adoc @@ -0,0 +1,142 @@ += Restore Operations +:type: lesson +:order: 5 +:optional: true + + +[.discrete] +== Introduction + +Despite best efforts, situations arise where you need to restore data from a backup. + +In this lesson, you will learn how to perform restore operations using snapshots and backup files, and understand the implications of restoring data. + + +== When to Restore from Backup + +Common scenarios requiring restoration: + +* **Accidental data deletion**: User error deleted critical data +* **Corrupted data**: Data integrity issues require reverting to a known good state +* **Failed migration**: A data migration or schema change caused problems +* **Security incident**: Malicious activity compromised data integrity +* **Testing rollback procedures**: Validating your disaster recovery plan + + +== Restore Options + +Aura provides two primary restore methods: + + +=== Option 1: Restore from Snapshot + +To restore your instance from a snapshot: + +. Navigate to your Aura instance card in the console +. Click the more menu icon (**…**) on the instance card +. Select **Inspect** +. Select the **Snapshots** tab +. Find the snapshot you want to restore from +. Click the **arrow icon** next to the more menu (**…**) +. Confirm the restore operation + +[WARNING] +.Data will be overwritten +==== +Restoring from a snapshot **overwrites all data** in your instance, replacing it with the data contained in the snapshot. + +You will be asked to confirm this action. + +If you don't want to overwrite your current data, create a new instance from the snapshot instead (see previous lesson). +==== + + +=== Option 2: Restore from Backup File + +You can upload a previously exported backup file: + +. Navigate to your Aura instance card in the console +. Open the instance details +. Select the **Restore from backup file** tab (next to the Snapshots tab) +. Drag and drop your backup file or browse for it: + * **AuraDB instances**: `.backup` file (latest version) or `.dump` file (version 4.x) + * **AuraDS instances**: `.tar` file + +. Wait for the restore operation to complete + +This option is useful when: + +* Restoring from a downloaded/exported snapshot +* Migrating data from another Neo4j instance +* Restoring from an offline backup + + +[WARNING] +.Data will be overwritten +==== +Uploading a backup file also **overwrites all data** currently in your instance. + +If you don't want to overwrite your data, create a new instance instead. +==== + + +== Database Upload Command + +If you have a local Neo4j database installation, you can use the `neo4j-admin database upload` command to upload database contents to your Aura instance. + +This command works for databases of any size, as long as they fit your Aura instance. + +[source,bash] +---- +neo4j-admin database upload \ + --from-path=/path/to/database \ + --to-uri=neo4j+s://xxxxx.databases.neo4j.io \ + --to-user=neo4j \ + --to-password=your-password +---- + +[IMPORTANT] +.Network Access Requirements +==== +The `database upload` command requires that your Aura instance is accessible from the machine running the command. + +If you have network access configurations that prevent public traffic, you must temporarily enable public traffic for the region where your instance is hosted. + +See the Aura documentation for details on network access configuration. +==== + + +== Post-Restore Verification + +After a restore operation, verify: + +* **Data integrity**: Run queries to confirm expected data is present +* **Application connectivity**: Ensure applications can connect +* **Index status**: Check that all indexes are available +* **Recent changes**: Document any data loss from the restore point +* **Application behavior**: Test critical application workflows + + +== Best Practices for Restore Operations + +* **Create a test instance first** to verify the snapshot before restoring to production +* **Take an on-demand snapshot** before restoring (gives you a way back) +* **Communicate with stakeholders** before restoring production data +* **Understand data loss implications** - you will lose all changes made after the snapshot time +* **Test restore procedures** regularly in non-production environments +* **Document restore procedures** step-by-step for your organization +* **Time restore operations** during maintenance windows when possible + + +read::Continue to next lesson[] + + +[.summary] +== Summary + +You learned how to perform restore operations for your Aura instances. + +You learned about restoring from snapshots, uploading backup files, using the `neo4j-admin database upload` command, and best practices for performing restores safely. + +In the next challenge, you will apply this knowledge to decide on the best restore approach for a production incident. + diff --git a/asciidoc/courses/aura-administration/modules/1-backup-restore/lessons/4-backup-challenge/lesson.adoc b/asciidoc/courses/aura-administration/modules/1-backup-restore/lessons/4-backup-challenge/lesson.adoc new file mode 100644 index 000000000..dc4c24352 --- /dev/null +++ b/asciidoc/courses/aura-administration/modules/1-backup-restore/lessons/4-backup-challenge/lesson.adoc @@ -0,0 +1,32 @@ += Creating Test Environments +:type: challenge +:order: 4 + + +== Scenario + +Your development team is planning to implement a major data model change that will affect millions of nodes in production. + +They need to test the migration script with real production data volumes to ensure performance is acceptable and the script works correctly. + +Your production instance has 50GB of data and receives continuous updates throughout the day. + +The team wants to run multiple test iterations over the next week. + + +include::questions/1-test-environment.adoc[leveloffset=+1] + + +[.summary] +== Summary + +For iterative testing scenarios, creating a test instance from a snapshot provides the most cost-effective approach. + +Key considerations: + +* Create instances from recent snapshots for realistic data +* Size appropriately (test instances can be smaller than production) +* Delete instances promptly when testing is complete to control costs +* Document which snapshot was used for reproducibility +* Consider data privacy when using production data for testing + diff --git a/asciidoc/courses/aura-administration/modules/1-backup-restore/lessons/4-backup-challenge/questions/1-test-environment.adoc b/asciidoc/courses/aura-administration/modules/1-backup-restore/lessons/4-backup-challenge/questions/1-test-environment.adoc new file mode 100644 index 000000000..aed55dd82 --- /dev/null +++ b/asciidoc/courses/aura-administration/modules/1-backup-restore/lessons/4-backup-challenge/questions/1-test-environment.adoc @@ -0,0 +1,35 @@ +[.question] += Test Environment Strategy + +What is the best course of action to provide a test environment for this scenario? + +* [ ] Create a production-sized instance from a snapshot and leave it running for the week +* [x] Create a smaller test instance from a snapshot and delete it when testing is complete +* [ ] Give developers access to production for testing +* [ ] Create a new instance and manually import test data + +[TIP,role=hint] +.Hint +==== +Consider both the data volume needs and how to minimize costs during a week-long testing period with intermittent usage. +==== + +[TIP,role=solution] +.Solution +==== +**Create a smaller test instance from a snapshot and delete it when testing is complete** is correct. + +This is the most cost-effective approach because: **Create from snapshot** gets real production data (50GB), **Smaller instance** means test instances can be smaller than production (adequate for testing performance and functionality), **Delete when done** means you only pay for the instance while actively testing, and **Recreate as needed** means you can create new test instances from snapshots for additional test iterations. + +Cost comparison for 1 week: + +- Full-size instance running continuously: $$$$ +- Smaller instance, deleted after testing: $-$$ +- Production access: Free but EXTREMELY DANGEROUS +- Manual data import: Time-consuming and data may not reflect production characteristics + +Why the alternatives have drawbacks: Full-size instance running continuously is unnecessarily expensive, production testing risks data corruption and user impact (NEVER do this), and manual import is time-consuming and may not replicate production data patterns. + +**Best practices**: Use clear naming (e.g., `test-migration-2024-11-07`), choose an appropriate smaller size for testing (doesn't need to match production), consider anonymizing sensitive data if required by compliance, delete the instance promptly when testing is complete, and document which snapshot was used for the test instance. +==== + diff --git a/asciidoc/courses/aura-administration/modules/1-backup-restore/lessons/6-restore-challenge/lesson.adoc b/asciidoc/courses/aura-administration/modules/1-backup-restore/lessons/6-restore-challenge/lesson.adoc new file mode 100644 index 000000000..fab67153e --- /dev/null +++ b/asciidoc/courses/aura-administration/modules/1-backup-restore/lessons/6-restore-challenge/lesson.adoc @@ -0,0 +1,41 @@ += Restore Decision +:type: challenge +:order: 6 + + +== Scenario + +At 3:00 PM on Friday, your monitoring alerts show that failed queries have spiked from 0.1% to 45% of all queries over the past hour. + +Investigation reveals that a deployment at 2:00 PM introduced a data migration that corrupted relationship properties affecting millions of edges. + +Your application is currently experiencing severe issues and users cannot complete critical business operations. + +You have: + +* Automatic snapshots from 1:00 AM today (before the deployment) +* The ability to restore from snapshot (requires 30-45 minutes downtime) +* A development team that says they "might" be able to fix the data corruption with a script in 2-3 hours + + + + +include::questions/1-restore-decision.adoc[leveloffset=+1] + + +[.summary] +== Summary + +In production incidents affecting users, restoring from a known good backup is often the fastest path to recovery. + +Key decision factors: + +* **Severity**: 45% query failure rate is severe and affecting users +* **Recovery time**: Restore takes 45 minutes (known) vs. fix takes 2-3 hours (estimated) +* **Risk**: Restore is proven vs. fix script is uncertain +* **Data loss**: 13 hours of data needs to be replayed or accepted as lost + +Best practice: Restore to recover service, then investigate how to replay lost changes from logs or application records. + +Always communicate with stakeholders about data loss and coordinate before executing the restore. + diff --git a/asciidoc/courses/aura-administration/modules/1-backup-restore/lessons/6-restore-challenge/questions/1-restore-decision.adoc b/asciidoc/courses/aura-administration/modules/1-backup-restore/lessons/6-restore-challenge/questions/1-restore-decision.adoc new file mode 100644 index 000000000..9d0adfc6b --- /dev/null +++ b/asciidoc/courses/aura-administration/modules/1-backup-restore/lessons/6-restore-challenge/questions/1-restore-decision.adoc @@ -0,0 +1,45 @@ +[.question] += Production Incident Response + +What is the best course of action to respond to this production incident? + +* [ ] Wait for the development team to create a fix script + +* [x] Restore from 1:00 AM snapshot to recover service immediately + +* [ ] Roll back the application deployment and investigate + +* [ ] Contact Neo4j support for guidance + +[TIP,role=hint] +.Hint +==== +Consider the severity (45% query failure), the certainty of each option, and the business impact of extended downtime. +==== + +[TIP,role=solution] +.Solution +==== +**Restore from 1:00 AM snapshot to recover service immediately** is correct. + +This is the best immediate action because: + +**Severity**: 45% query failure is critical - users can't work. **Certainty**: Restore takes 30-45 minutes (known time), while a "fix script" might work in "2-3 hours" (uncertain). **Business impact**: Friday afternoon means you need service restored ASAP. + +**Restore timeline**: Start restore at 3:00 PM, complete restore at 3:30-3:45 PM, and service is restored with data from 1:00 AM. + +**Trade-off**: Lose 13 hours of data (1:00 AM to 2:00 PM). + +**After restore**: Service is back online, users can work, the team can investigate how to replay lost transactions from logs, and you can deploy a fixed version in a controlled manner. + +Why waiting or rolling back the application won't work: Waiting 2-3 hours (maybe) means users suffer longer, app rollback doesn't fix already-corrupted data, and support can't fix this immediately. + +**Best practice**: In production incidents with severe user impact: +1. Restore service first (restore from backup) +2. Accept data loss if necessary +3. Investigate replay/recovery options after service is restored +4. Document and communicate data loss to stakeholders + +**Key principle**: Known, fast recovery > uncertain, slow fixes during outages. +==== + diff --git a/asciidoc/courses/aura-administration/modules/1-backup-restore/module.adoc b/asciidoc/courses/aura-administration/modules/1-backup-restore/module.adoc new file mode 100644 index 000000000..49d161085 --- /dev/null +++ b/asciidoc/courses/aura-administration/modules/1-backup-restore/module.adoc @@ -0,0 +1,30 @@ += Backup and Restore +:order: 1 + + +== Module Overview + +Data protection is critical for production databases. + +In this module, you will learn how to create and manage backups for your Aura instances, and how to restore data when needed. + +You will also discover how to use backups to create test environments from production data. + + +You will learn how to: + +* Understand scheduled and on-demand snapshots + +* Identify retention policies for different Aura tiers + +* Export snapshots as backup files + +* Restore data from snapshots and backup files + +* Create test instances from production snapshots + +* Use the `neo4j-admin database upload` command + + +link:./1-snapshot-management/[Ready? Let's go →, role=btn] + diff --git a/asciidoc/courses/aura-administration/modules/2-monitoring-resources/lessons/1-metrics-overview/lesson.adoc b/asciidoc/courses/aura-administration/modules/2-monitoring-resources/lessons/1-metrics-overview/lesson.adoc new file mode 100644 index 000000000..7ec884322 --- /dev/null +++ b/asciidoc/courses/aura-administration/modules/2-monitoring-resources/lessons/1-metrics-overview/lesson.adoc @@ -0,0 +1,135 @@ += Metrics Dashboard Overview +:type: lesson +:order: 1 + + +[.discrete] +== Introduction + +The Metrics dashboard is your primary tool for monitoring the health and performance of your Aura instances. + +In this lesson, you will learn how to navigate the Metrics dashboard and understand the three main monitoring categories: Resources, Instance, and Database. + + +== Accessing the Metrics Dashboard + +To access the Metrics dashboard: + +. Navigate to your Aura instance in the console +. Click on the instance name +. Select the **Metrics** tab + +The Metrics dashboard provides real-time and historical data about your instance's performance. + + +== Dashboard Layout + +The Metrics dashboard is organized into three tabs, each displaying different categories of metrics. + +The **Resources** tab displays high-level resource utilization: CPU, storage, and query rate. Use this tab for capacity health checks. + +The **Instance** tab displays Neo4j instance-level metrics: memory usage, connection pools, and garbage collection. Use this tab to evaluate Neo4j performance. + +The **Database** tab displays database-specific metrics: store size, transaction counts, and query latency percentiles. Use this tab for query performance analysis and data growth monitoring. + +Each tab contains multiple time-series charts. Switch between tabs to investigate issues or track trends. + + +== Dashboard Controls + +The Metrics dashboard includes several controls for customizing your view: + + +=== Date Range Filter + +The date range filter lets you zoom in or out on your metrics timeline. You can choose from preset ranges - 15 minutes, 1 hour, 6 hours, 24 hours, 7 days, or 30 days - or define a custom date range if you need to look at a specific period. + +[TIP] +.Choosing the right time range +==== +Time range selection depends on the monitoring objective. + +For troubleshooting active issues, use shorter ranges (15 minutes to 1 hour). This provides real-time visibility with sufficient detail to identify patterns. + +For trend analysis and capacity planning, use longer ranges (7 to 30 days). This identifies growth trends, recurring patterns (daily or weekly spikes), and capacity adequacy. +==== + + +=== Refresh + +The refresh button updates all metrics with the latest data. + +Metrics are automatically refreshed, but you can manually refresh for immediate updates when troubleshooting issues. + + +=== Detail View + +Detail view splits chart bars by availability zone, showing metrics for each region separately (e.g., `us-east-1a`, `us-east-1b`). + +This enables identification of region-specific issues. If one availability zone shows degraded performance while others remain normal, this indicates infrastructure rather than application problems. Detail view also shows cluster member performance and geographic workload distribution. + + +== Understanding Metric Charts + +Each metric chart displays time series data (values over the selected time period), the current value (most recent measurement), the metric name, and the unit of measurement (cores, bytes, percentage, etc.). + + +=== Reading Time Series Charts + +The dashboard uses different chart types for different metrics. Line charts show trends over time (e.g., CPU usage). Area charts display cumulative metrics (e.g., storage). Bar charts show discrete values (e.g., transaction counts per minute). + +Chart type affects data interpretation. Interpret spikes according to the visualization method. + + +== When to Check Metrics + +Establish a regular metrics review schedule: + +**Daily**: Perform health checks on production instances to identify issues before they escalate. + +**Before/after changes**: Compare metrics when making configuration changes to measure impact. + +**During incidents**: Use metrics to identify root causes of performance issues. + +**For capacity planning**: Analyze trends over time to predict future capacity requirements and identify growth patterns. + + +[NOTE] +.Tier-specific metrics +==== +Not all metrics are available for all Aura tiers. + +Some advanced metrics require Aura Professional or higher. +==== + + +== Interpreting Metrics + +Metric interpretation focuses on identifying four types of changes: + +**Spikes**: Sudden increases indicating potential issues. CPU spikes may indicate inefficient queries. Failed query spikes indicate active user-facing errors. + +**Trends**: Gradual changes over time, such as storage filling up or query latency increasing. Trends provide advance warning before capacity limits are reached. + +**Patterns**: Recurring behaviors specific to the workload, such as daily peaks during business hours or weekly spikes. Understanding normal patterns prevents false alarms. + +**Anomalies**: Unexpected values or behaviors that deviate from established patterns. Anomalies typically indicate issues requiring investigation. + +Establish baseline metrics for your workload. Normal behavior varies between systems. + + +[.quiz] +== Check Your Understanding + +include::questions/1-dashboard-navigation.adoc[leveloffset=+1] + + +[.summary] +== Summary + +You learned how to navigate the Metrics dashboard for your Aura instances. + +You learned about the three main monitoring categories (Resources, Instance, Database) and how to use dashboard controls to customize your view. + +In the next lesson, you will learn how to monitor CPU usage and identify performance bottlenecks. + diff --git a/asciidoc/courses/aura-administration/modules/2-monitoring-resources/lessons/1-metrics-overview/questions/1-dashboard-navigation.adoc b/asciidoc/courses/aura-administration/modules/2-monitoring-resources/lessons/1-metrics-overview/questions/1-dashboard-navigation.adoc new file mode 100644 index 000000000..a27b1dfd2 --- /dev/null +++ b/asciidoc/courses/aura-administration/modules/2-monitoring-resources/lessons/1-metrics-overview/questions/1-dashboard-navigation.adoc @@ -0,0 +1,31 @@ +[.question] += Metrics Dashboard Organization + +You need to check the query latency percentiles for your Aura instance. + +Which Metrics dashboard tab contains query latency metrics? + +* [ ] Resources + +* [ ] Instance + +* [x] Database + +* [ ] Operations + +[TIP,role=hint] +.Hint +==== +The Metrics dashboard is organized into three tabs, each focusing on a different level of monitoring. Query latency is a database-level metric. +==== + +[TIP,role=solution] +.Solution +==== +**Database** is correct. + +The Metrics dashboard has three tabs: **Resources** (high-level metrics like CPU, storage, and query rate), **Instance** (Neo4j instance-level metrics like memory, connections, and GC), and **Database** (database-specific metrics including store size, transactions, and query latency). + +Query latency percentiles (50th, 75th, 99th) are database-level metrics found in the Database tab. +==== + diff --git a/asciidoc/courses/aura-administration/modules/2-monitoring-resources/lessons/2-cpu-usage/lesson.adoc b/asciidoc/courses/aura-administration/modules/2-monitoring-resources/lessons/2-cpu-usage/lesson.adoc new file mode 100644 index 000000000..4dc0f153a --- /dev/null +++ b/asciidoc/courses/aura-administration/modules/2-monitoring-resources/lessons/2-cpu-usage/lesson.adoc @@ -0,0 +1,152 @@ += Monitoring CPU Usage +:type: lesson +:order: 3 + + +[.discrete] +== Introduction + +CPU is a critical resource for database performance, used for planning and executing queries. + +In this lesson, you will learn how to monitor CPU usage, identify bottlenecks, and determine when to scale your instance. + + +== Understanding CPU Metrics + +The CPU Usage metric shows the number of CPU cores being used by your instance. + +// UI Description: CPU usage (cores). CPU is used for planning and serving queries. +// If this metric is constantly spiking or at its limits, consider increasing the size of your instance. + +CPU is consumed by: + +* Query planning and execution +* Index operations +* Transaction processing +* Data compaction +* Backup operations + + +== Reading the CPU Usage Chart + +The CPU Usage chart displays: + +* **Current usage**: Number of cores currently in use +* **Usage over time**: Historical CPU consumption +* **Available cores**: Total cores allocated to your instance (varies by tier and size) + + +=== Normal CPU Usage Patterns + +Normal CPU patterns include a **steady baseline** from background operations and regular queries, **periodic peaks** from batch jobs or scheduled tasks, **gradual increases** as workload grows over time, and **sudden spikes** from large queries or unexpected load. Identifying these patterns distinguishes normal behavior from issues. + + +== Identifying CPU Issues + +Watch for these warning signs: + + +=== Constantly High CPU + +When CPU usage remains consistently near or at instance limits, this indicates a capacity problem. Queries slow down or time out, indicating either an under-provisioned instance or inefficient queries. + +**Action required**: Investigate query patterns to determine the cause before scaling the instance. + + +=== Frequent Spikes + +Regular CPU spikes typically indicate periodic batch operations, repeatedly executed inefficient queries, or lock contention. + +**Action required**: Review query logs during spike periods to identify problematic operations. + + +=== Sustained 100% Usage + +Sustained 100% CPU usage causes query failures or timeouts, degrades transaction processing, and indicates the system has reached capacity limits. + +**Action required**: Scale the instance immediately or identify and stop problematic queries. + + +== When to Scale Your Instance + +Consider increasing instance size when CPU usage consistently exceeds 70-80%, query performance remains degraded after optimization, peak usage regularly hits 100%, or growth trends indicate future capacity issues. + +[TIP] +.Proactive scaling +==== +Proactive scaling based on trends prevents performance degradation. Reactive scaling addresses problems after users are affected. + +Monitor metrics over weeks and months. Scale when trends indicate approaching capacity limits rather than waiting for 100% utilization. +==== + + +== Optimizing CPU Usage + +Before scaling, consider these optimization strategies: + + +=== Query Optimization + +* Review query logs for slow or frequent queries +* Add appropriate indexes to speed up queries +* Use query parameters to enable query plan caching +* Avoid Cartesian products and full graph scans + + +=== Workload Management + +* Schedule batch jobs during off-peak hours +* Distribute load across multiple queries instead of large single queries +* Implement connection pooling in applications +* Use read replicas for read-heavy workloads (Business Critical tier) + + +=== Application Changes + +* Cache frequently accessed data at the application level +* Implement pagination for large result sets +* Reduce query frequency when possible +* Use background jobs for non-urgent operations + + +== Monitoring CPU for Different Workloads + +Different workload types have different CPU patterns: + + +=== Read-Heavy Workloads + +* Lower CPU usage overall +* Spikes during complex traversals +* Benefit from page cache optimization + + +=== Write-Heavy Workloads + +* Higher CPU usage for transaction processing +* CPU peaks during index updates +* May benefit from batch write operations + + +=== Mixed Workloads + +* Variable CPU patterns +* May have competing resource needs +* Require careful monitoring and balancing + + +[.quiz] +== Check Your Understanding + +include::questions/1-cpu-action.adoc[leveloffset=+1] + + +[.summary] +== Summary + +You learned how to monitor CPU usage for your Aura instances. + +You learned to identify normal CPU patterns, recognize warning signs, and determine when to scale your instance or optimize queries. + +In the next lesson, you will learn how to monitor storage consumption and query rates. + diff --git a/asciidoc/courses/aura-administration/modules/2-monitoring-resources/lessons/2-cpu-usage/questions/1-cpu-action.adoc b/asciidoc/courses/aura-administration/modules/2-monitoring-resources/lessons/2-cpu-usage/questions/1-cpu-action.adoc new file mode 100644 index 000000000..5c07cfd01 --- /dev/null +++ b/asciidoc/courses/aura-administration/modules/2-monitoring-resources/lessons/2-cpu-usage/questions/1-cpu-action.adoc @@ -0,0 +1,33 @@ +[.question] += High CPU Usage Response + +You notice your Aura instance CPU usage has been consistently at 90-95% for the past 3 hours during normal business operations. + +What should you do first? + +* [ ] Wait and monitor for another day to confirm it's not temporary + +* [ ] Restart the instance to clear any issues + +* [x] Review query logs to identify resource-intensive queries, then consider scaling + +* [ ] Immediately scale up the instance without investigation + +[TIP,role=hint] +.Hint +==== +When CPU is consistently high, you need to understand the cause before taking action. Consider what information would help you make an informed decision. +==== + +[TIP,role=solution] +.Solution +==== +**Review query logs to identify resource-intensive queries, then consider scaling** is correct. + +This is the best approach because 90-95% sustained usage indicates the instance is under-provisioned or queries are inefficient, query logs will show if specific queries are consuming excessive CPU, you may be able to optimize queries instead of scaling, and understanding the root cause ensures the right fix. + +Why the alternatives are less effective: Waiting another day prolongs poor performance for users, restarting doesn't address the underlying cause, and scaling without investigation might be unnecessary if queries can be optimized. + +After reviewing query logs, you may find you can optimize problematic queries, or you may confirm that scaling is needed for the workload. +==== + diff --git a/asciidoc/courses/aura-administration/modules/2-monitoring-resources/lessons/3-storage-query-rate/lesson.adoc b/asciidoc/courses/aura-administration/modules/2-monitoring-resources/lessons/3-storage-query-rate/lesson.adoc new file mode 100644 index 000000000..8fb6070d1 --- /dev/null +++ b/asciidoc/courses/aura-administration/modules/2-monitoring-resources/lessons/3-storage-query-rate/lesson.adoc @@ -0,0 +1,206 @@ += Storage and Query Rate +:type: lesson +:order: 5 + + +[.discrete] +== Introduction + +Storage and query rate metrics help you understand database growth and workload patterns. + +In this lesson, you will learn how to monitor storage consumption and query rates to identify trends and plan for future capacity. + + +== Monitoring Storage + +The Storage metric shows the total disk space used by your instance. + +Storage is consumed by: + +* Node and relationship data +* Property values +* Indexes (especially full-text and vector indexes) +* Transaction logs +* Backup data + + +=== Understanding Storage Growth + +Monitor storage for: + +* **Linear growth**: Steady data addition over time +* **Sudden jumps**: Large data imports or batch operations +* **Plateaus**: Stable data size +* **Storage reduction**: After data deletion or compaction + +[NOTE] +.Allocated vs. used space +==== +Storage metrics may show a difference between **allocated space** and **used space**. + +This is normal and covered in detail in the Database monitoring module. +==== + + +=== Storage Considerations by Data Type + +Different data types have different storage impacts: + +* **Nodes and relationships**: Relatively small +* **Properties**: Variable size based on data types +* **Long strings**: Can consume significant space +* **Vector embeddings**: Very storage-intensive (hundreds of bytes per vector) +* **Full-text indexes**: Additional storage for text analysis + + +=== When to Act on Storage + +Consider action when: + +* Storage growth is faster than expected +* Storage is approaching instance limits +* Large difference between allocated and used space (covered in Module 4) +* Storage costs are becoming significant + + +== Query Rate Metrics + +The Query Rate metric shows the number of queries executed per minute. + +This metric helps you understand: + +* Workload intensity +* Usage patterns over time +* Impact of application changes +* Capacity requirements + + +=== Reading Query Rate Charts + +The Query Rate chart displays: + +* **Queries per minute**: Total query count +* **Rate over time**: Historical query patterns +* **Peak rates**: Maximum query load + + +=== Normal Query Rate Patterns + +Typical patterns include: + + +==== Business Hours Pattern + +* High query rates during business hours +* Low rates overnight and weekends +* Predictable daily cycles + + +==== Steady State + +* Consistent query rate 24/7 +* May indicate automated systems or global user base +* Easier to plan capacity + + +==== Batch Processing Pattern + +* Low baseline with regular spikes +* Spikes correspond to scheduled jobs +* Requires capacity for peak loads + + +==== Growth Trend + +* Gradually increasing query rates over weeks/months +* Indicates growing usage +* Requires capacity planning + + +== Correlating Storage and Query Rate + +Understanding the relationship between storage and query rate helps you: + +* **Predict performance**: Larger databases may need more resources for same query rate +* **Plan scaling**: Growing data and query volume both drive capacity needs +* **Optimize costs**: Balance instance size with actual usage patterns + + +=== Example Scenarios + +**High query rate, stable storage**: + +* Read-heavy workload +* May benefit from caching +* Page cache optimization is important + + +**Growing storage, stable query rate**: + +* Write-heavy workload +* May need storage optimization +* Consider data archival strategies + + +**Both growing together**: + +* Typical of growing application +* Proactive scaling needed +* Monitor trends closely + + +== Taking Action + +Based on storage and query rate metrics: + + +=== For High Storage Growth + +* Review data model for efficiency +* Identify and remove unnecessary data +* Implement data retention policies +* Consider database compaction +* Plan instance upgrades for additional capacity + + +=== For High Query Rates + +* Optimize frequently executed queries +* Implement application-level caching +* Use connection pooling +* Consider read replicas (Business Critical tier) +* Scale instance for more resources + + +=== For Both + +* Evaluate instance tier and size +* Implement comprehensive optimization strategy +* Plan for continued growth +* Consider architecture changes + + +[TIP] +.Setting up alerts +==== +Set up monitoring alerts when storage or query rates exceed thresholds. + +Use external monitoring tools (covered in Module 6) for automated alerting. +==== + + +[.quiz] +== Check Your Understanding + +include::questions/1-storage-query.adoc[leveloffset=+1] + + +[.summary] +== Summary + +You learned how to monitor storage consumption and query rates for your Aura instances. + +You learned to identify normal patterns, understand the relationship between metrics, and determine when action is needed. + +In the next module, you will learn about instance-level performance metrics including memory usage, page cache, and garbage collection. + diff --git a/asciidoc/courses/aura-administration/modules/2-monitoring-resources/lessons/3-storage-query-rate/questions/1-storage-query.adoc b/asciidoc/courses/aura-administration/modules/2-monitoring-resources/lessons/3-storage-query-rate/questions/1-storage-query.adoc new file mode 100644 index 000000000..dddc597e0 --- /dev/null +++ b/asciidoc/courses/aura-administration/modules/2-monitoring-resources/lessons/3-storage-query-rate/questions/1-storage-query.adoc @@ -0,0 +1,33 @@ +[.question] += Storage and Query Rate Analysis + +You're analyzing metrics and notice: + +1. Storage has grown steadily from 200GB to 500GB over 3 months +2. Query rate has remained stable at 1,000 queries/minute + +What does this pattern indicate? + +* [ ] The instance is misconfigured and needs attention +* [x] Write-heavy workload with stable read patterns +* [ ] Query performance is degrading +* [ ] Database compaction is urgently needed + +[TIP,role=hint] +.Hint +==== +Consider what causes storage to grow while query rate stays the same. What types of operations add data without increasing query volume? +==== + +[TIP,role=solution] +.Solution +==== +**Write-heavy workload with stable read patterns** is correct. + +This pattern indicates data is being added regularly (storage growing) while read query volume hasn't changed (stable query rate). This is typical of applications with continuous data ingestion, such as logging systems, IoT data collection, or event streaming. This is a normal, healthy pattern for certain workload types. + +Why the other answers are wrong: Nothing indicates misconfiguration (this is normal for some applications), query performance issues would show in latency metrics not query rate, and compaction is only needed when there's a large gap between allocated and used space, which isn't indicated here. + +**Action:** Monitor trends to plan for future storage scaling as data continues to grow. +==== + diff --git a/asciidoc/courses/aura-administration/modules/2-monitoring-resources/lessons/4-cpu-challenge/lesson.adoc b/asciidoc/courses/aura-administration/modules/2-monitoring-resources/lessons/4-cpu-challenge/lesson.adoc new file mode 100644 index 000000000..5810ed911 --- /dev/null +++ b/asciidoc/courses/aura-administration/modules/2-monitoring-resources/lessons/4-cpu-challenge/lesson.adoc @@ -0,0 +1,47 @@ += CPU Crisis +:type: challenge +:order: 4 + + +== Scenario + +It's Monday morning at 9:15 AM, and you receive alerts that the application is experiencing severe slowness. + +Checking the Metrics dashboard, you observe: + +* **CPU Usage**: Spiked to 8 cores (100% of available) at 9:00 AM and has stayed there +* **Query Rate**: 3,000 queries/minute (normal peak is 1,200) +* **Failed Queries**: 15% of queries failing (normally <0.1%) + +Looking at query logs filtered for "last 15 minutes": + +* A new query pattern started at 9:00 AM: `MATCH (u:User) MATCH (p:Product) WHERE u.region = p.region RETURN count(*)` +* This query is being executed 500+ times per minute +* Each execution takes 2,000-5,000ms (normally queries take 10-50ms) +* The query was introduced in a deployment Friday evening + + +include::questions/1-cpu-crisis.adoc[leveloffset=+1] + + +[.summary] +== Summary + +When a specific query is causing resource exhaustion, the fastest recovery is to stop the problematic application behavior. + +Why this is the best approach: + +* **Immediate relief**: Stopping the queries frees CPU resources within seconds +* **No downtime**: Instance remains available for other operations +* **Preserves data**: No risk of data loss from scaling operations +* **Time to fix**: Allows proper query optimization before re-deployment + +The problematic query has a Cartesian product (`MATCH (u:User) MATCH (p:Product)`) that's creating massive intermediate results. + +After stopping the deployment: + +1. Work with developers to add proper relationship or optimize with `WITH` clause +2. Test the fixed query in a test environment +3. Monitor resource usage during re-deployment +4. Consider scaling if baseline usage is high even after fix + diff --git a/asciidoc/courses/aura-administration/modules/2-monitoring-resources/lessons/4-cpu-challenge/questions/1-cpu-crisis.adoc b/asciidoc/courses/aura-administration/modules/2-monitoring-resources/lessons/4-cpu-challenge/questions/1-cpu-crisis.adoc new file mode 100644 index 000000000..d6a32bbcb --- /dev/null +++ b/asciidoc/courses/aura-administration/modules/2-monitoring-resources/lessons/4-cpu-challenge/questions/1-cpu-crisis.adoc @@ -0,0 +1,55 @@ +[.question] += CPU Crisis Response + +How should you respond to restore normal service? + +* [ ] Scale the instance to get more CPU capacity + +* [ ] Wait for the spike to resolve naturally + +* [x] Rollback or stop the Friday deployment causing the queries + +* [ ] Restart the instance to clear the issue + +[TIP,role=hint] +.Hint +==== +Consider what's causing the CPU spike and the fastest way to return to normal operation. +==== + +[TIP,role=solution] +.Solution +==== +**Rollback or stop the Friday deployment causing the queries** is correct. + +This is the fastest path to recovery because: + +**Root cause identified**: New query pattern started at 9:00 AM (same as CPU spike), query is executing 500+ times/minute (very high frequency), each execution takes 2-5 seconds (very slow), and it was introduced in Friday deployment (known cause). + +**Immediate relief**: Stop the problematic application/deployment. CPU frees up within seconds, normal operations resume, no downtime is required, and there's no data loss. + +**After stopping the deployment**: +1. Work with developers to fix the query +2. Test in non-production environment +3. Deploy fixed version +4. Monitor during re-deployment + +Why other approaches fall short: **Scaling** is expensive, doesn't fix the bad query, and the problem persists. **Waiting** means users suffer with no guarantee it resolves. **Restarting** doesn't stop the bad queries as they'll restart immediately. + +**The problematic query likely has**: +[source,cypher] +---- +// Bad - Cartesian product +MATCH (u:User) MATCH (p:Product) WHERE u.region = p.region +---- + +**Should be**: +[source,cypher] +---- +// Good - Proper relationship or WITH clause +MATCH (u:User) WITH u MATCH (p:Product) WHERE u.region = p.region +---- + +**Key lesson**: When a specific deployment causes resource exhaustion, stopping that deployment is the fastest recovery path. Fix, test, redeploy properly. +==== + diff --git a/asciidoc/courses/aura-administration/modules/2-monitoring-resources/lessons/6-storage-challenge/lesson.adoc b/asciidoc/courses/aura-administration/modules/2-monitoring-resources/lessons/6-storage-challenge/lesson.adoc new file mode 100644 index 000000000..3d9e5cd91 --- /dev/null +++ b/asciidoc/courses/aura-administration/modules/2-monitoring-resources/lessons/6-storage-challenge/lesson.adoc @@ -0,0 +1,68 @@ += Storage Emergency +:type: challenge +:order: 6 + + +== Scenario + +You receive an urgent alert at 11:45 PM on Saturday: + +**"Storage at 95% capacity - Instance will become read-only at 100%"** + +Checking the metrics: + +* **Storage**: 475GB used of 500GB allocated (instance maximum) +* **Storage growth**: Jumped from 420GB to 475GB in the past 3 hours +* **Query rate**: Higher than normal for weekend (normally low on Saturday nights) + +Looking at query logs from the past 3 hours, you see thousands of executions of: + +---- +CREATE (log:ActivityLog { + timestamp: $timestamp, + details: $details, + stackTrace: $stackTrace +}) +---- + +The `stackTrace` property contains large text blobs (10-50KB each). + +This appears to be an error-logging mechanism that started logging excessively at 9:00 PM. + + +== The Challenge + +What should you do to prevent the instance from becoming read-only? + + +include::questions/1-storage-emergency.adoc[leveloffset=+1] + + +[.summary] +== Summary + +When storage is critically full due to unexpected data growth, you must act quickly to prevent the instance from becoming read-only. + +The correct approach combines immediate data cleanup with fixing the root cause: + +1. **Stop the excessive logging** - contact the on-call developer or disable the logging service +2. **Delete the problematic data** - remove the ActivityLog nodes created in the past 3 hours +3. **Monitor storage levels** - ensure storage decreases and stabilizes + +**Why not just scale the instance?** + +* Scaling takes time and doesn't stop the ongoing data creation +* If logging continues unchecked, you'll fill any size instance +* May be costly for temporary problem +* Should fix root cause first + +**After the immediate crisis:** + +* Implement proper log rotation and retention policies +* Consider alternative logging solutions (external log storage) +* Set up storage monitoring alerts at 80% capacity + +Review what caused the excessive error logging + +This scenario highlights the importance of monitoring storage trends and having data retention policies. + diff --git a/asciidoc/courses/aura-administration/modules/2-monitoring-resources/lessons/6-storage-challenge/questions/1-storage-emergency.adoc b/asciidoc/courses/aura-administration/modules/2-monitoring-resources/lessons/6-storage-challenge/questions/1-storage-emergency.adoc new file mode 100644 index 000000000..f6251acc2 --- /dev/null +++ b/asciidoc/courses/aura-administration/modules/2-monitoring-resources/lessons/6-storage-challenge/questions/1-storage-emergency.adoc @@ -0,0 +1,58 @@ +[.question] += Storage Emergency + +What should you do immediately to prevent the instance from becoming read-only? + +* [ ] Scale the instance to get more storage capacity + +* [ ] Let it fill up and deal with it Monday morning + +* [ ] Run database compaction to free up space + +* [x] Stop the excessive logging and delete the ActivityLog nodes + +[TIP,role=hint] +.Hint +==== +The instance will become read-only at 100%. What action prevents that while addressing the root cause? +==== + +[TIP,role=solution] +.Solution +==== +**Stop the excessive logging and delete the ActivityLog nodes** is correct. + +This addresses both the symptom and root cause: + +**Immediate actions** (in order): +1. **Stop the excessive logging** - Disable the logging service or contact on-call developer +2. **Delete the problematic data**: +[source,cypher] +---- +// Delete ActivityLog nodes created in last 3 hours +MATCH (log:ActivityLog) +WHERE log.timestamp > datetime() - duration('PT3H') +DELETE log +---- +3. **Monitor storage** - Ensure it drops back down +4. **Investigate root cause** - Why did error logging go crazy? + +**Why this is correct**: Stops the ongoing problem immediately, frees up space (prevents read-only), addresses the root cause, and can be done right now. + +The other options have critical problems: + +**Scaling (incorrect)**: Takes time to provision, doesn't stop the logging, will fill up again, and is more expensive. + +**Wait until Monday (incorrect)**: Instance becomes read-only (production down), which is unacceptable for Saturday night. + +**Compaction (incorrect)**: Requires downtime, doesn't stop ongoing logging, and won't help with actively growing data. + +**After immediate fix**: +1. Implement log rotation policies +2. Use external log storage (not database) +3. Set up 80% storage alerts +4. Review what caused the excessive errors + +**Key lesson**: When storage is rapidly growing due to application behavior, stop the behavior first, then clean up, then scale if needed. +==== + diff --git a/asciidoc/courses/aura-administration/modules/2-monitoring-resources/module.adoc b/asciidoc/courses/aura-administration/modules/2-monitoring-resources/module.adoc new file mode 100644 index 000000000..62cd067bd --- /dev/null +++ b/asciidoc/courses/aura-administration/modules/2-monitoring-resources/module.adoc @@ -0,0 +1,28 @@ += Monitoring Resources +:order: 2 + + +== Module Overview + +Understanding resource utilization is essential for maintaining optimal database performance. + +In this module, you will learn how to navigate the Metrics dashboard and monitor key resource metrics that indicate when your instance needs attention. + +You will explore the Resources tab, which provides insights into CPU usage, storage consumption, and query rates. + + +You will learn how to: + +* Navigate the Metrics dashboard + +* Monitor CPU usage and identify performance bottlenecks + +* Track storage consumption and growth trends + +* Analyze query rates to understand workload patterns + +* Recognize when to scale your instance + + +link:./1-metrics-overview/[Ready? Let's go →, role=btn] + diff --git a/asciidoc/courses/aura-administration/modules/3-monitoring-instance/lessons/1-heap-memory/lesson.adoc b/asciidoc/courses/aura-administration/modules/3-monitoring-instance/lessons/1-heap-memory/lesson.adoc new file mode 100644 index 000000000..6586b7d12 --- /dev/null +++ b/asciidoc/courses/aura-administration/modules/3-monitoring-instance/lessons/1-heap-memory/lesson.adoc @@ -0,0 +1,215 @@ += Heap Memory Usage +:type: lesson +:order: 1 + + +[.discrete] +== Introduction + +The heap is a critical memory area where Neo4j stores temporary data during query execution and transaction processing. + +In this lesson, you will learn how to monitor heap memory usage and identify when memory pressure is affecting performance. + + +== Understanding Heap Memory + +// UI Description: The percentage of configured heap memory in use. The heap space is used for query execution, +// transaction state, management of the graph etc. The size needed for the heap is very dependent on the nature +// of the usage of Neo4j. For example, long-running queries, or very complicated queries, are likely to require +// a larger heap than simpler queries. To improve performance, the heap should be large enough to sustain +// concurrent operations. This value should not exceed 80% for long periods, short spikes can be normal. +// In case of performance issues, you may have to tune your queries, and monitor their memory usage, to determine +// whether the heap needs to be increased. If the workload of Neo4j and performance of queries, indicates that +// more heap space is required, consider increasing the size of your instance. This helps avoid unwanted pauses +// for garbage collection. + +The heap is used for: + +* Query execution and result building +* Transaction state management +* Caching query plans +* Graph algorithm computations +* Internal data structures + + +The amount of heap memory needed depends on: + +* Query complexity +* Number of concurrent queries +* Transaction size +* Working set size + + +== Reading the Heap Memory Metric + +The Heap Memory metric shows the **percentage of configured heap memory in use**. + +The chart displays: + +* Current heap usage as a percentage +* Historical heap usage patterns +* Trends over the selected time period + + +=== Healthy Heap Usage + +Normal heap usage patterns: + +* **Baseline usage**: 20-50% during regular operations +* **Query peaks**: Temporary spikes to 60-80% during complex queries +* **Recovery**: Returns to baseline after query completion + +[TIP] +.Understanding heap spikes +==== +Occasional spikes to 70-80% are normal, especially during complex queries. + +The concern is sustained high usage, not temporary peaks. +==== + + +== Identifying Memory Issues + + +=== Sustained High Usage (>80%) + +If heap usage stays above 80% for extended periods: + +* Queries may be consuming too much memory +* The instance is under-provisioned for the workload +* Memory leaks may be present (rare) + +**Impact**: + +* Increased garbage collection frequency +* Slower query execution +* Risk of out-of-memory errors + + +=== Constantly Near 100% + +Heap usage consistently near 100% indicates: + +* Immediate memory pressure +* Queries may fail with out-of-memory errors +* Performance is severely degraded +* Garbage collection is constant + +**Action required**: This requires immediate investigation and likely instance scaling. + + +=== Memory Spikes with Poor Recovery + +If heap usage spikes and doesn't return to baseline: + +* Memory is not being released properly +* Long-running transactions may be holding memory +* Query result sets may be too large + +**Action required**: Review query patterns and transaction management. + + +== When to Scale Your Instance + +Consider increasing instance size when: + +* Heap usage consistently exceeds 80% +* Queries fail with out-of-memory errors +* Performance is degraded despite query optimization + +Garbage collection time is excessive (covered in the next lesson) + +[WARNING] +.Scaling isn't always the answer +==== +Scaling provides more heap memory, but inefficient queries will eventually consume any amount of memory. + +Always investigate query patterns before scaling. +==== + + +== Optimizing Heap Usage + +Before scaling, try these optimization strategies: + + +=== Query Optimization + +* Avoid returning large result sets - use `LIMIT` clauses +* Use aggregations instead of collecting all results +* Break complex queries into smaller operations +* Stream results when possible using drivers + + +=== Transaction Management + +* Keep transactions short-lived +* Avoid holding transactions open for extended periods +* Commit or rollback transactions promptly +* Use explicit transactions only when necessary + + +=== Query Pattern Changes + +* Paginate large result sets +* Avoid collecting all nodes/relationships in memory +* Use `CALL {} IN TRANSACTIONS` for batch operations +* Process data in smaller chunks + + +=== Workload Distribution + +* Spread complex queries across time instead of running concurrently +* Schedule heavy operations during off-peak hours +* Limit concurrent complex query execution +* Use queue systems for background processing + + +== Monitoring Heap for Different Scenarios + + +=== Normal Operations + +* Heap usage: 30-60% +* Occasional spikes to 70% +* Quick recovery to baseline + + +=== Complex Query Execution + +* Temporary spike to 70-80% +* Returns to baseline within seconds/minutes +* Expected and healthy behavior + + +=== Memory Pressure + +* Sustained usage >80% +* Slow or no recovery +* Requires investigation + + +=== Memory Crisis + +* Usage constantly 95-100% +* Queries failing +* Requires immediate action + + +[.quiz] +== Check Your Understanding + +include::questions/1-heap-threshold.adoc[leveloffset=+1] + + +[.summary] +== Summary + +You learned how to monitor heap memory usage for your Aura instances. + +You learned to identify normal heap usage patterns, recognize memory pressure, and determine when to scale your instance or optimize queries. + +Heap usage should not exceed 80% for long periods - sustained high usage indicates the need for optimization or scaling. + +In the next lesson, you will learn about page cache metrics and how they affect query performance. + diff --git a/asciidoc/courses/aura-administration/modules/3-monitoring-instance/lessons/1-heap-memory/questions/1-heap-threshold.adoc b/asciidoc/courses/aura-administration/modules/3-monitoring-instance/lessons/1-heap-memory/questions/1-heap-threshold.adoc new file mode 100644 index 000000000..0edd5ee67 --- /dev/null +++ b/asciidoc/courses/aura-administration/modules/3-monitoring-instance/lessons/1-heap-memory/questions/1-heap-threshold.adoc @@ -0,0 +1,39 @@ +[.question] += Heap Memory Threshold + +Your monitoring shows heap memory usage has been between 82-88% for the past week. + +According to best practices, what action should you take? + +* [ ] This is normal - no action needed + +* [ ] Restart the instance to clear memory + +* [x] Investigate and plan to scale - heap should stay below 80% +* [ ] Wait until it reaches 95% before taking action + +[TIP,role=hint] +.Hint +==== +Review the healthy heap usage ranges covered in this lesson. What threshold indicates sustained memory pressure? +==== + +[TIP,role=solution] +.Solution +==== +**Investigate and plan to scale - heap should stay below 80%** is correct. + +Heap usage best practices: **Normal** is 30-70% with occasional spikes to 80%, **Warning** is sustained 80-85%, and **Problem** is >85% sustained. + +At 82-88% sustained for a week, you're in the warning/problem zone. This leads to increased garbage collection frequency, performance degradation, and risk of out-of-memory errors. + +Recommended actions: +1. Review query logs for memory-intensive queries +2. Check if specific queries can be optimized +3. Plan instance scaling if workload is legitimate + +Why the other answers are wrong: 82-88% is NOT normal (it's too high), restarting doesn't fix the underlying memory needs, and waiting until 95% risks crashes and severe performance issues. + +Heap usage should not exceed 80% for long periods. Short spikes are normal, but sustained high usage requires action. +==== + diff --git a/asciidoc/courses/aura-administration/modules/3-monitoring-instance/lessons/10-oom-challenge/lesson.adoc b/asciidoc/courses/aura-administration/modules/3-monitoring-instance/lessons/10-oom-challenge/lesson.adoc new file mode 100644 index 000000000..633013b9a --- /dev/null +++ b/asciidoc/courses/aura-administration/modules/3-monitoring-instance/lessons/10-oom-challenge/lesson.adoc @@ -0,0 +1,189 @@ += OOM Crisis Management +:type: challenge +:order: 10 + + +== Scenario + +Your e-commerce platform's Neo4j Aura instance starts throwing errors at 2 PM on a busy Friday. + +Customer-facing features are failing: + +* Product recommendations not loading +* Shopping cart operations timing out +* Order processing errors + +Your monitoring dashboard shows: + +**Current metrics (2:15 PM)**: + +* **OOM Error Rate**: 2.5 errors per minute +* **OOM Error Count**: 150 (started at 0 this morning) +* **Heap Usage**: 98% +* **Total GC Time**: 22% +* **Old Gen GC**: Running almost continuously +* **Query Rate**: 3,200 per minute (normal is 2,500) +* **Failed Query Rate**: 85 per minute (normally 0-2) +* **CPU Usage**: 85% +* **Page Cache Hit Ratio**: 97% + + +**What happened**: + +Reviewing recent changes, you discover: + +* Marketing launched a "similar products" feature at 2 PM +* New feature shows 20 recommendations per product page +* Product pages receiving 3x normal traffic due to promotion + +**Query logs reveal** (top memory consumer): + +```cypher +MATCH (p:Product {id: $productId}) +MATCH (p)-[:IN_CATEGORY]->(c:Category) +MATCH (similar:Product)-[:IN_CATEGORY]->(c) +MATCH (similar)-[:PURCHASED_WITH]->(other:Product) +RETURN DISTINCT similar, collect(other) as suggestions +``` + +This query: + +* Runs 800 times per minute +* Average allocation: 45 MB per query +* No `LIMIT` clause +* Returns 200-500 products per execution + + +== The Challenge + +What's your incident response plan? + + +include::questions/1-oom-triage.adoc[leveloffset=+1] + + +[.summary] +== Summary + +OOM incidents require a structured response: stabilize, investigate, and prevent recurrence. + +**The three-phase response**: + + +=== Phase 1: Stabilize (Minutes) + +**Immediate action - Scale the instance**: + +* Provides immediate relief from OOM errors +* Stops ongoing query failures +* Restores application functionality +* Buys time for investigation + + +**Why this comes first**: + +* OOM errors mean active failures +* Users are impacted RIGHT NOW +* Cannot investigate effectively while system is failing +* Scaling takes 5-10 minutes vs hours of optimization + + +=== Phase 2: Mitigate (Within an hour) + +**Temporary fix - Optimize the problematic query**: + +The query was consuming 36 GB per minute (45 MB × 800 queries): + +```cypher +// Original (memory hungry) +MATCH (p:Product {id: $productId}) +MATCH (p)-[:IN_CATEGORY]->(c:Category) +MATCH (similar:Product)-[:IN_CATEGORY]->(c) +MATCH (similar)-[:PURCHASED_WITH]->(other:Product) +RETURN DISTINCT similar, collect(other) as suggestions + +// Optimized (memory efficient) +MATCH (p:Product {id: $productId}) +MATCH (p)-[:IN_CATEGORY]->(c:Category) +MATCH (similar:Product)-[:IN_CATEGORY]->(c) +WHERE similar.id <> p.id +MATCH (similar)-[:PURCHASED_WITH]->(other:Product) +WITH similar, other +LIMIT 20 +RETURN similar, collect(other) as suggestions +``` + +**Key optimizations**: + +* Added `LIMIT 20` to match feature requirements +* Added `WHERE` clause to exclude the product itself +* Limited before collecting to reduce memory allocation +* Reduces allocation from 45 MB to ~200 KB per query + + +=== Phase 3: Prevent (Ongoing) + +**Long-term improvements**: + +* **Code review process**: Require `LIMIT` clauses on queries returning collections +* **Load testing**: Test new features at production scale before launch +* **Monitoring alerts**: Alert on heap >85% and any OOM errors +* **Capacity planning**: Review metrics weekly, scale proactively +* **Query guidelines**: Document memory-efficient query patterns + + +**The wrong approach**: + +Trying to optimize the query while OOM errors are occurring: + +* Users continue experiencing failures +* System instability makes testing difficult +* Team is under high pressure +* Risk of making things worse +* Delays resolution + + +**The right approach**: + +Scale → Stabilize → Investigate → Optimize → Prevent + +This incident demonstrates why monitoring is critical: + +* Early warning metrics (heap, GC) were showing problems +* OOM errors indicated critical failure +* Query logs identified the root cause +* Multiple metrics together told the complete story + + +**Key lessons**: + +* New features need load testing at production scale +* Queries should always include `LIMIT` when returning collections +* Memory allocation (`alloc bytes`) is a critical metric +* Scaling buys time to fix problems properly +* Optimization should happen after stabilization + + +**Capacity planning**: + +Even after optimization, consider: + +* Keeping the larger instance size +* Traffic is 3x normal due to promotion +* Future features may add workload +* Headroom prevents future incidents + + +**Monitoring setup**: + +Configure alerts to catch issues earlier: + +* Heap usage >80%: Warning +* Heap usage >85%: Critical +* GC time >5%: Investigation needed +* Any OOM errors: Page on-call +* Failed query rate >10/min: Alert + + +This scenario shows how operational monitoring, query optimization, and capacity planning work together to maintain a healthy production system. + diff --git a/asciidoc/courses/aura-administration/modules/3-monitoring-instance/lessons/10-oom-challenge/questions/1-oom-triage.adoc b/asciidoc/courses/aura-administration/modules/3-monitoring-instance/lessons/10-oom-challenge/questions/1-oom-triage.adoc new file mode 100644 index 000000000..2d56cf022 --- /dev/null +++ b/asciidoc/courses/aura-administration/modules/3-monitoring-instance/lessons/10-oom-challenge/questions/1-oom-triage.adoc @@ -0,0 +1,105 @@ +[.question] += Incident Triage + +With OOM errors occurring at 2.5 per minute and customers unable to complete purchases, what's the correct response order? + +* [ ] 1. Optimize the query, 2. Scale instance, 3. Monitor results + +* [ ] 1. Disable the new feature, 2. Restart instance, 3. Optimize query + +* [x] 1. Scale instance immediately, 2. Verify stabilization, 3. Optimize query + +* [ ] 1. Contact development team, 2. Plan optimization, 3. Schedule scaling + +[TIP,role=hint] +.Hint +==== +Consider what needs to happen first when users are experiencing active failures. What provides the fastest path to stability? +==== + +[TIP,role=solution] +.Solution +==== +**1. Scale instance immediately, 2. Verify stabilization, 3. Optimize query** is correct. + +This is the proper incident response for ongoing OOM errors: + + +**Step 1: Scale instance immediately (5-10 minutes)** + +Why this comes first: OOM errors are causing active failures RIGHT NOW. With 85 queries failing per minute, you're looking at 5,100 failed operations in an hour. Customers cannot complete purchases, causing revenue loss. Scaling provides immediate relief and takes only 5-10 minutes to complete. + +**What scaling accomplishes**: More heap memory becomes available (e.g., 8GB → 16GB), heap usage drops from 98% to ~60%, GC pressure decreases immediately, OOM errors stop occurring, queries can execute successfully, and application functionality is restored. + + +**Step 2: Verify stabilization (5 minutes)** + +Check metrics to confirm the immediate crisis is resolved: OOM error rate returns to 0, heap usage drops below 80%, GC time returns to <5%, failed query rate returns to normal, and application features are working. + + +**Step 3: Optimize the query (30-60 minutes)** + +Now that the system is stable, you can review query logs safely, identify memory allocation patterns, test optimizations in a stable environment, deploy query improvements, and monitor their impact. + +**The query optimization**: + +Original query allocating 45 MB × 800 queries/min = 36 GB/min memory allocation: + +```cypher +MATCH (p:Product {id: $productId}) +MATCH (p)-[:IN_CATEGORY]->(c:Category) +MATCH (similar:Product)-[:IN_CATEGORY]->(c) +MATCH (similar)-[:PURCHASED_WITH]->(other:Product) +RETURN DISTINCT similar, collect(other) as suggestions +``` + +Optimized query with `LIMIT`: + +```cypher +MATCH (p:Product {id: $productId}) +MATCH (p)-[:IN_CATEGORY]->(c:Category) +MATCH (similar:Product)-[:IN_CATEGORY]->(c) +WHERE similar.id <> p.id +MATCH (similar)-[:PURCHASED_WITH]->(other:Product) +WITH similar, other +LIMIT 20 +RETURN similar, collect(other) as suggestions +``` + +**Optimization results**: Memory allocation drops from 45 MB to ~200 KB (a 225x reduction) while maintaining the same functionality (feature still shows 20 recommendations) with a better user experience (faster queries). + + +Why the other approaches are wrong: + +**Option 1: Optimize first, then scale** - Users continue experiencing failures while you work, optimization takes 30-60 minutes, more failed transactions equals more revenue loss, it's harder to test optimizations with an unstable system, and this approach delays resolution by an hour. + + +**Option 2: Disable feature, restart, optimize** - Disabling the feature removes revenue-generating functionality, restart causes additional downtime, restart doesn't address the underlying capacity issue, and OOM errors would likely return when the feature is re-enabled. + + +**Option 3: Contact team, plan, schedule** - This is too slow for a critical incident, users would be experiencing failures for hours, coordination takes too long during an incident, and scaling can happen immediately without planning. + + +**The incident response principle**: + +For critical incidents with active failures: + +1. **Stop the bleeding** (scale/add capacity) +2. **Verify stability** (confirm metrics) +3. **Fix the cause** (optimize queries) +4. **Prevent recurrence** (monitoring, guidelines) + + +**Cost considerations**: + +Scaling a production instance during an incident is always justified: Revenue loss from failed transactions far exceeds infrastructure cost, customer trust and experience are critical, you can optimize and potentially scale back down after resolution, and infrastructure cost is minimal compared to incident impact. + + +**Real-world timing**: Scale instance (10 minutes), verify stability (5 minutes), optimize query (45 minutes) - **Total resolution: ~60 minutes**. + +Compare this to trying to optimize first: Hours of investigation during failures, continued user impact, risk of incomplete optimization under pressure - **Total resolution: 2-4+ hours with sustained impact**. + + +This scenario demonstrates why **scaling is not a failure** - it's a valid and often necessary incident response tool that enables proper investigation and optimization in a stable environment. +==== + diff --git a/asciidoc/courses/aura-administration/modules/3-monitoring-instance/lessons/2-heap-challenge/lesson.adoc b/asciidoc/courses/aura-administration/modules/3-monitoring-instance/lessons/2-heap-challenge/lesson.adoc new file mode 100644 index 000000000..953887de5 --- /dev/null +++ b/asciidoc/courses/aura-administration/modules/3-monitoring-instance/lessons/2-heap-challenge/lesson.adoc @@ -0,0 +1,48 @@ += Memory Pressure +:type: challenge +:order: 2 + + +== Scenario + +Your production instance has been experiencing intermittent slowness for the past week. + +Users report that "sometimes queries are fast, sometimes they take forever." + +Reviewing the Instance metrics for the past 7 days, you observe: + +* **Heap Memory Usage**: Consistently between 85-95%, with frequent spikes to 98% +* **Garbage Collection Total %**: 12% (was 2% last month) +* **Old Gen GC**: Occurring every 10-15 minutes (was hourly last month) +* **CPU Usage**: Normal, around 50% of capacity +* **Page Cache Hit Ratio**: Still at 99% + +Query logs show that query execution times are highly variable: + +* Same query might take 50ms or 5,000ms depending on when it runs +* No specific slow query pattern identified + + +== The Challenge + +Based on these metrics, what's causing the intermittent slowness? + + +include::questions/1-heap-pressure.adoc[leveloffset=+1] + + +[.summary] +== Summary + +High heap usage combined with excessive garbage collection causes intermittent slowness because GC pauses all query execution. Scale the instance to get more heap memory and reduce GC frequency + +**The solution:** + +1. **Immediate**: Scale instance to get more heap memory +2. **Investigate**: Review query patterns for memory-intensive operations +3. **Optimize**: Reduce memory usage through query optimization if possible + +Heap usage should stay below 80% for consistent performance. + +Even with good page cache hit ratio and normal CPU, excessive heap usage and GC will degrade performance significantly. + diff --git a/asciidoc/courses/aura-administration/modules/3-monitoring-instance/lessons/2-heap-challenge/questions/1-heap-pressure.adoc b/asciidoc/courses/aura-administration/modules/3-monitoring-instance/lessons/2-heap-challenge/questions/1-heap-pressure.adoc new file mode 100644 index 000000000..f8d3b79f8 --- /dev/null +++ b/asciidoc/courses/aura-administration/modules/3-monitoring-instance/lessons/2-heap-challenge/questions/1-heap-pressure.adoc @@ -0,0 +1,42 @@ +[.question] += Memory Pressure Diagnosis + +What's causing the intermittent slowness? + +* [ ] CPU bottleneck slowing queries + +* [ ] Page cache too small for the database + +* [x] High heap usage causing excessive garbage collection pauses + +* [ ] Network latency causing intermittent slowness + +[TIP,role=hint] +.Hint +==== +Look at the relationship between heap usage and GC metrics. What causes intermittent slowness with variable query times? +==== + +[TIP,role=solution] +.Solution +==== +**High heap usage causing excessive garbage collection pauses** is correct. + +**Why intermittent slowness**: When Old Gen GC runs (every 10-15 minutes), ALL queries pause. Old Gen GC takes seconds to complete, so queries running during GC appear very slow (2-5 second pause). Between GC cycles, queries run normally (fast), causing users to experience unpredictable performance. + +**Evidence**: Heap at 85-95% (too high, should be <80%), GC time at 12% (critical, should be <2%), and Old Gen GC occurring every 10-15 minutes (too frequent, should be hours). + +**The pattern**: +---- +Normal query: 50ms +Query during GC: 50ms + 3000ms GC pause = 3050ms +User: "Sometimes fast, sometimes slow!" +---- + +**Solution**: Scale instance to get more heap memory. This will cause heap usage to drop to 60-70%, Old Gen GC to become infrequent, and consistent query performance to be restored. + +Why the other answers are wrong: CPU at 50% is fine (not the bottleneck), page cache at 99% is excellent, and network issues would affect all queries equally, not create this intermittent pattern. + +**Key insight**: Even with good CPU and page cache, excessive GC from high heap usage causes intermittent performance problems that frustrate users. +==== + diff --git a/asciidoc/courses/aura-administration/modules/3-monitoring-instance/lessons/2-page-cache/lesson.adoc b/asciidoc/courses/aura-administration/modules/3-monitoring-instance/lessons/2-page-cache/lesson.adoc new file mode 100644 index 000000000..2e0f714b4 --- /dev/null +++ b/asciidoc/courses/aura-administration/modules/3-monitoring-instance/lessons/2-page-cache/lesson.adoc @@ -0,0 +1,288 @@ += Page Cache Performance +:type: lesson +:order: 3 + + +[.discrete] +== Introduction + +The page cache stores frequently accessed graph data in memory, dramatically improving query performance. + +In this lesson, you will learn how to monitor page cache metrics and optimize for maximum performance. + + +== Understanding the Page Cache + +The page cache stores: + +* Graph data (nodes and relationships) +* Property data +* Index data + +Schema information + +When a query needs data: + +. Check if data is in page cache (cache hit) +. If not, read from disk (cache miss) + +**Cache hits** are fast (microseconds) - data is already in memory. + +**Cache misses** are slow (milliseconds) - must read from disk. + + +== Page Cache Metrics + +Aura provides three page cache metrics: + + +=== Hit Ratio + +The percentage of times data was found in the page cache versus needing to be read from disk. + +// UI Description: The percentage of times data required during query execution was found in memory vs needing +// to be read from disk. Ideally the whole graph should fit into memory, and this should consistently be between +// 98% and 100%. If this value is consistently or significantly under 100%, check the page cache usage ratio to +// see if the graph is too large to fit into memory. A high amount of insert or update activity on a graph can +// also cause this value to change. + +**Target**: 98-100% + +This is your most important page cache metric. + + +=== Usage Ratio + +The percentage of allocated page cache currently in use. + +// UI Description: The percentage of the allocated page cache in use. If this is close to or at 100%, then it is +// likely that the hit ratio will start dropping, and you should consider increasing the size of your instance +// so that more memory is available for the page cache. + + +=== Evictions per Minute + +The number of times data in memory is replaced per minute. + +// UI Description: The number of times data in memory is being replaced per minute. A spike can mean your workload +// is exceeding the instance's available memory, and you may notice a degradation in performance or query execution +// errors. Consider increasing the size of your instance to improve performance if this metric remains high. + + +== Monitoring Hit Ratio + + +=== Ideal Hit Ratio: 98-100% + +A hit ratio between 98% and 100% indicates: + +* Most of your graph fits in memory +* Queries are executing efficiently + +Optimal performance + +This is the target for production workloads. + + +=== Good Hit Ratio: 90-98% + +A hit ratio between 90% and 98% may indicate: + +* Graph is slightly larger than page cache +* Some cold data exists +* Performance is good but could be better + +**Action**: Monitor trends and consider scaling if ratio continues to decline. + + +=== Poor Hit Ratio: <90% + +A hit ratio below 90% indicates: + +* Significant portions of the graph don't fit in memory +* Many queries are reading from disk +* Performance is degraded + +**Action required**: Investigation and likely scaling needed. + + +=== Fluctuating Hit Ratio + +If hit ratio varies significantly: + +* Workload accesses different data over time +* Batch operations may be loading new data +* Normal for some workload patterns + +**Action**: Understand the pattern and determine if it's acceptable. + + +[NOTE] +.Write activity impact +==== +A high amount of insert or update activity can cause the hit ratio to fluctuate as new data is loaded into the page cache. + +This is normal during data imports or heavy write operations. +==== + + +== Monitoring Usage Ratio + + +=== Usage Ratio Near 100% + +If the page cache usage ratio is close to or at 100%: + +* The entire page cache is in use +* Hit ratio may start declining +* Performance may degrade as cache thrashing begins + +**Action**: Consider increasing instance size for more page cache memory. + + +=== Usage Ratio Below 80% + +If the page cache usage ratio is well below 100%: + +* Graph comfortably fits in memory +* Instance is appropriately sized or over-provisioned +* Excellent performance potential + + +== Monitoring Page Cache Evictions + + +=== Low Evictions + +Low eviction rates (0-100 per minute) indicate: + +* Data fits well in memory +* Minimal cache thrashing +* Healthy state + + +=== High Evictions + +High eviction rates (>1000 per minute) indicate: + +* Workload exceeds available memory +* Cache thrashing is occurring + +Performance degradation likely + +A spike in evictions may indicate: + +* Workload burst exceeding memory capacity +* Query execution errors possible +* Immediate action may be needed + +**Action**: Consider increasing instance size to improve performance. + + +== Understanding the Relationship + +These three metrics work together: + + +=== Healthy State + +* Hit ratio: 98-100% +* Usage ratio: 70-100% +* Evictions: Low + +**Result**: Optimal performance + + +=== Warning Signs + +* Hit ratio: Declining +* Usage ratio: 100% +* Evictions: Increasing + +**Result**: Performance degrading, action needed soon + + +=== Critical State + +* Hit ratio: <90% +* Usage ratio: 100% +* Evictions: High + +**Result**: Poor performance, immediate action required + + +== Optimizing Page Cache Performance + + +=== Graph Size vs. Memory + +Ideally, your entire graph should fit in the page cache. + +Check your instance size: + +* **Small instances**: Limited page cache (GB range) +* **Medium instances**: Moderate page cache (tens of GB) +* **Large instances**: Large page cache (hundreds of GB) + +If your graph is larger than available page cache, consider scaling. + + +=== Query Patterns + +Some query patterns are more cache-friendly: + +**Cache-friendly**: + +* Queries accessing hot data (frequently used nodes/relationships) +* Queries following common traversal patterns +* Queries on indexed properties + + +**Cache-unfriendly**: + +* Full graph scans +* Random access patterns +* Queries accessing cold data + + +=== Data Model Optimization + +* Place frequently accessed properties on frequently accessed nodes +* Use appropriate indexes to reduce data scanning +* Denormalize data to reduce traversal depth when appropriate + + +== When to Scale for Page Cache + +Scale your instance when: + +* Hit ratio is consistently below 98% +* Usage ratio is at 100% and hit ratio is declining +* Evictions are high and performance is degraded +* Your graph has grown significantly + + +[TIP] +.Monitor during peak hours +==== +Monitor page cache metrics during peak usage periods to ensure adequate capacity when it matters most. +==== + + +[.quiz] +== Check Your Understanding + +include::questions/1-cache-hit-ratio.adoc[leveloffset=+1] + + +[.summary] +== Summary + +You learned how to monitor page cache performance for your Aura instances. + +You learned that ideally the page cache hit ratio should be between 98% and 100%, indicating that most of your graph fits in memory. + +You also learned to recognize warning signs like high evictions or declining hit ratios that indicate the need for scaling. + +In the next lesson, you will learn how to monitor Bolt connections and connection pool health. + diff --git a/asciidoc/courses/aura-administration/modules/3-monitoring-instance/lessons/2-page-cache/questions/1-cache-hit-ratio.adoc b/asciidoc/courses/aura-administration/modules/3-monitoring-instance/lessons/2-page-cache/questions/1-cache-hit-ratio.adoc new file mode 100644 index 000000000..73021ca47 --- /dev/null +++ b/asciidoc/courses/aura-administration/modules/3-monitoring-instance/lessons/2-page-cache/questions/1-cache-hit-ratio.adoc @@ -0,0 +1,37 @@ +[.question] += Page Cache Hit Ratio Target + +Your page cache hit ratio has dropped to 92% and usage ratio is at 100%. + +What does this indicate? + +* [ ] Performance is excellent - no action needed + +* [ ] The database has a configuration error + +* [x] The graph is larger than available memory - consider scaling + +* [ ] Queries are poorly written + +[TIP,role=hint] +.Hint +==== +What is the target range for page cache hit ratio? What does it mean when the usage ratio is at 100%? +==== + +[TIP,role=solution] +.Solution +==== +**The graph is larger than available memory - consider scaling** is correct. + +Page cache hit ratio analysis: **Ideal** is 98-100% (graph fits in memory), **Good** is 90-98% (mostly fits), and **Poor** is <90% (significant disk reads). + +At 92% with 100% usage ratio, the entire page cache is in use, hit ratio is declining because the graph doesn't fully fit, many queries are reading from disk (slow), and performance is degraded. This indicates the database has grown beyond available page cache memory. + +**Solution**: Scale to a larger instance with more memory so the graph fits in the page cache. + +Why the other answers are wrong: 92% is below the ideal 98-100% target, nothing indicates configuration error (this is a capacity issue), and while some queries might benefit from optimization, the root cause is insufficient memory for the data size. + +The ideal state is 98-100% hit ratio with the full graph in memory. +==== + diff --git a/asciidoc/courses/aura-administration/modules/3-monitoring-instance/lessons/3-bolt-connections/lesson.adoc b/asciidoc/courses/aura-administration/modules/3-monitoring-instance/lessons/3-bolt-connections/lesson.adoc new file mode 100644 index 000000000..62d7ec374 --- /dev/null +++ b/asciidoc/courses/aura-administration/modules/3-monitoring-instance/lessons/3-bolt-connections/lesson.adoc @@ -0,0 +1,302 @@ += Bolt Connections +:type: lesson +:order: 5 + + +[.discrete] +== Introduction + +Bolt is the protocol used by applications to connect to Neo4j. + +In this lesson, you will learn how to monitor Bolt connections to understand application connectivity patterns and identify potential issues. + + +== Understanding Bolt Connections + +Bolt connections represent: + +* Client applications connected to your database +* Active query execution sessions +* Idle connections in connection pools +* Connection lifecycle (opened and closed) + + +Applications typically use connection pools to: + +* Reuse connections for multiple queries +* Reduce connection overhead +* Manage concurrent query execution + + +== Bolt Connection Metrics + +Aura provides several Bolt connection metrics: + + +=== Running Connections + +The total number of Bolt connections currently executing Cypher transactions and returning results. + +// UI Description: The total number of Bolt connections that are currently executing Cypher transactions and +// returning results. This is a set of snapshots over time and may appear to spike if workloads are all +// completed quickly + +**Characteristics**: + +* Snapshots over time +* May spike if workloads complete quickly +* Indicates active query execution + + +=== Idle Connections + +The total number of Bolt connections connected to the database but not currently executing queries or returning results. + +// UI Description: The total number of Bolt connections that are connected to the Aura database but not currently +// executing Cypher or returning results. + +**Characteristics**: + +* Connections waiting in connection pools +* Available for immediate use +* Normal for well-configured applications + + +=== Opened (Count) + +The total number of Bolt connections opened since startup. + +// UI Description: The total number of Bolt connections opened since startup. This includes both successful and +// failed connections. This value may drop if background maintenance is performed by Aura. + +Includes both successful and failed connections. + +[NOTE] +.Background maintenance +==== +This value may drop if background maintenance is performed by Aura. + +This is normal and doesn't indicate a problem. +==== + + +=== Opened (Rate) + +The rate of Bolt connections opened per minute. + +// UI Description: The rate of of Bolt connections opened per minute. This includes both successful and failed connections. + +Includes both successful and failed connections. + + +=== Closed (Count) + +The total number of Bolt connections closed since startup. + +// UI Description: The total number of Bolt connections closed since startup. This includes both properly and +// abnormally ended connections. This value may drop if background maintenance is performed by Aura. + +Includes both properly and abnormally ended connections. + + +=== Closed (Rate) + +The rate of Bolt connections being closed per minute. + +// UI Description: The rate of Bolt connections being closed per minute. This includes both properly and +// abnormally ended connections. + +Includes both properly and abnormally ended connections. + + +== Interpreting Connection Metrics + + +=== Healthy Connection Patterns + +**Steady idle connections**: + +* Applications maintain connection pools +* Connections are reused efficiently +* Ready for immediate query execution + + +**Running connections scale with workload**: + +* Increases during high activity +* Decreases during low activity +* Matches expected usage patterns + + +**Balanced open/close rates**: + +* Similar number of connections opened and closed over time +* Indicates stable connection management + + +=== Warning Signs + + +==== High Connection Churn + +If connections are constantly opening and closing: + +* High open and close rates +* Few idle connections +* Connection pooling may not be configured + +**Impact**: + +* Increased overhead from connection setup +* Reduced performance +* Higher resource usage + +**Action**: Review application connection pool configuration. + + +==== Too Many Idle Connections + +Excessive idle connections indicate: + +* Connection pool size too large +* Resources held unnecessarily +* Potential connection leaks + +**Action**: Reduce connection pool size in applications. + + +==== Connection Spikes + +Sudden spikes in running connections may indicate: + +* Application deployment or restart +* Batch job execution +* Potential connection leak + +**Action**: Investigate application behavior during spikes. + + +==== Opened >> Closed + +If many more connections are opened than closed: + +* Potential connection leak +* Applications not closing connections properly +* Connection pool exhaustion possible + +**Action**: Review application code for proper connection handling. + + +== Connection Limits and Scaling + +Each Aura instance has connection limits based on: + +* Instance tier +* Instance size +* Available resources + + +=== When Limits are Reached + +When connection limits are approached: + +* New connection attempts may fail +* Application errors occur +* Users experience connectivity issues + +**Action**: Optimize connection usage or scale instance. + + +== Optimizing Connection Management + + +=== Application-Level Optimization + +**Configure connection pools properly**: + +* Minimum pool size: 5-10 connections +* Maximum pool size: Based on concurrent query needs +* Connection timeout: 30-60 seconds +* Idle connection timeout: 5-10 minutes + + +**Close connections properly**: + +[source,python] +---- +# Python example using context manager +with driver.session() as session: + result = session.run("MATCH (n) RETURN count(n)") + # Connection automatically closed +---- + + +**Avoid connection leaks**: + +* Always close sessions and transactions +* Use try-finally blocks or context managers +* Handle errors properly +* Monitor application logs for connection errors + + +=== Monitoring Best Practices + +* Track connection metrics during peak hours +* Compare running vs. idle connections +* Monitor for unexpected connection spikes +* Alert on high open/close rates + + +== Troubleshooting Common Issues + + +=== Issue: Application Can't Connect + +**Check**: + +* Running + Idle connections vs. instance limits +* Recent changes in application deployment +* Network connectivity issues + +**Action**: Scale instance or reduce connection pool sizes. + + +=== Issue: Slow Query Performance + +**Check**: + +* High number of running connections +* CPU and memory metrics +* Query logs for slow queries + +**Action**: May indicate too many concurrent queries; implement queuing. + + +=== Issue: Connection Timeouts + +**Check**: + +* Long-running queries holding connections +* Connection pool exhaustion +* Instance resource limits + +**Action**: Optimize queries, adjust timeouts, or scale instance. + + +[.quiz] +== Check Your Understanding + +include::questions/1-connection-patterns.adoc[leveloffset=+1] + + +[.summary] +== Summary + +You learned how to monitor Bolt connections for your Aura instances. + +You learned about the different connection metrics (running, idle, opened, closed) and how to identify healthy connection patterns versus warning signs. + +Properly configured connection pools maintain idle connections for reuse, while high connection churn indicates configuration issues. + +In the next lesson, you will learn about garbage collection metrics and their impact on performance. + diff --git a/asciidoc/courses/aura-administration/modules/3-monitoring-instance/lessons/3-bolt-connections/questions/1-connection-patterns.adoc b/asciidoc/courses/aura-administration/modules/3-monitoring-instance/lessons/3-bolt-connections/questions/1-connection-patterns.adoc new file mode 100644 index 000000000..117a223c1 --- /dev/null +++ b/asciidoc/courses/aura-administration/modules/3-monitoring-instance/lessons/3-bolt-connections/questions/1-connection-patterns.adoc @@ -0,0 +1,39 @@ +[.question] += Connection Pool Health + +You observe these Bolt connection metrics: + +. Running connections: 20 (varies with workload) +. Idle connections: 80 (stable) +. Opened rate: 25/minute +. Closed rate: 25/minute + +What does this indicate? + +* [ ] Connection leak - opened rate is too high +* [x] Healthy connection pooling with efficient reuse +* [ ] Connection pool is too large +* [ ] Application needs to increase pool size + +[TIP,role=hint] +.Hint +==== +Look at the relationship between running, idle, and the open/close rates. What pattern indicates healthy connection pooling? +==== + +[TIP,role=solution] +.Solution +==== +**Healthy connection pooling with efficient reuse** is correct. + +This is a healthy pattern because the pool maintains ready connections (**Stable idle connections: 80**), there's normal turnover (**Balanced open/close rates: 25/min each**, not excessive), running connections vary with workload (normal query execution), and there's **Low churn** (connections are being reused, not constantly recreated). + +Healthy connection pooling looks like idle connections available in the pool, low balanced open/close rates, running connections scaling with workload, and efficient resource usage. + +Why the other answers are wrong: 25/minute is low, not high (connection leak would show 500+ opens/minute), 80 idle connections is reasonable (not excessive), and the pool is working well (no need to increase size). + +Compare to unhealthy patterns: **Connection leak** shows 500+ opens/min with few idle connections, **No pooling** shows very high open/close rates (same as query rate), and **Pool exhaustion** shows no idle connections with connection timeouts. + +This pattern shows proper connection pool configuration and usage. +==== + diff --git a/asciidoc/courses/aura-administration/modules/3-monitoring-instance/lessons/4-cache-challenge/lesson.adoc b/asciidoc/courses/aura-administration/modules/3-monitoring-instance/lessons/4-cache-challenge/lesson.adoc new file mode 100644 index 000000000..d097ae1b0 --- /dev/null +++ b/asciidoc/courses/aura-administration/modules/3-monitoring-instance/lessons/4-cache-challenge/lesson.adoc @@ -0,0 +1,51 @@ += Cache Performance +:type: challenge +:order: 4 + + +== Scenario + +Over the past month, users have been complaining about slowly degrading query performance. + +"The same reports that used to take 2 seconds now take 10-15 seconds." + +Reviewing Instance metrics, you notice: + +* **Page Cache Hit Ratio**: Declined from 99% to 87% over the past month +* **Page Cache Usage Ratio**: Sitting at 100% +* **Page Cache Evictions**: 5,000-8,000 per minute (was 100-500 per minute) +* **Heap Memory**: Normal at 65% +* **CPU Usage**: Normal at 55% +* **Database Store Size**: Grew from 95GB to 145GB over the past month + +The application hasn't changed, but the amount of data continues to grow as users add more content. + + +== The Challenge + +How should you restore query performance? + + +include::questions/1-cache-performance.adoc[leveloffset=+1] + + +[.summary] +== Summary + +When database growth outpaces available memory, query performance degrades as the cache hit ratio drops. Scale the instance to fit the growing graph in memory + +**Why scaling is the answer:** + +* Larger instance provides more page cache memory +* Goal: Get the full graph back into memory +* Restores hit ratio to 98-100% +* Returns query performance to normal + +**Monitoring tip:** + +Watch the relationship between database size and page cache hit ratio. + +When your database grows beyond page cache capacity, performance will degrade predictably. + +Plan for scaling before this happens by monitoring growth trends. + diff --git a/asciidoc/courses/aura-administration/modules/3-monitoring-instance/lessons/4-cache-challenge/questions/1-cache-performance.adoc b/asciidoc/courses/aura-administration/modules/3-monitoring-instance/lessons/4-cache-challenge/questions/1-cache-performance.adoc new file mode 100644 index 000000000..1a9ec1ded --- /dev/null +++ b/asciidoc/courses/aura-administration/modules/3-monitoring-instance/lessons/4-cache-challenge/questions/1-cache-performance.adoc @@ -0,0 +1,50 @@ +[.question] += Page Cache Degradation + +What action restores performance? + +* [ ] Optimize queries - they've become inefficient + +* [ ] Increase heap memory allocation + +* [x] Scale instance to get more page cache memory + +* [ ] Run database compaction to reduce size + +[TIP,role=hint] +.Hint +==== +What changed over the month? What's the relationship between database size, page cache size, and hit ratio? +==== + +[TIP,role=solution] +.Solution +==== +**Scale instance to get more page cache memory** is correct. + +**Root cause analysis**: + +**What happened**: Database grew 53% (95GB → 145GB), page cache size stayed the same, graph no longer fits in memory, and many queries now read from disk (slow). + +**The math**: Month ago you had a 95GB database that fit in cache with 99% hit ratio. Now you have a 145GB database that doesn't fit in cache with 87% hit ratio. Evictions are up 10x, indicating cache thrashing. + +**Why queries are slower**: +---- +Before (in memory): + Query reads 1000 nodes from cache + Time: ~50ms + +After (disk reads): + Query reads 700 from cache, 300 from disk + Time: ~400ms (8x slower) +---- + +**Solution**: Scale to larger instance to get more page cache memory. The full graph will fit in memory again, hit ratio returns to 98-100%, and query performance is restored. + +The other options don't address the root cause: Queries haven't changed (the data/memory ratio changed), heap is fine at 65%, and compaction won't help because this is real data growth. + +**Prevention**: Monitor the relationship between database size and page cache capacity. Plan scaling when database approaches 80% of cache size. + +**Key lesson**: As your database grows, eventually it outgrows your instance's memory. This predictably degrades performance. +==== + diff --git a/asciidoc/courses/aura-administration/modules/3-monitoring-instance/lessons/4-garbage-collection/lesson.adoc b/asciidoc/courses/aura-administration/modules/3-monitoring-instance/lessons/4-garbage-collection/lesson.adoc new file mode 100644 index 000000000..116a80dcc --- /dev/null +++ b/asciidoc/courses/aura-administration/modules/3-monitoring-instance/lessons/4-garbage-collection/lesson.adoc @@ -0,0 +1,325 @@ += Garbage Collection +:type: lesson +:order: 7 + + +[.discrete] +== Introduction + +Garbage collection (GC) is the process of reclaiming memory from objects that are no longer needed. + +In this lesson, you will learn how to monitor garbage collection metrics and understand their impact on database performance. + + +== Understanding Garbage Collection + +Neo4j runs on the Java Virtual Machine (JVM), which automatically manages memory through garbage collection. + +During garbage collection: + +* The JVM pauses to clean up unused memory +* Query execution may be delayed +* Resources are reclaimed for future use + +**The goal**: Minimize GC frequency and duration to maintain consistent performance. + + +== Garbage Collection Metrics + +Aura provides several GC metrics to monitor: + + +=== Total GC Time (%) + +The amount of time spent in garbage collection as a percentage of total runtime. + +// UI Description: Shows the amount of time spent in garbage collection as a percentage. High values indicate +// that the instance is running low on memory for the workload and you should consider increasing the size of +// your instance. + +* **Normal**: <2% of time +* **Warning**: 5-10% of time +* **Critical**: >10% of time + +High values indicate memory pressure requiring action. + + +=== Total GC Time (Cumulative) + +The total time since startup spent in garbage collection. + +// UI Description: Shows the total time since startup spent clearing up heap space. High values indicate that +// the instance is running low on memory for the workload and you should consider increasing the size of your instance. + +This continuously increases over time - monitor the rate of increase rather than the absolute value. + +A sudden increase in the rate indicates a problem. + + +=== Young Generation GC + +Young generation GC handles short-lived objects. + +// UI Description (Young Gen %): Shows the amount of time spent in garbage collection as a percentage. Young +// garbage collections typically complete quickly, and the Aura instance waits while the garbage collector is run. +// High values indicate that the instance is running low on memory for the workload and you should consider +// increasing the size of your instance. + +// UI Description (Young Gen Cumulative): Shows the total time since startup spent clearing up heap space for +// short lived objects. Young garbage collections typically complete quickly, and the Aura instance waits while +// the garbage collector is run. High values indicate that the instance is running low on memory for the workload +// and you should consider increasing the size of your instance. + +**Characteristics**: + +* Frequent but fast (milliseconds) +* Collects recently created objects +* Minimal impact on performance + +**Young Gen (%)**: Time spent in young generation GC as a percentage. + +**Young Gen (Cumulative)**: Total time spent in young generation GC. + + +=== Old Generation GC + +Old generation GC handles long-lived objects. + +// UI Description (Old Gen %): Shows the amount of time spent in garbage collection as a percentage. Old garbage +// collections can take time to complete, and the Aura instance waits while the garbage collector is run. High +// values indicate that there are long-running processes or queries that could be optimized, or that your instance +// is running low on CPU or memory for the workload and you should consider reviewing these metrics and possibly +// increasing the size of your instance. + +// UI Description (Old Gen Cumulative): Shows the total time since startup spent clearing up heap space for +// long-lived objects. Old garbage collections can take time to complete, and the Aura instance waits while the +// garbage collector is run. High values indicate that there are long-running processes or queries that could be +// optimized, or that your instance is running low on CPU or memory for the workload and you should consider +// reviewing these metrics and possibly increasing the size of your instance. + +**Characteristics**: + +* Infrequent but slow (seconds) +* Can cause noticeable pauses +* Significant impact on performance if frequent + +**Old Gen (%)**: Time spent in old generation GC as a percentage. + +**Old Gen (Cumulative)**: Total time spent in old generation GC. + + +== Interpreting GC Metrics + + +=== Healthy GC Patterns + +**Young generation**: + +* Frequent (every few seconds to minutes) +* Short duration (milliseconds) +* Minimal percentage of total time + +**Old generation**: + +* Infrequent (minutes to hours) +* Longer duration (seconds) +* Very low percentage of total time + +**Total GC time**: <2% of runtime + + +=== Warning Signs + + +==== High Young Gen GC (>5%) + +Indicates: + +* High object creation rate +* Insufficient heap memory for workload +* Queries creating many temporary objects + +**Impact**: Slight performance degradation + + +==== Frequent Old Gen GC + +Indicates: + +* Long-running queries or processes +* Memory leaks (rare) +* Heap memory nearly full + +**Impact**: Noticeable query pauses + + +==== High Total GC Time (>10%) + +Indicates: + +* Severe memory pressure +* Instance undersized for workload +* Performance severely degraded + +**Impact**: Unacceptable performance, immediate action required + + +==== Increasing Old Gen Cumulative Time + +If old gen GC time is increasing rapidly: + +* Long-running processes consuming memory +* Queries that could be optimized +* Possible memory leak + +**Action**: Review query logs for long-running queries + + +== The Relationship Between Heap and GC + +GC metrics and heap memory are closely related: + +**High heap usage (>80%) + High GC time**: + +* Instance running low on memory +* GC working hard to reclaim space +* Performance degraded +* **Action**: Scale instance or optimize queries + +**High heap usage + Normal GC time**: + +* Memory in use but stable +* Generally acceptable +* Monitor for trends + +**Normal heap usage + High GC time**: + +* Unusual pattern +* May indicate specific query patterns +* **Action**: Investigate query logs + + +== When to Scale Your Instance + +Consider scaling when: + +* Total GC time consistently exceeds 5% +* Old generation GC is frequent (multiple times per hour) +* GC time is increasing over time + +Heap usage is consistently high (>80%) + +[WARNING] +==== +GC pauses can cause queries to timeout or fail. + +Old generation GC taking several seconds can impact user experience. +==== + + +== Optimizing to Reduce GC Pressure + + +=== Query-Level Optimization + +**Reduce object creation**: + +* Limit result set sizes with `LIMIT` +* Use aggregations instead of collecting all results +* Stream results when possible + + +**Shorter transactions**: + +* Keep transactions brief +* Commit or rollback promptly +* Avoid long-running transactions + + +**Memory-efficient queries**: + +* Avoid collecting large collections in memory +* Use `CALL {} IN TRANSACTIONS` for batch operations +* Process data in smaller chunks + + +=== Workload Management + +* Schedule memory-intensive operations during off-peak hours +* Limit concurrent complex queries +* Distribute batch operations over time +* Implement query queuing for heavy workloads + + +=== Application Changes + +* Implement pagination for large result sets +* Cache frequently accessed data at application level +* Reduce query frequency when possible + + +== Monitoring GC Over Time + +Track GC metrics to identify trends: + +**Daily patterns**: GC activity during peak hours is normal + +**Weekly trends**: Gradual increase may indicate growing workload + +**Sudden changes**: Investigate new queries or application changes + +**Seasonal patterns**: Business cycles may affect GC behavior + + +[TIP] +==== +Review GC metrics alongside heap memory and query logs for complete understanding. + +Often, optimizing a few queries can dramatically reduce GC pressure. +==== + + +== Common GC Scenarios + + +=== Normal Operations + +* Total GC: <2% +* Young Gen: Frequent, short +* Old Gen: Rare +* Performance: Excellent + + +=== Memory Pressure + +* Total GC: 5-10% +* Young Gen: Very frequent +* Old Gen: Occasional +* Performance: Degraded + + +=== Critical Memory Issues + +* Total GC: >10% +* Young Gen: Constant +* Old Gen: Frequent +* Performance: Severe degradation + + +[.quiz] +== Check Your Understanding + +include::questions/1-gc-indicators.adoc[leveloffset=+1] + + +[.summary] +== Summary + +You learned how to monitor garbage collection metrics for your Aura instances. + +You learned that high GC time (>10%) indicates memory pressure and that old generation GC can cause noticeable performance pauses. + +The relationship between heap memory usage and GC metrics helps you identify when to scale your instance or optimize queries. + +In the next module, you will learn about database-level metrics including store size, query latency, and transaction patterns. + diff --git a/asciidoc/courses/aura-administration/modules/3-monitoring-instance/lessons/4-garbage-collection/questions/1-gc-indicators.adoc b/asciidoc/courses/aura-administration/modules/3-monitoring-instance/lessons/4-garbage-collection/questions/1-gc-indicators.adoc new file mode 100644 index 000000000..1a70dd384 --- /dev/null +++ b/asciidoc/courses/aura-administration/modules/3-monitoring-instance/lessons/4-garbage-collection/questions/1-gc-indicators.adoc @@ -0,0 +1,41 @@ +[.question] += Garbage Collection Analysis + +Your monitoring shows: + +. Total GC time: 12% of runtime +. Old Gen GC: Occurring every 5 minutes, taking 3-5 seconds each +. Heap usage: 88-92% + +What's causing the user-facing freezes? + +* [ ] Young generation GC is too frequent + +* [x] High heap usage causing excessive Old Gen GC + +* [ ] Normal GC behavior for Neo4j + +* [ ] Database needs restart to clear GC issues + +[TIP,role=hint] +.Hint +==== +Review the GC thresholds in this lesson. What percentage of time in GC indicates a problem, and what's causing the Old Gen GC frequency? +==== + +[TIP,role=solution] +.Solution +==== +**High heap usage causing excessive Old Gen GC** is correct. + +GC analysis: **Total GC: 12%** is critical (should be <2%, warning at 5-10%), **Old Gen GC: Every 5 minutes** is too frequent (should be hours apart), **Old Gen duration: 3-5 seconds** causes user-facing pauses, and **Heap: 88-92%** is too high (should be below 80%). The root cause is high heap memory usage forcing frequent, expensive Old Gen garbage collections. + +**Impact on users**: Application "freezes" for 3-5 seconds every 5 minutes, creating poor user experience and potential query timeouts. + +**Solution**: Scale instance to get more heap memory, which will reduce heap usage percentage, decrease Old Gen GC frequency, and eliminate user-facing pauses. + +Why the other answers are wrong: Young Gen isn't mentioned as problematic, 12% GC time is NOT normal (it's at critical level), and restarting won't fix the underlying memory capacity issue. + +When Old Gen GC is frequent and heap usage is high, the instance needs more memory. +==== + diff --git a/asciidoc/courses/aura-administration/modules/3-monitoring-instance/lessons/6-connection-challenge/lesson.adoc b/asciidoc/courses/aura-administration/modules/3-monitoring-instance/lessons/6-connection-challenge/lesson.adoc new file mode 100644 index 000000000..80dc06bbc --- /dev/null +++ b/asciidoc/courses/aura-administration/modules/3-monitoring-instance/lessons/6-connection-challenge/lesson.adoc @@ -0,0 +1,64 @@ += Connection Issues +:type: challenge +:order: 6 + + +== Scenario + +Starting at 2:00 PM, users began reporting "database connection errors" in the application. + +Error logs show: `Unable to acquire connection from pool within 30 seconds` + +Reviewing Bolt Connection metrics: + +* **Running Connections**: 150 (normal) +* **Idle Connections**: 5 (normally 50-100) +* **Opened (rate)**: 500 connections/minute (normally 20/minute) +* **Closed (rate)**: 450 connections/minute (normally 20/minute) +* **Total Opened**: Growing rapidly +* **Total Closed**: Growing rapidly but slower than opened + +Looking at query logs: + +* Average query duration: Normal (50-100ms) +* No obvious slow queries + +Query rate: Normal + +A deployment occurred at 1:55 PM. + + +== The Challenge + +What's causing this and how should you fix it? + + +include::questions/1-connection-leak.adoc[leveloffset=+1] + + +[.summary] +== Summary + +Constant connection opening/closing with few idle connections indicates improper connection pooling. Fix the application to reuse driver instances rather than creating new connections for each query + +**Likely causes:** + +* Recent deployment changed database connection configuration +* Connection pooling was disabled or misconfigured +* Connection pool max lifetime set too low +* Application creating new driver instances per query + +**The fix:** + +1. **Rollback deployment** if possible for immediate relief +2. **Review connection pool configuration** in the deployment +3. **Ensure proper pooling**: Min 5-10, Max based on concurrent needs +4. **Verify driver reuse**: Application should reuse driver instance + +**Proper connection pooling behavior:** + +* Connections opened once and reused +* Idle connections wait in pool +* Low open/close rates +* Efficient resource usage + diff --git a/asciidoc/courses/aura-administration/modules/3-monitoring-instance/lessons/6-connection-challenge/questions/1-connection-leak.adoc b/asciidoc/courses/aura-administration/modules/3-monitoring-instance/lessons/6-connection-challenge/questions/1-connection-leak.adoc new file mode 100644 index 000000000..233d48881 --- /dev/null +++ b/asciidoc/courses/aura-administration/modules/3-monitoring-instance/lessons/6-connection-challenge/questions/1-connection-leak.adoc @@ -0,0 +1,63 @@ +[.question] += Connection Pool Misconfiguration + +What's causing the connection leak? + +* [ ] Instance has reached connection limits - need to scale + +* [ ] Network issues preventing connections + +* [x] Deployment changed connection pool config - connections not being reused + +* [ ] Database is slow causing connection exhaustion + +[TIP,role=hint] +.Hint +==== +Look at the opened and closed rates compared to normal. What pattern indicates connection pooling issues? +==== + +[TIP,role=solution] +.Solution +==== +**Deployment changed connection pool config - connections not being reused** is correct. + +**The smoking gun**: + +. Opened: 500/min (25x normal of 20/min) +. Closed: 450/min (22.5x normal) +. Idle: Only 5 (normally 50-100) +. Running: Normal + +**What this means**: Application is creating new connections for every query, connections are closed after use (so the total doesn't grow), but the constant open/close is inefficient. Connection pool overhead causes timeouts, and there are no connections waiting in the pool (only 5 idle). + +**The deployment likely**: +[source,python] +---- +# Before (GOOD) - connection pooling +driver = GraphDatabase.driver(uri, auth=auth) +# Reuse driver for all queries + +# After deployment (BAD) - no pooling +for query in queries: + driver = GraphDatabase.driver(uri, auth=auth) # NEW driver each time! + session = driver.session() + session.run(query) + driver.close() # Closes connection +---- + +**Solution**: +1. Rollback deployment +2. Fix connection pool configuration: + - Ensure driver is reused + - Proper min/max pool size + - Appropriate timeouts +3. Redeploy fixed version + +The other options don't fit the pattern: Not at instance limits (connections aren't accumulating), not network issues (connections do work, just inefficiently), and not slowness (running connections are normal). + +**Healthy pattern**: Low open/close rates, stable idle connections, running varies with workload. + +**Unhealthy pattern** (this scenario): High open/close rates, no idle connections, constant churn. +==== + diff --git a/asciidoc/courses/aura-administration/modules/3-monitoring-instance/lessons/8-gc-challenge/lesson.adoc b/asciidoc/courses/aura-administration/modules/3-monitoring-instance/lessons/8-gc-challenge/lesson.adoc new file mode 100644 index 000000000..daeda48b0 --- /dev/null +++ b/asciidoc/courses/aura-administration/modules/3-monitoring-instance/lessons/8-gc-challenge/lesson.adoc @@ -0,0 +1,62 @@ += Garbage Collection Impact +:type: challenge +:order: 8 + + +== Scenario + +Your company runs a customer-facing recommendation engine powered by Neo4j. + +Users are complaining about "the app freezing for a few seconds randomly." + +Investigating the Instance metrics: + +* **Total GC Time**: 8% of runtime +* **Young Gen GC**: 3% (frequent, every few seconds) +* **Old Gen GC**: 5% (occurring every 5-10 minutes, each taking 3-5 seconds) +* **Heap Memory Usage**: 88-92% consistently +* **Page Cache Hit Ratio**: 99% +* **CPU Usage**: 70% + +Correlation with user complaints: + +* Complaints align with timing of Old Gen GC events +* During GC, all queries pause +* Users experience 3-5 second delays + + +== The Challenge + +What will eliminate the user-facing freezes? + + +include::questions/1-gc-impact.adoc[leveloffset=+1] + + +[.summary] +== Summary + +Old Gen GC pauses directly impact user experience when heap usage is too high. Scale the instance to reduce heap pressure and eliminate user-facing freezes + +**Why scaling helps:** + +* More heap memory reduces memory pressure +* Old Gen GC becomes less frequent +* When it does run, it completes faster +* Heap usage drops to healthier 60-70% range + +**Heap usage targets:** + +* **Healthy**: 30-70% normal, spikes to 80% acceptable +* **Warning**: 80-85% sustained +* **Problem**: >85% sustained (causes excessive GC) + +**Alternative approach** (if scaling isn't immediate option): +* Review query logs for memory-intensive queries +* Optimize queries that create large intermediate results +* Reduce concurrent query execution + +But scaling is usually the most effective solution for sustained high heap usage + +This scenario demonstrates how GC metrics directly correlate with user experience. + diff --git a/asciidoc/courses/aura-administration/modules/3-monitoring-instance/lessons/8-gc-challenge/questions/1-gc-impact.adoc b/asciidoc/courses/aura-administration/modules/3-monitoring-instance/lessons/8-gc-challenge/questions/1-gc-impact.adoc new file mode 100644 index 000000000..21faba44f --- /dev/null +++ b/asciidoc/courses/aura-administration/modules/3-monitoring-instance/lessons/8-gc-challenge/questions/1-gc-impact.adoc @@ -0,0 +1,64 @@ +[.question] += Garbage Collection User Impact + +Users complain "the app freezes for a few seconds randomly." + +Metrics show: + +. Total GC: 8% +. Young Gen GC: 3% (every few seconds) +. Old Gen GC: 5% (every 5-10 minutes, each taking 3-5 seconds) +. Heap: 88-92% +. Page Cache: 99% +. CPU: 70% + +What will eliminate the user-facing freezes? + +* [ ] Optimize queries to reduce GC pressure + +* [ ] Reduce concurrent query load + +* [x] Scale instance to get more heap memory + +* [ ] Restart instance to clear GC issues + +[TIP,role=hint] +.Hint +==== +What's causing the multi-second pauses users experience? What's the fastest way to eliminate those pauses? +==== + +[TIP,role=solution] +.Solution +==== +**Scale instance to get more heap memory** is correct. + +**Understanding the freezes**: + +Users experience freezes because Old Gen GC runs every 5-10 minutes, and during Old Gen GC the database PAUSES for 3-5 seconds. All queries wait during this pause, so users see this as "app freezing". + +**Timeline from user perspective**: + +---- +00:00 - Query: 50ms (fast) +01:30 - Query: 45ms (fast) +05:00 - [OLD GEN GC STARTS - 4 SECOND PAUSE] +05:00 - Query: 4,050ms (appears to freeze!) +05:04 - [GC ENDS] +05:05 - Query: 52ms (fast again) +10:00 - [OLD GEN GC AGAIN] +---- + +**Root cause**: Heap at 88-92% forces frequent, expensive Old Gen GC. + +**Solution - Scale instance**: Get more heap memory so heap usage drops to 60-70%, Old Gen GC becomes rare (hours apart), and when it does run it's faster. This eliminates the freezes. + +**After scaling**: Total GC drops to <2%, Old Gen GC is rare (every few hours), users experience no more freezes, and you have consistent performance. + +The other options: Query optimization helps but doesn't eliminate the core issue, reducing load is not a solution (users need to use the system), and restarting doesn't change heap capacity (freezes will return). + +**Key principle**: Old Gen GC pauses directly impact user experience. High heap usage → frequent Old Gen GC → user-facing freezes. + +Scaling is the most effective solution to eliminate these pauses. +==== + diff --git a/asciidoc/courses/aura-administration/modules/3-monitoring-instance/lessons/9-oom-errors/lesson.adoc b/asciidoc/courses/aura-administration/modules/3-monitoring-instance/lessons/9-oom-errors/lesson.adoc new file mode 100644 index 000000000..f2e118973 --- /dev/null +++ b/asciidoc/courses/aura-administration/modules/3-monitoring-instance/lessons/9-oom-errors/lesson.adoc @@ -0,0 +1,477 @@ += Out of Memory Errors +:type: lesson +:order: 9 + + +== Introduction + +Out of Memory (OOM) errors are critical failures that occur when the JVM cannot allocate memory for an operation, even after attempting garbage collection. + +In this lesson, you will learn how to identify, interpret, and respond to OOM errors in your Aura instance. + + +== Understanding Out of Memory Errors + +An OOM error represents the most severe form of memory pressure: + + +**What happens during an OOM error**: + +* JVM attempts to allocate memory for an operation +* Garbage collection runs to free up space +* If still insufficient memory, the JVM throws an OutOfMemoryError +* The operation fails immediately +* Queries and transactions may be terminated + + +**Impact on your database**: + +* Failed queries and transactions +* Degraded application performance +* Potential data inconsistencies +* User-facing errors + + +[WARNING] +==== +OOM errors are **critical incidents** requiring immediate action. + +Unlike high GC time which degrades performance, OOM errors cause direct failures. +==== + + +== Out of Memory Error Metrics + +Aura provides OOM metrics to monitor critical memory failures: + + +=== OOM Error Rate + +The number of Out of Memory errors occurring per minute. + +// UI Description: The number of Out of Memory errors occurring per minute. OOM errors happen when the JVM +// cannot allocate memory for an operation, even after garbage collection. This indicates severe memory +// pressure that can cause queries to fail and transactions to be terminated. Any OOM errors require +// immediate attention as they directly impact application functionality and user experience. + +**Expected value**: 0 errors per minute + +**Any non-zero value**: Critical issue requiring immediate investigation + +This metric shows real-time memory exhaustion events. + + +=== OOM Error Count (Cumulative) + +The total number of Out of Memory errors since the server started. + +// UI Description: The total number of Out of Memory errors since the server started. This value may drop +// if background maintenance is performed by Aura. A non-zero count indicates that the instance has +// experienced critical memory exhaustion and needs investigation. Recurring OOM errors signal that the +// instance size is insufficient for the workload. + +**Expected value**: 0 total errors + +**Any value > 0**: Indicates past critical memory issues + +This helps identify if OOM errors are recurring or were a one-time event. + + +[NOTE] +==== +This value may drop if background maintenance is performed by Aura. + +Track trends over time rather than relying on absolute values. +==== + + +== Types of Out of Memory Errors + + +=== Heap Space Exhausted + +**Most common OOM error** - The heap memory is full and cannot accommodate new objects. + +**Causes**: + +* Query returning extremely large result sets +* Long-running transactions holding memory +* Memory-intensive aggregations +* Insufficient heap size for workload + + +=== GC Overhead Limit Exceeded + +The JVM is spending too much time in garbage collection with minimal memory reclaimed. + +**Causes**: + +* Heap nearly full with live objects +* Constant GC activity yielding little free space +* Workload consistently exceeding available memory + + +=== Direct Buffer Memory + +Less common - native memory outside the heap is exhausted. + +**Causes**: + +* Excessive Bolt connections +* Network buffer exhaustion +* Memory leaks in native code + + +== Why OOM Errors Occur + +OOM errors happen when memory demand exceeds capacity: + + +=== Query-Level Issues + +**Large result sets**: + +* Queries returning millions of nodes/relationships +* Missing `LIMIT` clauses +* Collecting entire graph traversals into memory + + +**Memory-intensive operations**: + +* Cartesian products (missing join conditions) +* Complex aggregations on large datasets +* Inefficient path-finding without constraints + + +**Long-running transactions**: + +* Transactions open for extended periods +* Accumulating changes in memory before commit +* Multiple large operations in single transaction + + +=== Instance-Level Issues + +**Undersized instance**: + +* Heap memory insufficient for normal workload +* Growth in data or query complexity +* Increased concurrent query load + + +**Memory leaks** (rare): + +* Application not closing connections +* Resources not properly released +* Driver configuration issues + + +== The Progression to OOM + +Memory pressure typically follows this pattern: + +**Stage 1: Normal Operations** + +* Heap usage: 40-70% +* GC time: <2% +* OOM errors: 0 + + +**Stage 2: Memory Pressure** + +* Heap usage: 75-85% +* GC time: 5-10% +* OOM errors: 0 + + +**Stage 3: Critical Pressure** + +* Heap usage: >85% +* GC time: >10% +* OOM errors: 0 (but at risk) + + +**Stage 4: Memory Exhaustion** + +* Heap usage: 95-100% +* GC time: >15% +* OOM errors: >0 (critical failure) + + +[TIP] +==== +Monitor heap usage and GC metrics to prevent reaching Stage 4. + +Taking action at Stage 2 or 3 prevents OOM errors from occurring. +==== + + +== Correlating Metrics + +When investigating OOM errors, review multiple metrics together: + + +=== High Heap + High GC + OOM Errors + +**Pattern**: Critical memory exhaustion + +* Heap usage: >90% +* GC time: >10% +* OOM errors: Multiple per minute + +**Root cause**: Instance severely undersized + +**Action**: Immediate instance scaling required + + +=== Spiky OOM Errors + +**Pattern**: Intermittent OOM during specific operations + +* Heap usage: Spikes to 100% periodically +* GC time: Spikes during events +* OOM errors: Occur during spikes + +**Root cause**: Specific memory-intensive queries + +**Action**: Identify and optimize problematic queries in query logs + + +=== Sustained OOM Errors + +**Pattern**: Continuous memory failures + +* Heap usage: Constantly >95% +* GC time: Constantly high +* OOM errors: Continuous + +**Root cause**: Workload permanently exceeds capacity + +**Action**: Scale instance AND optimize queries + + +== Immediate Response to OOM Errors + + +=== When You See OOM Errors + +**1. Assess severity**: + +* Check OOM error rate (errors per minute) +* Review time period affected +* Determine if ongoing or resolved + + +**2. Check application impact**: + +* Are queries currently failing? +* Are users experiencing errors? +* Review application error logs + + +**3. Review correlated metrics**: + +* Current heap usage +* GC time percentage +* Query rate and latency + + +**4. Take immediate action**: + +* Scale instance if errors are ongoing +* Identify problematic queries in query logs +* Consider temporarily reducing workload + + +== Preventing OOM Errors + + +=== Query Optimization + +**Always use LIMIT**: + +```cypher +// Bad - returns entire result set +MATCH (n:Product)-[:SIMILAR_TO]->(rec) +RETURN rec + +// Good - limits memory usage +MATCH (n:Product)-[:SIMILAR_TO]->(rec) +RETURN rec +LIMIT 100 +``` + + +**Paginate large results**: + +```cypher +MATCH (n:Customer) +RETURN n +ORDER BY n.created +SKIP $offset +LIMIT $pageSize +``` + + +**Use aggregations**: + +```cypher +// Bad - collects all nodes +MATCH (n:Order) +WITH collect(n) AS orders +RETURN size(orders) + +// Good - counts directly +MATCH (n:Order) +RETURN count(n) +``` + + +**Process in batches**: + +```cypher +// Use CALL {} IN TRANSACTIONS for large updates +MATCH (n:Customer) +WHERE n.status = 'inactive' +CALL { + WITH n + SET n.archived = true +} IN TRANSACTIONS OF 1000 ROWS +``` + + +=== Transaction Management + +* Keep transactions short +* Commit or rollback promptly +* Avoid accumulating large change sets +* Break batch operations into smaller transactions + + +=== Workload Management + +* Monitor concurrent query load +* Implement query queuing for heavy operations +* Schedule memory-intensive operations during off-peak hours +* Set connection pool limits appropriately + + +=== Instance Sizing + +* Size instance for peak workload plus headroom +* Plan for data growth +* Review metrics regularly +* Scale proactively before reaching critical levels + + +== Investigating OOM Errors + +When OOM errors occur, use query logs to investigate: + + +**Filter query logs for the time period**: + +* Set time range to when OOM errors occurred +* Look for queries with high duration +* Check for failed queries +* Review memory allocation (`alloc bytes`) + + +**Look for problematic patterns**: + +* Queries without `LIMIT` clauses +* High `alloc bytes` values +* Queries with high `page faults` +* Failed queries with "terminated" errors + + +**Identify the culprit**: + +* Which query was running during OOM? +* Is it a one-time query or recurring? +* What user/application initiated it? +* Can it be optimized? + + +[TIP] +==== +Query logs show `alloc bytes` - the memory allocated for each query. + +Queries with extremely high `alloc bytes` are likely candidates for causing OOM errors. +==== + + +== The Relationship Between Metrics + +Understanding how metrics relate helps diagnose issues: + +**Heap → GC → OOM progression**: + +* High heap usage triggers more GC +* Excessive GC indicates approaching limits +* When GC cannot free enough memory, OOM occurs + + +**OOM errors are the outcome, not the cause**: + +* The cause is insufficient memory +* OOM is the symptom when limits are reached +* Fix the underlying memory pressure, not just the error + + +== Recovery and Prevention + + +=== After an OOM Incident + +**1. Immediate recovery**: + +* Scale instance to provide more heap memory +* Clear or optimize problematic queries +* Monitor for recurrence + + +**2. Root cause analysis**: + +* Review query logs for memory-intensive queries +* Check application code for inefficient queries +* Identify data growth or workload changes + + +**3. Long-term prevention**: + +* Implement query optimization best practices +* Set up alerts for heap usage thresholds +* Regular capacity planning reviews +* Monitor trends in memory usage + + +=== Monitoring Strategy + +Set up alerts for early warning: + +* **Alert on Heap >80%**: Warning level +* **Alert on Heap >85%**: Urgent action needed +* **Alert on GC >5%**: Investigation required +* **Alert on any OOM errors**: Critical incident + + +[.quiz] +== Check Your Understanding + +include::questions/1-oom-response.adoc[leveloffset=+1] + + +[.summary] +== Summary + +You learned how to monitor and respond to Out of Memory errors in Aura. + +OOM errors are critical failures that occur when the JVM cannot allocate memory even after garbage collection, causing queries and transactions to fail. + +You learned that OOM errors rate and cumulative count should always be zero, and any non-zero values require immediate investigation. + +The progression from normal operations through memory pressure to OOM errors can be tracked through heap usage and GC metrics, allowing you to take preventive action. + +In the next challenge, you will apply this knowledge to diagnose and resolve an OOM crisis scenario. + diff --git a/asciidoc/courses/aura-administration/modules/3-monitoring-instance/lessons/9-oom-errors/questions/1-oom-response.adoc b/asciidoc/courses/aura-administration/modules/3-monitoring-instance/lessons/9-oom-errors/questions/1-oom-response.adoc new file mode 100644 index 000000000..0e08b93ec --- /dev/null +++ b/asciidoc/courses/aura-administration/modules/3-monitoring-instance/lessons/9-oom-errors/questions/1-oom-response.adoc @@ -0,0 +1,45 @@ +[.question] += OOM Error Response + +Your Aura instance shows the following metrics: + +. OOM Error Rate: 0.5 errors per minute (sustained for 10 minutes) +. OOM Error Count: 15 (and increasing) +. Heap Usage: 96% +. Total GC Time: 18% +. Query Rate: Normal + +What should be your immediate priority? + +* [ ] Wait for garbage collection to resolve the issue + +* [ ] Restart the instance to clear memory + +* [ ] Review query logs to find the problematic query + +* [x] Scale the instance immediately to provide more heap memory + +[TIP,role=hint] +.Hint +==== +Consider the severity of OOM errors and their impact. What needs to happen first - investigation or stabilization? +==== + +[TIP,role=solution] +.Solution +==== +**Scale the instance immediately to provide more heap memory** is correct. + +This is a critical, ongoing incident that requires immediate action: + +**Why immediate scaling is priority #1**: OOM errors are ongoing (0.5 per minute means queries are actively failing), the count is increasing (problem is not resolving itself), heap is at 96% (dangerously close to maximum capacity), GC is at 18% (critical level, should be <2%), and there's user impact (applications are experiencing failures NOW). + +**What happens after scaling**: More heap memory becomes available, heap usage percentage drops (e.g., 96% → 65%), GC pressure decreases immediately, OOM errors stop occurring, and queries can execute successfully. + +**Then investigate**: Once the instance is stable, you can review query logs to identify memory-intensive queries, optimize problematic queries, and implement preventive measures. + +Why the other answers are wrong: **Waiting for GC** - GC is already running at 18% (critical level) and failing to prevent OOM errors, so the problem won't resolve itself. **Restarting** - would cause downtime and doesn't address the root cause (insufficient memory for workload). **Query log review** - important but secondary; first stop the bleeding (ongoing failures), then investigate the cause. + +**The principle**: When OOM errors are ongoing, stabilize first by adding capacity, then investigate to prevent recurrence. +==== + diff --git a/asciidoc/courses/aura-administration/modules/3-monitoring-instance/module.adoc b/asciidoc/courses/aura-administration/modules/3-monitoring-instance/module.adoc new file mode 100644 index 000000000..08cca4b53 --- /dev/null +++ b/asciidoc/courses/aura-administration/modules/3-monitoring-instance/module.adoc @@ -0,0 +1,30 @@ += Monitoring Instance Performance +:order: 3 + + +== Module Overview + +Instance-level metrics provide critical insights into how your database is performing under workload. + +In this module, you will learn how to monitor memory usage, connection patterns, and garbage collection to identify and address performance issues. + +These metrics help you understand when your instance is under stress and what actions to take. + + +You will learn how to: + +* Monitor heap memory usage and identify memory pressure + +* Understand and optimize page cache performance + +* Track Bolt connection patterns and connection pool health + +* Interpret garbage collection metrics and their impact on performance + +* Identify and respond to Out of Memory errors + +* Recognize warning signs that require intervention + + +link:./1-heap-memory/[Ready? Let's go →, role=btn] + diff --git a/asciidoc/courses/aura-administration/modules/4-monitoring-database/lessons/1-store-size/lesson.adoc b/asciidoc/courses/aura-administration/modules/4-monitoring-database/lessons/1-store-size/lesson.adoc new file mode 100644 index 000000000..956a7cd5c --- /dev/null +++ b/asciidoc/courses/aura-administration/modules/4-monitoring-database/lessons/1-store-size/lesson.adoc @@ -0,0 +1,292 @@ += Database Store Size +:type: lesson +:order: 1 + + +[.discrete] +== Introduction + +The database store contains all your graph data, and monitoring its size helps you understand data growth and storage efficiency. + +In this lesson, you will learn how to monitor database store size and identify when compaction is needed. + + +== Understanding Store Size Metrics + +Aura provides two store size metrics: + + +=== Allocated Space + +The total amount of space currently allocated to the database, measured in bytes. + +// UI Description: Allocated space refers to the total amount of space currently allocated to the database, +// measured in bytes. + +This represents the disk space reserved for the database, including: + +* Active data +* Deleted data (not yet reclaimed) +* Internal structures +* Buffer space + + +=== Used Space + +The portion of allocated space that the database is actively utilizing, measured in bytes. + +// UI Description: Used space is the portion of the allocated space that the database is actively utilizing, +// also measured in bytes. It is calculated by subtracting the available allocated space from the total +// allocated space. + +Calculated as: `Allocated Space - Available Allocated Space` + +This represents the actual data stored in the database. + + +== The Difference Between Allocated and Used + +It's normal to see a difference between allocated and used space. + + +=== Why the Difference Exists + +**Delete operations**: When you delete nodes, relationships, or properties, space is marked as available but not immediately reclaimed. + +**Update operations**: Updating properties can leave old values allocated but unused. + +**Database growth**: Neo4j allocates space in chunks for efficiency. + + +=== When the Difference is Acceptable + +A small difference (10-20%) is normal and expected. + + +=== When the Difference is a Problem + +A large difference (>30-40%) indicates: + +* Significant data has been deleted +* Storage is being wasted + +Database compaction would be beneficial + +[NOTE] +==== +Store size metrics are updated every 10 minutes due to the cost of measurement. + +There may be a delay between data additions or deletions and their reflection in the chart. + +// UI Description: Please note that these metrics are updated every 10 minutes due to the cost of measurement. +// Consequently, there might be a delay between data additions or deletions and their reflection in this chart. + +// If there is a big difference between the used space and the allocated space, consider compacting your database: +// https://support.neo4j.com/s/article/23560594934419-How-to-use-Neo4j-Desktop-to-compact-your-Aura-instance-s-database-store +==== + + +== Monitoring Store Size Growth + + +=== Normal Growth Patterns + +**Linear growth**: + +* Steady data addition over time +* Predictable storage needs +* Easy to plan capacity + + +**Stepped growth**: + +* Periodic bulk data imports +* Jumps followed by stable periods +* Normal for batch processing + + +**Stable size**: + +* Data additions balanced with deletions +* Consistent workload +* Steady-state operation + + +=== Concerning Patterns + +**Rapid growth**: + +* Much faster than expected +* May indicate data model issues +* Review data model efficiency + + +**Allocated space growing, used space stable**: + +* Many deletions occurring +* Storage waste increasing +* Compaction needed + + +**Sudden jumps**: + +* Large unexpected data additions +* Possible data issue or attack +* Investigate source of data + + +== Database Compaction + +Compaction reclaims unused space in the database. + + +=== When to Compact + +Consider compaction when: + +* Allocated space is significantly larger than used space (>30% difference) +* Large amounts of data have been deleted +* Storage costs are a concern +* You want to optimize storage usage + + +=== How to Compact + +To compact your Aura database: + +. Export data using `neo4j-admin database dump` +. Create a new instance or clear the existing one +. Import the dump using `neo4j-admin database load` + +Alternatively, use Neo4j Desktop to compact the database (see the link:https://support.neo4j.com/s/article/23560594934419-How-to-use-Neo4j-Desktop-to-compact-your-Aura-instance-s-database-store[Neo4j Support article^]). + + +[WARNING] +.Compaction requires downtime +==== +Compaction requires downtime as it involves export and import operations. + +Plan compaction during maintenance windows. +==== + + +=== After Compaction + +Following compaction: + +* Used space remains the same +* Allocated space reduces to match used space +* Storage costs may decrease +* Performance may improve slightly + + +== Storage Considerations by Data Type + +Different data types consume different amounts of storage: + + +=== Nodes and Relationships + +* Small fixed overhead per node/relationship +* Property storage adds to size +* Labels and relationship types have minimal overhead + + +=== Properties + +**Small properties** (integers, booleans): +* Minimal storage impact +* Stored inline when possible + + +**Strings**: + +* Variable size +* Long strings consume significant space +* Consider string length in data model + + +**Arrays**: + +* Storage based on size and element type +* Large arrays can be expensive + + +**Vector embeddings**: + +* Very storage-intensive +* Hundreds of bytes per vector +* Significant impact on storage for AI/ML workloads + + +=== Indexes + +**Regular indexes**: + +* Moderate storage overhead +* Essential for query performance + + +**Full-text indexes**: + +* Additional storage for text analysis +* Can be substantial for large text properties + + +**Vector indexes**: + +* Very large storage requirements +* Multiple times the size of vector data + + +== Planning for Storage Growth + +Use store size metrics for capacity planning: + + +=== Calculate Growth Rate + +. Note store size at two points in time +. Calculate: `(Size2 - Size1) / Time Difference` +. Project future storage needs + + +=== Plan Instance Upgrades + +* Upgrade before reaching instance storage limits +* Leave buffer (20-30%) for growth +* Consider data retention policies + + +=== Optimize Data Model + +* Remove unnecessary properties +* Use appropriate data types +* Implement data archival strategies +* Consider reference data vs. operational data + + +[TIP] +.Further reading +==== +Learn more about link:https://neo4j.com/docs/aura/auradb/managing-databases/advanced-metrics/#_store_size_metrics[store size metrics^] in the Neo4j documentation. +==== + + +[.quiz] +== Check Your Understanding + +include::questions/1-store-compaction.adoc[leveloffset=+1] + + +[.summary] +== Summary + +You learned how to monitor database store size for your Aura instances. + +You learned the difference between allocated space and used space, and when a large difference indicates the need for database compaction. + +Compaction reclaims unused space after deletions and can reduce storage costs. + +In the next lesson, you will learn about query rate and latency metrics at the database level. + diff --git a/asciidoc/courses/aura-administration/modules/4-monitoring-database/lessons/1-store-size/questions/1-store-compaction.adoc b/asciidoc/courses/aura-administration/modules/4-monitoring-database/lessons/1-store-size/questions/1-store-compaction.adoc new file mode 100644 index 000000000..5d2f69c07 --- /dev/null +++ b/asciidoc/courses/aura-administration/modules/4-monitoring-database/lessons/1-store-size/questions/1-store-compaction.adoc @@ -0,0 +1,43 @@ +[.question] += Compaction Decision + +You observe: + +. Allocated space: 500GB +. Used space: 280GB +. Difference: 220GB (44% unused) + +This occurred after a major data cleanup where 3 million nodes were deleted last month. + +When should you perform database compaction? + +* [ ] Never - the space will be reused automatically + +* [ ] Immediately - any unused space should be reclaimed + +* [x] When ready for planned downtime - the 44% gap justifies compaction + +* [ ] Wait until the gap reaches 75% + +[TIP,role=hint] +.Hint +==== +Review the guidance on when the difference between allocated and used space indicates compaction is beneficial. +==== + +[TIP,role=solution] +.Solution +==== +**When ready for planned downtime - the 44% gap justifies compaction** is correct. + +Compaction guidance: **Small gap (<30%)** is normal with no action needed, **Large gap (>30-40%)** means compaction is beneficial, and **Very large gap (>50%)** means compaction is recommended. + +At 44% unused space (220GB), compaction is justified because there's significant wasted storage (220GB), storage costs are accumulating, space after major deletions won't be reused quickly, and 44% is well above the 30-40% threshold. + +**Compaction benefits**: Reclaim 220GB of storage, reduce storage costs, and gain slight performance improvement. **Important**: Compaction requires downtime, so plan during a maintenance window. + +Why the other answers are wrong: Space after major deletions won't be reused efficiently, you don't need to compact immediately (plan for a maintenance window), and 44% already justifies compaction (don't wait for 75%). + +**When to compact**: When there's a large gap between allocated and used space (>30-40%), after major data deletions, when storage costs are a concern, or during scheduled maintenance windows. +==== + diff --git a/asciidoc/courses/aura-administration/modules/4-monitoring-database/lessons/2-query-metrics/lesson.adoc b/asciidoc/courses/aura-administration/modules/4-monitoring-database/lessons/2-query-metrics/lesson.adoc new file mode 100644 index 000000000..898ba9f15 --- /dev/null +++ b/asciidoc/courses/aura-administration/modules/4-monitoring-database/lessons/2-query-metrics/lesson.adoc @@ -0,0 +1,314 @@ += Query Rate and Latency +:type: lesson +:order: 3 + + +[.discrete] +== Introduction + +Understanding query performance at the database level helps you identify slow queries and performance trends. + +In this lesson, you will learn how to monitor query rates, failed queries, and query latency percentiles. + + +== Query Rate Metrics + +The Database tab provides query rate metrics at the database level. + + +=== Queries Per Minute + +The number of successful queries executed per minute on the database. + +// UI Description: Queries per minute is the number of successful queries executed per minute on this database. + +This metric shows: + +* Overall database query load +* Usage patterns over time +* Impact of application changes + + +=== Failed Queries Per Minute + +The number of failed queries executed per minute on the database. + +// UI Description: Failed queries per minute is the number of falied queries executed per minute on this database. + +Failed queries may result from: + +* Syntax errors in Cypher +* Constraint violations +* Query timeouts +* Permission issues +* Out-of-memory errors + + +== Interpreting Query Rates + + +=== Normal Query Rate Patterns + +**Healthy database**: + +* Queries per minute: Stable or gradually increasing +* Failed queries: Very low (< 1% of total) +* Consistent with application usage + + +=== Warning Signs + + +==== High Failed Query Rate + +If failed queries are a significant portion of total queries: + +* Application issues or bugs +* Query syntax errors +* Resource constraints causing timeouts +* Permission configuration issues + +**Action**: Review query logs to identify failing queries and root causes. + + +==== Sudden Changes in Query Rate + +Unexpected changes in query rate: + +**Sudden increase**: + +* New feature deployment +* Increased application usage +* Possible retry storms +* DDoS or abuse + +**Sudden decrease**: + +* Application outage +* Network issues +* Scheduled downtime +* Users unable to connect + + +==== Growing Failed Queries + +Increasing failed query rate over time: + +* Growing resource pressure +* Degrading performance +* Application issues spreading +* Query timeouts becoming common + +**Action**: Investigate failed queries and address root causes. + + +== Query Latency Metrics + +Query latency metrics show query execution time in milliseconds. + + +[NOTE] +.Neo4j version requirement +==== +Query latency percentiles are available from Neo4j Version 5. +==== + + +=== 50th Percentile (Median) + +The query execution time where 50% of queries executed faster than the reported time. + +// UI Description: The query execution time in milliseconds where 50% of queries executed faster than the +// reported time. This also corresponds to the median of the query execution time. Available from Neo4j Version 5. + +This represents the typical query performance for your workload. + + +=== 75th Percentile + +The query execution time where 75% of queries executed faster than the reported time. + +// UI Description: The query execution time in milliseconds where 75% of queries executed faster than the +// reported time. Available from Neo4j Version 5. + +This shows how most queries perform, excluding the slowest 25%. + + +=== 99th Percentile + +The query execution time where 99% of queries executed faster than the reported time. + +// UI Description: The query execution time in milliseconds where 99% of queries executed faster than the +// reported time. Available from Neo4j Version 5. + +This captures the performance of your slowest queries. + +[TIP] +.Why 99th percentile matters +==== +The 99th percentile is crucial for user experience - even if most queries are fast, slow outliers can frustrate users. +==== + + +== Interpreting Query Latency + + +=== Understanding Percentiles + +**Low 50th percentile (< 100ms)**: + +* Typical queries are fast +* Good user experience for most operations + + +**High 99th percentile (> 1000ms)**: + +* Some queries are slow +* Occasional poor user experience +* May indicate queries needing optimization + + +**Wide spread (50th vs. 99th)**: + +* Inconsistent query performance +* Some queries much slower than others +* Consider query optimization + + +=== Healthy Latency Patterns + +**Simple read queries**: 1-50ms + +**Complex traversals**: 50-500ms + +**Large aggregations**: 500-5000ms + +**Batch operations**: Seconds to minutes + + +These are guidelines - your acceptable latency depends on use case. + + +=== Warning Signs + + +==== Increasing Latency Over Time + +If latency percentiles are increasing: + +* Database growing beyond available resources +* Page cache hit ratio may be declining +* Indexes may be needed or not being used +* Query patterns changing + +**Action**: Review page cache metrics and query logs. + + +==== High 99th Percentile + +If 99th percentile is very high (>10x median): + +* Some queries are outliers +* Specific queries need optimization +* May cause timeout errors + +**Action**: Review query logs to identify slow queries. + + +==== All Percentiles High + +If all percentiles (50th, 75th, 99th) are high: + +* Systemic performance issues +* Instance undersized for workload +* General optimization needed + +**Action**: Review all metrics (CPU, memory, page cache) and consider scaling. + + +== Correlating Metrics + +Query rate and latency metrics work together: + + +=== High Rate + Low Latency + +* Efficient queries executing frequently +* Well-optimized database +* Healthy state + + +=== High Rate + High Latency + +* Many slow queries executing +* Performance degraded +* Resource pressure likely +* **Action**: Optimization or scaling needed + + +=== Low Rate + High Latency + +* Few queries but they're slow +* Specific queries need optimization +* May not be resource issue +* **Action**: Focus on query optimization + + +=== Growing Failed Queries + Increasing Latency + +* Performance degradation +* Queries timing out +* Resource exhaustion likely +* **Action**: Immediate investigation required + + +== Using Query Metrics for Optimization + + +=== Identify Optimization Opportunities + +. Monitor 99th percentile latency +. Look for spikes or trends +. Correlate with query logs +. Identify specific slow queries +. Optimize those queries + + +=== Measure Impact of Changes + +Before and after optimization: + +. Record latency percentiles +. Make optimization changes +. Monitor percentiles for improvement +. Validate success + + +=== Set Latency Targets + +Define acceptable latency for your application: + +* User-facing queries: < 100ms (50th percentile) +* Reports: < 5000ms (99th percentile) +* Batch operations: Custom limits + +Alert when targets are exceeded. + + +[.quiz] +== Check Your Understanding + +include::questions/1-query-latency.adoc[leveloffset=+1] + + +[.summary] +== Summary + +You learned how to monitor query rates and latency for your Aura databases. + +You learned that failed queries should be very low (<1% of total) and that the 99th percentile latency is crucial for user experience. + +Query latency percentiles help you identify slow queries and measure the impact of optimizations. + +In the next lesson, you will learn about monitoring transactions and identifying transaction issues. + diff --git a/asciidoc/courses/aura-administration/modules/4-monitoring-database/lessons/2-query-metrics/questions/1-query-latency.adoc b/asciidoc/courses/aura-administration/modules/4-monitoring-database/lessons/2-query-metrics/questions/1-query-latency.adoc new file mode 100644 index 000000000..e19428f86 --- /dev/null +++ b/asciidoc/courses/aura-administration/modules/4-monitoring-database/lessons/2-query-metrics/questions/1-query-latency.adoc @@ -0,0 +1,46 @@ +[.question] += Query Latency Interpretation + +Your database metrics show: + +. 50th percentile latency: 35ms +. 75th percentile latency: 95ms +. 99th percentile latency: 8,200ms +. Failed queries: <1% + +What does this tell you? + +* [ ] Overall performance is excellent - no action needed + +* [ ] The database needs scaling immediately + +* [x] Most queries are fast, but specific slow queries need optimization + +* [ ] Network latency is causing issues + +[TIP,role=hint] +.Hint +==== +Compare the 50th percentile (median) to the 99th percentile. What does a large gap indicate? +==== + +[TIP,role=solution] +.Solution +==== +**Most queries are fast, but specific slow queries need optimization** is correct. + +Latency analysis: **50th percentile: 35ms** is excellent (half of queries are very fast), **75th percentile: 95ms** is good (75% of queries under 100ms), and **99th percentile: 8,200ms** is a problem (slowest 1% taking 8+ seconds). **The gap** between median (35ms) and 99th percentile (8,200ms) is huge - over 200x difference! + +This pattern indicates the database and instance are performing well overall, but specific queries (the slowest 1%) are extremely slow, these outlier queries need optimization, and scaling won't fix poorly written queries. + +**Action**: +1. Filter query logs for duration > 5000ms +2. Identify the specific slow queries +3. Optimize those queries (add indexes, rewrite, etc.) +4. Measure improvement + +Why the other answers are wrong: Can't ignore 8-second queries affecting users, scaling won't fix inefficient queries, and network latency would affect all queries, not just 1%. + +**Key lesson**: When 99th percentile is much higher than median, optimize specific queries rather than scaling. +==== + diff --git a/asciidoc/courses/aura-administration/modules/4-monitoring-database/lessons/2-store-challenge/lesson.adoc b/asciidoc/courses/aura-administration/modules/4-monitoring-database/lessons/2-store-challenge/lesson.adoc new file mode 100644 index 000000000..be3e322ba --- /dev/null +++ b/asciidoc/courses/aura-administration/modules/4-monitoring-database/lessons/2-store-challenge/lesson.adoc @@ -0,0 +1,61 @@ += Database Compaction +:type: challenge +:order: 2 + + +== Scenario + +You're reviewing monthly database metrics and notice something interesting about your storage usage. + +**Current metrics:** + +* **Allocated Space**: 180GB +* **Used Space**: 95GB +* **Difference**: 85GB (47% of allocated space unused) + +**History:** + +* 3 months ago: Allocated 120GB, Used 110GB (8% difference) +* 2 months ago: Allocated 160GB, Used 120GB (25% difference) +* Last month: Allocated 180GB, Used 95GB (47% difference) + +**Context:** + +* Last month, the development team performed a major data cleanup +* Deleted approximately 2 million obsolete nodes and their relationships +* Database functionality is normal +* Query performance is good +* Storage costs are becoming noticeable + + +== The Challenge + +What should you do about the large difference between allocated and used space? + + +include::questions/1-compaction-decision.adoc[leveloffset=+1] + + +[.summary] +== Summary + +When allocated space exceeds used space by more than 30-40%, compact the database to reclaim wasted storage. This is especially beneficial after major data deletions + +**Compaction process:** + +1. Schedule during maintenance window (requires downtime) +2. Export database using `neo4j-admin database dump` +3. Create new instance or clear existing +4. Import dump using `neo4j-admin database load` +5. Verify data integrity + +**After compaction:** + +* Allocated space reduces to match used space +* Storage costs decrease +* Slight performance improvement possible + +Reclaimed space available for future growth + +Compacting from 180GB to 95GB would save 85GB of storage costs. + diff --git a/asciidoc/courses/aura-administration/modules/4-monitoring-database/lessons/2-store-challenge/questions/1-compaction-decision.adoc b/asciidoc/courses/aura-administration/modules/4-monitoring-database/lessons/2-store-challenge/questions/1-compaction-decision.adoc new file mode 100644 index 000000000..0b328a6bb --- /dev/null +++ b/asciidoc/courses/aura-administration/modules/4-monitoring-database/lessons/2-store-challenge/questions/1-compaction-decision.adoc @@ -0,0 +1,57 @@ +[.question] += Compaction Cost-Benefit + +After reviewing monthly metrics: + +. Allocated: 180GB +. Used: 95GB +. Gap: 85GB (47% unused) + +History shows allocated space grew from 120GB to 180GB while used space declined from 110GB to 95GB after a major data cleanup last month (2 million nodes deleted). + +What should you do? + +* [ ] Nothing - space will be automatically reclaimed + +* [ ] Immediately scale down the instance + +* [x] Schedule compaction during next maintenance window + +* [ ] Wait until gap reaches 75% before acting + +[TIP,role=hint] +.Hint +==== +Consider the size of the gap, the cause (major deletion), and the appropriate timing for compaction. +==== + +[TIP,role=solution] +.Solution +==== +**Schedule compaction during next maintenance window** is correct. + +**Analysis**: + +**The gap (47%)** is significant: Guideline suggests >30-40% gap justifies compaction, you have 85GB of unused storage, after major deletion space won't be reused efficiently, and storage costs are accumulating. + +**Why compaction makes sense**: Major data cleanup (2M nodes deleted), large persistent gap, reduces from 180GB to ~100GB, saves 80GB of storage costs, and provides slight performance improvement. + +**Why "during maintenance window"**: Compaction requires downtime (30min-2hours depending on size), it's an Export → Recreate → Import process, you should plan during low-usage period, and coordinate with stakeholders. + +**Compaction process**: +1. Schedule maintenance window +2. Export database: `neo4j-admin database dump` +3. Create new instance or clear existing +4. Import dump: `neo4j-admin database load` +5. Verify data integrity +6. Resume operations + +**After compaction**: Allocated space will be ~100GB (matches used), storage costs are reduced, and you have more efficient storage. + +The other options: Space won't be automatically reclaimed after major deletions, you can't scale down with data still there, and 47% already warrants action (don't wait for 75%). + +**Key decision factors**: Gap size is >40% (YES - 47%), it's after major deletion (YES), storage costs matter (YES), and you can schedule downtime (so schedule it). + +**Result**: Compaction is justified, schedule it appropriately. +==== + diff --git a/asciidoc/courses/aura-administration/modules/4-monitoring-database/lessons/3-transactions/lesson.adoc b/asciidoc/courses/aura-administration/modules/4-monitoring-database/lessons/3-transactions/lesson.adoc new file mode 100644 index 000000000..e25ea1bf6 --- /dev/null +++ b/asciidoc/courses/aura-administration/modules/4-monitoring-database/lessons/3-transactions/lesson.adoc @@ -0,0 +1,296 @@ += Transaction Monitoring +:type: lesson +:order: 5 + + +[.discrete] +== Introduction + +Transactions are the unit of work in Neo4j, and monitoring transaction patterns helps you understand database activity and identify issues. + +In this lesson, you will learn how to monitor active transactions, transaction rates, and identify transaction problems. + + +== Understanding Transactions + +Every operation in Neo4j occurs within a transaction: + +* Queries that modify data (write transactions) +* Queries that only read data (read transactions) +* Both explicit and implicit transactions + + +Healthy transaction patterns indicate: + +* Proper application behavior +* Good transaction management +* Efficient data operations + + +== Transaction Metrics + +Aura provides several transaction-related metrics: + + +=== Active Transactions + +The number of currently active transactions. + +// UI Description (Read): The number of currently active read transactions. +// UI Description (Write): The number of currently active write transactions. + +**Read transactions**: Currently executing read operations + +**Write transactions**: Currently executing write operations + +These metrics show real-time database activity. + + +=== Committed Transactions + +**Committed (count)**: Total transactions committed since server startup + +// UI Description (Committed count): The total number of committed transactions since the server was started. +// This value may drop if background maintenance is performed by Aura. + +**Committed (rate)**: Transactions being committed per minute + +// UI Description (Committed rate): The number of transactions being committed per minute. + +Successful transaction completion. + + +=== Rolled Back Transactions + +**Rollbacks (count)**: Total transactions rolled back + +// UI Description (Rollbacks count): The total number of rolled-back transactions. This value may drop if +// background maintenance is performed by Aura. + +**Rollbacks (rate)**: Transaction rollbacks per minute + +// UI Description (Rollbacks rate): The number of transaction rollbacks happening per minute. + +Indicates transaction failures or explicit rollbacks. + + +=== Peak Concurrent Transactions + +The highest number of concurrent transactions detected since server started. + +// UI Description: The highest number of concurrent transactions detected since the server started. This value +// may drop if background maintenance is performed by Aura. + +Shows maximum transaction concurrency experienced. + + +=== Last Committed Transaction ID + +The ID of the last committed transaction. + +// UI Description: The id of the last committed transaction. Track this for primary cluster members of your Aura +// instance. It should show overlapping, ever-increasing lines and if one of the lines levels off or falls behind, +// it is clear that this cluster member is no longer replicating data, and action is needed to rectify the situation. + +**For clustered instances**: Track this for each primary cluster member. + + +[NOTE] +.Background maintenance +==== +Some values may drop if background maintenance is performed by Aura. + +This is normal and doesn't indicate a problem. +==== + + +== Interpreting Transaction Metrics + + +=== Healthy Transaction Patterns + +**Active transactions**: + +* Fluctuates with workload +* Read transactions > write transactions (typical) +* Returns to low baseline between operations + + +**Commit rate**: + +* Matches application activity +* Consistent with query rate +* Follows business hours patterns + + +**Rollback rate**: + +* Very low (< 1% of commits) +* Occasional rollbacks are normal + + +=== Warning Signs + + +==== High Rollback Rate + +If rollbacks are a significant portion of commits: + +* Application errors or bugs +* Constraint violations +* Deadlock conditions +* Conflicting concurrent writes + +**Action**: Review application logs and query logs for errors. + + +==== Many Long-Running Active Transactions + +High number of active transactions that persist: + +* Long-running queries +* Transactions not being committed +* Possible connection leaks holding transactions open + +**Impact**: + +* Holds memory +* May block other operations +* Can cause performance issues + + +==== Growing Active Transactions + +If active transaction count continuously grows: + +* Transactions not being closed properly +* Application bug (missing commits/rollbacks) +* Connection pool issues + +**Action**: Review application transaction management. + + +==== Last Transaction ID Not Advancing (Clustered) + +In Business Critical clustered instances: + +If one cluster member's transaction ID levels off or falls behind: + +* That member is no longer replicating +* Cluster synchronization issue +* Action needed to rectify replication + +**Action**: Contact Neo4j support for cluster issues. + + +== Transaction Best Practices + + +=== Application-Level Best Practices + +**Keep transactions short**: + +* Commit or rollback promptly +* Don't hold transactions during user input +* Avoid long-running transactions when possible + + +**Handle errors properly**: + +[source,python] +---- +with driver.session() as session: + try: + result = session.execute_write(create_data, params) + # Transaction automatically committed + except Exception as e: + # Transaction automatically rolled back + logger.error(f"Transaction failed: {e}") +---- + + +**Use appropriate transaction types**: + +* Use read transactions for read-only operations +* Use write transactions only when modifying data +* Read transactions can run concurrently more efficiently + + +=== Monitoring Best Practices + +* Monitor rollback rates for application health +* Alert on growing active transaction counts +* Track commit rates to understand workload +* For clusters, monitor transaction ID progression + + +== Common Transaction Issues + + +=== Issue: High Rollback Rate + +**Symptoms**: + +* Rollbacks >5% of commits +* Application errors +* Data not being saved + + +**Causes**: + +* Constraint violations (duplicate data) +* Concurrent update conflicts +* Application bugs + + +Action: === Issue: Transaction Timeouts + +**Symptoms**: + +* Transactions failing with timeout errors +* Long-running active transactions +* User complaints about failures + + +**Causes**: + +* Queries taking too long +* Insufficient resources +* Lock contention + + +Action: + +=== Issue: Growing Active Transactions + +**Symptoms**: + +* Active transaction count increasing over time +* Not returning to baseline +* Memory usage increasing + + +**Causes**: + +* Transactions not being closed +* Application connection leak +* Missing commit/rollback in code + + +Action: + + +read::Continue to next lesson[] + + +[.summary] +== Summary + +You learned how to monitor transaction patterns for your Aura databases. + +You learned that rollbacks should be very low (<1% of commits) and that growing active transaction counts indicate transaction management issues. + +For clustered instances, monitoring the last committed transaction ID helps identify replication issues. + +In the next lesson, you will learn about monitoring checkpoint and replan events. + diff --git a/asciidoc/courses/aura-administration/modules/4-monitoring-database/lessons/4-query-challenge/lesson.adoc b/asciidoc/courses/aura-administration/modules/4-monitoring-database/lessons/4-query-challenge/lesson.adoc new file mode 100644 index 000000000..73bf4d1b5 --- /dev/null +++ b/asciidoc/courses/aura-administration/modules/4-monitoring-database/lessons/4-query-challenge/lesson.adoc @@ -0,0 +1,59 @@ += Query Performance Investigation +:type: challenge +:order: 4 + + +== Scenario + +Users are complaining that "reports are slow" in your application. + +Investigating the Database metrics for the past 24 hours: + +* **Queries per minute**: 800 (normal) +* **Failed queries per minute**: 2-3 (< 1%, normal) +* **Query Latency 50th percentile**: 45ms (acceptable) +* **Query Latency 75th percentile**: 120ms (acceptable) +* **Query Latency 99th percentile**: 8,500ms (concerning) + +The reports that users mention are generated by a dashboard that runs every hour. + +Looking at query logs filtered for "duration > 5000ms": + +* Same 3 queries appear repeatedly +* All 3 are dashboard queries +* Each returns 50,000+ rows +* Average duration: 7,000-9,000ms +* These queries run 24 times per day (hourly) + + +== The Challenge + +Based on this analysis, how should you address the performance complaints? + + +include::questions/1-percentile-analysis.adoc[leveloffset=+1] + + +[.summary] +== Summary + +When 99th percentile latency is much higher than median, investigate and optimize the specific slow queries rather than scaling. Most queries are performing well—focus on the outliers +3. ✅ Filtered query logs for slow queries (>5000ms) +4. ✅ Found specific problematic queries +5. ✅ Ready to optimize those queries + +**Common optimizations for dashboard queries:** + +* Add appropriate indexes +* Use aggregations instead of returning all rows +* Implement pagination or limiting +* Pre-compute results for frequently run reports +* Cache results at application level + +**When to scale vs. optimize:** + +* **Optimize first**: Specific slow queries, high 99th percentile, low 50th percentile +* **Scale instance**: All percentiles high, resources maxed, queries already optimized + +The 99th percentile is crucial for identifying outlier queries that need attention. + diff --git a/asciidoc/courses/aura-administration/modules/4-monitoring-database/lessons/4-query-challenge/questions/1-percentile-analysis.adoc b/asciidoc/courses/aura-administration/modules/4-monitoring-database/lessons/4-query-challenge/questions/1-percentile-analysis.adoc new file mode 100644 index 000000000..bd919d2de --- /dev/null +++ b/asciidoc/courses/aura-administration/modules/4-monitoring-database/lessons/4-query-challenge/questions/1-percentile-analysis.adoc @@ -0,0 +1,68 @@ +[.question] += Percentile-Based Optimization + +Users complain "reports are slow." + +Database metrics (last 24 hours): + +. Queries/min: 800 (normal) +. Failed queries: 2-3/min (<1%, normal) +. 50th percentile: 45ms (acceptable) +. 75th percentile: 120ms (acceptable) +. 99th percentile: 8,500ms (concerning) + +Query logs filtered for duration >5000ms show the same 3 dashboard queries repeatedly, each returning 50,000+ rows. + +How should you address this? + +* [ ] Scale instance - overall performance is degraded + +* [x] Optimize the 3 specific slow queries identified in logs + +* [ ] Reduce query frequency to lower load + +* [ ] Add more indexes across the database + +[TIP,role=hint] +.Hint +==== +Compare the median query time to the 99th percentile. What does the gap tell you about whether this is a systemic or specific issue? +==== + +[TIP,role=solution] +.Solution +==== +**Optimize the 3 specific slow queries identified in logs** is correct. + +**Reading the percentiles**: + +**The story the metrics tell**: 50% of queries are <45ms (EXCELLENT), 75% of queries are <120ms (GOOD), and 99% of queries are <8,500ms (but that 1% are VERY slow). + +**Gap analysis**: Median to 99th percentile shows 45ms → 8,500ms (189x difference!). This massive gap means most queries are fine, but specific queries are terrible. + +**Evidence**: Query logs show 3 specific dashboard queries, the same queries repeatedly appearing, each returning 50,000+ rows. These are the slow 1%. + +**Optimization strategy**: +1. **Identify**: Already done - 3 dashboard queries +2. **Analyze**: Likely issues: + ```cypher + // Probably returning too much data + MATCH (u:User)-[:PURCHASED]->(p:Product) + RETURN u, p // Returns 50,000+ rows! + ``` +3. **Fix**: Add pagination + ```cypher + MATCH (u:User)-[:PURCHASED]->(p:Product) + RETURN u, p + ORDER BY p.date DESC + SKIP $offset LIMIT 100 + ``` +4. **Measure**: Verify improvement in 99th percentile + +**Expected results after fix**: 99th percentile drops from 8,500ms to 200ms, user complaints stop, and the 99% of users who were already happy are joined by the last 1% who are now fixed. + +The other options miss the point: Scaling won't fix poorly written queries, 99% of queries are already fast (instance is fine), reducing frequency doesn't help users who need reports, and random indexing is a shotgun approach (target the specific queries instead). + +**Key lesson**: When 99th percentile >> median, you have specific query problems, not systemic performance issues. Fix the outliers. +==== + diff --git a/asciidoc/courses/aura-administration/modules/4-monitoring-database/lessons/6-transaction-challenge/lesson.adoc b/asciidoc/courses/aura-administration/modules/4-monitoring-database/lessons/6-transaction-challenge/lesson.adoc new file mode 100644 index 000000000..705a1beb7 --- /dev/null +++ b/asciidoc/courses/aura-administration/modules/4-monitoring-database/lessons/6-transaction-challenge/lesson.adoc @@ -0,0 +1,76 @@ += Transaction Rollback Issue +:type: challenge +:order: 6 + + +== Scenario + +The application development team reports that users are experiencing "random save failures" when creating new records. + +Reviewing Database metrics: + +* **Committed transactions rate**: 200/minute (normal) +* **Rollback rate**: 40/minute (20% of commits - concerning!) +* **Active write transactions**: 5-10 (normal) +* **Query rate**: 800/minute (normal) +* **Failed queries per minute**: 35 (matches rollback rate) + +Filtering query logs for "status: failed" shows the same pattern repeatedly: + +[source,cypher] +---- +CREATE (p:Product { + id: $productId, + name: $name, + sku: $sku +}) +---- + +Error message: `Constraint violation: Node with property 'id' = '12345' already exists` + +Looking at the application logs, you see that when a user clicks "Save" multiple times quickly, the application sends multiple create requests with the same product ID. + + +== The Challenge + +What's causing this and how should you fix it? + + +include::questions/1-rollback-analysis.adoc[leveloffset=+1] + + +[.summary] +== Summary + +High rollback rates usually indicate application logic issues. Investigate constraint violations and fix the application to prevent duplicates rather than changing database configuration + +1. **Disable button after first click** to prevent multiple submissions +2. **Check for existence** before creating: + +[source,cypher] +---- +MERGE (p:Product {id: $productId}) +ON CREATE SET p.name = $name, p.sku = $sku +ON MATCH SET p.updated = true +RETURN p +---- + +3. **Implement idempotency** tokens in the application +4. **Handle errors gracefully** on the frontend + +**Why not change the database?** + +* The constraint is correctly preventing duplicate data +* Removing the constraint would allow data corruption +* The database is working as designed + +**When rollbacks ARE a database issue:** + +* Deadlocks from lock contention +* Transaction timeouts from resource constraints +* Cluster synchronization issues + +**But in this case:** Application needs to handle duplicate submissions properly. + +Always review the specific errors causing rollbacks to identify the true root cause. + diff --git a/asciidoc/courses/aura-administration/modules/4-monitoring-database/lessons/6-transaction-challenge/questions/1-rollback-analysis.adoc b/asciidoc/courses/aura-administration/modules/4-monitoring-database/lessons/6-transaction-challenge/questions/1-rollback-analysis.adoc new file mode 100644 index 000000000..d2e7e7e1f --- /dev/null +++ b/asciidoc/courses/aura-administration/modules/4-monitoring-database/lessons/6-transaction-challenge/questions/1-rollback-analysis.adoc @@ -0,0 +1,87 @@ +[.question] += High Rollback Rate Diagnosis + +Users report "random save failures" when creating records. + +Transaction metrics: + +. Committed: 200/minute (normal) +. Rollbacks: 40/minute (20% of commits - HIGH) +. Failed queries: 35/minute (matches rollback rate) + +Query logs (status: failed): +---- +CREATE (p:Product {id: $productId, name: $name, sku: $sku}) +Error: Constraint violation: Node with property 'id' = '12345' already exists +---- + +Application logs show users clicking "Save" multiple times when the form is slow to respond. + +What's causing this and how should you fix it? + +* [ ] Database constraint is too strict - remove it + +* [ ] Instance is too slow - scale up + +* [x] Application doesn't prevent duplicate submissions - fix the UI + +* [ ] Network latency causing duplicate requests + +[TIP,role=hint] +.Hint +==== +Look at the error message and user behavior. Is this a database problem or an application problem? +==== + +[TIP,role=solution] +.Solution +==== +**Application doesn't prevent duplicate submissions - fix the UI** is correct. + +**Root cause analysis**: + +**What's happening**: +1. User clicks "Save" on form +2. Response is slow (processing...) +3. User clicks "Save" again (impatient) +4. Application sends both requests +5. First request succeeds (creates Product id=12345) +6. Second request fails (id=12345 already exists!) +7. User sees error message + +**Why 20% rollback rate**: Not all users double-click, but many do, which explains the 20% rate (1 in 5 submissions duplicated). This matches the failed query rate exactly, and the constraint is working correctly to prevent duplicates. + +**The fix is APPLICATION-SIDE**: + +**Option 1**: Disable button after click +[source,javascript] +---- +button.onclick = async () => { + button.disabled = true; // Prevent double-click + await saveProduct(); + button.disabled = false; +} +---- + +**Option 2**: Use idempotent operations +[source,cypher] +---- +// Instead of CREATE (fails on duplicate) +MERGE (p:Product {id: $productId}) +ON CREATE SET p.name = $name, p.sku = $sku +ON MATCH SET p.updated = timestamp() +---- + +**Option 3**: Add loading state +[source,javascript] +---- +if (isSubmitting) return; // Prevent double-submission +---- + +**Why this is correct**: The error message clearly shows a constraint violation, user behavior (double-clicking) causes duplicates, the database is working correctly, and the application needs to handle user behavior. + +The other options are wrong: **Removing constraint** would allow duplicate data (bad!), **Scaling** doesn't prevent duplicate submissions, and **Network** issues would cause other symptoms, not just duplicates. + +**Key principle**: High rollback rates usually indicate application logic issues, not database problems. The constraint is protecting data integrity - fix the application to prevent duplicate submissions. +==== + diff --git a/asciidoc/courses/aura-administration/modules/4-monitoring-database/lessons/7-checkpoints-replan/lesson.adoc b/asciidoc/courses/aura-administration/modules/4-monitoring-database/lessons/7-checkpoints-replan/lesson.adoc new file mode 100644 index 000000000..c2f70ce5c --- /dev/null +++ b/asciidoc/courses/aura-administration/modules/4-monitoring-database/lessons/7-checkpoints-replan/lesson.adoc @@ -0,0 +1,313 @@ += Checkpoint and Replan Events +:type: lesson +:order: 7 + + +[.discrete] +== Introduction + +Checkpoints and replan events are internal database operations that can indicate performance issues or configuration problems. + +In this lesson, you will learn how to monitor these events and what they tell you about database health. + + +== Checkpoint Events + +Checkpoints are the process of writing committed transaction data from memory to disk. + + +=== Understanding Checkpoints + +Checkpoints ensure: + +* Data durability +* Transaction data is persisted +* Recovery point in case of failure + + +Neo4j performs checkpoints periodically to maintain data integrity. + + +=== Checkpoint Metrics + +**Total count**: Number of checkpoint events since server startup + +// UI Description (Total count): The total number of checkpoint events executed since the server started. +// This value may drop if background maintenance is performed by Aura. + +**Rate**: Checkpoint events per minute + +// UI Description (Rate): The total number of checkpoint events executed since the server started. + +**Cumulative time**: Total time spent in checkpointing since startup + +// UI Description (Cumulative time): The total time in milliseconds spent in checkpointing since the server +// started. This value may drop if background maintenance is performed by Aura. + +**Duration**: Duration of the last checkpoint event in milliseconds + +// UI Description (Duration): The duration of the last checkpoint event in milliseconds. Checkpoints should +// typically take several seconds to several minutes. Values over 30 minutes warrant investigation. + + +[NOTE] +.Background maintenance +==== +Checkpoint values may drop if background maintenance is performed by Aura. + +This is normal and doesn't indicate a problem. +==== + + +=== Normal Checkpoint Behavior + +**Duration**: Several seconds to several minutes + +**Frequency**: Depends on write activity + +**Impact**: Minimal during normal operations + + +=== Checkpoint Warning Signs + +**Duration >30 minutes**: + +* Warrants investigation +* May indicate I/O issues +* Heavy write load +* Storage performance problems + + +**Very frequent checkpoints**: + +* High write transaction rate +* May impact performance +* Consider write optimization + + +**Increasing cumulative time**: + +* Track the rate of increase +* Sudden increases indicate issues +* May correlate with write-heavy periods + + +== Replan Events + +Cypher query replanning occurs when Neo4j recreates the execution plan for a query. + + +=== Understanding Replan Events + +Neo4j caches query execution plans for performance. + +Replanning occurs when: + +* A query is seen for the first time +* Statistics have changed significantly +* Schema has been modified +* Cache has been cleared + + +**The goal**: Minimize replanning by using query parameters correctly. + + +=== Replan Metrics + +**Total count**: Times Cypher has replanned a query since startup + +// UI Description (Total count): The total number of times Cypher has replanned a query since the server started. +// If this spikes or is increasing, check that the queries executed are using parameters correctly. This value +// may drop if background maintenance is performed by Aura. + +**Rate**: Replanning events per minute + +// UI Description (Rate): The total number of times Cypher has replanned a query since the server started. +// If this spikes or is increasing, check that the queries executed are using parameters correctly. + + +=== Normal Replan Behavior + +**Low rate**: Occasional replanning is normal + +**After schema changes**: Expected spike in replanning + +**New queries**: First execution triggers planning + + +=== Replan Warning Signs + +**Spikes in replan rate**: + +* Check if queries are using parameters correctly +* Queries with literal values instead of parameters +* Each unique literal value triggers a new plan + + +**Consistently high replan rate**: + +* Queries not using parameters +* Each query execution treated as new query +* Massive waste of planning resources + + +**Growing replan count**: + +* Review query patterns +* Ensure parameterized queries +* Check application query generation + + +== The Importance of Query Parameters + +Using parameters is critical for performance: + + +=== Without Parameters (Bad) + +Although the queries below are similar, the query engine will create a new plan for each User ID. + +[source,cypher] +.Hardcoded values require re-planning +---- +// +MATCH (u:User {id: 12345}) +RETURN u +---- + +The execution of the same query with a different `id` will be identical, but the query engine will have to run the planning process again. + +.Two different plans, twice the work +[source,cypher] +---- +MATCH (u:User {id: 67890}) +RETURN u +---- + + +=== With Parameters (Good) + +By replacing the hardcoded values with parameters, the query engine can reuse the same plan for all executions. + +[source,cypher] +.Single plan reused for all executions +---- +MATCH (u:User {id: $userId}) +RETURN u +---- + + +== Monitoring Best Practices + + +=== For Checkpoints + +* Monitor checkpoint duration +* Alert if duration exceeds 30 minutes +* Correlate with write transaction rates +* Track cumulative time trends + + +=== For Replan Events + +* Monitor replan rate +* Alert on sustained high replan rates +* Review query logs for non-parameterized queries +* Educate developers on parameter usage + + +== Common Issues + + +=== Issue: Long Checkpoint Duration + +**Symptoms**: + +* Checkpoint taking >30 minutes +* May cause write slowdowns + + +**Causes**: + +* Heavy write load +* Storage performance issues +* Large transaction logs + + +Action: === Issue: High Replan Rate + +**Symptoms**: + +* Replan events spiking +* High planning overhead +* Reduced query performance + + +**Causes**: + +* Queries not using parameters +* Application generating unique queries +* Dynamic query construction with literals + + +**Action**: + +Review query logs for queries that don't use parameters. + +Bad example - each query gets its own plan: + +[source,cypher] +---- +MATCH (u:User) WHERE u.age > 25 RETURN u +---- + +[source,cypher] +---- +MATCH (u:User) WHERE u.age > 30 RETURN u +---- + +[source,cypher] +---- +MATCH (u:User) WHERE u.age > 35 RETURN u +---- + +Good example - single plan reused: + +[source,cypher] +---- +MATCH (u:User) WHERE u.age > $minAge RETURN u +---- + +Work with development team to ensure queries use parameters. + + +=== Issue: Sudden Replan Spike After Deployment + +**Symptoms**: + +* Replan rate spikes after deployment +* Eventually stabilizes + + +**Causes**: + +* New queries introduced +* Schema changes +* Query cache cleared + + +Action: read::Continue to next module[] + +[.summary] +== Summary + +You learned how to monitor checkpoint and replan events for your Aura databases. + +You learned that checkpoint duration should typically be seconds to a few minutes, with values over 30 minutes warranting investigation. + +For replan events, spikes or consistently high rates indicate queries aren't using parameters correctly, which wastes resources. + +Using parameterized queries is essential for query plan caching and optimal performance. + +In the next module, you will learn how to use query logs to identify and optimize slow queries. + diff --git a/asciidoc/courses/aura-administration/modules/4-monitoring-database/lessons/8-checkpoint-challenge/lesson.adoc b/asciidoc/courses/aura-administration/modules/4-monitoring-database/lessons/8-checkpoint-challenge/lesson.adoc new file mode 100644 index 000000000..900d7e613 --- /dev/null +++ b/asciidoc/courses/aura-administration/modules/4-monitoring-database/lessons/8-checkpoint-challenge/lesson.adoc @@ -0,0 +1,97 @@ += Replan Event Investigation +:type: challenge +:order: 8 + + +== Scenario + +After a new feature deployment yesterday, you notice unusual activity in the database metrics. + +**Replan Event metrics:** + +* **Before deployment**: 100-200 replans per minute +* **After deployment**: 5,000-8,000 replans per minute (40x increase!) +* **Not stabilizing**: Still elevated 24 hours later + +**Other metrics:** + +* CPU usage: Increased from 50% to 75% +* Query latency 50th percentile: Increased from 30ms to 85ms + +Query rate: Same as before (1,000 queries/minute) + +Reviewing query logs for the new feature, you see thousands of queries like: + +---- +MATCH (u:User {region: 'US'}) RETURN count(u); +MATCH (u:User {region: 'UK'}) RETURN count(u); +MATCH (u:User {region: 'DE'}) RETURN count(u); +MATCH (u:User {region: 'FR'}) RETURN count(u); +MATCH (u:User {region: 'ES'}) RETURN count(u); +// ...hundreds of unique region values +---- + +Each unique region value creates a different query from Neo4j's perspective. + + +== The Challenge + +What is causing the replan event spike and how should you fix it? + + +include::questions/1-replan-investigation.adoc[leveloffset=+1] + + +[.summary] +== Summary + +High replan rates indicate queries using literal values instead of parameters. Fix the queries to use parameters so Neo4j can reuse execution plans instead of creating new ones for each unique value +* Plans aren't reused (memory waste) + +**The fix - use parameters:** + +[source,cypher] +.Bad - each region value triggers new plan +---- +MATCH (u:User {region: 'US'}) RETURN count(u); +---- + +[source,cypher] +.Good - single plan reused for all regions +---- +MATCH (u:User {region: $region}) RETURN count(u); +---- + +**Application code fix:** + +[source,python] +.Before (bad) +---- +query = f"MATCH (u:User {{region: '{region}'}}) RETURN count(u)" +session.run(query) +---- + +[source,python] +.After (good) +---- +query = "MATCH (u:User {region: $region}) RETURN count(u)" +session.run(query, region=region) +---- + +**Why parameters matter:** + +* **Single plan**: One plan cached and reused +* **Reduced CPU**: No repeated planning overhead +* **Better performance**: Queries execute faster +* **Memory efficient**: One plan vs. thousands + +**After the fix:** + +* Replan rate drops back to normal 100-200/minute +* CPU usage returns to 50% +* Query latency improves to previous levels + +Performance restored + +This is one of the most common performance issues and easiest to fix - always use parameters! + diff --git a/asciidoc/courses/aura-administration/modules/4-monitoring-database/lessons/8-checkpoint-challenge/questions/1-replan-investigation.adoc b/asciidoc/courses/aura-administration/modules/4-monitoring-database/lessons/8-checkpoint-challenge/questions/1-replan-investigation.adoc new file mode 100644 index 000000000..306774a7f --- /dev/null +++ b/asciidoc/courses/aura-administration/modules/4-monitoring-database/lessons/8-checkpoint-challenge/questions/1-replan-investigation.adoc @@ -0,0 +1,82 @@ +[.question] += Replan Event Spike + +After yesterday's deployment, you notice: + +. Replan events: 5,000-8,000/minute (was 100-200/minute) - 40x increase +. CPU: Increased from 50% to 75% +. Query latency 50th percentile: Increased from 30ms to 85ms +. Query rate: Unchanged at 1,000/minute + +Query logs show thousands of queries like: +---- +MATCH (u:User {region: 'US'}) RETURN count(u) +MATCH (u:User {region: 'UK'}) RETURN count(u) +MATCH (u:User {region: 'DE'}) RETURN count(u) +... hundreds of unique region values +---- + +What is causing the replan spike and how do you fix it? + +* [ ] Database schema changed - run statistics update + +* [ ] Too many users - scale the instance + +* [x] Queries use literal values instead of parameters - fix the queries + +* [ ] Replan events are normal - no action needed + +[TIP,role=hint] +.Hint +==== +Look at the query pattern. Why would each unique region value cause a replan? +==== + +[TIP,role=solution] +.Solution +==== +**Queries use literal values instead of parameters - fix the queries** is correct. + +**Root cause**: + +**What Neo4j sees**: +[source,cypher] +---- +MATCH (u:User {region: 'US'}) RETURN count(u) // Query 1 +MATCH (u:User {region: 'UK'}) RETURN count(u) // Query 2 (different!) +MATCH (u:User {region: 'DE'}) RETURN count(u) // Query 3 (different!) +---- + +Each unique literal value creates a **completely different query** from Neo4j's perspective! + +**The planning overhead**: Each unique query needs a new execution plan, resulting in 5,000-8,000 replans/minute of constant planning. Planning takes CPU (50% → 75%), planning takes time (latency 30ms → 85ms), and plans aren't reused (memory waste). + +**The fix**: + +**Before (BAD) - literals**: +[source,cypher] +---- +// Application code +query = f"MATCH (u:User {{region: '{region}'}}) RETURN count(u)" +session.run(query) +// Creates new plan for each region! +---- + +**After (GOOD) - parameters**: +[source,cypher] +---- +// Application code +query = "MATCH (u:User {region: $region}) RETURN count(u)" +session.run(query, region=region) +// Single plan reused for all regions! +---- + +**Impact of fix**: Replan rate drops from 5,000/min to 200/min (back to normal), CPU drops from 75% to 50% (planning overhead eliminated), latency drops from 85ms to 30ms (no constant replanning), and you have one cached plan vs. thousands of plans. + +**Why parameters matter**: They enable query plan caching (same query, different data), improve performance (no repeated planning overhead), reduce memory usage (single plan vs. thousands), and free CPU for actual query execution. + +The other options: Not a schema change (deployment introduced new code), not a scaling issue (same query rate), and a 40x increase is NOT normal (requires immediate fix). + +**Key lesson**: Always use parameters (`$param`) not literals. One of the most common and easily fixable performance issues! +==== + diff --git a/asciidoc/courses/aura-administration/modules/4-monitoring-database/module.adoc b/asciidoc/courses/aura-administration/modules/4-monitoring-database/module.adoc new file mode 100644 index 000000000..bccb8f7d4 --- /dev/null +++ b/asciidoc/courses/aura-administration/modules/4-monitoring-database/module.adoc @@ -0,0 +1,28 @@ += Monitoring Database Health +:order: 4 + + +== Module Overview + +Database-level metrics provide visibility into the health and activity of your data. + +In this module, you will learn how to monitor database size, query performance, transaction patterns, and maintenance operations. + +These metrics help you ensure your database is operating efficiently and identify opportunities for optimization. + + +You will learn how to: + +* Monitor database store size and identify when compaction is needed + +* Track query rates and latency percentiles + +* Analyze transaction patterns and identify rollback issues + +* Monitor checkpoint and replan events + +* Recognize signs of replication issues in clustered environments + + +link:./1-store-size/[Ready? Let's go →, role=btn] + diff --git a/asciidoc/courses/aura-administration/modules/5-query-logs/lessons/1-query-log-overview/lesson.adoc b/asciidoc/courses/aura-administration/modules/5-query-logs/lessons/1-query-log-overview/lesson.adoc new file mode 100644 index 000000000..cd732c32f --- /dev/null +++ b/asciidoc/courses/aura-administration/modules/5-query-logs/lessons/1-query-log-overview/lesson.adoc @@ -0,0 +1,231 @@ += Query Log Overview +:type: lesson +:order: 1 + + +[.discrete] +== Introduction + +Query logs provide detailed visibility into every query executed against your database. + +In this lesson, you will learn how to access query logs and understand the two main views: Summary and Details. + + +== Accessing Query Logs + +To access query logs: + +. Navigate to your Aura instance +. Click **Operations** in the menu +. Select **Logs** +. Choose the **Query** tab + +Query logs show detailed information about all queries executed against your database. + + +== Query Log Interface + +The query log interface provides: + + +=== Query Timeline + +Visual representation showing: + +* **Queries per minute**: Overall query volume +* **Failed queries per minute**: Error rate +* **Query latency (99th percentile)**: Performance trend + +This timeline helps you quickly identify when issues occurred. + + +=== Two Main Tabs + +**Summary tab**: Aggregated view of query patterns + +**Details tab**: Individual query executions + + +== The Summary Tab + +The Summary tab aggregates queries by their Cypher text (minus parameters). + + +=== What It Shows + +For each unique query pattern: + +* **Status**: Completed or failed +* **Query**: Cypher text without parameter values +* **Count**: Number of times executed in the period +* **From**: First execution time +* **To**: Last execution time +* **Total time spent**: Total execution time for all runs +* **Avg time**: Average execution time +* **Min time**: Fastest execution +* **Max time**: Slowest execution +* **Avg waiting**: Average time waiting for resources +* **Avg bytes**: Average memory allocated +* **Avg page hits**: Average page cache hits (good) +* **Avg page faults**: Average page cache misses (bad) +* **Cypher language**: Version of Cypher used + + +=== When to Use Summary Tab + +The Summary tab is perfect for: + +* Identifying frequently executed queries +* Finding queries that consume the most total time +* Spotting queries with high average duration +* Understanding overall query patterns +* Finding queries to optimize for maximum impact + + +== The Details Tab + +The Details tab shows individual query executions. + + +=== What It Shows + +For each query execution: + +* **Status**: Completed or failed +* **Query**: Exact Cypher text executed +* **End time**: When the query completed +* **Duration**: Total execution time (ms) +* **Planning**: Time spent planning the query (ms) +* **Waiting**: Time waiting for resources (ms) +* **User**: Database user who executed the query +* **Transaction ID**: Unique transaction identifier +* **Database**: Target database name +* **Driver**: Client driver signature +* **Application**: Application name (from driver metadata) +* **Alloc bytes**: Memory allocated +* **Page hits**: Data found in page cache +* **Page faults**: Data read from disk +* **Query language**: Cypher version + + +=== When to Use Details Tab + +The Details tab is perfect for: + +* Investigating specific slow query executions +* Understanding when a particular query ran +* Correlating queries with user complaints +* Debugging application-specific issues +* Tracking down intermittent problems + + +== Understanding Query Metrics + + +=== Duration + +Total time from query start to completion. + +* **Fast**: < 100ms +* **Acceptable**: 100-1000ms +* **Slow**: > 1000ms +* **Very slow**: > 5000ms + +Context matters - complex reports can legitimately take seconds. + + +=== Planning Time + +Time spent creating the execution plan. + +* **Normal**: < 50ms +* **High**: > 100ms + +High planning time indicates: + +* Novel query (first time executed) +* Query not using parameters +* Database schema changes +* Statistics need updating + + +=== Waiting Time + +Time spent waiting for resources. + +* **Normal**: 0-10ms +* **High**: > 100ms + +High waiting time indicates: + +* Lock contention +* Resource constraints +* Concurrent query competition + + +=== Page Hits vs. Page Faults + +**Page hits**: Data found in memory (fast) + +**Page faults**: Data read from disk (slow) + +* **Good**: High page hits, low page faults +* **Bad**: Low page hits, high page faults + +High page faults indicate data not fitting in page cache. + + +== Common Indicators + + +=== Frequently Run Queries + +High count indicates: + +* Core application functionality +* Prime optimization target +* Small improvements have big impact + + +=== High Total Time Spent + +Combination of frequency and duration: + +* May not be slowest query +* But consumes most database time overall +* Excellent optimization candidate + + +=== High Average Duration + +Slow query that needs optimization: + +* May run infrequently +* But takes long time when it does +* Impact on user experience + + +=== High Page Faults + +Query reads lots of data from disk: + +* Not finding data in cache +* May need indexes +* May access cold data +* Instance may need more memory + + +read::Continue to next lesson[] + + +[.summary] +== Summary + +You learned how to access and navigate query logs for your Aura instances. + +You learned about the Summary tab for understanding query patterns and the Details tab for investigating specific executions. + +Key metrics include duration, planning time, waiting time, and the ratio of page hits to page faults. + +In the next lesson, you will learn how to filter query logs to find specific queries and issues. + diff --git a/asciidoc/courses/aura-administration/modules/5-query-logs/lessons/2-filtering-queries/lesson.adoc b/asciidoc/courses/aura-administration/modules/5-query-logs/lessons/2-filtering-queries/lesson.adoc new file mode 100644 index 000000000..ba26c3006 --- /dev/null +++ b/asciidoc/courses/aura-administration/modules/5-query-logs/lessons/2-filtering-queries/lesson.adoc @@ -0,0 +1,323 @@ += Filtering and Finding Queries +:type: lesson +:order: 2 + + +[.discrete] +== Introduction + +Query logs can contain thousands of entries, making it essential to filter effectively to find specific queries or issues. + +In this lesson, you will learn how to use query log filters to quickly identify problematic queries. + + +== Filter Options + +The query log interface provides multiple filters to narrow down results: + + +=== Time Period + +Filter by when queries were executed: + +* **Last 15 minutes**: Real-time troubleshooting +* **Last 30 minutes**: Recent activity +* **Last hour**: Short-term investigation +* **Last 6 hours**: Trend analysis +* **Last 24 hours**: Daily patterns +* **Custom range**: Specific time windows + +[TIP] +.Time-based filtering +==== +When investigating user complaints, ask for the approximate time of the issue and filter to that specific period. +==== + + +=== Status + +Filter by query completion status: + +* **Completed**: Successfully executed queries +* **Failed**: Queries that encountered errors + +Use failed status to quickly identify error patterns. + + +=== GQL Status Codes + +Filter by specific Graph Query Language status codes: + +* **00000**: Success +* **02000**: No data (query found nothing) +* **22000**: Data violation (constraint errors) +* **42000**: Syntax error +* **40000**: Transaction error + +Understanding common status codes helps identify issue types. + + +=== User + +Filter by database username: + +* Typically `neo4j` or custom user names +* Useful for multi-user environments + +Identifies which application or user caused issues + +Example: `neo4j`, `app_user`, `admin` + + +=== Driver + +Filter by client driver signature: + +* Identifies driver language and version + +Format: `neo4j-/` + +Examples: + +* `neo4j-python/5.9.0` +* `neo4j-java/5.9.0` +* `neo4j-javascript/5.9.0` + +Useful for narrowing down to specific application versions. + + +=== Application + +Filter by application name supplied in driver metadata: + +* Custom name set in driver configuration +* Examples: `explore`, `browser`, `my-app` + +**Setting application name in driver:** + +[source,python] +---- +# Python example +driver = GraphDatabase.driver( + uri, + auth=(user, password), + user_agent="my-application-name" +) +---- + +This makes it easy to identify which application generated queries. + + +=== Initiation Type + +Filter by how the query was initiated: + +* User-initiated queries +* System queries +* Background operations + + +=== Query Language + +Filter by Cypher version: + +* `cypher 5` +* `cypher 5.1` +* `cypher 5.2` +* etc. + +Useful when upgrading or testing new Cypher features. + + +=== Minimum Duration + +Filter for slow queries by minimum execution time (milliseconds): + +* `1000`: Queries taking > 1 second +* `5000`: Queries taking > 5 seconds +* `10000`: Queries taking > 10 seconds + +**This is one of the most useful filters for performance optimization.** + + +=== Query Text + +Filter queries containing specific text: + +* Search within Cypher query text +* Case-insensitive + +Partial matches + +Examples: + +* `Product`: Find queries on Product nodes +* `CREATE`: Find write operations +* `DELETE`: Find deletion operations + + +=== Error Text + +Filter by error message content: + +* Search within error messages + +Useful for identifying specific errors + +Common searches: + +* `terminated`: Transaction timeout errors +* `constraint`: Constraint violations +* `memory`: Out of memory errors +* `locked`: Lock conflicts + + +## Effective Filtering Strategies + + +=== Finding Slow Queries + +**Goal**: Identify optimization candidates + +**Filters**: + +1. Status: Completed +2. Minimum duration: 1000ms +3. Time period: Last 24 hours + +**Analysis**: Review Summary tab, sort by total time spent + + +=== Investigating Errors + +**Goal**: Understand what's failing + +**Filters**: + +1. Status: Failed +2. Time period: When errors occurred +3. Error text: (if you know the error) + +**Analysis**: Review Details tab for specific error messages + + +=== Finding Queries from Specific Application + +**Goal**: Isolate one application's queries + +**Filters**: + +1. Application: Your app name +2. Time period: Relevant window + +**Analysis**: Review all queries from that application + + +=== Identifying Non-Parameterized Queries + +**Goal**: Find queries not using parameters + +**Filters**: + +1. Time period: Recent +2. Review Summary tab + +**Analysis**: Look for many similar queries with literal values + + +=== Investigating User Complaint + +**Goal**: Find queries related to specific issue + +**Filters**: + +1. Time period: When issue occurred (e.g., "around 2 PM") +2. Duration: If "slow", filter for > 1000ms +3. Status: If "error", filter for Failed + +**Analysis**: Correlate with user actions + + +== Combining Filters + +Filters work together - combine them for precision: + + +=== Example 1: Slow Python Queries + +* Minimum duration: 5000ms +* Driver: `neo4j-python` +* Status: Completed + + +=== Example 2: Recent Constraint Violations + +* Status: Failed +* Error text: `constraint` +* Time: Last hour + + +=== Example 3: Specific App Failures + +* Application: `my-app` +* Status: Failed +* Time: Last 24 hours + + +== Best Practices + + +=== Start Broad, Then Narrow + +1. Begin with time period +2. Add status if relevant +3. Add duration for performance issues +4. Add specific text filters last + + +=== Use Summary for Patterns + +* Identify common issues +* Find frequently failing queries +* Spot high-impact slow queries + + +=== Use Details for Investigation + +* Understand specific failures +* See exact parameter values (if logged) +* Correlate with other events + + +=== Set Custom Application Names + +Configure your applications to send meaningful names: + +* Makes filtering much easier +* Helps identify issue sources quickly +* Improves troubleshooting efficiency + + +=== Save Common Filter Combinations + +Document useful filter combinations for your team: + +* "Slow queries check": Duration > 1000ms +* "Error investigation": Status Failed, Last hour +* "App X issues": Application "my-app", Status Failed + + +read::Continue to next lesson[] + + +[.summary] +== Summary + +You learned how to filter query logs to find specific queries and issues. + +Key filters include minimum duration (for finding slow queries), status (for finding failures), and error text (for specific errors). + +Combining filters helps narrow down to exactly the queries you need to investigate. + +In the next lesson, you will learn how to analyze query logs to optimize performance. + diff --git a/asciidoc/courses/aura-administration/modules/5-query-logs/lessons/3-query-log-challenge/lesson.adoc b/asciidoc/courses/aura-administration/modules/5-query-logs/lessons/3-query-log-challenge/lesson.adoc new file mode 100644 index 000000000..7e212c0ff --- /dev/null +++ b/asciidoc/courses/aura-administration/modules/5-query-logs/lessons/3-query-log-challenge/lesson.adoc @@ -0,0 +1,66 @@ += Finding the Problem Query +:type: challenge +:order: 3 + + +== Scenario + +Users report that "the application has been really slow since this morning around 9 AM." + +It's now 11 AM, and you need to identify what's causing the slowness. + +You access the query logs and apply initial filters: + +* **Time period**: Last 3 hours (since 8 AM) +* **Status**: Completed (focusing on slow, not failed) + +**Summary tab shows** (sorted by total time spent): + +| Query Pattern | Count | Total Time | Avg Time | Max Time | +|--------------|-------|------------|----------|----------| +| `MATCH (u:User)-[:FRIEND*]->(f) RETURN f.name` | 2,500 | 375min | 9,000ms | 12,000ms | +| `MATCH (p:Product) WHERE p.price > $min RETURN p` | 15,000 | 75min | 300ms | 800ms | +| `MATCH (o:Order {id: $id}) RETURN o` | 10,000 | 5min | 30ms | 50ms | + + +== The Challenge + +Which query should you prioritize optimizing to have the biggest impact? + + +include::questions/1-prioritization.adoc[leveloffset=+1] + + +[.summary] +== Summary + +Prioritize query optimization by total time spent (frequency × duration), not just duration. A frequently executed slow query consumes more resources than an occasionally executed slower query + +**Query 3**: `MATCH (o:Order {id: $id})` + +* Executed 10,000 times (high frequency) +* Average 30ms (fast!) +* Total impact: 5 minutes (negligible) +* No optimization needed - performing well + +**Optimization approach for Query 1:** + +The variable-length path query `[:FRIEND*]` with no maximum is extremely expensive: + +[source,cypher] +---- +// Problem query +MATCH (u:User)-[:FRIEND*]->(f) RETURN f.name; + +// Fixes: +// 1. Add maximum depth +MATCH (u:User)-[:FRIEND*..3]->(f) RETURN f.name; + +// 2. Or use specific depth +MATCH (u:User)-[:FRIEND]->()-[:FRIEND]->(f) RETURN f.name; +---- + +**Key lesson**: A less frequent but very slow query can have more total impact than many fast queries. + +Always calculate: Frequency × Duration = Total Impact + diff --git a/asciidoc/courses/aura-administration/modules/5-query-logs/lessons/3-query-log-challenge/questions/1-prioritization.adoc b/asciidoc/courses/aura-administration/modules/5-query-logs/lessons/3-query-log-challenge/questions/1-prioritization.adoc new file mode 100644 index 000000000..024ee3e48 --- /dev/null +++ b/asciidoc/courses/aura-administration/modules/5-query-logs/lessons/3-query-log-challenge/questions/1-prioritization.adoc @@ -0,0 +1,77 @@ +[.question] += Query Optimization Priority + +Users report "the application has been slow since 9 AM." It's now 11 AM. + +Query logs Summary tab (last 3 hours, sorted by total time spent): + +| Query | Count | Total Time | Avg Time | Max Time | +|-------|-------|------------|----------|----------| +| A: `MATCH (u:User)-[:FRIEND*]->(f) RETURN f.name` | 2,500 | 375min | 9,000ms | 12,000ms | +| B: `MATCH (p:Product) WHERE p.price > $min RETURN p` | 15,000 | 75min | 300ms | 800ms | +| C: `MATCH (o:Order {id: $id}) RETURN o` | 10,000 | 5min | 30ms | 50ms | + +Which query should you prioritize optimizing for the biggest impact? + +* [ ] Query C - highest execution count + +* [ ] Query B - good candidate for indexing + +* [x] Query A - consumes most total database time + +* [ ] All three need equal attention + +[TIP,role=hint] +.Hint +==== +Calculate frequency × duration = total impact. Which query is using the most database resources overall? +==== + +[TIP,role=solution] +.Solution +==== +**Query A - consumes most total database time** is correct. + +**Total impact calculation**: + +**Query A**: 2,500 executions × 9 seconds = 22,500 seconds = **375 minutes**. These are the slowest queries (9 seconds each) with moderate frequency (2,500 times), creating a MASSIVE total impact (6+ hours of database time!). + +**Query B**: 15,000 executions × 0.3 seconds = 4,500 seconds = **75 minutes**. These are fast queries (300ms each) with very high frequency (15,000 times), creating a moderate total impact. + +**Query C**: 10,000 executions × 0.03 seconds = 300 seconds = **5 minutes**. These are very fast queries (30ms each) with high frequency (10,000 times), creating minimal total impact (performing excellently!). + +**Priority**: Query A → Query B → Query C (already fine) + +**Why Query A is the problem**: + +[source,cypher] +---- +// The problematic query +MATCH (u:User)-[:FRIEND*]->(f) RETURN f.name +// No maximum depth on variable-length path = DISASTER +---- + +**The fix**: +[source,cypher] +---- +// Option 1: Add maximum depth +MATCH (u:User)-[:FRIEND*..3]->(f) RETURN f.name + +// Option 2: Use specific depth +MATCH (u:User)-[:FRIEND]->()-[:FRIEND]->(f) RETURN f.name +---- + +**Expected improvement**: Query A execution time drops from 9,000ms to ~50ms (180x faster!), total impact drops from 375 minutes to 2 minutes (saving **373 minutes/day**), and user experience is dramatically improved. + +**After fixing Query A**, then optimize Query B: +[source,cypher] +---- +// Add index if not exists +CREATE INDEX product_price FOR (p:Product) ON (p.price) +---- + +**Query C**: Leave it alone - 30ms is excellent performance! + +**Key lesson**: Optimize by **total impact** (frequency × duration), not just by how slow or how frequent. A moderately slow query running frequently can have bigger impact than a very slow query running rarely. +==== + diff --git a/asciidoc/courses/aura-administration/modules/5-query-logs/lessons/4-optimizing-performance/lesson.adoc b/asciidoc/courses/aura-administration/modules/5-query-logs/lessons/4-optimizing-performance/lesson.adoc new file mode 100644 index 000000000..58672c8b9 --- /dev/null +++ b/asciidoc/courses/aura-administration/modules/5-query-logs/lessons/4-optimizing-performance/lesson.adoc @@ -0,0 +1,437 @@ += Optimizing Query Performance +:type: lesson +:order: 4 + + +[.discrete] +== Introduction + +Query logs provide the data you need to identify and fix performance issues. + +In this lesson, you will learn how to use query logs to optimize slow queries and improve overall database performance. + + +== The Optimization Process + +A systematic approach to query optimization: + +. **Identify** slow or problematic queries +. **Analyze** what makes them slow +. **Optimize** the query or data model +. **Measure** the improvement +. **Monitor** for regression + + +== Identifying Queries to Optimize + + +=== Prioritization Criteria + +Not all slow queries are equal. Prioritize based on: + + +**Total time spent** (Frequency × Duration): +* A query running 1,000 times/day at 500ms = 500 seconds/day +* A query running once/day at 60 seconds = 60 seconds/day +* First query has bigger impact despite being faster + + +**User-facing vs. background**: + +* User-facing slow queries hurt UX immediately +* Background jobs can often tolerate longer duration + + +**Business criticality**: + +* Core business operations take priority +* Nice-to-have features can wait + + +=== Finding High-Impact Queries + +Using query logs: + +. **Filter for slow queries**: Minimum duration > 1000ms +. **Review Summary tab**: Sort by "Total time spent" +. **Identify patterns**: Look for queries that appear frequently +. **Calculate impact**: Frequency × Average Duration + + +== Analyzing Query Performance + + +=== Key Metrics to Review + +For each slow query, examine: + + +**Duration**: + +* How long does it take? +* Is it consistently slow or variable? + + +**Planning time**: + +* High planning (>100ms) suggests non-parameterized query +* First-time queries show higher planning + + +**Page faults**: + +* High page faults mean data not in cache +* May need indexes or more memory + + +**Execution pattern**: + +* When does it run? +* How often? +* Who/what triggers it? + + +=== Common Slow Query Patterns + + +==== Full Graph Scans + +**Symptom**: Query examines many nodes/relationships + +**Example**: + +[source,cypher] +---- +MATCH (n:User) +WHERE n.email = 'user@example.com' +RETURN n +---- + +**Problem**: No index on email property + +**Fix**: Create index + +[source,cypher] +---- +CREATE INDEX user_email FOR (n:User) ON (n.email) +---- + + +==== Cartesian Products + +**Symptom**: Intermediate result set explodes + +**Example**: + +[source,cypher] +---- +MATCH (u:User) +MATCH (p:Product) +WHERE u.region = p.region +RETURN u, p +---- + +**Problem**: Creates all combinations before filtering + +**Fix**: Add relationship or use WITH + +[source,cypher] +---- +MATCH (u:User) +WITH u +MATCH (p:Product) +WHERE u.region = p.region +RETURN u, p +---- + + +==== Returning Too Much Data + +**Symptom**: High duration, high memory + +**Example**: + +[source,cypher] +---- +MATCH (u:User)-[:PURCHASED]->(p:Product) +RETURN u, p +// Returns 100,000+ rows +---- + +**Problem**: Returns massive result set + +**Fix**: Add pagination + +[source,cypher] +---- +MATCH (u:User)-[:PURCHASED]->(p:Product) +RETURN u, p +SKIP $offset LIMIT $limit +---- + + +==== Missing Indexes + +**Symptom**: High page faults, slow lookups + +**Example**: + +[source,cypher] +---- +MATCH (p:Product) +WHERE p.sku = $sku +RETURN p +---- + +**Problem**: No index on frequently queried property + +**Fix**: Create appropriate index + +[source,cypher] +---- +CREATE INDEX product_sku FOR (n:Product) ON (n.sku) +---- + + +==== Deep Traversals + +**Symptom**: Long duration, many page faults + +**Example**: + +[source,cypher] +---- +MATCH path = (u:User)-[*5..10]->(p:Product) +RETURN path +---- + +**Problem**: Variable length path with wide range + +**Fix**: Narrow range, add direction, use specific relationship types + +[source,cypher] +---- +MATCH path = (u:User)-[:FRIEND*2..3]->(friend)-[:PURCHASED]->(p:Product) +RETURN path +---- + + +## Optimization Techniques + + +=== Add Indexes + +Indexes dramatically speed up property lookups. +Consider the following example: + +[source,cypher] +.Lookup users by email +---- +// Before: Scans all User nodes +MATCH (u:User {email: $email}); +---- + +Without an index on the `email` property, the query will scan all nodes with a User label and check the `email` property of each individually. + +Creating an index on the `email` property will allow the query engine to quickly find the user by email address. + +[source,cypher] +.Creating an index +---- +CREATE INDEX user_email FOR (n:User) ON (n.email) +---- + +When the property is expected to be unique, you should create a unique constraint. Creating a unique constraint will also create an index on the property. + +[source,cypher] +.Creating a unique constraint +---- +CREATE CONSTRAINT user_email_unique FOR (n:User) REQUIRE n.email IS UNIQUE +---- + +=== Use Parameters + +Parameters enable query plan caching: + +[source,cypher] +---- +// Bad - new plan each time +MATCH (u:User) WHERE u.age > 25 RETURN u; + +// Good - plan reused +MATCH (u:User) WHERE u.age > $minAge RETURN u; +---- + + +=== Limit Result Sets + +Don't return more data than needed: + +[source,cypher] +---- +// Add LIMIT +MATCH (u:User) RETURN u LIMIT 100; + +// Use aggregation instead of collecting +MATCH (u:User) RETURN count(u) // Not: RETURN collect(u) +---- + + +=== Optimize Traversals + +Be specific about relationships and direction: + +[source,cypher] +---- +// Vague - slow +MATCH (u:User)-[*]-(p:Product); + +// Specific - fast +MATCH (u:User)-[:PURCHASED]->(p:Product); +---- + + +=== Use EXPLAIN and PROFILE + +Before changing production, test optimizations: + +[source,cypher] +.See planned execution +---- +EXPLAIN +MATCH (u:User {email: $email}) RETURN u; +---- + +[source,cypher] +.See actual execution statistics +// +PROFILE +MATCH (u:User {email: $email}) RETURN u; +---- + + +## Measuring Improvement + + +=== Before and After Comparison + +. **Record baseline**: Average duration from query logs +. **Implement optimization**: Apply fixes in test environment +. **Test thoroughly**: Verify results are correct +. **Deploy to production**: Apply changes +. **Monitor query logs**: Check new average duration + +**Calculate improvement**: + +---- +Improvement % = ((Old Duration - New Duration) / Old Duration) × 100 +---- + +Example: 5000ms → 50ms = 99% improvement + + +=== Validate with Query Logs + +After optimization: + +. Filter for the optimized query +. Check Details tab for recent executions +. Verify duration is improved +. Check page faults are reduced +. Confirm planning time is normal + + +## Monitoring for Regression + + +=== Set Up Ongoing Monitoring + +* Review query logs weekly for new slow queries +* Alert on queries exceeding duration thresholds +* Track trends in average query latency +* Monitor Total time spent for top queries + + +=== Common Causes of Regression + +**Data growth**: + +* Queries slow down as data grows +* May need additional indexes +* May need data archival + + +**Schema changes**: + +* New properties or relationships +* Changed data model +* Index modifications + + +**Application changes**: + +* New queries introduced +* Changed query patterns +* Removed parameters (accidental) + + +## Real-World Example + +**Scenario**: Dashboard report slow + +**Investigation**: + +1. Query logs filtered for duration > 5000ms +2. Found: `MATCH (u:User)-[:PURCHASED]->(p:Product) RETURN u.name, p.name` +3. Frequency: 100 times/day +4. Average duration: 7,500ms +5. Total impact: 750 seconds/day (12.5 minutes) + +**Analysis**: + +* High page faults: 50,000 +* Returns 10,000+ rows +* No pagination + +**Optimization**: + +[source,cypher] +.Before - Returns all rows +---- +MATCH (u:User)-[:PURCHASED]->(p:Product) +RETURN u.name, p.name; +---- + +[source,cypher] +.After - added pagination and sorting +---- +MATCH (u:User)-[:PURCHASED]->(p:Product) +RETURN u.name, p.name +ORDER BY p.purchaseDate DESC +SKIP $offset LIMIT 100 +---- + +**Results**: + +* Duration: 7,500ms → 150ms (98% improvement) +* Page faults: 50,000 → 500 +* Returns: 10,000 rows → 100 rows per request +* Total impact: Saved 12 minutes/day + + +read::Continue to next lesson[] + + +[.summary] +== Summary + +You learned how to use query logs to optimize database performance. + +The optimization process involves identifying high-impact queries (by total time spent), analyzing what makes them slow, applying appropriate optimizations, and measuring improvement. + +Common optimization techniques include adding indexes, using parameters, limiting result sets, and optimizing traversals. + +Always measure before and after performance using query logs to validate improvements. + +In the next module, you will learn about advanced monitoring features including metrics integration and security logs. + diff --git a/asciidoc/courses/aura-administration/modules/5-query-logs/lessons/6-optimization-challenge/lesson.adoc b/asciidoc/courses/aura-administration/modules/5-query-logs/lessons/6-optimization-challenge/lesson.adoc new file mode 100644 index 000000000..bca07831c --- /dev/null +++ b/asciidoc/courses/aura-administration/modules/5-query-logs/lessons/6-optimization-challenge/lesson.adoc @@ -0,0 +1,114 @@ += Query Optimization Strategy +:type: challenge +:order: 5 + + +== Scenario + +You've identified a slow query that needs optimization: + +[source,cypher] +---- +MATCH (u:User) +WHERE u.email = 'john@example.com' +MATCH (u)-[:PURCHASED]->(o:Order) +WHERE o.date > '2024-01-01' +RETURN u.name, o.id, o.total +---- + +**Current Performance (from query logs):** + +* Executions: 500 times/day +* Average duration: 2,800ms +* Planning time: 15ms +* Page faults: 12,000 +* Page hits: 3,000 + +**Database stats:** + +* 2 million User nodes +* 10 million Order nodes +* User.email has NO index +* Order.date has NO index + +**Testing your optimization**, you try three approaches: + + +**Option A**: Add index on User.email only + +* New average duration: 450ms +* Page faults: 8,000 + + +**Option B**: Add index on Order.date only + +* New average duration: 2,100ms +* Page faults: 10,000 + + +**Option C**: Add indexes on BOTH User.email AND Order.date + +* New average duration: 85ms +* Page faults: 800 + + +== The Challenge + +Which optimization approach provides the best balance of performance improvement and resource usage? + + +include::questions/1-optimization-strategy.adoc[leveloffset=+1] + + +[.summary] +== Summary + +When a query has multiple lookup properties, adding indexes on all of them provides the best performance improvement. Consider the total time saved across all query executions when evaluating optimization impact + +1. **Finding the user by email**: `WHERE u.email = 'john@example.com'` + - Without index: Scans 2 million User nodes + - With index: Direct lookup to one User + +2. **Filtering orders by date**: `WHERE o.date > '2024-01-01'` + - Without index: Examines all orders for that user + - With index: Only reads relevant orders + +**Why partial optimization (A or B) isn't enough:** + +* **Option A** improves user lookup but still scans all orders +* **Option B** doesn't help the expensive user scan +* Both leave significant performance on the table + +**Implementation**: + +[source,cypher] +---- +// Create both indexes +CREATE INDEX user_email FOR (n:User) ON (n.email); +CREATE INDEX order_date FOR (n:Order) ON (n.date); +---- + +**Index cost considerations**: + +* Indexes use storage and memory +* Indexes slow down writes slightly +* But for lookup properties on large datasets, the benefit far outweighs the cost + +**General principle**: + +For queries with multiple lookup conditions, index all properties used in WHERE clauses for optimal performance. + +Test with PROFILE to verify indexes are being used: + +[source,cypher] +---- +PROFILE +MATCH (u:User) +WHERE u.email = 'john@example.com' +MATCH (u)-[:PURCHASED]->(o:Order) +WHERE o.date > '2024-01-01' +RETURN u.name, o.id, o.total +---- + +Look for "Index Seek" operations in the plan instead of "Node By Label Scan". + diff --git a/asciidoc/courses/aura-administration/modules/5-query-logs/lessons/6-optimization-challenge/questions/1-optimization-strategy.adoc b/asciidoc/courses/aura-administration/modules/5-query-logs/lessons/6-optimization-challenge/questions/1-optimization-strategy.adoc new file mode 100644 index 000000000..2d4c11ccf --- /dev/null +++ b/asciidoc/courses/aura-administration/modules/5-query-logs/lessons/6-optimization-challenge/questions/1-optimization-strategy.adoc @@ -0,0 +1,94 @@ +[.question] += Index Optimization Strategy + +You're optimizing this slow query (2,800ms average): + +[source,cypher] +---- +MATCH (u:User) +WHERE u.email = 'john@example.com' +MATCH (u)-[:PURCHASED]->(o:Order) +WHERE o.date > '2024-01-01' +RETURN u.name, o.id, o.total +---- + +Database: 2M User nodes, 10M Order nodes +Current state: NO indexes on User.email or Order.date + +You test three approaches: + +**Option A**: Index on User.email only → 450ms +**Option B**: Index on Order.date only → 2,100ms +**Option C**: Indexes on BOTH User.email AND Order.date → 85ms + +How should you balance performance and resource usage? + +* [ ] Option A - indexing email gives good enough improvement + +* [ ] Option B - focusing on the larger dataset (orders) +* [x] Option C - indexing both properties gives optimal performance + +* [ ] None - query needs rewriting instead + +[TIP,role=hint] +.Hint +==== +Consider that this query has TWO lookup operations. What happens when you index both versus just one? +==== + +[TIP,role=solution] +.Solution +==== +**Option C - indexing both properties gives optimal performance** is correct. + +**Performance analysis**: + +**No indexes (2,800ms)**: +1. Scan 2M User nodes to find email match → ~1,500ms +2. For that user, scan all their orders for date match → ~1,300ms +3. Total: 2,800ms + +**Option A (450ms)** - User.email index only: +1. Index lookup finds user instantly → ~10ms +2. Still scan that user's orders for date → ~440ms +3. Total: 450ms +4. **Improvement**: 84% faster + +**Option B (2,100ms)** - Order.date index only: +1. Still scan 2M users for email → ~1,500ms +2. Index helps filter orders → ~600ms +3. Total: 2,100ms +4. **Improvement**: Only 25% faster (not much!) + +**Option C (85ms)** - BOTH indexes: +1. Index lookup finds user → ~10ms +2. Index finds relevant orders → ~70ms +3. Traverse relationship → ~5ms +4. Total: 85ms +5. **Improvement**: 97% faster! + +**Why Option C is best**: + +**The query has two lookups**: +[source,cypher] +---- +WHERE u.email = 'john@example.com' // Lookup 1 +WHERE o.date > '2024-01-01' // Lookup 2 +---- + +Without indexes on both, at least one lookup requires scanning, scanning is expensive, and performance suffers. With indexes on both, both lookups are direct, minimal data is touched, and you get optimal performance. + +**Implementation**: +[source,cypher] +---- +CREATE INDEX user_email FOR (n:User) ON (n.email); +CREATE INDEX order_date FOR (n:Order) ON (n.date); +---- + +**Cost-benefit**: **Indexes use storage** (yes, but minimal compared to benefit), **Indexes slow writes** (slightly, but read performance gain is huge), and **For lookup properties** they're always worth it. + +**Daily time saved** (500 executions/day): Before optimization takes 2,800ms × 500 = 1,400,000ms = 23 minutes, after takes 85ms × 500 = 42,500ms = 0.7 minutes. **Saved: 22.3 minutes/day**. + +**General principle**: For queries with multiple WHERE clauses on properties, index all properties used for lookups to achieve optimal performance. +==== + diff --git a/asciidoc/courses/aura-administration/modules/5-query-logs/module.adoc b/asciidoc/courses/aura-administration/modules/5-query-logs/module.adoc new file mode 100644 index 000000000..e605dabf9 --- /dev/null +++ b/asciidoc/courses/aura-administration/modules/5-query-logs/module.adoc @@ -0,0 +1,28 @@ += Query Logs and Optimization +:order: 5 + + +== Module Overview + +Query logs provide detailed visibility into every query executed against your database. + +In this module, you will learn how to use query logs to identify slow queries, troubleshoot errors, and optimize database performance. + +As a database administrator, query logs are your primary tool for understanding application behavior and improving query efficiency. + + +You will learn how to: + +* Access and navigate the query logs interface + +* Filter queries by status, duration, user, and application + +* Analyze query patterns using the Summary and Details tabs + +* Identify and troubleshoot slow-running queries + +* Recognize common query issues and their solutions + + +link:./1-query-log-overview/[Ready? Let's go →, role=btn] + diff --git a/asciidoc/courses/aura-administration/modules/6-advanced-monitoring/lessons/1-metrics-integration/lesson.adoc b/asciidoc/courses/aura-administration/modules/6-advanced-monitoring/lessons/1-metrics-integration/lesson.adoc new file mode 100644 index 000000000..68d32dc55 --- /dev/null +++ b/asciidoc/courses/aura-administration/modules/6-advanced-monitoring/lessons/1-metrics-integration/lesson.adoc @@ -0,0 +1,330 @@ += Metrics Integration +:type: lesson +:order: 1 + + +[.discrete] +== Introduction + +Beyond the Aura console, you can integrate metrics with external monitoring platforms for advanced alerting and dashboarding. + +In this lesson, you will learn how to configure metrics integration with Prometheus and other monitoring tools. + + +== Why External Monitoring? + +External monitoring platforms provide: + +* **Custom dashboards**: Combine Aura metrics with other system metrics +* **Alerting**: Automated notifications when thresholds are exceeded +* **Long-term retention**: Keep metrics beyond Aura's retention period +* **Team collaboration**: Share dashboards with broader teams +* **Integration**: Connect with incident management tools + + +== Accessing Metrics Integration Settings + +To configure metrics integration: + +. Navigate to your project in Aura +. Click **Settings** in the project menu +. Select **Metrics Integration** + +You'll see configuration options for scraping metrics from your Aura instances. + + +== Metrics Endpoint + +Aura provides a Prometheus-compatible metrics endpoint for each project. + + +=== Endpoint URL + +The URL format: + +---- +https://customer-metrics-api.neo4j.io/api/v1///metrics +---- + +Example: + +---- +https://customer-metrics-api.neo4j.io/api/v1/cb50b84c-0a53-565c-ba05-008e104efee0/6b0c65e8/metrics +---- + +Copy this URL from the Metrics Integration settings page. + + +=== Authentication + +Metrics endpoints require OAuth2 authentication: + +* **Client ID**: Provided in Aura settings +* **Client Secret**: Generated in Aura settings +* **Token URL**: `https://api.neo4j.io/oauth/token` + + +[WARNING] +.Keep credentials secure +==== +Keep your client secret secure. + +Treat it like a password and never commit it to version control. +==== + + +== Prometheus Configuration + +Prometheus is a popular open-source monitoring system. + + +=== Prometheus Job Configuration + +Add this job to your Prometheus configuration: + +[source,yaml] +---- +- job_name: 'aura-metrics' + scrape_timeout: 30s + metrics_path: '/api/v1///metrics' + scheme: 'https' + static_configs: + - targets: ['customer-metrics-api.neo4j.io'] + oauth2: + client_id: '' + client_secret: '' + token_url: 'https://api.neo4j.io/oauth/token' +---- + +Replace placeholders with your actual values from Aura settings. + + +=== Scrape Interval + +Configure scraping based on your needs: + +* **15-30 seconds**: Real-time monitoring +* **1 minute**: Standard monitoring +* **5 minutes**: Less frequent, reduced load + + +=== Available Metrics + +The endpoint exposes all Aura metrics: + +* CPU usage +* Memory usage (heap, page cache) +* Storage metrics +* Query rates and latency +* Transaction metrics +* Connection metrics +* Garbage collection stats +* And more + + +== Grafana Dashboards + +Grafana is a popular visualization platform that works with Prometheus. + + +=== Setting Up Grafana + +. Install Grafana +. Add Prometheus as a data source +. Create dashboards or import Neo4j templates + + +=== Example Dashboard Panels + +**CPU Usage Panel**: + +[source,promql] +---- +# CPU cores used +neo4j_system_cpu_count +---- + +**Page Cache Hit Ratio**: + +[source,promql] +---- +# Calculate hit ratio percentage +(neo4j_page_cache_hits / (neo4j_page_cache_hits + neo4j_page_cache_faults)) * 100 +---- + +**Query Rate**: + +[source,promql] +---- +# Queries per second +rate(neo4j_database_query_execution_success_total[5m]) +---- + + +## Creating Alerts + +Set up automated alerts for critical conditions: + + +=== Example Alert Rules + +**High CPU Usage**: + +[source,yaml] +---- +- alert: HighCPUUsage + expr: neo4j_system_cpu_count > 7 + for: 5m + labels: + severity: warning + annotations: + summary: "Aura instance CPU usage high" + description: "CPU usage above 7 cores for 5 minutes" +---- + +**Low Page Cache Hit Ratio**: + +[source,yaml] +---- +- alert: LowPageCacheHitRatio + expr: (neo4j_page_cache_hits / (neo4j_page_cache_hits + neo4j_page_cache_faults)) < 0.90 + for: 10m + labels: + severity: warning + annotations: + summary: "Page cache hit ratio below 90%" + description: "Database may not fit in memory" +---- + +**High Heap Usage**: + +[source,yaml] +---- +- alert: HighHeapUsage + expr: neo4j_vm_heap_used / neo4j_vm_heap_max > 0.85 + for: 15m + labels: + severity: critical + annotations: + summary: "Heap memory usage above 85%" + description: "Instance may need scaling" +---- + + +== Other Monitoring Platforms + + +=== Datadog + +Datadog can scrape Prometheus endpoints: + +. Configure Datadog Prometheus integration +. Add Aura metrics endpoint +. Configure OAuth2 authentication + + +=== New Relic + +New Relic supports Prometheus data: + +. Use New Relic Prometheus OpenMetrics integration +. Configure endpoint and authentication +. Create custom dashboards + + +=== CloudWatch + +For AWS environments: + +. Use Prometheus CloudWatch exporter +. Export Aura metrics to CloudWatch +. Set up CloudWatch alarms + + +## Best Practices + + +=== Security + +* Store credentials in secrets management (e.g., Vault, AWS Secrets Manager) +* Rotate client secrets periodically +* Limit access to metrics endpoints +* Use HTTPS only (enforced by Aura) + + +=== Performance + +* Set appropriate scrape intervals (30s - 1m recommended) +* Don't scrape too frequently (increases load) +* Monitor your monitoring system's resource usage + + +=== Alerting + +* Start with critical alerts only +* Avoid alert fatigue with appropriate thresholds +* Use alert grouping and suppression +* Include runbooks in alert descriptions + + +=== Dashboard Design + +* Group related metrics together +* Use consistent time ranges +* Add context with annotations +* Include threshold indicators + + +== Troubleshooting + +Common issues and solutions: + + +=== Authentication Failures + +**Problem**: `401 Unauthorized` + +**Solutions**: + +* Verify client ID and secret are correct +* Check token URL is correct +* Ensure OAuth2 configuration is properly formatted + + +=== No Data Appearing + +**Problem**: Metrics endpoint returns empty + +**Solutions**: + +* Verify endpoint URL is correct +* Check scrape timeout (increase to 30s+) +* Confirm instance is running +* Review Prometheus logs + + +=== Intermittent Failures + +**Problem**: Scraping works sometimes + +**Solutions**: + +* Increase scrape timeout +* Check network connectivity +* Review rate limits + + +read::Continue to next lesson[] + + +[.summary] +== Summary + +You learned how to integrate Aura metrics with external monitoring platforms like Prometheus and Grafana. + +You learned how to configure the metrics endpoint with OAuth2 authentication and set up automated alerts for critical conditions. + +External monitoring enables advanced dashboarding, long-term metric retention, and integration with your broader monitoring infrastructure. + +In the next lesson, you will learn about security logs for tracking authentication and authorization events. + diff --git a/asciidoc/courses/aura-administration/modules/6-advanced-monitoring/lessons/2-security-logs/lesson.adoc b/asciidoc/courses/aura-administration/modules/6-advanced-monitoring/lessons/2-security-logs/lesson.adoc new file mode 100644 index 000000000..abf4fa58f --- /dev/null +++ b/asciidoc/courses/aura-administration/modules/6-advanced-monitoring/lessons/2-security-logs/lesson.adoc @@ -0,0 +1,278 @@ += Security Logs +:type: lesson +:order: 2 + + +[.discrete] +== Introduction + +Security logs track authentication, authorization, and other security-related events in your Aura database. + +In this lesson, you will learn how to access and use security logs for auditing and compliance purposes. + + +== Accessing Security Logs + +To access security logs: + +. Navigate to your Aura instance +. Click **Operations** +. Select **Logs** +. Choose the **Security** tab + +Security logs provide a complete audit trail of security events. + + +== Security Log Interface + +The security log interface provides: + + +=== Filter Options + +* **Status**: Info, Warning, Error +* **Executing user**: Database user who performed the action +* **Authenticated user**: User who authenticated the session +* **Driver**: Client driver used +* **Message text**: Search within log messages + + +=== Two Main Tabs + +**Summary tab**: Aggregated view of security events + +**Details tab**: Individual security event records + + +== The Summary Tab + + +=== What It Shows + +For each unique security event pattern: + +* **Status**: Info, Warning, or Error +* **Message**: Event description +* **Executing user**: User who performed the action +* **Count**: Number of occurrences +* **From**: First occurrence time +* **To**: Last occurrence time +* **Authenticated users**: List of users involved + + +=== Common Events + +**Successful authentication**: + +* Message: "Successful authentication" +* Status: Info +* Indicates normal login activity + + +**Failed authentication**: + +* Message: "Authentication failed" +* Status: Warning or Error +* May indicate attacks or configuration issues + + +**Authorization failures**: + +* Message: "Permission denied" +* Status: Warning +* User attempted unauthorized operation + + +**Connection events**: + +* Message: "Logged in" / "Logged out" +* Status: Info +* Tracks session lifecycle + + +## The Details Tab + + +=== What It Shows + +For each security event: + +* **Status**: Info, Warning, Error +* **Message**: Specific event description +* **Executing user**: User who performed the action +* **Time**: When the event occurred +* **Authenticated user**: Original authenticated user +* **Database**: Target database +* **Driver**: Client driver information + + +== Common Security Monitoring Use Cases + + +=== Audit Compliance + +Track all database access for compliance requirements: + +**Filter settings**: + +* Time period: Audit period (e.g., last 30 days) +* No other filters (capture all events) + +**Export** the logs for compliance reporting. + + +=== Failed Login Monitoring + +Identify potential security threats: + +**Filter settings**: + +* Status: Error or Warning +* Message text: "authentication failed" + +**Look for**: + +* Repeated failures from same source +* Failures for non-existent users +* Brute force attack patterns + + +=== Permission Issues + +Debug authorization problems: + +**Filter settings**: + +* Status: Warning +* Message text: "permission" or "denied" + +**Identify**: + +* Users lacking necessary permissions +* Role configuration issues +* Application misconfiguration + + +=== Suspicious Activity + +Identify unusual patterns: + +**Look for**: + +* Logins at unusual times +* Access from unexpected locations +* Unusual user activity patterns +* Privilege escalation attempts + + +== Security Event Types + + +=== Authentication Events + +**Successful login**: + +---- +Status: Info +Message: Successful authentication +User: app_user +---- + +Normal operation - user successfully authenticated. + + +**Failed login**: + +---- +Status: Warning +Message: Authentication failed: Invalid credentials +User: attempted_user +---- + +May indicate: + +* User typo +* Expired credentials +* Attack attempt + + +=== Authorization Events + +**Permission denied**: + +---- +Status: Warning +Message: Permission denied: Insufficient privileges +User: app_user +---- + +User attempted operation they're not authorized for. + + +**Role change**: + +---- +Status: Info +Message: Role granted to user +User: admin +---- + +Administrative action modifying permissions. + + +=== Session Events + +**Session started**: + +---- +Status: Info +Message: Logged in +User: app_user +---- + +User session began. + + +**Session ended**: + +---- +Status: Info +Message: Logged out +User: app_user +---- + +User session terminated (normally or abnormally). + + +== Security Best Practices + +Review security logs weekly, monitoring for failed authentication patterns, permission denied events, and anomalies. Alert on multiple failed attempts, permission violations, and unusual activity. + +For user management, review accounts regularly, remove departed personnel, use least-privilege access, and rotate credentials periodically. Retain logs per compliance requirements and maintain an audit trail. + + +== Investigating Security Incidents + +**Multiple Failed Logins**: Filter by Status: Warning, the username, and recent time period. Check if the driver is from a known application, look for automated attack patterns, verify the account is legitimate, and check if a successful login followed. Block malicious IPs or reset compromised passwords. + +**Permission Denied Errors**: Filter for Status: Warning and "permission" in messages. Identify affected users and operations, check for recent permission changes, then grant access or fix role assignments as needed. + +**Correlating with Other Data**: Match security events with query logs (failed auth followed by queries, permission denials with specific queries), resource metrics (login events during CPU spikes), and application logs (errors matching database permission denials). + + +read::Course complete[] + + +[.summary] +== Summary + +You learned how to access and use security logs for your Aura instances. + +Security logs provide an audit trail of authentication, authorization, and session events. + +Regular review of security logs helps identify security threats, debug permission issues, and maintain compliance with audit requirements. + +Filter security logs by status, user, and message text to find specific events and patterns. + +Congratulations! You've completed the Aura In Production course and are now equipped to effectively monitor and manage Neo4j Aura databases in production environments. + diff --git a/asciidoc/courses/aura-administration/modules/6-advanced-monitoring/module.adoc b/asciidoc/courses/aura-administration/modules/6-advanced-monitoring/module.adoc new file mode 100644 index 000000000..e345025d7 --- /dev/null +++ b/asciidoc/courses/aura-administration/modules/6-advanced-monitoring/module.adoc @@ -0,0 +1,26 @@ += Advanced Monitoring +:order: 6 + + +== Module Overview + +Beyond the Aura console, you can integrate metrics with external monitoring tools and review security logs for audit and compliance purposes. + +In this module, you will learn how to export Aura metrics to Prometheus and other monitoring platforms, and how to use security logs to track authentication and authorization events. + + +You will learn how to: + +* Configure metrics integration with Prometheus + +* Set up external monitoring and alerting + +* Access and filter security logs + +* Monitor authentication and authorization events + +* Identify suspicious activity and security issues + + +link:./1-metrics-integration/[Ready? Let's go →, role=btn] + diff --git a/asciidoc/courses/aura-administration/summary.adoc b/asciidoc/courses/aura-administration/summary.adoc new file mode 100644 index 000000000..e5f532f71 --- /dev/null +++ b/asciidoc/courses/aura-administration/summary.adoc @@ -0,0 +1,256 @@ += Course Summary + +Congratulations on completing "Aura In Production"! + +You have successfully learned how to manage and monitor Neo4j Aura instances in production environments. + +As a database administrator, you now have the skills to identify and respond to performance issues, optimize resource usage, and maintain data integrity. + + +== Backup and Restore + +You now know how to: + +* Configure and manage automated snapshots + +* Use backups to create test environments from production data + +* Perform restore operations safely with minimal downtime + +* Use the Aura API to automate backup workflows + +* Make informed decisions during data recovery scenarios + + +== Resource Monitoring + +You've mastered monitoring resource utilization: + +* Navigate the Metrics dashboard effectively + +* Monitor CPU usage and identify performance bottlenecks + +* Track storage consumption and growth trends + +* Analyze query rates to understand workload patterns + +* Recognize when to scale instances proactively + + +== Instance Performance + +You've learned to monitor and optimize instance-level metrics: + +* Monitor heap memory usage and identify memory pressure + +* Understand page cache performance and optimize hit ratios + +* Track Bolt connection patterns and identify connection issues + +* Interpret garbage collection metrics and their impact on performance + +* Identify and respond to Out of Memory errors + +* Determine when scaling is needed versus when optimization is sufficient + + +== Database Health + +You've gained knowledge about database-level monitoring: + +* Monitor database store size and perform compaction when needed + +* Track query rates, failed queries, and latency percentiles + +* Analyze transaction patterns and identify rollback issues + +* Monitor checkpoint and replan events + +* Recognize signs of replication issues in clustered environments + + +== Query Optimization + +You've learned essential query optimization skills: + +* Access and navigate query logs effectively + +* Filter queries by duration, status, user, and application + +* Identify high-impact queries for optimization + +* Apply common optimization techniques (indexes, parameters, limits) +* Measure performance improvements and validate fixes + + +== Advanced Monitoring + +You've learned about advanced monitoring capabilities: + +* Configure metrics integration with Prometheus + +* Set up external monitoring and alerting + +* Create custom dashboards in Grafana + +* Access and review security logs for audit compliance + +* Correlate security events with performance metrics + + +== Pro Tips + + +=== Proactive Monitoring + +* Review metrics daily for production instances + +* Set up automated alerts at 70-80% resource thresholds + +* Monitor trends to predict future capacity needs + +* Document normal patterns for your workload + + +=== Performance Optimization + +* Start with query logs to identify high-impact queries + +* Always use parameters in queries for plan caching + +* Add indexes on frequently queried properties + +* Test optimizations in non-production first + +* Measure before and after performance + + +=== Incident Response + +* Keep calm and gather data first + +* Use metrics to identify what changed and when + +* Correlate metrics, query logs, and application logs + +* Fix the immediate issue, then investigate root cause + +* Document incidents and resolutions for future reference + + +=== Capacity Planning + +* Track growth rates for CPU, storage, and query volume + +* Plan for 20-30% headroom above current usage + +* Scale before resources are constrained, not after + +* Consider cost vs. performance tradeoffs + + +=== Query Best Practices + +* Always use parameters: `$param` not literal values + +* Add LIMIT to queries that could return large result sets + +* Use appropriate indexes for lookup properties + +* Avoid variable-length paths without maximum depth + +* Profile queries before deploying to production + + +=== Backup and Recovery + +* Test restore procedures regularly in non-production + +* Document restore steps and expected downtime + +* Communicate with stakeholders before restoring production + +* Use backups to create test environments + +* Automate test instance creation with the Aura API + + +== Real-World Scenarios You Can Now Handle + + +=== "The Application is Slow" + +Your approach: + +1. Check Metrics dashboard for resource spikes +2. Review query logs filtered for slow queries +3. Identify if it's specific queries or systemic +4. Apply appropriate fix (optimize queries or scale instance) +5. Monitor to verify improvement + + +=== "Users Can't Log In" + +Your approach: + +1. Check security logs for failed authentication events +2. Review connection metrics for connection pool issues +3. Verify instance is running and accessible +4. Check for recent deployments or configuration changes +5. Take corrective action and document resolution + + +=== "We're Running Out of Storage" + +Your approach: + +1. Check store size metrics for allocated vs. used space +2. Determine if it's growth or wasted space +3. If growth: Plan instance scaling +4. If waste: Schedule database compaction +5. Implement data retention policies + + +=== "Out of Memory Errors During Peak Traffic" + +Your approach: + +1. Scale instance immediately to stop ongoing failures +2. Verify OOM errors have stopped and metrics stabilized +3. Review query logs for high memory allocation queries +4. Optimize problematic queries with LIMIT clauses +5. Set up alerts to prevent future occurrences + + +=== "Queries Started Failing After Deployment" + +Your approach: + +1. Filter query logs for failed queries since deployment +2. Identify the specific failing queries +3. Review error messages for root cause +4. Work with developers to fix query or rollback +5. Monitor for resolution + + +== Continue Your Learning + +To further develop your Neo4j skills, consider: + +* link:/courses/cypher-fundamentals/[Cypher Fundamentals^] - Master advanced query techniques + +* link:/courses/modeling-fundamentals/[Graph Data Modeling Fundamentals^] - Design efficient data models + +* link:/courses/graph-data-science-fundamentals/[Graph Data Science Fundamentals^] - Learn graph algorithms + +For developer-focused learning: + +* Neo4j driver courses for Python, Java, JavaScript, and .NET + +* Building applications with Neo4j + +* Import and export strategies + + +You can now confidently manage Neo4j Aura databases in production with the skills to monitor, troubleshoot, and optimize for performance and reliability! + diff --git a/public/img/courses/banners/aura-administration.png b/public/img/courses/banners/aura-administration.png new file mode 100644 index 0000000000000000000000000000000000000000..0c51a337abad98d73097de5d1eb075a805e0a18f GIT binary patch literal 72247 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 ep`RA;-<9v4M_R(O + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/qa.test.js b/tests/qa.test.js index 62321d641..035e7c886 100644 --- a/tests/qa.test.js +++ b/tests/qa.test.js @@ -223,258 +223,266 @@ describe("QA Tests", () => { for (const lessonPath of lessonPaths) { const lessonSlug = lessonPath.split(sep).reverse()[0]; - - const lessonAdoc = readFileSync( - join(lessonPath, "lesson.adoc") - ).toString(); - - const optional = getAttribute(lessonAdoc, "optional") === "true"; - const hasReadButton = lessonAdoc.match(/read::(.*)\[\]/) !== null; - const includesSandbox = lessonAdoc.includes( - 'include::{shared}/courses/apps/sandbox.adoc[tags="summary' - ); + const lessonAdocPath = join(lessonPath, "lesson.adoc") describe(lessonSlug, () => { - const questionPaths = globSync( - globJoin( - __dirname, - "..", - "asciidoc", - "courses", - slug, - "modules", - moduleSlug, - "lessons", - lessonSlug, - "questions", - "*.adoc" - ) - ); - - it("should have a title", () => { - const titleMatch = lessonAdoc.match(/^=\s+(.+)$/m); - expect(titleMatch).toBeTruthy(); - expect(titleMatch[1].trim().length).toBeGreaterThan(0); - }); - - it("should have an :order: attribute", () => { - expect(getAttribute(lessonAdoc, "order")).toBeDefined(); - }); - - it("should have a valid :type: attribute if defined", () => { - const type = getAttribute(lessonAdoc, "type"); - if (type) { - const validTypes = [ - "video", - "lesson", - "text", - "quiz", - "activity", - "challenge", - ]; - expect(validTypes).toContain(type); - } - }); - it("should have a [.summary] section", () => { - expect(lessonAdoc).toMatch(/^\[\.summary\]/m); + it("should have a lesson.adoc file", () => { + expect(existsSync(lessonAdocPath)).toBe(true); }); - it("should have meaningful content", () => { - const lines = lessonAdoc.split("\n"); - const content = lines.slice(1).join("\n").trim(); - expect(content.length).toBeGreaterThanOrEqual(200); - }); - - // it("all admonitions should have titles", () => { - // const admonitionRegex = - // /^\[(TIP|NOTE|WARNING|CAUTION|IMPORTANT)\]/gm; - // const admonitions = [...lessonAdoc.matchAll(admonitionRegex)]; - - // for (const match of admonitions) { - // const startIndex = match.index; - // const afterAdmonition = lessonAdoc.substring(startIndex); - // const nextLines = afterAdmonition.split("\n").slice(1, 3); - - // const hasTitle = - // nextLines[0] && nextLines[0].trim().startsWith("."); - // expect(hasTitle).toBe(true); - // } - // }); - - it("should be optional, mark as read or have one or more questions", () => { - expect( - optional || - hasReadButton || - includesSandbox || - questionPaths.length > 0 - ).toBe(true); - }); - - // TODO: Test all verify & solution cypher files - // if (type === 'challenge' && !optional) { - // it('should contain a valid verify.cypher', async () => { - // expect(existsSync(join(__dirname, '..', 'asciidoc', - // 'courses', slug, 'modules', moduleSlug, 'lessons', - // lessonSlug, 'verify.cypher')) - // ).toBe(true) - - // const cypher = readFileSync(join(lessonPath, 'verify.cypher')).toString() - // expect(await explainCypherError(cypher)).toBeUndefined() - // }) - - // it('should contain a valid solution.cypher', async () => { - // expect(existsSync(join(__dirname, '..', 'asciidoc', - // 'courses', slug, 'modules', moduleSlug, 'lessons', - // lessonSlug, 'solution.cypher')) - // ).toBe(true) - // }) - // } - - if (!skipLinkChecks) { - it("should not have any broken links", async () => { - for (const link of findLinks(lessonAdoc)) { - if ( - !link.includes("openai") && - !link.includes("example") && - !link.includes("localhost") - ) { - const statusCode = await getStatusCode(link); - try { - expect(statusCode).toBe(200); - } catch (e) { - throw new Error(`${link} returns ${statusCode}`); + if (existsSync(lessonAdocPath)) { + const lessonAdoc = readFileSync( + lessonAdocPath + ).toString(); + + const optional = getAttribute(lessonAdoc, "optional") === "true"; + const hasReadButton = lessonAdoc.match(/read::(.*)\[\]/) !== null; + const includesSandbox = lessonAdoc.includes( + 'include::{shared}/courses/apps/sandbox.adoc[tags="summary' + ); + + const questionPaths = globSync( + globJoin( + __dirname, + "..", + "asciidoc", + "courses", + slug, + "modules", + moduleSlug, + "lessons", + lessonSlug, + "questions", + "*.adoc" + ) + ); + + it("should have a title", () => { + const titleMatch = lessonAdoc.match(/^=\s+(.+)$/m); + expect(titleMatch).toBeTruthy(); + expect(titleMatch[1].trim().length).toBeGreaterThan(0); + }); + + it("should have an :order: attribute", () => { + expect(getAttribute(lessonAdoc, "order")).toBeDefined(); + }); + + it("should have a valid :type: attribute if defined", () => { + const type = getAttribute(lessonAdoc, "type"); + if (type) { + const validTypes = [ + "video", + "lesson", + "text", + "quiz", + "activity", + "challenge", + ]; + expect(validTypes).toContain(type); + } + }); + + it("should have a [.summary] section", () => { + expect(lessonAdoc).toMatch(/^\[\.summary\]/m); + }); + + it("should have meaningful content", () => { + const lines = lessonAdoc.split("\n"); + const content = lines.slice(1).join("\n").trim(); + expect(content.length).toBeGreaterThanOrEqual(200); + }); + + // it("all admonitions should have titles", () => { + // const admonitionRegex = + // /^\[(TIP|NOTE|WARNING|CAUTION|IMPORTANT)\]/gm; + // const admonitions = [...lessonAdoc.matchAll(admonitionRegex)]; + + // for (const match of admonitions) { + // const startIndex = match.index; + // const afterAdmonition = lessonAdoc.substring(startIndex); + // const nextLines = afterAdmonition.split("\n").slice(1, 3); + + // const hasTitle = + // nextLines[0] && nextLines[0].trim().startsWith("."); + // expect(hasTitle).toBe(true); + // } + // }); + + it("should be optional, mark as read or have one or more questions", () => { + expect( + optional || + hasReadButton || + includesSandbox || + questionPaths.length > 0 + ).toBe(true); + }); + + // TODO: Test all verify & solution cypher files + // if (type === 'challenge' && !optional) { + // it('should contain a valid verify.cypher', async () => { + // expect(existsSync(join(__dirname, '..', 'asciidoc', + // 'courses', slug, 'modules', moduleSlug, 'lessons', + // lessonSlug, 'verify.cypher')) + // ).toBe(true) + + // const cypher = readFileSync(join(lessonPath, 'verify.cypher')).toString() + // expect(await explainCypherError(cypher)).toBeUndefined() + // }) + + // it('should contain a valid solution.cypher', async () => { + // expect(existsSync(join(__dirname, '..', 'asciidoc', + // 'courses', slug, 'modules', moduleSlug, 'lessons', + // lessonSlug, 'solution.cypher')) + // ).toBe(true) + // }) + // } + + if (!skipLinkChecks) { + it("should not have any broken links", async () => { + for (const link of findLinks(lessonAdoc)) { + if ( + !link.includes("openai") && + !link.includes("example") && + !link.includes("localhost") + ) { + const statusCode = await getStatusCode(link); + try { + expect(statusCode).toBe(200); + } catch (e) { + throw new Error(`${link} returns ${statusCode}`); + } } } - } - }, 10000); - } - - it("should contain valid [source,cypher] blocks", async () => { - if (!skipCypherChecks) { - for (const cypher of findCypherStatements(lessonAdoc)) { - expect(await explainCypherError(cypher)).toBeUndefined(); - } + }, 10000); } - }); - if (!optional && !hasReadButton) { - for (const questionPath of questionPaths) { - const questionSlug = questionPath.split(sep).reverse()[0]; - const asciidoc = readFileSync(questionPath).toString(); - const isVerificationQuestion = - asciidoc.includes("verify::"); - - describe(questionSlug, () => { - it("should have [.question] or [.verify] marker", () => { - expect(asciidoc).toMatch(/^\[\.question|\[\.verify/m); - }); + it("should contain valid [source,cypher] blocks", async () => { + if (!skipCypherChecks) { + for (const cypher of findCypherStatements(lessonAdoc)) { + expect(await explainCypherError(cypher)).toBeUndefined(); + } + } + }); + + if (!optional && !hasReadButton) { + for (const questionPath of questionPaths) { + const questionSlug = questionPath.split(sep).reverse()[0]; + const asciidoc = readFileSync(questionPath).toString(); + const isVerificationQuestion = + asciidoc.includes("verify::"); + + describe(questionSlug, () => { + it("should have [.question] or [.verify] marker", () => { + expect(asciidoc).toMatch(/^\[\.question|\[\.verify/m); + }); - it("should have a title", () => { - const titleMatch = asciidoc.match(/^=\s+(.+)$/m); - expect(titleMatch).toBeTruthy(); - expect(titleMatch[1].trim().length).toBeGreaterThan(0); - }); + it("should have a title", () => { + const titleMatch = asciidoc.match(/^=\s+(.+)$/m); + expect(titleMatch).toBeTruthy(); + expect(titleMatch[1].trim().length).toBeGreaterThan(0); + }); - it(`should have a hint`, () => { - expect(asciidoc).toContain("\n[TIP,role=hint]"); - }); + it(`should have a hint`, () => { + expect(asciidoc).toContain("\n[TIP,role=hint]"); + }); - it(`should have a solution`, () => { - expect(asciidoc).toContain("\n[TIP,role=solution]"); - }); + it(`should have a solution`, () => { + expect(asciidoc).toContain("\n[TIP,role=solution]"); + }); - if (isVerificationQuestion) { - describe("Verify Question", () => { - it("should have a valid verify.cypher", async () => { - expect( - existsSync( - join( - __dirname, - "..", - "asciidoc", - "courses", - slug, - "modules", - moduleSlug, - "lessons", - lessonSlug, - "verify.cypher" + if (isVerificationQuestion) { + describe("Verify Question", () => { + it("should have a valid verify.cypher", async () => { + expect( + existsSync( + join( + __dirname, + "..", + "asciidoc", + "courses", + slug, + "modules", + moduleSlug, + "lessons", + lessonSlug, + "verify.cypher" + ) ) - ) - ).toBe(true); - - if (!skipCypherChecks) { - const contents = readFileSync( - join(lessonPath, "verify.cypher") - ).toString(); - - for (const cypher of contents - .split(";") - .filter((e) => e.trim() != "")) { - expect( - await explainCypherError(cypher) - ).toBeUndefined(); + ).toBe(true); + + if (!skipCypherChecks) { + const contents = readFileSync( + join(lessonPath, "verify.cypher") + ).toString(); + + for (const cypher of contents + .split(";") + .filter((e) => e.trim() != "")) { + expect( + await explainCypherError(cypher) + ).toBeUndefined(); + } } - } - }); - - it("should have a valid solution.cypher", async () => { - expect( - existsSync( - join( - __dirname, - "..", - "asciidoc", - "courses", - slug, - "modules", - moduleSlug, - "lessons", - lessonSlug, - "solution.cypher" + }); + + it("should have a valid solution.cypher", async () => { + expect( + existsSync( + join( + __dirname, + "..", + "asciidoc", + "courses", + slug, + "modules", + moduleSlug, + "lessons", + lessonSlug, + "solution.cypher" + ) ) - ) - ).toBe(true); - - if (!skipCypherChecks) { - const contents = readFileSync( - join(lessonPath, "solution.cypher") - ).toString(); - - for (const cypher of contents - .split(";") - .filter((e) => e.trim() != "")) { - expect( - await explainCypherError(cypher) - ).toBeUndefined(); + ).toBe(true); + + if (!skipCypherChecks) { + const contents = readFileSync( + join(lessonPath, "solution.cypher") + ).toString(); + + for (const cypher of contents + .split(";") + .filter((e) => e.trim() != "")) { + expect( + await explainCypherError(cypher) + ).toBeUndefined(); + } } - } + }); }); - }); - } else { - describe("Multiple Choice Question", () => { - it("should have at least 1 answer option", () => { - const answerMatches = asciidoc.match( - /^[\*\-]\s*\[[\sxX\*]\]/gm - ); - expect(answerMatches).toBeTruthy(); - expect(answerMatches.length).toBeGreaterThanOrEqual( - 1 - ); - }); - - it("should have a correct answer", () => { - const hasCorrectAnswer = - asciidoc.includes("* [x] ") || - asciidoc.includes("* [*] ") || - asciidoc.includes("- [x] ") || - asciidoc.includes("- [*] "); - expect(hasCorrectAnswer).toBe(true); + } else { + describe("Multiple Choice Question", () => { + it("should have at least 1 answer option", () => { + const answerMatches = asciidoc.match( + /^[\*\-]\s*\[[\sxX\*]\]/gm + ); + expect(answerMatches).toBeTruthy(); + expect(answerMatches.length).toBeGreaterThanOrEqual( + 1 + ); + }); + + it("should have a correct answer", () => { + const hasCorrectAnswer = + asciidoc.includes("* [x] ") || + asciidoc.includes("* [*] ") || + asciidoc.includes("- [x] ") || + asciidoc.includes("- [*] "); + expect(hasCorrectAnswer).toBe(true); + }); }); - }); - } - }); + } + }); + } } } });